diff --git a/docs/reference/antigravity-readiness-evidence.md b/docs/reference/antigravity-readiness-evidence.md index 3c65c631863..56605f4597c 100644 --- a/docs/reference/antigravity-readiness-evidence.md +++ b/docs/reference/antigravity-readiness-evidence.md @@ -1,5 +1,20 @@ # Antigravity readiness: what the transcripts show +## 2026-09-19: host contact and live prompt submission + +A regression marked an SSH terminal `unverifiable` after caching a ready screen. +The wait incorrectly returned ready. Readiness now refuses that cached verdict +while host contact is unverifiable, and the adopted-screen fallback rechecks +liveness when an outstanding snapshot completes. Both cases pass runtime tests; +this is simulated host loss, not a claim of real SSH validation. Reconnection +snapshot freshness remains a separate validation task. + +In the hidden app, a real `terminal.send` with text plus Enter submitted a harmless +prompt to installed agy. The rendered request failed with HTTP 401 and +`ACCESS_TOKEN_TYPE_UNSUPPORTED`; the subsequent empty composer satisfied the +readiness wait. The public RPC's guarded prompt route is limited to Claude/Codex, +so this agy check proves ordinary input delivery, not guarded worker submission. + ## 2026-09-19: ordinary wait and delivery integration The ordinary wait paths now consult the same current-screen classifier for diff --git a/src/main/runtime/antigravity-ordinary-wait.test.ts b/src/main/runtime/antigravity-ordinary-wait.test.ts index d945e887799..47bd447138c 100644 --- a/src/main/runtime/antigravity-ordinary-wait.test.ts +++ b/src/main/runtime/antigravity-ordinary-wait.test.ts @@ -127,4 +127,23 @@ describe('Antigravity ordinary waits use the current screen', () => { runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, capture('antigravity-ready-plan-127'), Date.now()) await vi.waitFor(() => expect(deliver).toHaveBeenCalled(), { timeout: 4500 }) }, 10000) + + it('does not reuse a ready screen after the execution host becomes unverifiable', async () => { + const { runtime, handle } = await createTranscriptPane({ + paneTitle: 'agy', + foregroundProcess: 'agy', + data: '', + connectionId: 'ssh-host' + }) + runtime.seedHeadlessTerminal(TRANSCRIPT_PANE_PTY_ID, '\x1b[0m', { cols: 120, rows: 40 }) + runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, capture('antigravity-ready-default-127'), Date.now()) + await expect( + runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 3500 }) + ).resolves.toMatchObject({ satisfied: true }) + runtime.markPtyLivenessUnverifiable(TRANSCRIPT_PANE_PTY_ID, 'SSH transport disconnected') + await expect( + runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 2500 }) + ).rejects.toThrow('timeout') + expect(runtime.getPtyLivenessVerdict(TRANSCRIPT_PANE_PTY_ID)?.status).toBe('unverifiable') + }, 10000) }) diff --git a/src/main/runtime/antigravity-visible-wait.test.ts b/src/main/runtime/antigravity-visible-wait.test.ts index dfe6f7c8a26..f77e1b6cc14 100644 --- a/src/main/runtime/antigravity-visible-wait.test.ts +++ b/src/main/runtime/antigravity-visible-wait.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs' import { join } from 'node:path' import { describe, expect, it, vi } from 'vitest' import { HeadlessEmulator } from '../daemon/headless-emulator' -import { createTranscriptPane } from './agent-transcript-pane-test-harness' +import { createTranscriptPane, TRANSCRIPT_PANE_PTY_ID } from './agent-transcript-pane-test-harness' import { projectTerminalVisibleLines } from './orca-runtime-terminal-projection' vi.mock('electron', () => ({ @@ -58,4 +58,48 @@ describe('Antigravity adopted-terminal visible readiness probe', () => { } }) } + + it('does not accept a snapshot that finishes after losing contact with its host', async () => { + const emulator = new HeadlessEmulator({ cols: 120, rows: 40, scrollback: 0 }) + try { + await emulator.write( + readFileSync(join(__dirname, '__fixtures__', 'antigravity-ready-default-127.txt'), 'utf8') + ) + const screen = projectTerminalVisibleLines(emulator) + const { runtime, handle } = await createTranscriptPane({ + paneTitle: 'agy', + foregroundProcess: 'agy', + data: '', + connectionId: 'ssh-host' + }) + let release = () => {} + const pending = new Promise((resolve) => { + release = resolve + }) + const read = vi.spyOn(runtime, 'readTerminal').mockImplementation(async () => { + await pending + return { + handle, + status: 'running', + tail: screen.lines, + draft: screen.draft, + source: 'screen', + truncated: false, + limited: false, + oldestCursor: '0', + nextCursor: '0', + latestCursor: '0', + returnedLineCount: screen.lines.length + } + }) + const waiting = runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 600 }) + const outcome = expect(waiting).rejects.toThrow('timeout') + expect(read).toHaveBeenCalled() + runtime.markPtyLivenessUnverifiable(TRANSCRIPT_PANE_PTY_ID, 'SSH transport disconnected') + release() + await outcome + } finally { + emulator.dispose() + } + }) }) diff --git a/src/main/runtime/orca-runtime-start-tui-idle-visible-read-probe.ts b/src/main/runtime/orca-runtime-start-tui-idle-visible-read-probe.ts index c53584f1b7e..67dc7f54d8a 100644 --- a/src/main/runtime/orca-runtime-start-tui-idle-visible-read-probe.ts +++ b/src/main/runtime/orca-runtime-start-tui-idle-visible-read-probe.ts @@ -64,6 +64,12 @@ export class OrcaRuntimeWithStartTuiIdleVisibleReadProbe extends OrcaRuntimeWith ) { return } + const ptyId = + this.getLivePtyForHandle(waiter.handle)?.pty.ptyId ?? + this.getLiveLeafForHandle(waiter.handle).leaf.ptyId + if (ptyId && this.getPtyLivenessVerdict(ptyId)?.status === 'unverifiable') { + return + } const snapshotText = projection.tail.join('\n') const blockedReason = detectTerminalWaitBlockedReason(snapshotText) if (!blockedReason && !isKnownReadyTerminalScreen(projection)) { diff --git a/src/main/runtime/orca-runtime-visible-snapshot-preview.ts b/src/main/runtime/orca-runtime-visible-snapshot-preview.ts index 67142a6795a..3c334285509 100644 --- a/src/main/runtime/orca-runtime-visible-snapshot-preview.ts +++ b/src/main/runtime/orca-runtime-visible-snapshot-preview.ts @@ -27,6 +27,9 @@ export class OrcaRuntimeWithVisibleSnapshotPreview extends OrcaRuntimeWithCaptur ) { return null } + if (this.getPtyLivenessVerdict(ptyId)?.status === 'unverifiable') { + return { ready: false, blockedReason: null } + } const cached = this.providerVisibleStateByPtyId.get(ptyId) if ( cached?.generation === this.getPtyLifecycleGeneration(ptyId) &&