From d6a4303e9d238de3e6d545f1f02ae574ad80768e Mon Sep 17 00:00:00 2001 From: brennanb2025 Date: Thu, 9 Jul 2026 00:57:02 -0700 Subject: [PATCH] fix(agents): keep remote launches on the optimistic delivery verdict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Receipt eligibility reads LOCAL evidence — locally-observed hook statuses and the local managed-home install probe — which says nothing about a remote host whose hooks may silently not fire. In a mixed local+remote session that evidence would strict-gate an SSH launch and false-fail a delivered prompt (review finding F1). Remote launches keep the optimistic legacy verdict until host-scoped observed evidence exists (follow-up). Co-authored-by: Orca --- .../lib/agent-launch-prompt-delivery.test.ts | 21 +++++++++++++++++++ .../src/lib/agent-launch-prompt-delivery.ts | 13 ++++++++++-- .../src/lib/launch-agent-in-new-tab.ts | 1 + .../src/lib/launch-work-item-direct-agent.ts | 11 +++++++++- .../src/lib/launch-work-item-direct.ts | 3 ++- 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/lib/agent-launch-prompt-delivery.test.ts b/src/renderer/src/lib/agent-launch-prompt-delivery.test.ts index c5e33e20d24..85992129f41 100644 --- a/src/renderer/src/lib/agent-launch-prompt-delivery.test.ts +++ b/src/renderer/src/lib/agent-launch-prompt-delivery.test.ts @@ -284,5 +284,26 @@ describe('deliverLaunchPromptToAgentTab', () => { expect(mocks.watchForPromptSubmitReceipt).not.toHaveBeenCalled() }) + + it('keeps the optimistic verdict for remote launches', async () => { + // Why: eligibility reads LOCAL hook evidence, which says nothing about a + // remote host whose hooks may silently not fire — strict-gating there + // would false-fail delivered prompts. Remote graduates in a follow-up + // with host-scoped observed evidence. + mocks.isPromptReceiptEligible.mockResolvedValue(true) + + await expect( + deliverLaunchPromptToAgentTab({ + tabId: 'tab-1', + agent: 'codex', + content: 'Fix failing checks', + submit: true, + forcePaste: true, + remoteLaunch: true + }) + ).resolves.toBe(true) + + expect(mocks.watchForPromptSubmitReceipt).not.toHaveBeenCalled() + }) }) }) diff --git a/src/renderer/src/lib/agent-launch-prompt-delivery.ts b/src/renderer/src/lib/agent-launch-prompt-delivery.ts index 4797c7f2c69..7a58ab530df 100644 --- a/src/renderer/src/lib/agent-launch-prompt-delivery.ts +++ b/src/renderer/src/lib/agent-launch-prompt-delivery.ts @@ -20,10 +20,16 @@ export async function deliverLaunchPromptToAgentTab(args: { content: string submit: boolean forcePaste: boolean + /** Why: receipt eligibility reads LOCAL evidence (local hook install state, + * locally-observed statuses), which says nothing about a remote host whose + * hooks may silently not fire — strict-gating there would false-fail + * delivered prompts. Remote launches keep the optimistic verdict until + * host-scoped evidence exists (follow-up). */ + remoteLaunch?: boolean timeoutMs?: number onTimeout?: (reason?: LaunchPromptDeliveryFailureReason) => void }): Promise { - const { tabId, agent, content, submit, forcePaste, timeoutMs, onTimeout } = args + const { tabId, agent, content, submit, forcePaste, remoteLaunch, timeoutMs, onTimeout } = args const shouldSeed = submit === true && content.trim().length > 0 && isNativeChatSupportedAgent(agent) @@ -49,7 +55,10 @@ export async function deliverLaunchPromptToAgentTab(args: { // delivery has no submitted paste, so it never waits on a receipt (which // would never arrive). const requireReceipt = - submit === true && !deliversViaNativePrefill && (await isPromptReceiptEligible(agent)) + submit === true && + remoteLaunch !== true && + !deliversViaNativePrefill && + (await isPromptReceiptEligible(agent)) const receiptWatch = requireReceipt ? watchForPromptSubmitReceipt({ tabId, agent, since: Date.now() }) : null diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.ts b/src/renderer/src/lib/launch-agent-in-new-tab.ts index 8cfef8cccdb..7b2b4ac408b 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.ts @@ -309,6 +309,7 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI agent, submit: submitPastedPrompt, forcePaste: forcePasteAfterLaunch, + remoteLaunch: isRemote || runtimeEnvironmentId != null, onTimeout: (reason) => { const state = useAppStore.getState() const tabsForWorktree = state.tabsByWorktree[worktreeId] ?? [] diff --git a/src/renderer/src/lib/launch-work-item-direct-agent.ts b/src/renderer/src/lib/launch-work-item-direct-agent.ts index 593afc5ae78..46f33e8f16f 100644 --- a/src/renderer/src/lib/launch-work-item-direct-agent.ts +++ b/src/renderer/src/lib/launch-work-item-direct-agent.ts @@ -142,14 +142,23 @@ export async function pasteDirectWorkItemDraftWhenAgentReady(args: { content: string submit?: boolean forcePaste?: boolean + remoteLaunch?: boolean }): Promise { - const { primaryTabId, startupPlan, content, submit = false, forcePaste = false } = args + const { + primaryTabId, + startupPlan, + content, + submit = false, + forcePaste = false, + remoteLaunch + } = args await deliverLaunchPromptToAgentTab({ tabId: primaryTabId, content, agent: startupPlan.agent, submit, forcePaste, + remoteLaunch, onTimeout: (reason) => { const label = submit ? 'prompt' : 'work item context' toast.message( diff --git a/src/renderer/src/lib/launch-work-item-direct.ts b/src/renderer/src/lib/launch-work-item-direct.ts index 61f49274edb..61b38ceb8dc 100644 --- a/src/renderer/src/lib/launch-work-item-direct.ts +++ b/src/renderer/src/lib/launch-work-item-direct.ts @@ -326,7 +326,8 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom startupPlan, content: draftContent, submit: promptDelivery === 'submit-after-ready', - forcePaste: promptDelivery === 'submit-after-ready' + forcePaste: promptDelivery === 'submit-after-ready', + remoteLaunch: typeof getConnectionId(worktreeId) === 'string' }) return true }