Revert "fix(terminal): launch prior/default agent when a woken terminal can’t resume" (#7524)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong 2026-07-06 00:27:32 -07:00 committed by GitHub
parent 3dd3cc9f68
commit d9103d08c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 29 additions and 419 deletions

View File

@ -137,7 +137,7 @@ type StoreState = {
| { kind: 'windows-host' }
| { kind: 'wsl'; distro: string }
}[]
sshConnectionStates: Map<string, { status: string; remotePlatform?: NodeJS.Platform }>
sshConnectionStates: Map<string, { status: string }>
cacheTimerByKey: Record<string, number | null>
settings: {
theme?: 'system' | 'dark' | 'light'
@ -5620,285 +5620,6 @@ describe('connectPanePty', () => {
expect(mockStoreState.clearSleepingAgentSession).toHaveBeenCalledWith(paneKey)
})
// #4557: an agent terminal that can't be resumed should come back as a fresh
// agent (prior agent, else default) instead of a blank shell.
const findReattachConnect = (transport: ReturnType<typeof createMockTransport>) => {
const call = transport.connect.mock.calls.find(
(args) => (args[0] as { sessionId?: string })?.sessionId === 'lost-pty'
)
return call?.[0] as
| {
command?: string
launchAgent?: string
launchConfig?: {
agentCommand?: string
agentArgs?: string
agentEnv?: Record<string, string>
}
env?: Record<string, string>
}
| undefined
}
const mountColdRestore = async (
transport: ReturnType<typeof createMockTransport>,
depsOverrides: Record<string, unknown> = {}
) => {
const { connectPanePty } = await import('./pty-connection')
transport.connect.mockImplementation(async ({ sessionId }: { sessionId?: string }) => {
if (sessionId) {
return { id: 'fresh-pty', coldRestore: { scrollback: 'cold-payload', cwd: '/tmp/wt-1' } }
}
return 'fresh-pty'
})
transportFactoryQueue.push(transport)
const pane = createPane(1)
const manager = createManager(1)
const deps = createDeps({
restoredLeafId: LEAF_1,
restoredPtyIdByLeafId: { [LEAF_1]: 'lost-pty' },
...depsOverrides
})
connectPanePty(pane as never, manager as never, deps as never)
await flushAsyncTicks(20)
await new Promise((resolve) => setTimeout(resolve, 70))
return deps
}
it('cold-restores a fresh agent from the sleeping record when its session is unresumable', async () => {
const transport = createMockTransport('fresh-pty')
const paneKey = makePaneKey('tab-1', LEAF_1)
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: 'lost-pty' }] },
settings: { ...mockStoreState.settings, agentCmdOverrides: {} },
agentStatusByPaneKey: {},
sleepingAgentSessionsByPaneKey: {
// Agent known, but no resumable provider session id.
[paneKey]: {
paneKey,
tabId: 'tab-1',
worktreeId: 'wt-1',
agent: 'codex',
prompt: 'finish the task',
state: 'working',
capturedAt: 1,
updatedAt: 1
}
}
} as StoreState
await mountColdRestore(transport)
const reattach = findReattachConnect(transport)
expect(reattach?.command).toMatch(/^codex /)
expect(reattach?.command).not.toContain('resume')
expect(reattach?.launchAgent).toBe('codex')
expect(reattach?.env?.ORCA_AGENT_LAUNCH_TOKEN).toMatch(new RegExp(`^${UUID_RE}$`))
})
it('cold-restores a fresh agent with the sleeping record launch config when resume is unavailable', async () => {
const transport = createMockTransport('fresh-pty')
const paneKey = makePaneKey('tab-1', LEAF_1)
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: 'lost-pty' }] },
settings: {
...mockStoreState.settings,
agentCmdOverrides: { codex: 'current-codex' },
agentDefaultArgs: { codex: '--current-args' }
},
agentStatusByPaneKey: {},
sleepingAgentSessionsByPaneKey: {
[paneKey]: {
paneKey,
tabId: 'tab-1',
worktreeId: 'wt-1',
agent: 'codex',
prompt: 'finish the task',
state: 'working',
capturedAt: 1,
updatedAt: 1,
launchConfig: {
agentCommand: 'sleeping-codex --stored-args',
agentArgs: '--stored-args',
agentEnv: { CODEX_HOME: '/tmp/orca-stored-codex' }
}
}
}
} as StoreState
await mountColdRestore(transport)
const reattach = findReattachConnect(transport)
expect(reattach?.command).toBe('sleeping-codex --stored-args')
expect(reattach?.launchAgent).toBe('codex')
expect(reattach?.launchConfig).toMatchObject({
agentCommand: 'sleeping-codex --stored-args',
agentArgs: '--stored-args',
agentEnv: { CODEX_HOME: '/tmp/orca-stored-codex' }
})
expect(reattach?.env).toMatchObject({
CODEX_HOME: '/tmp/orca-stored-codex',
ORCA_AGENT_LAUNCH_TOKEN: expect.stringMatching(new RegExp(`^${UUID_RE}$`))
})
})
it('uses the known SSH remote platform when fresh-launching an unresumable agent', async () => {
const transport = createMockTransport('fresh-pty')
mockStoreState = {
...mockStoreState,
tabsByWorktree: {
'wt-1': [{ id: 'tab-1', ptyId: 'lost-pty', launchAgent: 'claude-agent-teams' }]
},
repos: [{ id: 'repo1', connectionId: 'ssh-win' }],
sshConnectionStates: new Map([['ssh-win', { status: 'connected', remotePlatform: 'win32' }]]),
settings: { ...mockStoreState.settings, agentCmdOverrides: {} },
agentStatusByPaneKey: {},
sleepingAgentSessionsByPaneKey: {}
} as StoreState
await mountColdRestore(transport)
const reattach = findReattachConnect(transport)
expect(reattach?.command).toMatch(/^orca\.cmd claude-teams/)
expect(reattach?.command).not.toContain('orca-ide')
expect(reattach?.launchAgent).toBe('claude-agent-teams')
})
it('cold-restores the prior agent from the persisted tab launchAgent after a restart', async () => {
const transport = createMockTransport('fresh-pty')
mockStoreState = {
...mockStoreState,
// After a restart there is no live status nor sleeping record — only the
// persisted tab.launchAgent tells us this terminal ran an agent.
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: 'lost-pty', launchAgent: 'claude' }] },
settings: { ...mockStoreState.settings, agentCmdOverrides: {} },
agentStatusByPaneKey: {},
sleepingAgentSessionsByPaneKey: {}
} as StoreState
await mountColdRestore(transport)
const reattach = findReattachConnect(transport)
expect(reattach?.command).toContain('claude')
expect(reattach?.command).not.toContain('resume')
expect(reattach?.launchAgent).toBe('claude')
})
it('cold-restores the default agent when an agent terminal has no recoverable agent', async () => {
const transport = createMockTransport('fresh-pty')
const paneKey = makePaneKey('tab-1', LEAF_1)
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: 'lost-pty' }] },
settings: { ...mockStoreState.settings, agentCmdOverrides: {}, defaultTuiAgent: 'codex' },
agentStatusByPaneKey: {},
// A record exists (it was an agent terminal) but carries no usable agent id.
sleepingAgentSessionsByPaneKey: {
[paneKey]: { paneKey, tabId: 'tab-1', worktreeId: 'wt-1', capturedAt: 1, updatedAt: 1 }
}
} as StoreState
await mountColdRestore(transport)
const reattach = findReattachConnect(transport)
expect(reattach?.command).toMatch(/^codex /)
expect(reattach?.command).not.toContain('resume')
expect(reattach?.launchAgent).toBe('codex')
})
it('does not cold-restore a fresh agent from only a completed live status row', async () => {
const transport = createMockTransport('fresh-pty')
const paneKey = makePaneKey('tab-1', LEAF_1)
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: 'lost-pty' }] },
settings: { ...mockStoreState.settings, agentCmdOverrides: {}, defaultTuiAgent: 'codex' },
agentStatusByPaneKey: {
[paneKey]: {
agentType: 'codex',
paneKey,
prompt: 'done task',
state: 'done',
stateHistory: [],
stateStartedAt: 1,
updatedAt: 1
}
},
sleepingAgentSessionsByPaneKey: {}
} as StoreState
await mountColdRestore(transport)
const reattach = findReattachConnect(transport)
expect(reattach?.command).toBeUndefined()
expect(reattach?.launchAgent).toBeUndefined()
})
it('ignores malformed sleeping launch config when a live resumable status exists', async () => {
const transport = createMockTransport('fresh-pty')
const paneKey = makePaneKey('tab-1', LEAF_1)
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: 'lost-pty' }] },
settings: { ...mockStoreState.settings, agentCmdOverrides: {} },
agentStatusByPaneKey: {
[paneKey]: {
agentType: 'codex',
paneKey,
prompt: 'live task',
providerSession: { key: 'session_id', id: 'live-session' },
state: 'working',
stateHistory: [],
stateStartedAt: 1,
updatedAt: 1
}
},
sleepingAgentSessionsByPaneKey: {
[paneKey]: {
paneKey,
tabId: 'tab-1',
worktreeId: 'wt-1',
agent: 'codex',
prompt: 'malformed sleeping task',
state: 'working',
capturedAt: 1,
updatedAt: 1,
launchConfig: {
agentCommand: 'malformed-sleeping-codex',
agentArgs: '',
agentEnv: {}
}
}
}
} as StoreState
await mountColdRestore(transport)
const reattach = findReattachConnect(transport)
expect(reattach?.command).toContain("'resume' 'live-session'")
expect(reattach?.command).not.toBe('malformed-sleeping-codex')
expect(reattach?.launchAgent).toBe('codex')
})
it('leaves a plain shell blank on cold restore (no prior agent, no default)', async () => {
const transport = createMockTransport('fresh-pty')
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: 'lost-pty' }] },
settings: { ...mockStoreState.settings, agentCmdOverrides: {} },
agentStatusByPaneKey: {},
sleepingAgentSessionsByPaneKey: {}
} as StoreState
await mountColdRestore(transport)
const reattach = findReattachConnect(transport)
expect(reattach?.command).toBeUndefined()
expect(reattach?.launchAgent).toBeUndefined()
})
it('resumes from an unambiguous legacy sleeping record when cold-restoring a preserved pane', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('fresh-pty')

View File

@ -149,17 +149,16 @@ import {
getRuntimeEnvironmentIdForWorktree
} from '@/lib/worktree-runtime-owner'
import { CLIENT_PLATFORM } from '@/lib/new-workspace'
import { buildAgentResumeStartupPlan, buildAgentStartupPlan } from '@/lib/tui-agent-startup'
import { buildAgentResumeStartupPlan } from '@/lib/tui-agent-startup'
import { resolveAgentStatusTerminalTitle } from '@/lib/agent-status-terminal-title'
import {
resolveTuiAgentLaunchArgs,
resolveTuiAgentLaunchEnv
} from '../../../../shared/tui-agent-launch-defaults'
import { isTuiAgent } from '../../../../shared/tui-agent-config'
import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection'
import {
isResumableTuiAgent,
normalizeAgentProviderSession,
type ResumableTuiAgent,
type SleepingAgentSessionRecord
} from '../../../../shared/agent-session-resume'
import {
@ -373,9 +372,7 @@ type FreshSpawnOptions = {
}
type ColdRestoreAgentResumeStartup = PendingStartupCommand & {
// TuiAgent (not just ResumableTuiAgent): the fresh-launch fallback can spawn any
// agent the terminal previously ran, including ones without resume support.
agent: TuiAgent
agent: ResumableTuiAgent
launchConfig: NonNullable<ReturnType<typeof buildAgentResumeStartupPlan>>['launchConfig']
launchToken: string
useLiveEntry: boolean
@ -1129,34 +1126,32 @@ export function connectPanePty(
)
}
)
const legacyProviderSessionKeys = legacyMatches.map(([, record]) => {
const providerSession = normalizeAgentProviderSession(record.providerSession)
return providerSession
? [record.worktreeId, record.agent, providerSession.key, providerSession.id].join('\0')
: null
})
const exactLegacyMatch = legacyMatches.find(([paneKey]) => {
const legacy = parseLegacyNumericPaneKey(paneKey)
return legacy?.numericPaneId === String(pane.id)
})
const providerSessionKeys = new Set(
legacyProviderSessionKeys.filter((key): key is string => key !== null)
legacyMatches.map(([, record]) =>
[
record.worktreeId,
record.agent,
record.providerSession.key,
record.providerSession.id
].join('\0')
)
)
const oldestLegacyMatch = legacyMatches
.slice()
.sort(([, a], [, b]) => a.capturedAt - b.capturedAt || a.updatedAt - b.updatedAt)[0]
// Why: duplicate legacy aliases can point at one provider session; consume
// the oldest capture as canonical and clear its aliases after resume.
const allLegacyMatchesHaveProviderSession = legacyProviderSessionKeys.every(
(key) => key !== null
)
const selectedLegacyMatch =
exactLegacyMatch ??
(legacyMatches.length === 1
? legacyMatches[0]
: allLegacyMatchesHaveProviderSession && providerSessionKeys.size === 1
? oldestLegacyMatch
: null)
(providerSessionKeys.size === 1
? legacyMatches.length === 1
? legacyMatches[0]
: oldestLegacyMatch
: null)
if (!selectedLegacyMatch) {
return null
}
@ -1168,19 +1163,13 @@ export function connectPanePty(
consumed: { paneKey: string; record: SleepingAgentSessionRecord }
): void => {
state.clearSleepingAgentSession(consumed.paneKey)
const consumedSession = normalizeAgentProviderSession(consumed.record.providerSession)
// No provider session (e.g. a fresh-launch fallback record) means there is no
// shared session to alias, so there is nothing else to clear.
if (!consumedSession) {
return
}
for (const [paneKey, record] of Object.entries(state.sleepingAgentSessionsByPaneKey)) {
if (
paneKey !== consumed.paneKey &&
record.worktreeId === consumed.record.worktreeId &&
record.agent === consumed.record.agent &&
record.providerSession?.key === consumedSession.key &&
record.providerSession?.id === consumedSession.id
record.providerSession.key === consumed.record.providerSession.key &&
record.providerSession.id === consumed.record.providerSession.id
) {
// Why: legacy pane aliases can leave multiple sleeping rows for one
// provider session; once this pane resumes it, every alias is stale.
@ -3495,10 +3484,6 @@ export function connectPanePty(
if (projectRuntime?.status === 'resolved' && projectRuntime.runtime.kind === 'wsl') {
return 'linux'
}
const sshRemotePlatform = getTerminalPasteSshRemotePlatform(connectionId)
if (sshRemotePlatform) {
return sshRemotePlatform
}
if (connectionId || (worktree?.path && isWslUncPath(worktree.path))) {
return 'linux'
}
@ -3512,123 +3497,27 @@ export function connectPanePty(
const entry = state.agentStatusByPaneKey[cacheKey]
const sleepingRecordEntry = getSleepingRecordForPane(state)
const sleepingRecord = sleepingRecordEntry?.record
const liveEntry = entry && entry.state !== 'done' ? entry : null
const useLiveEntry = Boolean(liveEntry)
// When the provider session can't be resumed, an agent terminal should still
// come back as its agent (or the default), not a blank shell (#4557). A plain
// shell - no agent ever ran here - stays blank (buildFreshFallback returns null).
const buildFreshFallback = (): ColdRestoreAgentResumeStartup | null => {
const tab = (state.tabsByWorktree[deps.worktreeId] ?? []).find(
(candidate) => candidate.id === deps.tabId
)
const liveAgent =
liveEntry && isTuiAgent(liveEntry.agentType) ? liveEntry.agentType : undefined
const priorAgent =
liveAgent ?? sleepingRecord?.agent ?? tab?.launchAgent ?? paneStartup?.launchAgent
const defaultPref = state.settings?.defaultTuiAgent
const defaultAgent =
defaultPref &&
defaultPref !== 'blank' &&
isTuiAgentEnabled(defaultPref, state.settings?.disabledTuiAgents)
? defaultPref
: undefined
// Fall back to the default agent only when this was an active/sleeping
// agent terminal whose specific agent is unrecoverable - never for a
// genuine plain shell or a stale completed live row.
const fallbackAgent = priorAgent ?? (liveEntry || sleepingRecord ? defaultAgent : undefined)
if (!fallbackAgent) {
return null
}
const liveLaunchConfig =
liveEntry && liveAgent === fallbackAgent
? state.getAgentLaunchConfigForStatusEntry(liveEntry)
: undefined
const fallbackLaunchConfig =
liveLaunchConfig ??
(sleepingRecord?.agent === fallbackAgent ? sleepingRecord.launchConfig : undefined) ??
(paneStartup?.launchAgent === fallbackAgent ? paneStartup.launchConfig : undefined)
if (fallbackLaunchConfig?.agentCommand?.trim()) {
const freshLaunchToken = createBrowserUuid()
return {
agent: fallbackAgent,
command: fallbackLaunchConfig.agentCommand.trim(),
env: {
...fallbackLaunchConfig.agentEnv,
ORCA_AGENT_LAUNCH_TOKEN: freshLaunchToken
},
launchConfig: {
agentCommand: fallbackLaunchConfig.agentCommand.trim(),
agentArgs: fallbackLaunchConfig.agentArgs,
agentEnv: { ...fallbackLaunchConfig.agentEnv }
},
launchToken: freshLaunchToken,
useLiveEntry,
// A fresh launch is a new session, not a restored one, so no
// "restored" banner; the stale sleeping record is still cleared
// after the spawn.
hasSleepingRecord: false,
sleepingRecordEntry
}
}
const freshPlan = buildAgentStartupPlan({
agent: fallbackAgent,
prompt: '',
cmdOverrides: state.settings?.agentCmdOverrides ?? {},
agentArgs:
fallbackLaunchConfig !== undefined
? fallbackLaunchConfig.agentArgs
: resolveTuiAgentLaunchArgs(fallbackAgent, state.settings?.agentDefaultArgs),
agentEnv:
fallbackLaunchConfig !== undefined
? fallbackLaunchConfig.agentEnv
: resolveTuiAgentLaunchEnv(fallbackAgent, state.settings?.agentDefaultEnv),
platform: getColdRestoreAgentResumePlatform(),
allowEmptyPromptLaunch: true
})
if (!freshPlan) {
return null
}
const freshLaunchToken = createBrowserUuid()
return {
agent: fallbackAgent,
command: freshPlan.launchCommand,
env: {
...freshPlan.env,
ORCA_AGENT_LAUNCH_TOKEN: freshLaunchToken
},
launchConfig: freshPlan.launchConfig,
launchToken: freshLaunchToken,
useLiveEntry,
// A fresh launch is a new session, not a restored one, so no
// "restored" banner; the stale sleeping record is still cleared after
// the spawn.
hasSleepingRecord: false,
sleepingRecordEntry
}
}
const agent = liveEntry ? liveEntry.agentType : sleepingRecord?.agent
const useLiveEntry = entry && entry.state !== 'done'
const agent = useLiveEntry ? entry.agentType : sleepingRecord?.agent
if (!agent || !isResumableTuiAgent(agent)) {
return buildFreshFallback()
return null
}
const providerSession = normalizeAgentProviderSession(
liveEntry ? liveEntry.providerSession : sleepingRecord?.providerSession
useLiveEntry ? entry.providerSession : sleepingRecord?.providerSession
)
if (!providerSession) {
return buildFreshFallback()
return null
}
const sleepingProviderSession = normalizeAgentProviderSession(sleepingRecord?.providerSession)
const matchingSleepingLaunchConfig =
sleepingRecord?.launchConfig &&
(!useLiveEntry ||
(sleepingRecord.agent === agent &&
sleepingProviderSession?.key === providerSession.key &&
sleepingProviderSession?.id === providerSession.id))
sleepingRecord.providerSession.key === providerSession.key &&
sleepingRecord.providerSession.id === providerSession.id))
? sleepingRecord.launchConfig
: undefined
const launchConfig =
(liveEntry ? state.getAgentLaunchConfigForStatusEntry(liveEntry) : undefined) ??
(useLiveEntry && entry ? state.getAgentLaunchConfigForStatusEntry(entry) : undefined) ??
matchingSleepingLaunchConfig
const resumePlatform = getColdRestoreAgentResumePlatform()
const startupPlan = buildAgentResumeStartupPlan({
@ -3647,7 +3536,7 @@ export function connectPanePty(
platform: resumePlatform
})
if (!startupPlan) {
return buildFreshFallback()
return null
}
const coldRestoreLaunchToken = createBrowserUuid()
// Why: cold restore means the PTY process is gone but the agent provider

View File

@ -94,7 +94,7 @@ function fileNameFromUrl(url: string): string | null {
}
try {
const parsed = new URL(url)
const last = parsed.pathname.split('/').filter(Boolean).pop()
const last = parsed.pathname.split('/').findLast(Boolean)
return last ? decodeURIComponent(last) : null
} catch {
return null