From 80252600fc939bdf4173030db6c97628b3495e8b Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:50:56 -0700 Subject: [PATCH 1/3] fix(notes): reuse verified host prompt delivery --- notes/linear-fix-report.md | 51 +++++++++++ .../terminal/terminal-query-methods.ts | 7 +- .../methods/terminal/terminal-send-method.ts | 24 ++--- .../rpc/terminal-agent-prompt-send.test.ts | 71 +++++++++++++++ .../terminal-prompt-delivery-receipt.test.ts | 43 +++++---- src/main/runtime/rpc/terminal-send.test.ts | 4 +- .../lib/active-agent-note-send-delivery.ts | 30 +++++++ .../active-agent-note-send-verified.test.ts | 90 +++++++++++++++++++ .../active-agent-terminal-send-readiness.ts | 9 +- src/shared/runtime-terminal-contracts.ts | 1 + 10 files changed, 296 insertions(+), 34 deletions(-) create mode 100644 notes/linear-fix-report.md create mode 100644 src/renderer/src/lib/active-agent-note-send-verified.test.ts diff --git a/notes/linear-fix-report.md b/notes/linear-fix-report.md new file mode 100644 index 000000000000..f52cfaaf91b7 --- /dev/null +++ b/notes/linear-fix-report.md @@ -0,0 +1,51 @@ +# Prompt delivery repair + +Base: `588240043e84b1f8e58013298e692e7a57ba8ad5` (newer than triage `9f044031fc9`). All 18 named Linear descriptions/comments refreshed through `orca linear issue --full --json`; targeted open-PR searches checked before implementation. No Linear mutations, user-session cleanup, visible app launches, or other authors' PR edits. Hidden validation startup had an unexpected provider-bootstrap side effect noted below. + +## Implemented + +Notes sent to a proven Claude/Codex target now use the existing runtime `terminal.send` / `sendTerminalAgentPrompt` transaction, instead of independent paste and Enter RPCs separated by 50 ms. Runtime still checks permission and exact PTY binding before paste and before submit, owns host-specific ingestion timing and serializes delivery, and uses the incumbent submission verifier. Errors do not trigger a legacy re-paste. An additive, optional `terminal.agentStatus.supportsGuardedAgentPrompt` flag gates the client behavior; old clients ignore it, new clients retain the incumbent two-phase path on old/unsupported hosts. Unsupported or changed targets cannot turn a combined guarded request into unguarded shell input. + +The change is intentionally focused: no second verifier, startup shell gate changes, new submit binding, automatic retries, lifecycle/cleanup changes, or provider-local slash-command success inference. Renderer calls currently have no durable orchestration request ID; durable CLI receipt replay remains the existing flow and is regression-tested, not claimed as newly implemented UI retry support. + +## Issue disposition + +No complete historical ticket closure is claimed by this draft. + +| Issue | Disposition and evidence | +| --- | --- | +| STA-4495 | Partial: notes now reuse the shipped verifier; silent-success half already changed in v1.4.187 per latest corrective comment. Core composer/receipt work remains in #18022 / #16198. | +| STA-2380 | Pending existing #10020 for launchAgent/quiescence trust gate; Codex MCP input-missing variant explicitly outside that PR and not reproduced here. | +| STA-3793 | Pending: Codex MCP cold-start loss not exercised by this notes repair; historical presence/readiness asymmetry is not closure evidence. | +| STA-2655 | Pending: comments identify both composer-pending and input-missing variants; no blanket historical closure. Related #10020 / #18022. | +| STA-2912 | Pending existing #15698 (large atomic paste) and #18196 (render-gate ingestion floor). Main already has size-dependent host ingestion; no duplicate core patch. | +| STA-4318 | Pending: Windows/macOS Claude worker symptoms remain outside notes caller coverage; #18022 / #16198 overlap. | +| STA-5642 | Partial: notes half fixed through host transaction. Startup `agent-paste-draft.ts` remains separate and overlaps #16716 / #18996; 50 ms fallback retained for old hosts. | +| STA-5951 | Pending existing #18020: interactive CLI launchAgent inference already has a focused open fix, including remote clients. | +| STA-6211 | Partial mechanism coverage only: notes change does not close worker cold delivery or wrong-pane capability binding; latter belongs to identity worker. #18022 addresses composer proof. | +| STA-5869 | Pending: manual Enter recovery/capability revocation is orchestration core, overlapping #16198 / #15941; no timeout inflation here. | +| STA-6779 | Pending: automation reuse still calls renderer submit helper; selected-session submission and #18996 integration need validation. Resource worker owns reuse ownership. | +| STA-6437 | Pending existing #15275 late shell-ready startup-command repair; shell readiness is distinct from agent readiness and untouched. Current daemon startup tests already exercise shell-ready routing. | +| STA-6432 | Pending existing #18087: fresh working-to-done completion fence; no duplicate automation edit. | +| STA-6755 | Pending existing #18996: configured Enter/Ctrl+Enter setting and recipe/reuse submit bytes. Do not claim fixed by routing notes. | +| STA-5379 | Pending: historical Git AI action recipe symptom not reproduced; startup caller overlaps #18996 / #16716. | +| STA-5150 | Pending existing #16198 and #16290: local slash-command/non-turn verdict and dirty composer refusal are distinct. Notes errors never trigger automatic re-paste, but no new local-command success outcome. | +| STA-6763 | Pending: description is title-only; comments propose timeout inflation without measured evidence. No arbitrary acknowledgment-window change. | +| STA-2631 | Partial existing guards regression-tested; current verifier already blocks permission sequences. Historical trust/Antigravity scrollback detection requires live provider reproduction; #10020 is adjacent. | + +All PR numbers above link under `https://github.com/stablyai/orca/pull/` and were found open when checked, not treated as merged fixes. + +## Validation + +- All commands used `ORCA_BACKGROUND_LAUNCH=1`. +- `pnpm tc:node` and `pnpm tc:web`: passed. +- Focused oxlint, changed-code quality (native/type-aware/React Doctor: zero findings), and `git diff --check`: passed. +- Final focused regressions: **8 files, 129 tests passed**, including Windows ingestion, runtime submission, RPC guard, durable receipt replay, old-host notes fallback, and new local/paired notes tests. +- New coverage: single large notes RPC on local/paired targets, wait for verified result, permission refusal, no automatic replay after ambiguous failure, guarded transaction routing/rechecks, signal forwarding, rejection after settlement support changes, durable guarded receipt replay without duplicate body/Enter. +- Existing focused suites cover Windows/POSIX host ingestion, SSH Windows write-host selection, permission/cancellation, small/large prompt verification, durable receipts, and old-host focused/explicit notes paths. These are deterministic tests on macOS, not live Windows/SSH/WSL end-to-end proof. +- Coordinator supplied the Electron skill after local lookup failed. Rebuilt and launched this worktree hidden with isolated ORCA_DEV_USER_DATA_PATH, CDP 9341 and renderer 5183; app.getIdentity confirmed this checkout. Inspected /tmp/orca-prompt-hidden-renderer.png: empty workspace welcome screen, zero terminal sessions. This is startup/render proof only, not before/after notes-delivery evidence; manual checklist remains unchecked. +- Validation stopped after startup unexpectedly logged two `codex-trust-grant` messages with `host=native, wrote=true` (8 managed hook entries). Isolated app profile does not fully isolate provider bootstrap. Escalated to coordinator, launched no agents, performed no config rollback; stopped only recorded validation process tree and verified both ports closed. Own-profile ledger confirms the two affected homes: `/Users/nwparker/.codex` and `/tmp/orca-prompt-delivery-validation-profile/codex-runtime-home/home`, each 8 entries. Source is `codex-real-home-hook-install.ts` real-home grant with `useDefaultCodexHome: true`; native launcher deletes CODEX_HOME. Existing `tests/e2e/helpers/electron-home-isolation.ts` is the required full home boundary. `ORCA_DISABLE_CODEX_TRUST_RPC` alone does not prevent all hook/config writes. + +## Remaining work + +Review this focused draft alongside the existing PRs above; validate hidden live provider sessions on isolated Windows/WSL and SSH hosts, including configured Ctrl+Enter and provider-local commands. Preserve separately owned automation/session changes. Coordinator handles Linear accounting; do not close partially covered reports. diff --git a/src/main/runtime/rpc/methods/terminal/terminal-query-methods.ts b/src/main/runtime/rpc/methods/terminal/terminal-query-methods.ts index 82edd55cd79e..e71b66289468 100644 --- a/src/main/runtime/rpc/methods/terminal/terminal-query-methods.ts +++ b/src/main/runtime/rpc/methods/terminal/terminal-query-methods.ts @@ -105,7 +105,12 @@ export const TERMINAL_QUERY_METHODS: RpcAnyMethod[] = [ name: 'terminal.agentStatus', params: TerminalHandle, handler: async (params, { runtime }) => ({ - agentStatus: await runtime.getTerminalAgentStatus(params.terminal) + agentStatus: { + ...(await runtime.getTerminalAgentStatus(params.terminal)), + supportsGuardedAgentPrompt: await runtime.isTerminalRunningSettledPromptAgent( + params.terminal + ) + } }) }), defineMethod({ diff --git a/src/main/runtime/rpc/methods/terminal/terminal-send-method.ts b/src/main/runtime/rpc/methods/terminal/terminal-send-method.ts index ad471098e49d..43ce0b036d3f 100644 --- a/src/main/runtime/rpc/methods/terminal/terminal-send-method.ts +++ b/src/main/runtime/rpc/methods/terminal/terminal-send-method.ts @@ -114,7 +114,7 @@ export const TERMINAL_SEND_METHODS: RpcAnyMethod[] = [ 'refresh', true ) - // Why: a stream-less request can't safely create ownership, so never write at stale geometry. + // Stream-less input must not write at stale geometry. if (!claim.updated || isTerminalInputLockedForClient(runtime, leaf.ptyId, params.client)) { return { send: { @@ -127,8 +127,16 @@ export const TERMINAL_SEND_METHODS: RpcAnyMethod[] = [ } const hasText = typeof params.text === 'string' && params.text.length > 0 const hasSuffix = params.enter === true || params.interrupt === true - if (params.requireAgentStatus === 'sendable' && hasText && hasSuffix) { - // Why: guarded sends are two-phase; reject combined payload + submit so a guard flip can't cause partial delivery. + const useSettledAgentPrompt = + params.agentPrompt === true && + hasText && + params.enter === true && + params.interrupt !== true && + params.client?.type === 'desktop' && + (await runtime.isTerminalRunningSettledPromptAgent(params.terminal)) + const combinedGuardedSend = params.requireAgentStatus === 'sendable' && hasText && hasSuffix + if (combinedGuardedSend && !useSettledAgentPrompt) { + // Only the verified transaction rechecks the guard between paste and submit. return { send: { handle: params.terminal, @@ -189,13 +197,6 @@ export const TERMINAL_SEND_METHODS: RpcAnyMethod[] = [ markMutationEffectPossible?.() } : assertSendPreconditions - const useSettledAgentPrompt = - params.agentPrompt === true && - hasText && - params.enter === true && - params.interrupt !== true && - params.client?.type === 'desktop' && - (await runtime.isTerminalRunningSettledPromptAgent(params.terminal)) const reserveWrite = params.inputKind !== 'query-reply' && leaf?.ptyId && mobileFloorClientId ? (ptyId: string): void => { @@ -244,8 +245,7 @@ export const TERMINAL_SEND_METHODS: RpcAnyMethod[] = [ } catch (error) { mobileFloorClaim.current?.rollback() if (isAgentSessionPtyWriteRefusedError(error)) { - // Why: name the owner and the stage instead of a bare not-writable, so a client can say - // who holds the session rather than retrying into a lease it will never win. + // Report the session owner so callers do not retry a refused lease. return { send: { handle: params.terminal, diff --git a/src/main/runtime/rpc/terminal-agent-prompt-send.test.ts b/src/main/runtime/rpc/terminal-agent-prompt-send.test.ts index 29254fbcf3ad..ebd5e65c0ae5 100644 --- a/src/main/runtime/rpc/terminal-agent-prompt-send.test.ts +++ b/src/main/runtime/rpc/terminal-agent-prompt-send.test.ts @@ -51,6 +51,77 @@ describe('terminal agent prompt send RPC', () => { expect(sendTerminal).not.toHaveBeenCalled() }) + it.each([true, false])( + 'rechecks permission before verified submit (blocked=%s)', + async (blocked) => { + const controller = new AbortController() + let permission = false + const writes: string[] = [] + const runtime = makeRuntime({ + resolveLiveLeafForHandle: vi.fn().mockReturnValue({ ptyId: 'pty-1' }), + getDriver: vi.fn().mockReturnValue({ kind: 'idle' }), + isTerminalRunningSettledPromptAgent: vi.fn().mockResolvedValue(true), + getTerminalAgentStatus: vi.fn(async () => ({ + handle: 'terminal-1', + isRunningAgent: true, + status: permission ? ('permission' as const) : ('idle' as const) + })), + sendTerminal: vi.fn(), + sendTerminalAgentPrompt: vi.fn(async (_handle, text, options) => { + expect(options?.signal).toBe(controller.signal) + await options?.beforeWrite?.('pty-1') + writes.push(text) + permission = blocked + await options?.beforeWrite?.('pty-1') + writes.push('submit') + return { handle: 'terminal-1', accepted: true, bytesWritten: text.length + 1 } + }) + }) + const dispatcher = new RpcDispatcher({ runtime, methods: TERMINAL_METHODS }) + const response = await dispatcher.dispatch( + makeRequest({ + terminal: 'terminal-1', + text: 'review this', + enter: true, + agentPrompt: true, + requireAgentStatus: 'sendable', + client: { id: 'orca-desktop', type: 'desktop' } + }), + { signal: controller.signal } + ) + expect(response).toMatchObject({ ok: true, result: { send: { accepted: !blocked } } }) + expect(writes).toEqual(blocked ? ['review this'] : ['review this', 'submit']) + expect(runtime.sendTerminal).not.toHaveBeenCalled() + } + ) + + it('refuses combined guarded input when settlement support disappeared', async () => { + const runtime = makeRuntime({ + resolveLiveLeafForHandle: vi.fn().mockReturnValue({ ptyId: 'pty-1' }), + getDriver: vi.fn().mockReturnValue({ kind: 'idle' }), + isTerminalRunningSettledPromptAgent: vi.fn().mockResolvedValue(false), + sendTerminal: vi.fn(), + sendTerminalAgentPrompt: vi.fn() + }) + const dispatcher = new RpcDispatcher({ runtime, methods: TERMINAL_METHODS }) + const response = await dispatcher.dispatch( + makeRequest({ + terminal: 'terminal-1', + text: 'review this', + enter: true, + agentPrompt: true, + requireAgentStatus: 'sendable', + client: { id: 'orca-desktop', type: 'desktop' } + }) + ) + expect(response).toMatchObject({ + ok: true, + result: { send: { accepted: false, bytesWritten: 0 } } + }) + expect(runtime.sendTerminal).not.toHaveBeenCalled() + expect(runtime.sendTerminalAgentPrompt).not.toHaveBeenCalled() + }) + it('preserves direct input when the CLI target is not a proven settlement agent', async () => { const sendTerminal = vi.fn().mockResolvedValue({ handle: 'terminal-1', diff --git a/src/main/runtime/rpc/terminal-prompt-delivery-receipt.test.ts b/src/main/runtime/rpc/terminal-prompt-delivery-receipt.test.ts index e1bc03f18abf..e22132aec18b 100644 --- a/src/main/runtime/rpc/terminal-prompt-delivery-receipt.test.ts +++ b/src/main/runtime/rpc/terminal-prompt-delivery-receipt.test.ts @@ -31,7 +31,8 @@ function request( terminal: string, promptRequestId: string, text: string, - waitSubmitMs?: number + waitSubmitMs?: number, + guarded = false ): RpcRequest { return { id: `rpc-${promptRequestId}`, @@ -44,6 +45,7 @@ function request( enter: true, agentPrompt: true, waitSubmitMs, + ...(guarded ? { requireAgentStatus: 'sendable' } : {}), client: { id: 'orca-cli', type: 'desktop' } } } @@ -146,25 +148,28 @@ describe('durable terminal prompt delivery receipts', () => { harness.db.close() }) - it('replays after a dispatcher replacement without duplicate text or Enter', async () => { - vi.useFakeTimers() - const harness = await createHarness('codex', true) - const firstPromise = harness.dispatcher.dispatch( - request(harness.handle, 'crash-retry', 'preserve once') - ) - await vi.runAllTimersAsync() - const first = await firstPromise - const writesAfterFirst = [...harness.writes] - const replacement = new RpcDispatcher({ runtime: harness.runtime, methods: TERMINAL_METHODS }) - const replay = await replacement.dispatch( - request(harness.handle, 'crash-retry', 'preserve once') - ) + it.each([false, true])( + 'replays after dispatcher replacement without duplicate input (guarded=%s)', + async (guarded) => { + vi.useFakeTimers() + const harness = await createHarness('codex', true) + const firstPromise = harness.dispatcher.dispatch( + request(harness.handle, 'crash-retry', 'preserve once', undefined, guarded) + ) + await vi.runAllTimersAsync() + const first = await firstPromise + const writesAfterFirst = [...harness.writes] + const replacement = new RpcDispatcher({ runtime: harness.runtime, methods: TERMINAL_METHODS }) + const replay = await replacement.dispatch( + request(harness.handle, 'crash-retry', 'preserve once', undefined, guarded) + ) - expect(first).toMatchObject({ ok: true, result: { mutation: { replayed: false } } }) - expect(replay).toMatchObject({ ok: true, result: { mutation: { replayed: true } } }) - expect(harness.writes).toEqual(writesAfterFirst) - harness.db.close() - }) + expect(first).toMatchObject({ ok: true, result: { mutation: { replayed: false } } }) + expect(replay).toMatchObject({ ok: true, result: { mutation: { replayed: true } } }) + expect(harness.writes).toEqual(writesAfterFirst) + harness.db.close() + } + ) it('keeps an ambiguous partial write pending and refuses to resend it', async () => { vi.useFakeTimers() diff --git a/src/main/runtime/rpc/terminal-send.test.ts b/src/main/runtime/rpc/terminal-send.test.ts index e9fb481aae06..4db6ec26c356 100644 --- a/src/main/runtime/rpc/terminal-send.test.ts +++ b/src/main/runtime/rpc/terminal-send.test.ts @@ -57,6 +57,7 @@ describe('terminal send RPC', () => { it('reports runtime-owned terminal agent status', async () => { const runtime = stubRuntime({ + isTerminalRunningSettledPromptAgent: vi.fn().mockResolvedValue(true), getTerminalAgentStatus: vi.fn().mockResolvedValue({ handle: 'terminal-1', isRunningAgent: true, @@ -79,7 +80,8 @@ describe('terminal send RPC', () => { agentStatus: { handle: 'terminal-1', isRunningAgent: true, - status: 'permission' + status: 'permission', + supportsGuardedAgentPrompt: true } }) expect(runtime.getTerminalAgentStatus).toHaveBeenCalledWith('terminal-1') diff --git a/src/renderer/src/lib/active-agent-note-send-delivery.ts b/src/renderer/src/lib/active-agent-note-send-delivery.ts index 2d9e88ae5078..2b9c6fccbceb 100644 --- a/src/renderer/src/lib/active-agent-note-send-delivery.ts +++ b/src/renderer/src/lib/active-agent-note-send-delivery.ts @@ -67,6 +67,36 @@ export async function sendPromptWithGuardedPasteAndEnter( } } + if (initialAgentStatus.supportsGuardedAgentPrompt) { + try { + const { send } = await callRuntimeRpc<{ send: RuntimeTerminalSend }>( + runtimeTarget, + 'terminal.send', + { + terminal: terminalHandle, + text: prompt, + enter: true, + agentPrompt: true, + requireAgentStatus: 'sendable', + client: ORCA_DESKTOP_TERMINAL_CLIENT + }, + { timeoutMs: ACTIVE_AGENT_SEND_RPC_TIMEOUT_MS } + ) + if (send.refusedReason === 'permission') { + return { status: 'permission', code: 'terminal-send-permission' } + } + if (send.refusedReason === 'no-agent') { + return { status: 'no-agent', code: 'no-agent' } + } + return send.accepted + ? { status: 'sent' } + : { status: 'partial-submit-failed', code: 'submit-send-refused' } + } catch { + // A failed observation may follow a paste; never replay it through the legacy path. + return { status: 'partial-submit-failed', code: 'submit-send-error' } + } + } + const pastePayload = `${BRACKETED_PASTE_BEGIN}${sanitizeTerminalPasteText(prompt)}${BRACKETED_PASTE_END}` try { const { send } = await callRuntimeRpc<{ send: RuntimeTerminalSend }>( diff --git a/src/renderer/src/lib/active-agent-note-send-verified.test.ts b/src/renderer/src/lib/active-agent-note-send-verified.test.ts new file mode 100644 index 000000000000..25e6ffebeb62 --- /dev/null +++ b/src/renderer/src/lib/active-agent-note-send-verified.test.ts @@ -0,0 +1,90 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { sendPromptWithGuardedPasteAndEnter } from './active-agent-note-send-delivery' + +const rpc = vi.hoisted(() => vi.fn()) +vi.mock('@/runtime/runtime-rpc-client', () => ({ + callRuntimeRpc: rpc, + RuntimeRpcCallError: class extends Error {} +})) +vi.mock('./agent-paste-draft', () => ({ + BRACKETED_PASTE_BEGIN: '\x1b[200~', + BRACKETED_PASTE_END: '\x1b[201~', + POST_PASTE_SUBMIT_DELAY_MS: 50 +})) + +const targets = [{ kind: 'local' }, { kind: 'environment', environmentId: 'paired-host' }] as const + +describe('verified note delivery', () => { + beforeEach(() => { + rpc.mockReset() + }) + + it.each(targets)('submits one complete large prompt on its owning runtime %j', async (target) => { + let finish: (() => void) | undefined + rpc.mockImplementation(async (_target, method) => { + if (method === 'terminal.agentStatus') { + return { + agentStatus: { isRunningAgent: true, status: 'idle', supportsGuardedAgentPrompt: true } + } + } + await new Promise((resolve) => { + finish = resolve + }) + return { send: { accepted: true } } + }) + const prompt = 'Review this file\n'.repeat(500) + let settled = false + const send = sendPromptWithGuardedPasteAndEnter(target, 'term-owner', prompt, { + allowLegacyFallback: false + }).then((result) => { + settled = true + return result + }) + await vi.waitFor(() => expect(finish).toBeDefined()) + expect(settled).toBe(false) + finish!() + await expect(send).resolves.toEqual({ status: 'sent' }) + const sends = rpc.mock.calls.filter((call) => call[1] === 'terminal.send') + expect(sends).toEqual([ + [ + target, + 'terminal.send', + { + terminal: 'term-owner', + text: prompt, + enter: true, + agentPrompt: true, + requireAgentStatus: 'sendable', + client: { id: 'orca-desktop', type: 'desktop' } + }, + { timeoutMs: 15000 } + ] + ]) + }) + + it('does not paste again or claim success after an ambiguous verified send', async () => { + rpc + .mockResolvedValueOnce({ + agentStatus: { isRunningAgent: true, status: 'idle', supportsGuardedAgentPrompt: true } + }) + .mockRejectedValueOnce(new Error('agent_prompt_stalled')) + await expect( + sendPromptWithGuardedPasteAndEnter(targets[0], 'term-owner', 'review', { + allowLegacyFallback: false + }) + ).resolves.toEqual({ status: 'partial-submit-failed', code: 'submit-send-error' }) + expect(rpc).toHaveBeenCalledTimes(2) + }) + + it('refuses permission prompts before writing', async () => { + rpc.mockResolvedValue({ + agentStatus: { isRunningAgent: true, status: 'permission', supportsGuardedAgentPrompt: true } + }) + await expect( + sendPromptWithGuardedPasteAndEnter(targets[0], 'term-owner', 'review', { + allowLegacyFallback: false + }) + ).resolves.toMatchObject({ status: 'permission' }) + expect(rpc).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/renderer/src/lib/active-agent-terminal-send-readiness.ts b/src/renderer/src/lib/active-agent-terminal-send-readiness.ts index b432366527f7..963c8fe52958 100644 --- a/src/renderer/src/lib/active-agent-terminal-send-readiness.ts +++ b/src/renderer/src/lib/active-agent-terminal-send-readiness.ts @@ -20,6 +20,7 @@ export type TerminalAgentSendReadiness = export type TerminalAgentSendReadinessResult = { status: TerminalAgentSendReadiness supportsGuardedSend: boolean + supportsGuardedAgentPrompt?: boolean code?: ActiveAgentNotesSendFailureCode } @@ -41,7 +42,13 @@ export async function getTerminalAgentSendReadiness( if (agentStatus.status === 'permission') { return { status: 'permission', supportsGuardedSend: true } } - return { status: 'sendable', supportsGuardedSend: true } + return { + status: 'sendable', + supportsGuardedSend: true, + ...(agentStatus.supportsGuardedAgentPrompt === true + ? { supportsGuardedAgentPrompt: true } + : {}) + } } catch (error) { if (error instanceof RuntimeRpcCallError && error.code === 'method_not_found') { if (!options.allowLegacyFallback) { diff --git a/src/shared/runtime-terminal-contracts.ts b/src/shared/runtime-terminal-contracts.ts index db1c3751ba82..47f84404a381 100644 --- a/src/shared/runtime-terminal-contracts.ts +++ b/src/shared/runtime-terminal-contracts.ts @@ -240,6 +240,7 @@ export type RuntimeTerminalAgentStatus = { handle: string isRunningAgent: boolean status: RuntimeTerminalAgentStatusState + supportsGuardedAgentPrompt?: boolean } export type RuntimeTerminalPresentation = 'background' | 'focused' From 59879c202830b6ea8ec9933deb4a805e89e8aa40 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:51:31 -0700 Subject: [PATCH 2/3] docs: record prompt delivery draft and validation --- notes/linear-fix-report.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/notes/linear-fix-report.md b/notes/linear-fix-report.md index f52cfaaf91b7..59159125422a 100644 --- a/notes/linear-fix-report.md +++ b/notes/linear-fix-report.md @@ -49,3 +49,9 @@ All PR numbers above link under `https://github.com/stablyai/orca/pull/` ## Remaining work Review this focused draft alongside the existing PRs above; validate hidden live provider sessions on isolated Windows/WSL and SSH hosts, including configured Ctrl+Enter and provider-local commands. Preserve separately owned automation/session changes. Coordinator handles Linear accounting; do not close partially covered reports. + +## Published artifact + +Draft PR: https://github.com/stablyai/orca/pull/19417 + +Implementation commit: `80252600fc9` (`fix(notes): reuse verified host prompt delivery`). Pushed to `origin/nwparker/linear-prompt-delivery`, targeting `stablyai/orca:main`; not merged. Worktree clean after publication. From e632f3c1bf7ab5471647c3b8b5c5120dcda9609b Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:55:54 -0700 Subject: [PATCH 3/3] docs: keep prompt delivery report local --- notes/linear-fix-report.md | 57 -------------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 notes/linear-fix-report.md diff --git a/notes/linear-fix-report.md b/notes/linear-fix-report.md deleted file mode 100644 index 59159125422a..000000000000 --- a/notes/linear-fix-report.md +++ /dev/null @@ -1,57 +0,0 @@ -# Prompt delivery repair - -Base: `588240043e84b1f8e58013298e692e7a57ba8ad5` (newer than triage `9f044031fc9`). All 18 named Linear descriptions/comments refreshed through `orca linear issue --full --json`; targeted open-PR searches checked before implementation. No Linear mutations, user-session cleanup, visible app launches, or other authors' PR edits. Hidden validation startup had an unexpected provider-bootstrap side effect noted below. - -## Implemented - -Notes sent to a proven Claude/Codex target now use the existing runtime `terminal.send` / `sendTerminalAgentPrompt` transaction, instead of independent paste and Enter RPCs separated by 50 ms. Runtime still checks permission and exact PTY binding before paste and before submit, owns host-specific ingestion timing and serializes delivery, and uses the incumbent submission verifier. Errors do not trigger a legacy re-paste. An additive, optional `terminal.agentStatus.supportsGuardedAgentPrompt` flag gates the client behavior; old clients ignore it, new clients retain the incumbent two-phase path on old/unsupported hosts. Unsupported or changed targets cannot turn a combined guarded request into unguarded shell input. - -The change is intentionally focused: no second verifier, startup shell gate changes, new submit binding, automatic retries, lifecycle/cleanup changes, or provider-local slash-command success inference. Renderer calls currently have no durable orchestration request ID; durable CLI receipt replay remains the existing flow and is regression-tested, not claimed as newly implemented UI retry support. - -## Issue disposition - -No complete historical ticket closure is claimed by this draft. - -| Issue | Disposition and evidence | -| --- | --- | -| STA-4495 | Partial: notes now reuse the shipped verifier; silent-success half already changed in v1.4.187 per latest corrective comment. Core composer/receipt work remains in #18022 / #16198. | -| STA-2380 | Pending existing #10020 for launchAgent/quiescence trust gate; Codex MCP input-missing variant explicitly outside that PR and not reproduced here. | -| STA-3793 | Pending: Codex MCP cold-start loss not exercised by this notes repair; historical presence/readiness asymmetry is not closure evidence. | -| STA-2655 | Pending: comments identify both composer-pending and input-missing variants; no blanket historical closure. Related #10020 / #18022. | -| STA-2912 | Pending existing #15698 (large atomic paste) and #18196 (render-gate ingestion floor). Main already has size-dependent host ingestion; no duplicate core patch. | -| STA-4318 | Pending: Windows/macOS Claude worker symptoms remain outside notes caller coverage; #18022 / #16198 overlap. | -| STA-5642 | Partial: notes half fixed through host transaction. Startup `agent-paste-draft.ts` remains separate and overlaps #16716 / #18996; 50 ms fallback retained for old hosts. | -| STA-5951 | Pending existing #18020: interactive CLI launchAgent inference already has a focused open fix, including remote clients. | -| STA-6211 | Partial mechanism coverage only: notes change does not close worker cold delivery or wrong-pane capability binding; latter belongs to identity worker. #18022 addresses composer proof. | -| STA-5869 | Pending: manual Enter recovery/capability revocation is orchestration core, overlapping #16198 / #15941; no timeout inflation here. | -| STA-6779 | Pending: automation reuse still calls renderer submit helper; selected-session submission and #18996 integration need validation. Resource worker owns reuse ownership. | -| STA-6437 | Pending existing #15275 late shell-ready startup-command repair; shell readiness is distinct from agent readiness and untouched. Current daemon startup tests already exercise shell-ready routing. | -| STA-6432 | Pending existing #18087: fresh working-to-done completion fence; no duplicate automation edit. | -| STA-6755 | Pending existing #18996: configured Enter/Ctrl+Enter setting and recipe/reuse submit bytes. Do not claim fixed by routing notes. | -| STA-5379 | Pending: historical Git AI action recipe symptom not reproduced; startup caller overlaps #18996 / #16716. | -| STA-5150 | Pending existing #16198 and #16290: local slash-command/non-turn verdict and dirty composer refusal are distinct. Notes errors never trigger automatic re-paste, but no new local-command success outcome. | -| STA-6763 | Pending: description is title-only; comments propose timeout inflation without measured evidence. No arbitrary acknowledgment-window change. | -| STA-2631 | Partial existing guards regression-tested; current verifier already blocks permission sequences. Historical trust/Antigravity scrollback detection requires live provider reproduction; #10020 is adjacent. | - -All PR numbers above link under `https://github.com/stablyai/orca/pull/` and were found open when checked, not treated as merged fixes. - -## Validation - -- All commands used `ORCA_BACKGROUND_LAUNCH=1`. -- `pnpm tc:node` and `pnpm tc:web`: passed. -- Focused oxlint, changed-code quality (native/type-aware/React Doctor: zero findings), and `git diff --check`: passed. -- Final focused regressions: **8 files, 129 tests passed**, including Windows ingestion, runtime submission, RPC guard, durable receipt replay, old-host notes fallback, and new local/paired notes tests. -- New coverage: single large notes RPC on local/paired targets, wait for verified result, permission refusal, no automatic replay after ambiguous failure, guarded transaction routing/rechecks, signal forwarding, rejection after settlement support changes, durable guarded receipt replay without duplicate body/Enter. -- Existing focused suites cover Windows/POSIX host ingestion, SSH Windows write-host selection, permission/cancellation, small/large prompt verification, durable receipts, and old-host focused/explicit notes paths. These are deterministic tests on macOS, not live Windows/SSH/WSL end-to-end proof. -- Coordinator supplied the Electron skill after local lookup failed. Rebuilt and launched this worktree hidden with isolated ORCA_DEV_USER_DATA_PATH, CDP 9341 and renderer 5183; app.getIdentity confirmed this checkout. Inspected /tmp/orca-prompt-hidden-renderer.png: empty workspace welcome screen, zero terminal sessions. This is startup/render proof only, not before/after notes-delivery evidence; manual checklist remains unchecked. -- Validation stopped after startup unexpectedly logged two `codex-trust-grant` messages with `host=native, wrote=true` (8 managed hook entries). Isolated app profile does not fully isolate provider bootstrap. Escalated to coordinator, launched no agents, performed no config rollback; stopped only recorded validation process tree and verified both ports closed. Own-profile ledger confirms the two affected homes: `/Users/nwparker/.codex` and `/tmp/orca-prompt-delivery-validation-profile/codex-runtime-home/home`, each 8 entries. Source is `codex-real-home-hook-install.ts` real-home grant with `useDefaultCodexHome: true`; native launcher deletes CODEX_HOME. Existing `tests/e2e/helpers/electron-home-isolation.ts` is the required full home boundary. `ORCA_DISABLE_CODEX_TRUST_RPC` alone does not prevent all hook/config writes. - -## Remaining work - -Review this focused draft alongside the existing PRs above; validate hidden live provider sessions on isolated Windows/WSL and SSH hosts, including configured Ctrl+Enter and provider-local commands. Preserve separately owned automation/session changes. Coordinator handles Linear accounting; do not close partially covered reports. - -## Published artifact - -Draft PR: https://github.com/stablyai/orca/pull/19417 - -Implementation commit: `80252600fc9` (`fix(notes): reuse verified host prompt delivery`). Pushed to `origin/nwparker/linear-prompt-delivery`, targeting `stablyai/orca:main`; not merged. Worktree clean after publication.