fix(native-chat): preserve coalesced launch recovery

This commit is contained in:
Merge Sim
2026-09-02 17:55:29 -07:00
parent 1db736ddb8
commit 7235574d90
14 changed files with 746 additions and 144 deletions
@@ -258,7 +258,7 @@ export async function submitFolderWorkspaceCreate({
await launch.launchResult
} catch (error) {
if (!(error instanceof StructuredAgentSessionCreateRefusalError)) {
return true
return !launch.isVisibilityUnknown()
}
await refusalFallback
}
@@ -236,25 +236,32 @@ export function useFullCreationExecution(input: FullCreationExecutionInput) {
...(structuredLaunch ? { providesInitialSurface: true } : {})
})
const { structuredLaunchAccepted, activation } = await settleFullCreationStructuredLaunch({
structuredLaunch,
agent: tuiAgent,
worktreeId: worktree.id,
prompt: startupPlan?.draftPrompt ?? submitStartupPrompt,
initialActivation,
onDefinitiveRefusal: async () => {
if (pendingFirstAgentMessageRename) {
await applyWorktreeMeta(worktree.id, { pendingFirstAgentMessageRename: true }).catch(
() => undefined
)
const { structuredLaunchAccepted, visibilityUnknown, activation } =
await settleFullCreationStructuredLaunch({
structuredLaunch,
agent: tuiAgent,
worktreeId: worktree.id,
prompt: startupPlan?.draftPrompt ?? submitStartupPrompt,
initialActivation,
onDefinitiveRefusal: async () => {
if (pendingFirstAgentMessageRename) {
await applyWorktreeMeta(worktree.id, { pendingFirstAgentMessageRename: true }).catch(
() => undefined
)
}
return activateAndRevealWorktree(worktree.id, {
sidebarRevealBehavior: 'auto',
createNewTerminalForStartup: true,
...(startup ? { startup } : {})
})
}
return activateAndRevealWorktree(worktree.id, {
sidebarRevealBehavior: 'auto',
createNewTerminalForStartup: true,
...(startup ? { startup } : {})
})
}
})
})
if (visibilityUnknown) {
setSidebarOpen(true)
onCreated?.()
return
}
if (!structuredLaunchAccepted && startupPlan) {
const optionScopeKey =
@@ -0,0 +1,79 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
startStructuredCodexLaunch: vi.fn(),
activateStructuredAgentSessionById: vi.fn()
}))
vi.mock('@/lib/structured-agent-session-launch', () => ({
startStructuredCodexLaunch: mocks.startStructuredCodexLaunch
}))
vi.mock('@/lib/structured-agent-session-tab-activation', () => ({
activateStructuredAgentSessionById: mocks.activateStructuredAgentSessionById
}))
vi.mock('@/lib/launch-structured-codex-session', () => ({
StructuredAgentSessionCreateRefusalError: class extends Error {}
}))
import { StructuredAgentSessionCreateRefusalError } from '@/lib/launch-structured-codex-session'
import { settleFullCreationStructuredLaunch } from './full-creation-structured-launch'
describe('settleFullCreationStructuredLaunch', () => {
beforeEach(() => vi.clearAllMocks())
it('runs the legacy terminal fallback after a definitive refusal', async () => {
const fallbackActivation = { primaryTabId: 'fallback-tab' }
const onDefinitiveRefusal = vi.fn().mockResolvedValue(fallbackActivation)
mocks.startStructuredCodexLaunch.mockReturnValue({
launchResult: Promise.reject(new StructuredAgentSessionCreateRefusalError('unsupported')),
isVisibilityUnknown: () => false,
claimDefinitiveRefusalFallback: (fallback: () => Promise<unknown>) =>
Promise.resolve()
.then(fallback)
.then(() => true)
})
await expect(
settleFullCreationStructuredLaunch({
structuredLaunch: true,
agent: 'codex',
worktreeId: 'worktree-1',
prompt: 'Fix the route',
initialActivation: false,
onDefinitiveRefusal
})
).resolves.toEqual({
structuredLaunchAccepted: false,
visibilityUnknown: false,
activation: fallbackActivation
})
expect(onDefinitiveRefusal).toHaveBeenCalledOnce()
})
it('reports an unknown outcome without starting a fallback terminal', async () => {
const onDefinitiveRefusal = vi.fn()
mocks.startStructuredCodexLaunch.mockReturnValue({
launchResult: Promise.reject(new Error('connection lost')),
isVisibilityUnknown: () => true,
claimDefinitiveRefusalFallback: vi.fn(() => Promise.resolve(false))
})
await expect(
settleFullCreationStructuredLaunch({
structuredLaunch: true,
agent: 'codex',
worktreeId: 'worktree-1',
prompt: 'Fix the route',
initialActivation: false,
onDefinitiveRefusal
})
).resolves.toEqual({
structuredLaunchAccepted: true,
visibilityUnknown: true,
activation: false
})
expect(onDefinitiveRefusal).not.toHaveBeenCalled()
})
})
@@ -13,11 +13,15 @@ export async function settleFullCreationStructuredLaunch(args: {
prompt: string
initialActivation: Activation
onDefinitiveRefusal: () => Activation | Promise<Activation>
}): Promise<{ structuredLaunchAccepted: boolean; activation: Activation }> {
}): Promise<{
structuredLaunchAccepted: boolean
visibilityUnknown: boolean
activation: Activation
}> {
let activation = args.initialActivation
let structuredLaunchAccepted = args.structuredLaunch
if (!args.structuredLaunch || args.agent !== 'codex') {
return { structuredLaunchAccepted, activation }
return { structuredLaunchAccepted, visibilityUnknown: false, activation }
}
const launch = startStructuredCodexLaunch(args.worktreeId, { prompt: args.prompt })
@@ -34,7 +38,9 @@ export async function settleFullCreationStructuredLaunch(args: {
} catch (error) {
if (error instanceof StructuredAgentSessionCreateRefusalError) {
await refusalFallback
} else if (launch.isVisibilityUnknown()) {
return { structuredLaunchAccepted, visibilityUnknown: true, activation }
}
}
return { structuredLaunchAccepted, activation }
return { structuredLaunchAccepted, visibilityUnknown: false, activation }
}
@@ -0,0 +1,82 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
startStructuredCodexLaunch: vi.fn(),
activateAndRevealWorktree: vi.fn(),
preflightAgentTrust: vi.fn()
}))
vi.mock('@/lib/structured-agent-session-launch', () => ({
startStructuredCodexLaunch: mocks.startStructuredCodexLaunch
}))
vi.mock('@/lib/worktree-activation', () => ({
activateAndRevealWorktree: mocks.activateAndRevealWorktree
}))
vi.mock('@/lib/agent-trust-preflight', () => ({
preflightAgentTrust: mocks.preflightAgentTrust
}))
vi.mock('@/lib/launch-structured-codex-session', () => ({
StructuredAgentSessionCreateRefusalError: class extends Error {}
}))
vi.mock('@/lib/native-chat-transcript-readability', () => ({
isNativeChatTranscriptLocalReadable: vi.fn(() => true)
}))
import { StructuredAgentSessionCreateRefusalError } from '@/lib/launch-structured-codex-session'
import { settleDirectWorkItemStructuredLaunch } from './launch-work-item-direct-agent-routing'
const baseArgs = {
structuredLaunch: true,
agent: 'codex' as const,
worktreeId: 'worktree-1',
workspacePath: '/repo/worktree',
connectionId: null,
draftContent: 'Fix the route',
promptDelivery: 'draft' as const,
primaryTabId: null,
startupPlan: null,
launchSource: 'task_page' as const
}
describe('settleDirectWorkItemStructuredLaunch', () => {
beforeEach(() => vi.clearAllMocks())
it('runs the legacy terminal fallback after a definitive refusal', async () => {
mocks.activateAndRevealWorktree.mockReturnValue({ primaryTabId: 'fallback-tab' })
mocks.startStructuredCodexLaunch.mockReturnValue({
launchResult: Promise.reject(new StructuredAgentSessionCreateRefusalError('unsupported')),
isVisibilityUnknown: () => false,
claimDefinitiveRefusalFallback: (fallback: () => Promise<unknown>) =>
Promise.resolve()
.then(fallback)
.then(() => true)
})
await expect(settleDirectWorkItemStructuredLaunch(baseArgs)).resolves.toEqual({
completed: false,
structuredLaunch: false,
visibilityUnknown: false,
primaryTabId: 'fallback-tab'
})
})
it('reports an unknown outcome without starting a fallback terminal', async () => {
mocks.startStructuredCodexLaunch.mockReturnValue({
launchResult: Promise.reject(new Error('connection lost')),
isVisibilityUnknown: () => true,
claimDefinitiveRefusalFallback: vi.fn(() => Promise.resolve(false))
})
await expect(settleDirectWorkItemStructuredLaunch(baseArgs)).resolves.toEqual({
completed: false,
structuredLaunch: true,
visibilityUnknown: true,
primaryTabId: null
})
expect(mocks.activateAndRevealWorktree).not.toHaveBeenCalled()
})
})
@@ -123,11 +123,12 @@ export async function settleDirectWorkItemStructuredLaunch(args: {
}): Promise<{
completed: boolean
structuredLaunch: boolean
visibilityUnknown: boolean
primaryTabId: string | null
}> {
let { structuredLaunch, primaryTabId } = args
if (!structuredLaunch || args.agent !== 'codex') {
return { completed: false, structuredLaunch, primaryTabId }
return { completed: false, structuredLaunch, visibilityUnknown: false, primaryTabId }
}
const launch = startStructuredCodexLaunch(args.worktreeId, {
@@ -155,12 +156,18 @@ export async function settleDirectWorkItemStructuredLaunch(args: {
})
try {
await launch.launchResult
return { completed: true, structuredLaunch, primaryTabId }
return { completed: true, structuredLaunch, visibilityUnknown: false, primaryTabId }
} catch (error) {
if (!(error instanceof StructuredAgentSessionCreateRefusalError)) {
return { completed: true, structuredLaunch, primaryTabId }
const visibilityUnknown = launch.isVisibilityUnknown()
return {
completed: !visibilityUnknown,
structuredLaunch,
visibilityUnknown,
primaryTabId
}
}
await refusalFallback
}
return { completed: false, structuredLaunch, primaryTabId }
return { completed: false, structuredLaunch, visibilityUnknown: false, primaryTabId }
}
@@ -263,6 +263,9 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom
startupPlan,
launchSource
})
if (structuredResult.visibilityUnknown) {
return false
}
if (structuredResult.completed) {
return true
}
@@ -0,0 +1,235 @@
import { StructuredAgentSessionCreateRefusalError } from '@/lib/launch-structured-codex-session'
import {
settleStructuredCodexLaunchPrompt,
type StructuredPromptDeliveryResult
} from '@/lib/structured-agent-session-launch-prompt'
import type { StructuredAgentSessionOutboxEntry } from '../../../shared/structured-agent-session-outbox'
export type StructuredRefusalFallback = () =>
| void
| StructuredPromptDeliveryResult
| Promise<void | StructuredPromptDeliveryResult>
export type StructuredCodexLaunchOptions = {
prompt?: string
promptDelivery?: 'auto-submit' | 'submit-after-ready'
onPromptDelivered?: () => void
}
export type StructuredLaunchCaller = {
promptDeliveryResult?: Promise<StructuredPromptDeliveryResult>
refusalFallback: {
callback: StructuredRefusalFallback | null
promise: Promise<boolean>
resolve: (ran: boolean) => void
reject: (error: unknown) => void
promptDeliveryPromise: Promise<StructuredPromptDeliveryResult | null>
resolvePromptDelivery: (result: StructuredPromptDeliveryResult | null) => void
started: boolean
settled: boolean
ran: boolean
}
}
export type StructuredLaunchCallerGroup = {
outcome: 'pending' | 'published' | 'failed' | 'refused' | 'unknown' | 'cancelled'
entries: Set<StructuredLaunchCaller>
promptDeliveryResults: Set<Promise<StructuredPromptDeliveryResult>>
refusalSettlement: {
promise: Promise<boolean>
resolve: (ran: boolean) => void
reject: (error: unknown) => void
settled: boolean
failure: { error: unknown } | null
}
onSettled: () => void
}
export function createStructuredLaunchCallerGroup(): StructuredLaunchCallerGroup {
const refusalSettlement = Promise.withResolvers<boolean>()
return {
outcome: 'pending',
entries: new Set(),
promptDeliveryResults: new Set(),
refusalSettlement: {
promise: refusalSettlement.promise,
resolve: refusalSettlement.resolve,
reject: refusalSettlement.reject,
settled: false,
failure: null
},
onSettled: () => {}
}
}
function settleCallerWithoutFallback(caller: StructuredLaunchCaller): void {
if (caller.refusalFallback.settled) {
return
}
caller.refusalFallback.settled = true
caller.refusalFallback.resolve(false)
caller.refusalFallback.resolvePromptDelivery(null)
}
function finalizeRefusalSettlement(group: StructuredLaunchCallerGroup): void {
if (
group.outcome !== 'refused' ||
group.refusalSettlement.settled ||
[...group.entries].some((caller) => !caller.refusalFallback.settled)
) {
return
}
group.refusalSettlement.settled = true
if (group.refusalSettlement.failure) {
group.refusalSettlement.reject(group.refusalSettlement.failure.error)
} else {
group.refusalSettlement.resolve([...group.entries].some((caller) => caller.refusalFallback.ran))
}
group.onSettled()
}
function runCallerRefusalFallback(
group: StructuredLaunchCallerGroup,
caller: StructuredLaunchCaller
): void {
if (caller.refusalFallback.started || caller.refusalFallback.settled) {
return
}
caller.refusalFallback.started = true
const fallback = caller.refusalFallback.callback
if (!fallback) {
settleCallerWithoutFallback(caller)
finalizeRefusalSettlement(group)
return
}
void Promise.resolve()
.then(fallback)
.then(
(result) => {
caller.refusalFallback.ran = true
caller.refusalFallback.resolve(true)
caller.refusalFallback.resolvePromptDelivery(result ?? null)
},
(error) => {
group.refusalSettlement.failure ??= { error }
caller.refusalFallback.reject(error)
caller.refusalFallback.resolvePromptDelivery(null)
}
)
.finally(() => {
caller.refusalFallback.settled = true
finalizeRefusalSettlement(group)
})
}
function trackPromptDelivery(
group: StructuredLaunchCallerGroup,
promptDeliveryResult: Promise<StructuredPromptDeliveryResult>
): void {
group.promptDeliveryResults.add(promptDeliveryResult)
const settled = (): void => {
group.promptDeliveryResults.delete(promptDeliveryResult)
group.onSettled()
}
void promptDeliveryResult.then(settled, settled)
}
export function addStructuredLaunchCaller(args: {
group: StructuredLaunchCallerGroup
launchResult: Promise<{ sessionId: string; fence: number }>
options: StructuredCodexLaunchOptions
stagedEntry: StructuredAgentSessionOutboxEntry | null
}): StructuredLaunchCaller {
const fallback = Promise.withResolvers<boolean>()
const fallbackPromptDelivery = Promise.withResolvers<StructuredPromptDeliveryResult | null>()
const caller: StructuredLaunchCaller = {
refusalFallback: {
callback: null,
promise: fallback.promise,
resolve: fallback.resolve,
reject: fallback.reject,
promptDeliveryPromise: fallbackPromptDelivery.promise,
resolvePromptDelivery: fallbackPromptDelivery.resolve,
started: false,
settled: false,
ran: false
}
}
args.group.entries.add(caller)
const promptDeliveryResult = settleStructuredCodexLaunchPrompt({
launchResult: args.launchResult,
options: args.options,
stagedEntry: args.stagedEntry
})
caller.promptDeliveryResult = promptDeliveryResult?.catch(async (error) => {
if (error instanceof StructuredAgentSessionCreateRefusalError) {
return (
(await caller.refusalFallback.promptDeliveryPromise) ?? {
delivered: false,
failureNotified: true
}
)
}
return { delivered: false, failureNotified: true }
})
if (caller.promptDeliveryResult) {
trackPromptDelivery(args.group, caller.promptDeliveryResult)
}
if (['published', 'failed', 'cancelled'].includes(args.group.outcome)) {
settleCallerWithoutFallback(caller)
} else if (args.group.outcome === 'refused') {
queueMicrotask(() => runCallerRefusalFallback(args.group, caller))
}
return caller
}
export function settleStructuredLaunchCallersWithoutFallback(
group: StructuredLaunchCallerGroup,
outcome: 'published' | 'failed' | 'cancelled'
): void {
group.outcome = outcome
for (const caller of group.entries) {
settleCallerWithoutFallback(caller)
}
if (!group.refusalSettlement.settled) {
group.refusalSettlement.settled = true
group.refusalSettlement.resolve(false)
}
group.onSettled()
}
export function settleStructuredLaunchCallersWithFallback(
group: StructuredLaunchCallerGroup
): void {
if (group.outcome === 'refused') {
return
}
group.outcome = 'refused'
for (const caller of group.entries) {
runCallerRefusalFallback(group, caller)
}
finalizeRefusalSettlement(group)
}
export function claimStructuredLaunchCallerFallback(
group: StructuredLaunchCallerGroup,
caller: StructuredLaunchCaller,
fallback: StructuredRefusalFallback
): Promise<boolean> {
caller.refusalFallback.callback ??= fallback
if (group.outcome === 'refused') {
runCallerRefusalFallback(group, caller)
}
return caller.refusalFallback.promise
}
export function structuredLaunchCallersHavePendingWork(
group: StructuredLaunchCallerGroup
): boolean {
return (
group.outcome === 'pending' ||
group.outcome === 'unknown' ||
group.promptDeliveryResults.size > 0 ||
(group.outcome === 'refused' && !group.refusalSettlement.settled)
)
}
@@ -196,6 +196,39 @@ describe('startStructuredCodexLaunch', () => {
)
})
it('keeps the launch reserved until every coalesced prompt delivery settles', async () => {
const worktreeId = 'wt-coalesced-prompt-reservation'
const intent = launchIntent(worktreeId)
let resolveLaunch!: (receipt: { sessionId: string; fence: number }) => void
let resolveDelivery!: (result: {
ok: true
value: { submission: { dispatchState: 'accepted' } }
}) => void
mocks.createIntent.mockReturnValue(intent)
mocks.launch.mockImplementationOnce(() => new Promise((resolve) => (resolveLaunch = resolve)))
vi.mocked(refreshLocalStructuredSessionTabs).mockResolvedValue([
publishedSnapshot(worktreeId, intent.sessionId)
])
mocks.callStructuredAgentSession.mockImplementationOnce(
() => new Promise((resolve) => (resolveDelivery = resolve))
)
startStructuredCodexLaunch(worktreeId)
const coalesced = startStructuredCodexLaunch(worktreeId, { prompt: 'second prompt' })
resolveLaunch({ sessionId: intent.sessionId, fence: 1 })
await vi.waitFor(() => expect(mocks.callStructuredAgentSession).toHaveBeenCalledOnce())
startStructuredCodexLaunch(worktreeId)
expect(mocks.createIntent).toHaveBeenCalledOnce()
expect(mocks.launch).toHaveBeenCalledOnce()
resolveDelivery({ ok: true, value: { submission: { dispatchState: 'accepted' } } })
await expect(coalesced.promptDeliveryResult).resolves.toEqual({
delivered: true,
failureNotified: false
})
})
it('keeps one launch identity per worktree while the outcome is unknown', async () => {
const worktreeId = 'wt-unknown-different-prompts'
const intent = launchIntent(worktreeId)
@@ -315,7 +348,7 @@ describe('startStructuredCodexLaunch', () => {
storageFailure.mockRestore()
})
it('discards every coalesced prompt when the launch is definitively refused', async () => {
it('runs each caller fallback and preserves its delivery result after refusal', async () => {
const worktreeId = 'wt-refused-coalesced-prompts'
const intent = launchIntent(worktreeId)
let rejectLaunch!: (error: unknown) => void
@@ -325,16 +358,36 @@ describe('startStructuredCodexLaunch', () => {
)
const first = startStructuredCodexLaunch(worktreeId, { prompt: 'first prompt' })
startStructuredCodexLaunch(worktreeId, { prompt: 'second prompt' })
const fallback = first.claimDefinitiveRefusalFallback(vi.fn())
const second = startStructuredCodexLaunch(worktreeId, { prompt: 'second prompt' })
const firstFallback = vi.fn().mockResolvedValue({
delivered: true,
failureNotified: false
})
const secondFallback = vi.fn().mockResolvedValue({
delivered: false,
failureNotified: true
})
const firstFallbackResult = first.claimDefinitiveRefusalFallback(firstFallback)
const secondFallbackResult = second.claimDefinitiveRefusalFallback(secondFallback)
expect(readOutbox(intent.sessionId)).toHaveLength(2)
rejectLaunch(new StructuredAgentSessionCreateRefusalError('unsupported'))
await expect(first.launchResult).rejects.toBeInstanceOf(
StructuredAgentSessionCreateRefusalError
)
await expect(fallback).resolves.toBe(true)
await expect(firstFallbackResult).resolves.toBe(true)
await expect(secondFallbackResult).resolves.toBe(true)
await expect(first.promptDeliveryResult).resolves.toEqual({
delivered: true,
failureNotified: false
})
await expect(second.promptDeliveryResult).resolves.toEqual({
delivered: false,
failureNotified: true
})
expect(firstFallback).toHaveBeenCalledOnce()
expect(secondFallback).toHaveBeenCalledOnce()
expect(readOutbox(intent.sessionId)).toEqual([])
})
@@ -16,47 +16,37 @@ import {
type StructuredCodexLaunchReceipt,
type StructuredLaunchRecoveryState
} from '@/lib/structured-agent-session-launch-recovery'
import type { StructuredPromptDeliveryResult } from '@/lib/structured-agent-session-launch-prompt'
import {
settleStructuredCodexLaunchPrompt,
type StructuredPromptDeliveryResult
} from '@/lib/structured-agent-session-launch-prompt'
addStructuredLaunchCaller,
claimStructuredLaunchCallerFallback,
createStructuredLaunchCallerGroup,
settleStructuredLaunchCallersWithFallback,
settleStructuredLaunchCallersWithoutFallback,
structuredLaunchCallersHavePendingWork,
type StructuredCodexLaunchOptions,
type StructuredLaunchCaller,
type StructuredLaunchCallerGroup,
type StructuredRefusalFallback
} from '@/lib/structured-agent-session-launch-callers'
export type { StructuredCodexLaunchReceipt }
type StructuredRefusalFallback = () =>
| void
| StructuredPromptDeliveryResult
| Promise<void | StructuredPromptDeliveryResult>
export type { StructuredCodexLaunchOptions, StructuredCodexLaunchReceipt }
type StructuredLaunchState = StructuredLaunchRecoveryState & {
identity: string
promptDeliveryResult?: Promise<StructuredPromptDeliveryResult>
refusalFallback: {
callback: StructuredRefusalFallback | null
promise: Promise<boolean>
resolve: (ran: boolean) => void
reject: (error: unknown) => void
promptDeliveryPromise: Promise<StructuredPromptDeliveryResult | null>
resolvePromptDelivery: (result: StructuredPromptDeliveryResult | null) => void
started: boolean
}
callers: StructuredLaunchCallerGroup
}
type StructuredLaunchStateResult = {
state: StructuredLaunchState
promptDeliveryResult?: Promise<StructuredPromptDeliveryResult>
}
export type StructuredCodexLaunchOptions = {
prompt?: string
promptDelivery?: 'auto-submit' | 'submit-after-ready'
onPromptDelivered?: () => void
caller: StructuredLaunchCaller
}
export type StructuredCodexLaunchResult = {
sessionId: string
launchResult: Promise<StructuredCodexLaunchReceipt>
promptDeliveryResult?: Promise<StructuredPromptDeliveryResult>
isVisibilityUnknown: () => boolean
claimDefinitiveRefusalFallback: (fallback: StructuredRefusalFallback) => Promise<boolean>
}
@@ -72,33 +62,20 @@ function cleanupLaunchState(state: StructuredLaunchState): void {
}
}
function maybeCleanupLaunchState(state: StructuredLaunchState): void {
if (structuredLaunchCallersHavePendingWork(state.callers)) {
return
}
cleanupLaunchState(state)
}
function settleDefinitiveRefusalFallback(state: StructuredLaunchState): void {
if (state.refusalFallback.started) {
if (state.callers.outcome === 'refused') {
return
}
state.refusalFallback.started = true
abandonStructuredAgentSessionLaunchIntent(state.intent)
discardStructuredAgentSessionLaunchOutbox(state.intent.sessionId)
const fallback = state.refusalFallback.callback
if (!fallback) {
state.refusalFallback.resolve(false)
state.refusalFallback.resolvePromptDelivery(null)
cleanupLaunchState(state)
return
}
void Promise.resolve()
.then(fallback)
.then(
(result) => {
state.refusalFallback.resolve(true)
state.refusalFallback.resolvePromptDelivery(result ?? null)
},
(error) => {
state.refusalFallback.reject(error)
state.refusalFallback.resolvePromptDelivery(null)
}
)
.finally(() => cleanupLaunchState(state))
settleStructuredLaunchCallersWithFallback(state.callers)
}
function trackLaunchSettlement(
@@ -110,12 +87,8 @@ function trackLaunchSettlement(
if (state.promise !== promise) {
return
}
state.refusalFallback.resolve(false)
if (state.promptDeliveryResult) {
void state.promptDeliveryResult.finally(() => cleanupLaunchState(state))
} else {
cleanupLaunchState(state)
}
settleStructuredLaunchCallersWithoutFallback(state.callers, 'published')
maybeCleanupLaunchState(state)
},
(error) => {
if (state.promise !== promise || state.cancelled) {
@@ -124,8 +97,10 @@ function trackLaunchSettlement(
if (error instanceof StructuredAgentSessionCreateRefusalError) {
settleDefinitiveRefusalFallback(state)
} else if (!state.visibilityUnknown) {
state.refusalFallback.resolve(false)
cleanupLaunchState(state)
settleStructuredLaunchCallersWithoutFallback(state.callers, 'failed')
maybeCleanupLaunchState(state)
} else {
state.callers.outcome = 'unknown'
}
}
)
@@ -138,7 +113,7 @@ function trackLaunchFailureToast(state: StructuredLaunchState): void {
}
if (
error instanceof StructuredAgentSessionCreateRefusalError &&
(await state.refusalFallback.promise.catch(() => false))
(await state.callers.refusalSettlement.promise.catch(() => false))
) {
return
}
@@ -160,61 +135,42 @@ function structuredCodexLaunchState(
const existing = pendingStructuredLaunchesByIdentity.get(identity)
if (existing) {
if (existing.visibilityUnknown) {
existing.callers.outcome = 'pending'
existing.promise = reconcileUnknownLaunch(existing)
trackLaunchSettlement(existing, existing.promise)
trackLaunchFailureToast(existing)
}
const text = options.prompt?.trim() ?? ''
const stagedPrompt = text
? enqueueStructuredAgentSessionLaunchPrompt(existing.intent.sessionId, text)
: null
const promptDeliveryResult = settleStructuredCodexLaunchPrompt({
launchResult: existing.promise,
options,
stagedEntry: stagedPrompt
})
const recoveredPromptDeliveryResult = promptDeliveryResult?.catch(async (error) => {
if (error instanceof StructuredAgentSessionCreateRefusalError) {
return (
(await existing.refusalFallback.promptDeliveryPromise) ?? {
delivered: false,
failureNotified: true
}
)
}
return { delivered: false, failureNotified: true }
})
const stagedPrompt =
text && existing.callers.outcome !== 'refused'
? enqueueStructuredAgentSessionLaunchPrompt(existing.intent.sessionId, text)
: null
return {
state: existing,
...(recoveredPromptDeliveryResult
? { promptDeliveryResult: recoveredPromptDeliveryResult }
: {})
caller: addStructuredLaunchCaller({
group: existing.callers,
launchResult: existing.promise,
options,
stagedEntry: stagedPrompt
})
}
}
const fallback = Promise.withResolvers<boolean>()
const fallbackPromptDelivery = Promise.withResolvers<StructuredPromptDeliveryResult | null>()
const intent = createStructuredCodexSessionLaunchIntent(worktreeId)
const text = options.prompt?.trim() ?? ''
const stagedPrompt = text
? enqueueStructuredAgentSessionLaunchPrompt(intent.sessionId, text)
: null
const callers = createStructuredLaunchCallerGroup()
const state: StructuredLaunchState = {
identity,
intent,
promise: Promise.resolve({ sessionId: '', fence: 0 }),
visibilityUnknown: false,
cancelled: false,
refusalFallback: {
callback: null,
promise: fallback.promise,
resolve: fallback.resolve,
reject: fallback.reject,
promptDeliveryPromise: fallbackPromptDelivery.promise,
resolvePromptDelivery: fallbackPromptDelivery.resolve,
started: false
}
callers
}
callers.onSettled = () => maybeCleanupLaunchState(state)
state.promise =
text && !stagedPrompt
? Promise.reject(
@@ -223,28 +179,18 @@ function structuredCodexLaunchState(
)
)
: launchAndReconcile(state)
const promptDeliveryResult = settleStructuredCodexLaunchPrompt({
const caller = addStructuredLaunchCaller({
group: state.callers,
launchResult: state.promise,
options,
stagedEntry: stagedPrompt
})
state.promptDeliveryResult = promptDeliveryResult?.catch(async (error) => {
if (error instanceof StructuredAgentSessionCreateRefusalError) {
return (
(await state.refusalFallback.promptDeliveryPromise) ?? {
delivered: false,
failureNotified: true
}
)
}
return { delivered: false, failureNotified: true }
})
pendingStructuredLaunchesByIdentity.set(identity, state)
trackLaunchSettlement(state, state.promise)
trackLaunchFailureToast(state)
return {
state,
...(state.promptDeliveryResult ? { promptDeliveryResult: state.promptDeliveryResult } : {})
caller
}
}
@@ -257,6 +203,7 @@ export function cancelStructuredCodexLaunch(worktreeId: string, sessionId: strin
return false
}
state.cancelled = true
settleStructuredLaunchCallersWithoutFallback(state.callers, 'cancelled')
cleanupLaunchState(state)
discardStructuredAgentSessionLaunchOutbox(state.intent.sessionId)
abandonStructuredAgentSessionLaunchIntent(state.intent)
@@ -267,14 +214,13 @@ export function startStructuredCodexLaunch(
worktreeId: string,
options: StructuredCodexLaunchOptions = {}
): StructuredCodexLaunchResult {
const { state, promptDeliveryResult } = structuredCodexLaunchState(worktreeId, options)
const { state, caller } = structuredCodexLaunchState(worktreeId, options)
return {
sessionId: state.intent.sessionId,
launchResult: state.promise,
...(promptDeliveryResult ? { promptDeliveryResult } : {}),
claimDefinitiveRefusalFallback: (fallback) => {
state.refusalFallback.callback ??= fallback
return state.refusalFallback.promise
}
...(caller.promptDeliveryResult ? { promptDeliveryResult: caller.promptDeliveryResult } : {}),
isVisibilityUnknown: () => state.visibilityUnknown,
claimDefinitiveRefusalFallback: (fallback) =>
claimStructuredLaunchCallerFallback(state.callers, caller, fallback)
}
}
@@ -1,4 +1,5 @@
import { toast } from 'sonner'
import { translate } from '@/i18n/i18n'
import { useAppStore } from '@/store'
import { preflightAgentTrust as preflightWorkspaceAgentTrust } from '@/lib/agent-trust-preflight'
import { activateAndRevealWorktree, type ActivateAndRevealResult } from '@/lib/worktree-activation'
@@ -225,6 +226,16 @@ export async function executeWorktreeCreation(
if (structuredSession.cancelled) {
return
}
if (structuredSession.visibilityUnknown) {
useAppStore.getState().updatePendingWorktreeCreation(creationId, {
status: 'error',
error: translate(
'auto.lib.worktree.creation.flow.structured.launch.unknown',
'Could not confirm whether Codex chat opened. Retry to check again.'
)
})
return
}
}
// Why: clearing synchronously right after activation lets React commit the
@@ -81,6 +81,7 @@ describe('launchStructuredWorktreeSession', () => {
mocks.startStructuredCodexLaunch.mockReturnValue({
sessionId: 'session-1',
launchResult,
isVisibilityUnknown: () => false,
claimDefinitiveRefusalFallback: vi.fn(() => Promise.resolve(false))
})
@@ -111,6 +112,7 @@ describe('launchStructuredWorktreeSession', () => {
await expect(resultPromise).resolves.toEqual({
accepted: true,
cancelled: true,
visibilityUnknown: false,
activation: false,
primaryTabId: null
})
@@ -124,4 +126,44 @@ describe('launchStructuredWorktreeSession', () => {
expect(mocks.activateStructuredAgentSessionById).not.toHaveBeenCalled()
expect(mocks.unsubscribe).toHaveBeenCalledOnce()
})
it('reports an unknown launch without claiming a visible surface', async () => {
mocks.startStructuredCodexLaunch.mockReturnValue({
sessionId: 'session-unknown',
launchResult: Promise.reject(new Error('connection lost')),
isVisibilityUnknown: () => true,
claimDefinitiveRefusalFallback: vi.fn(() => Promise.resolve(false))
})
await expect(
launchStructuredWorktreeSession({
creationId: 'creation-1',
request: {
repoId: 'repo-1',
name: 'routing-recovery',
setupDecision: 'run',
agent: 'codex',
pendingFirstAgentMessageRename: false,
note: '',
startupPlan: null,
quickPrompt: 'Fix the route',
quickTelemetry: null
},
worktreeId: 'worktree-1',
shouldActivateOnCompletion: true,
fallbackStartupOpt: undefined,
activation: false,
primaryTabId: null
})
).resolves.toEqual({
accepted: true,
cancelled: false,
visibilityUnknown: true,
activation: false,
primaryTabId: null
})
expect(mocks.activateStructuredAgentSessionById).not.toHaveBeenCalled()
expect(mocks.unsubscribe).toHaveBeenCalledOnce()
})
})
@@ -17,6 +17,7 @@ import { toRuntimeWorktreeSelector } from '@/runtime/runtime-worktree-selector'
export type WorktreeCreationStructuredSessionResult = {
accepted: boolean
cancelled: boolean
visibilityUnknown: boolean
activation: ActivateAndRevealResult | false
primaryTabId: string | null
}
@@ -45,11 +46,12 @@ export async function launchStructuredWorktreeSession(args: {
}): Promise<WorktreeCreationStructuredSessionResult> {
let { activation, primaryTabId } = args
let accepted = true
let visibilityUnknown = false
if (args.request.agent !== 'codex') {
return { accepted, cancelled: false, activation, primaryTabId }
return { accepted, cancelled: false, visibilityUnknown, activation, primaryTabId }
}
if (!useAppStore.getState().pendingWorktreeCreations[args.creationId]) {
return { accepted, cancelled: true, activation, primaryTabId }
return { accepted, cancelled: true, visibilityUnknown, activation, primaryTabId }
}
const launch = startStructuredCodexLaunch(args.worktreeId, {
@@ -127,7 +129,7 @@ export async function launchStructuredWorktreeSession(args: {
const receipt = await launch.launchResult
if (cancelled) {
await retireCancelledStructuredSession(args.worktreeId, launch.sessionId)
return { accepted, cancelled, activation, primaryTabId }
return { accepted, cancelled, visibilityUnknown, activation, primaryTabId }
}
if (args.shouldActivateOnCompletion) {
activateStructuredAgentSessionById({
@@ -138,13 +140,15 @@ export async function launchStructuredWorktreeSession(args: {
} catch (error) {
if (cancelled) {
await retireCancelledStructuredSession(args.worktreeId, launch.sessionId)
return { accepted, cancelled, activation, primaryTabId }
return { accepted, cancelled, visibilityUnknown, activation, primaryTabId }
}
if (error instanceof StructuredAgentSessionCreateRefusalError) {
await refusalFallback
} else {
visibilityUnknown = launch.isVisibilityUnknown()
}
} finally {
unsubscribe()
}
return { accepted, cancelled, activation, primaryTabId }
return { accepted, cancelled, visibilityUnknown, activation, primaryTabId }
}
@@ -0,0 +1,127 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { PendingWorktreeCreation, WorktreeCreationRequest } from './pending-worktree-creation'
const mocks = vi.hoisted(() => ({
activateAndRevealWorktree: vi.fn(),
ensureWorktreeHasInitialTerminal: vi.fn(),
launchStructuredWorktreeSession: vi.fn()
}))
const request: WorktreeCreationRequest = {
repoId: 'repo-1',
name: 'routing-recovery',
setupDecision: 'run',
agent: 'codex',
agentLaunchRoute: 'structured-native-chat',
pendingFirstAgentMessageRename: false,
note: '',
startupPlan: null,
quickPrompt: 'Recover the route',
quickTelemetry: null
}
const store = {
activeView: 'terminal',
activePendingCreationId: 'creation-1' as string | null,
pendingWorktreeCreations: {} as Record<string, PendingWorktreeCreation>,
repos: [],
createWorktree: vi.fn(),
updatePendingWorktreeCreation: vi.fn(),
removePendingWorktreeCreation: vi.fn(),
updateWorktreeMeta: vi.fn()
}
vi.mock('@/store', () => ({
useAppStore: { getState: () => store }
}))
vi.mock('@/lib/worktree-activation', () => ({
activateAndRevealWorktree: mocks.activateAndRevealWorktree
}))
vi.mock('@/lib/worktree-initial-terminal-seeding', () => ({
ensureWorktreeHasInitialTerminal: mocks.ensureWorktreeHasInitialTerminal
}))
vi.mock('@/lib/new-workspace', () => ({
ensureAgentStartupInTerminal: vi.fn()
}))
vi.mock('@/lib/workspace-activation-terminal-focus', () => ({
queueWorkspaceActivationTerminalFocus: vi.fn()
}))
vi.mock('@/lib/ephemeral-vm-worktree-creation', () => ({
prepareRequestForCreate: vi.fn(async () => request),
attachEphemeralVmRuntimeToWorkspace: vi.fn(),
cleanupEphemeralVmRuntimeForFailedCreate: vi.fn()
}))
vi.mock('@/lib/provisioned-root-create-options', () => ({
getProvisionedRootCreateOptions: vi.fn()
}))
vi.mock('@/lib/worktree-creation-agent-seeds', () => ({
seedAgentTabStateAfterWorktreeCreate: vi.fn()
}))
vi.mock('@/lib/worktree-draft-startup-view-mode', () => ({
resolveBackendDraftStartup: vi.fn()
}))
vi.mock('@/lib/worktree-creation-flow-startup', () => ({
buildWorktreeCreationStartupOpt: vi.fn()
}))
vi.mock('@/lib/worktree-creation-structured-session', () => ({
launchStructuredWorktreeSession: mocks.launchStructuredWorktreeSession
}))
vi.mock('sonner', () => ({
toast: { error: vi.fn() }
}))
vi.mock('@/i18n/i18n', () => ({
translate: (_key: string, fallback: string) => fallback
}))
import { executeWorktreeCreation } from './worktree-creation-flow-execute'
describe('structured worktree creation unknown outcome', () => {
beforeEach(() => {
vi.clearAllMocks()
store.pendingWorktreeCreations = {
'creation-1': {
creationId: 'creation-1',
phase: 'creating',
status: 'creating',
startedAt: 1,
indeterminate: false,
loaderVisible: true,
request
}
}
store.createWorktree.mockResolvedValue({
worktree: { id: 'worktree-1', repoId: 'repo-1' }
})
mocks.activateAndRevealWorktree.mockReturnValue(false)
mocks.launchStructuredWorktreeSession.mockResolvedValue({
accepted: true,
cancelled: false,
visibilityUnknown: true,
activation: false,
primaryTabId: null
})
})
it('keeps the operation on its retry surface instead of reporting completion', async () => {
await executeWorktreeCreation('creation-1', request)
expect(store.updatePendingWorktreeCreation).toHaveBeenCalledWith('creation-1', {
status: 'error',
error: 'Could not confirm whether Codex chat opened. Retry to check again.'
})
expect(store.removePendingWorktreeCreation).not.toHaveBeenCalled()
expect(mocks.ensureWorktreeHasInitialTerminal).not.toHaveBeenCalled()
})
})