fix(agents): treat runtime-hosted direct launches as remote for receipt gating

launch-work-item-direct classified a launch as remote only when
getConnectionId returned an SSH id, so a direct work-item launch on a
local-backed repo while a runtime environment is focused (its worktree
resolves to activeRuntimeEnvironmentId) was strict-gated on a local hook
receipt even though the agent runs off-host. Its prompt-submit hook may
never surface a local receipt, re-exposing the F1 false-receipt-timeout
the remote guard was meant to close.

Mirror launch-agent-in-new-tab: OR in a non-null runtime environment so
SSH and runtime-hosted launches both keep the optimistic verdict. Local
launches with no focused runtime still gate.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
brennanb2025
2026-07-09 01:14:07 -07:00
co-authored by Orca
parent d6a4303e9d
commit 84440c1757
2 changed files with 100 additions and 1 deletions
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AppState } from '@/store'
import type * as TuiAgentSelectionModule from '../../../shared/tui-agent-selection'
import type * as TuiAgentStartupModule from '@/lib/tui-agent-startup'
import type * as LaunchWorkItemDirectAgentModule from '@/lib/launch-work-item-direct-agent'
const mocks = vi.hoisted(() => ({
toastError: vi.fn(),
@@ -57,6 +58,19 @@ vi.mock('@/lib/connection-context', () => ({
getConnectionId: mocks.getConnectionId
}))
// Why: wrap the real paste helper in a spy so existing paste assertions keep
// exercising the real delivery path while we can still inspect the threaded
// `remoteLaunch` value the direct flow computes.
vi.mock('@/lib/launch-work-item-direct-agent', async () => {
const actual = await vi.importActual<typeof LaunchWorkItemDirectAgentModule>(
'@/lib/launch-work-item-direct-agent'
)
return {
...actual,
pasteDirectWorkItemDraftWhenAgentReady: vi.fn(actual.pasteDirectWorkItemDraftWhenAgentReady)
}
})
vi.mock('@/runtime/runtime-hooks-client', () => ({
checkRuntimeHooks: vi
.fn()
@@ -115,6 +129,7 @@ vi.mock('../../../shared/tui-agent-selection', async () => {
})
import { launchWorkItemDirect } from './launch-work-item-direct'
import { pasteDirectWorkItemDraftWhenAgentReady } from '@/lib/launch-work-item-direct-agent'
import { pasteDraftWhenAgentReady } from '@/lib/agent-paste-draft'
import { buildAgentDraftLaunchPlan, buildAgentStartupPlan } from '@/lib/tui-agent-startup'
import { pickTuiAgent } from '../../../shared/tui-agent-selection'
@@ -711,4 +726,82 @@ describe('launchWorkItemDirect', () => {
})
)
})
it('threads remoteLaunch=true for a runtime-hosted launch on a local-backed repo', async () => {
// Why: a focused runtime environment runs the agent off the local host, so
// its prompt-submit hook may never surface a local receipt. The direct flow
// must keep those optimistic (like launch-agent-in-new-tab) even though
// getConnectionId is null for the local-backed repo (finding F1).
mocks.getConnectionId.mockReturnValue(null)
mocks.store.settings = {
defaultTuiAgent: 'codex',
disabledTuiAgents: [],
agentCmdOverrides: {},
activeRuntimeEnvironmentId: 'runtime-web-1'
}
vi.mocked(buildAgentStartupPlan).mockReturnValueOnce({
agent: 'codex',
launchCommand: 'codex',
expectedProcess: 'codex',
followupPrompt: null,
launchConfig: { agentArgs: '', agentEnv: {} }
})
const { launchWorkItemDirect } = await import('./launch-work-item-direct')
await expect(
launchWorkItemDirect({
item: {
title: 'Fix failing checks',
url: 'https://github.com/acme/repo/pull/1',
type: 'issue',
number: 1,
pasteContent: 'Fix the failing checks.'
},
repoId: 'repo-1',
openModalFallback: mocks.openModalFallback,
launchSource: 'task_page',
agentOverride: 'codex',
promptDelivery: 'submit-after-ready'
})
).resolves.toBe(true)
expect(vi.mocked(pasteDirectWorkItemDraftWhenAgentReady)).toHaveBeenCalledWith(
expect.objectContaining({ submit: true, remoteLaunch: true })
)
})
it('threads remoteLaunch=false for a purely local launch (receipt gate applies)', async () => {
// Why: no SSH connection and no focused runtime — the agent runs locally, so
// the receipt gate must stay active and remoteLaunch must be false.
mocks.getConnectionId.mockReturnValue(null)
vi.mocked(buildAgentStartupPlan).mockReturnValueOnce({
agent: 'codex',
launchCommand: 'codex',
expectedProcess: 'codex',
followupPrompt: null,
launchConfig: { agentArgs: '', agentEnv: {} }
})
const { launchWorkItemDirect } = await import('./launch-work-item-direct')
await expect(
launchWorkItemDirect({
item: {
title: 'Fix failing checks',
url: 'https://github.com/acme/repo/pull/1',
type: 'issue',
number: 1,
pasteContent: 'Fix the failing checks.'
},
repoId: 'repo-1',
openModalFallback: mocks.openModalFallback,
launchSource: 'task_page',
agentOverride: 'codex',
promptDelivery: 'submit-after-ready'
})
).resolves.toBe(true)
expect(vi.mocked(pasteDirectWorkItemDraftWhenAgentReady)).toHaveBeenCalledWith(
expect.objectContaining({ submit: true, remoteLaunch: false })
)
})
})
@@ -14,6 +14,7 @@ import {
} from '@/lib/launch-work-item-direct-messages'
import { ensureHooksConfirmed } from '@/lib/ensure-hooks-confirmed'
import { getConnectionId } from '@/lib/connection-context'
import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner'
import type { GitPushTarget, SetupDecision, TuiAgent } from '../../../shared/types'
import { getLinearIssueWorkspaceName } from '../../../shared/workspace-name'
import { resolveGitHubWorkItemIdentity } from '@/lib/github-work-item-identity'
@@ -327,7 +328,12 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom
content: draftContent,
submit: promptDelivery === 'submit-after-ready',
forcePaste: promptDelivery === 'submit-after-ready',
remoteLaunch: typeof getConnectionId(worktreeId) === 'string'
// Why: SSH or a focused runtime environment runs the agent off the local
// host, where its prompt-submit hook may not surface a local receipt; keep
// those on the optimistic verdict like launch-agent-in-new-tab (F1).
remoteLaunch:
typeof getConnectionId(worktreeId) === 'string' ||
getRuntimeEnvironmentIdForWorktree(useAppStore.getState(), worktreeId) != null
})
return true
}