fix(remote): a paired client reopening a slept agent tab resumes its conversation (#29272)

* fix(remote): opening a slept agent pane from a paired client resumes its session

When a paired client opened a pane whose PTY was gone, the host started a
bare shell (or a fresh agent) instead of resuming the agent session it had
recorded for that pane. Resume from the pane-owned record the way the host
window's cold restore does.

* fix(remote): label a resumed pane with the agent it resumes

* fix(remote): do not resume a conversation another pane also records

* fix(remote): send a pane resume through restore admission

* fix(remote): do not resume a conversation already live in another pane

* fix(remote): block a pane resume while another holder is not proven exited

* fix(remote): do not resume a record the pane's hook identity has superseded

* fix(remote): an unhydrated session holder blocks a pane resume

* fix(remote): compare full provider-session identity in the resume guards
This commit is contained in:
Neil
2026-10-15 19:10:56 -07:00
committed by GitHub
parent dcdf3860e1
commit 9d1cd6a7cf
5 changed files with 457 additions and 16 deletions
@@ -50,18 +50,26 @@ describe('OrcaRuntimeService', () => {
)
}
function makeParkedRuntime(runtimeStore: unknown): {
function makeParkedRuntime(
runtimeStore: unknown,
host: {
deps?: ConstructorParameters<typeof OrcaRuntimeService>[2]
livePtyIds?: string[]
} = {}
): {
runtime: OrcaRuntimeService
spawn: ReturnType<typeof vi.fn>
} {
const spawn = vi.fn().mockResolvedValue({ id: 'persisted-pty' })
const runtime = new OrcaRuntimeService(runtimeStore as never)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store implements the runtime store methods these tests reach.
const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, host.deps)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null,
listProcesses: async () => []
listProcesses: async () =>
(host.livePtyIds ?? []).map((id) => ({ id, cwd: '/tmp', title: 'Claude' }))
})
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
return { runtime, spawn }
@@ -90,6 +98,21 @@ describe('OrcaRuntimeService', () => {
runtime.markGraphReady(TEST_WINDOW_ID)
}
function firstSpawnArgs(spawn: ReturnType<typeof vi.fn>): {
command?: string
sessionId?: string
} {
const args: unknown = spawn.mock.calls[0]?.[0]
if (typeof args !== 'object' || args === null) {
return {}
}
const command =
'command' in args && typeof args.command === 'string' ? args.command : undefined
const sessionId =
'sessionId' in args && typeof args.sessionId === 'string' ? args.sessionId : undefined
return { command, sessionId }
}
const userActivate = (
runtime: OrcaRuntimeService,
leafId?: string
@@ -190,6 +213,214 @@ describe('OrcaRuntimeService', () => {
expect(activated.tabs[0]).toMatchObject({ status: 'ready' })
})
// #27147 / #27777: the tap used to start a bare shell (or a fresh agent), which lost the
// conversation the host recorded for this pane. The host window resumes it; so must this.
it('resumes the slept agent session when a paired client opens the pane', async () => {
const { runtimeStore } = makeParkedSessionStore('worktree-sleep', {
launchConfig: { agentArgs: '--dangerously-skip-permissions', agentEnv: {} }
})
const { runtime, spawn } = makeParkedRuntime(runtimeStore)
setParkedRuntimeNotifier(runtime, vi.fn())
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the runtime reads only isDestroyed from the window mock.
electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false } as never)
await runtime.activateManagedWorktree(`id:${TEST_WORKTREE_ID}`, {
notifyClients: false,
clientKind: 'runtime'
})
const activated = await userActivate(runtime)
expect(spawn).toHaveBeenCalledOnce()
const spawnArgs = firstSpawnArgs(spawn)
expect(spawnArgs.sessionId).toBe('persisted-pty')
expect(spawnArgs.command).toContain('--resume')
expect(spawnArgs.command).toContain('provider-session-1')
expect(spawnArgs.command).toContain('--dangerously-skip-permissions')
expect(activated.tabs[0]).toMatchObject({ status: 'ready' })
})
it('resumes from a live anchor when the pane was lost while its agent ran', async () => {
const { runtimeStore } = makeParkedSessionStore('live')
const { runtime, spawn } = makeParkedRuntime(runtimeStore)
await userActivate(runtime)
expect(firstSpawnArgs(spawn).command).toContain('provider-session-1')
})
// Why: Codex resume routing keys off the launch agent, so it must name the resumed agent.
it('labels the spawn with the recorded agent, not the agent the tab first launched', async () => {
const { runtimeStore, getSession, setSession } = makeParkedSessionStore('worktree-sleep', {
agent: 'codex',
providerSession: { key: 'session_id', id: 'codex-session-1' }
})
const session = structuredClone(getSession())
const tab = session.tabsByWorktree[TEST_WORKTREE_ID]?.[0]
if (tab) {
tab.launchAgent = 'claude'
}
setSession(session)
const { runtime, spawn } = makeParkedRuntime(runtimeStore)
await userActivate(runtime)
expect(firstSpawnArgs(spawn).command).toContain('codex-session-1')
expect(spawn.mock.calls[0]?.[0]).toMatchObject({ launchAgent: 'codex' })
})
it('does not resume a session another pane also records', async () => {
const { runtimeStore, getSession, setSession } = makeParkedSessionStore('worktree-sleep')
const session = structuredClone(getSession())
const records = session.sleepingAgentSessionsByPaneKey ?? {}
const own = records[`host-tab:${HEADLESS_LEAF_ID}`]
if (own) {
records[`host-tab:${HEADLESS_SECOND_LEAF_ID}`] = {
...own,
paneKey: `host-tab:${HEADLESS_SECOND_LEAF_ID}`
}
}
setSession(session)
const { runtime, spawn } = makeParkedRuntime(runtimeStore)
await userActivate(runtime)
expect(spawn).toHaveBeenCalledOnce()
expect(firstSpawnArgs(spawn).command ?? '').not.toContain('provider-session-1')
})
// Why: a resume is restore work; paused or rate-limited restore admission must see it.
it('declares a reconnect resume as automatic restore and a tap as a user resume', async () => {
const automatic = makeParkedRuntime(makeParkedSessionStore('live').runtimeStore)
await automaticActivate(automatic.runtime)
expect(automatic.spawn.mock.calls[0]?.[0]).toMatchObject({
spawnIntent: 'automatic-restore'
})
const tap = makeParkedRuntime(makeParkedSessionStore('live').runtimeStore)
await userActivate(tap.runtime)
expect(tap.spawn.mock.calls[0]?.[0]).toMatchObject({ spawnIntent: 'user-resume' })
})
function makeRuntimeWithOtherPaneHolder(livePtyIds: string[]) {
const { runtimeStore } = makeParkedSessionStore('worktree-sleep')
const parked = makeParkedRuntime(runtimeStore, {
livePtyIds,
deps: {
getAgentProviderSessionSnapshot: () => [
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resume guard reads only paneKey and providerSession.
{
paneKey: `other-tab:${HEADLESS_SECOND_LEAF_ID}`,
providerSession: { key: 'session_id', id: 'provider-session-1' }
} as never
]
}
})
parked.runtime.registerPty('other-pty', TEST_WORKTREE_ID, null, {
tabId: 'other-tab',
leafId: HEADLESS_SECOND_LEAF_ID
})
return parked
}
it('does not resume a session the hook store places in another live pane', async () => {
const { runtime, spawn } = makeRuntimeWithOtherPaneHolder(['other-pty'])
await userActivate(runtime)
expect(firstSpawnArgs(spawn).command ?? '').not.toContain('provider-session-1')
})
// Why: losing sight of the holder (a dropped SSH relay) is not evidence it stopped writing.
it('does not resume while the other pane holding the session is unverifiable', async () => {
const { runtime, spawn } = makeRuntimeWithOtherPaneHolder([])
await userActivate(runtime)
expect(firstSpawnArgs(spawn).command ?? '').not.toContain('provider-session-1')
})
it('does not resume while the other holder has no host terminal record yet', async () => {
const { runtimeStore } = makeParkedSessionStore('worktree-sleep')
const { runtime, spawn } = makeParkedRuntime(runtimeStore, {
deps: {
getAgentProviderSessionSnapshot: () => [
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resume guard reads only paneKey and providerSession.
{
paneKey: `other-tab:${HEADLESS_SECOND_LEAF_ID}`,
providerSession: { key: 'session_id', id: 'provider-session-1' }
} as never
]
}
})
await userActivate(runtime)
expect(firstSpawnArgs(spawn).command ?? '').not.toContain('provider-session-1')
})
it('resumes once the other pane holding the session has proven exited', async () => {
const { runtime, spawn } = makeRuntimeWithOtherPaneHolder([])
runtime.onPtyExit('other-pty', 0)
await userActivate(runtime)
expect(firstSpawnArgs(spawn).command).toContain('provider-session-1')
})
it('does not resume a record older than the pane conversation the hook store names', async () => {
const { runtimeStore } = makeParkedSessionStore('worktree-sleep')
const { runtime, spawn } = makeParkedRuntime(runtimeStore, {
deps: {
getAgentProviderSessionRowsForPane: (paneKey) =>
paneKey === `host-tab:${HEADLESS_LEAF_ID}`
? [
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resume guard reads only providerSession.
{ paneKey, providerSession: { key: 'session_id', id: 'newer-session' } } as never
]
: []
}
})
await userActivate(runtime)
expect(spawn).toHaveBeenCalledOnce()
expect(firstSpawnArgs(spawn).command ?? '').not.toContain('provider-session-1')
})
// Why: Pi identities are id plus transcript; a shared id alone does not name one conversation.
it('does not treat another Pi transcript with the same id as this pane session', async () => {
const piSession = { key: 'session_id', id: 'pi-1', transcriptPath: '/t/a.jsonl' } as const
const { runtimeStore } = makeParkedSessionStore('worktree-sleep', {
agent: 'pi',
providerSession: piSession
})
const { runtime, spawn } = makeParkedRuntime(runtimeStore, {
deps: {
getAgentProviderSessionRowsForPane: (paneKey) => [
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resume guard reads only providerSession.
{ paneKey, providerSession: { ...piSession, transcriptPath: '/t/b.jsonl' } } as never
]
}
})
await userActivate(runtime)
expect(firstSpawnArgs(spawn).command ?? '').not.toContain('/t/a.jsonl')
})
// Why: a session id names a transcript on the machine that captured it.
it('does not resume a record captured on another execution host', async () => {
const { runtimeStore } = makeParkedSessionStore('worktree-sleep', {
connectionId: 'ssh-elsewhere'
})
const { runtime, spawn } = makeParkedRuntime(runtimeStore)
await userActivate(runtime)
expect(spawn).toHaveBeenCalledOnce()
expect(firstSpawnArgs(spawn).command ?? '').not.toContain('provider-session-1')
})
// Why: manual sleep of a finished agent stamps restoreOnTabOpenOnly, which the
// background wake skips and resume classifies pane-owned, so the record survives.
it('materializes a slept pane whose completed-agent record is restore-on-tab-open-only', async () => {
@@ -14,6 +14,14 @@ import {
projectClientSessionTabSelection
} from '../../client-session-tab-selection'
import { makePaneKey } from '../../../../shared/stable-pane-id'
import {
agentProviderSessionsEqual,
type SleepingAgentSessionRecord
} from '../../../../shared/agent-session-resume'
import type {
MobileTerminalResumeCommand,
OrcaRuntimeWithResolveMobileSessionTerminalCommand as MobileTerminalStartup
} from '../../terminal/launch/mobile-terminal-startup'
import { runtimeWorktreeIdsEqual } from '../../runtime-worktree-path-identity'
import {
buildHeadlessMobileSessionTabGroups,
@@ -24,6 +32,9 @@ export abstract class OrcaRuntimeWithApplyMobileSessionTabNavigation extends Orc
protected abstract emitMobileSessionTabsSnapshotToClient(
...args: Parameters<MobileSessionPublication['emitMobileSessionTabsSnapshotToClient']>
): ReturnType<MobileSessionPublication['emitMobileSessionTabsSnapshotToClient']>
protected abstract resolveMobileSessionResumeCommand(
...args: Parameters<MobileTerminalStartup['resolveMobileSessionResumeCommand']>
): ReturnType<MobileTerminalStartup['resolveMobileSessionResumeCommand']>
protected abstract isHeadlessMobileSessionPublication(
...args: Parameters<MobileSessionPreservation['isHeadlessMobileSessionPublication']>
): ReturnType<MobileSessionPreservation['isHeadlessMobileSessionPublication']>
@@ -115,16 +126,112 @@ export abstract class OrcaRuntimeWithApplyMobileSessionTabNavigation extends Orc
worktreeId: string,
tab: RuntimeMobileSessionTerminalTab
): boolean {
// Why: 'live'/'quit' captures describe a pane that was still running, so a reconnect
// must still mint its replacement PTY (#11542). Only a worktree-owned capture records
// a deliberate takedown the user did not ask to undo.
return this.getPaneSleepingAgentRecord(worktreeId, tab)?.origin === 'worktree-sleep'
}
/**
* The record the host window's cold restore would resume this pane from, if the pane owns it.
* Why: a running agent's worktree-sleep record is relaunched in a fresh tab by the window's
* worktree activation, so resuming it in the pane as well would run one transcript twice.
*/
protected getPaneOwnedResumeRecord(
worktreeId: string,
tab: RuntimeMobileSessionTerminalTab
): SleepingAgentSessionRecord | null {
const record = this.getPaneSleepingAgentRecord(worktreeId, tab)
if (!record) {
return null
}
const paneOwned =
record.origin === 'live' || record.origin === 'quit' || record.state === 'done'
return paneOwned ? record : null
}
/**
* The pane's resume launch: undefined when it owns no record, `{}` (plain shell, record kept,
* as the window does) when the record cannot be resumed here.
*/
protected async resolvePaneOwnedResumeStartup(
worktreeId: string,
tab: RuntimeMobileSessionTerminalTab
): Promise<MobileTerminalResumeCommand | Record<string, never> | undefined> {
const record = this.getPaneOwnedResumeRecord(worktreeId, tab)
if (!record) {
return undefined
}
// Why: nothing here spends the record, so a session another pane also records could be
// resumed twice (two writers on one transcript), as could one already live in another pane.
const records = this.getWorkspaceSessionForWorktree(worktreeId)?.sleepingAgentSessionsByPaneKey
const sharedWithAnotherPane = Object.values(records ?? {}).some(
(other) =>
other !== record &&
other.agent === record.agent &&
agentProviderSessionsEqual(record.agent, other.providerSession, record.providerSession)
)
if (
sharedWithAnotherPane ||
this.isProviderSessionLiveInAnotherPane(record) ||
this.hookStoreNamesAnotherSessionForPane(record)
) {
return {}
}
try {
const workspace = await this.resolveTerminalWorkspaceLaunchScope(`id:${worktreeId}`)
return this.resolveMobileSessionResumeCommand(workspace, record) ?? {}
} catch {
return {}
}
}
/** The host's hook store places this conversation in another pane not proven exited. */
private isProviderSessionLiveInAnotherPane(record: SleepingAgentSessionRecord): boolean {
const otherPaneKeys = new Set(
(this.getAgentProviderSessionSnapshotFn?.() ?? [])
.filter(
(row) =>
row.paneKey !== record.paneKey &&
agentProviderSessionsEqual(record.agent, row.providerSession, record.providerSession)
)
.map((row) => row.paneKey)
)
if (otherPaneKeys.size === 0) {
return false
}
// Why: only a proven exit clears a holder; an unverifiable PTY (dropped SSH relay) or one not
// yet hydrated after a restart may still be writing the transcript.
const provenExitedPaneKeys = new Set<string>()
for (const pty of this.ptysById.values()) {
if (pty.paneKey && otherPaneKeys.has(pty.paneKey)) {
if (!this.isPtyKnownExited(pty.ptyId)) {
return true
}
provenExitedPaneKeys.add(pty.paneKey)
}
}
return [...otherPaneKeys].some((paneKey) => !provenExitedPaneKeys.has(paneKey))
}
/** The pane moved on to another conversation after the record was saved; the hook store is authoritative. */
private hookStoreNamesAnotherSessionForPane(record: SleepingAgentSessionRecord): boolean {
return (this.getAgentProviderSessionRowsForPaneFn?.(record.paneKey) ?? []).some(
(row) =>
row.providerSession !== undefined &&
!agentProviderSessionsEqual(record.agent, row.providerSession, record.providerSession)
)
}
private getPaneSleepingAgentRecord(
worktreeId: string,
tab: RuntimeMobileSessionTerminalTab
): SleepingAgentSessionRecord | null {
const record =
this.getWorkspaceSessionForWorktree(worktreeId)?.sleepingAgentSessionsByPaneKey?.[
makePaneKey(tab.parentTabId, tab.leafId)
]
// Why: 'live'/'quit' captures describe a pane that was still running, so a reconnect
// must still mint its replacement PTY (#11542). Only a worktree-owned capture records
// a deliberate takedown the user did not ask to undo.
return (
record?.origin === 'worktree-sleep' && runtimeWorktreeIdsEqual(record.worktreeId, worktreeId)
)
return record && runtimeWorktreeIdsEqual(record.worktreeId, worktreeId) ? record : null
}
protected shouldMaterializeHeadlessMobileSessionTab(
@@ -1,7 +1,10 @@
import type { RuntimeWorkspaceSelectors as WorkspaceSelectors } from '../../workspaces/workspace-selectors'
import type { OrcaRuntimeWithApplyMobileSessionTabNavigation as MobileSessionNavigation } from './apply-mobile-session-tab-navigation'
import type { OrcaRuntimeWithStopTerminalsForWorktree as WorktreeTerminalStop } from '../../terminal/stop/worktree-terminal-stop'
import type { MobileTerminalCommand } from '../../terminal/launch/mobile-terminal-startup'
import type {
MobileTerminalCommand,
MobileTerminalResumeCommand
} from '../../terminal/launch/mobile-terminal-startup'
import { OrcaRuntimeWithBuildHeadlessMobileSessionBrowserTabs } from './build-headless-mobile-session-browser-tabs'
import type { PtyControllerInventory } from '../../runtime-pty-controller-contract'
import { isAgentLaunchRunningIn } from '../../../agent-launch/agent-launch-pane-attachment'
@@ -32,6 +35,9 @@ export abstract class OrcaRuntimeWithPerformMobileSessionPtyRecordsRefresh exten
protected abstract isDeliberatelyParkedPane(
...args: Parameters<MobileSessionNavigation['isDeliberatelyParkedPane']>
): ReturnType<MobileSessionNavigation['isDeliberatelyParkedPane']>
protected abstract resolvePaneOwnedResumeStartup(
...args: Parameters<MobileSessionNavigation['resolvePaneOwnedResumeStartup']>
): ReturnType<MobileSessionNavigation['resolvePaneOwnedResumeStartup']>
protected abstract isWorktreeTerminalSleepHeld(
...args: Parameters<WorktreeTerminalStop['isWorktreeTerminalSleepHeld']>
): ReturnType<WorktreeTerminalStop['isWorktreeTerminalSleepHeld']>
@@ -199,8 +205,11 @@ export abstract class OrcaRuntimeWithPerformMobileSessionPtyRecordsRefresh exten
// bare materialize would put a plain shell under the agent icon.
// Re-resolve the launch like the create path; providers skip startup
// commands when attaching to live sessions, so this cannot double-launch.
let agentStartup: MobileTerminalCommand = {}
if (tab.launchAgent) {
// Why: an agent already ran here and the host recorded its session; resume it rather than
// start a bare shell or a fresh agent (#27147, #27777).
const resumeStartup = await this.resolvePaneOwnedResumeStartup(worktreeId, tab)
let agentStartup: MobileTerminalCommand | MobileTerminalResumeCommand = resumeStartup ?? {}
if (resumeStartup === undefined && tab.launchAgent) {
let workspace
try {
workspace = await this.resolveTerminalWorkspaceLaunchScope(`id:${worktreeId}`)
@@ -228,7 +237,14 @@ export abstract class OrcaRuntimeWithPerformMobileSessionPtyRecordsRefresh exten
env: agentStartup.env,
startupCommandDelivery: agentStartup.startupCommandDelivery,
launchConfig: agentStartup.launchConfig,
launchAgent: tab.launchAgent,
launchAgent: agentStartup.launchAgent ?? tab.launchAgent,
// Why: a resume is restore work, so the host's restore admission must see it.
...('resumeProviderSession' in agentStartup
? {
resumeProviderSession: agentStartup.resumeProviderSession,
spawnIntent: automatic ? 'automatic-restore' : 'user-resume'
}
: {}),
launchRuntime: agentStartup.launchRuntime,
targetGroupId,
...(automatic ? { refuseSleptWorktree: true } : {})
@@ -1,7 +1,11 @@
import { OrcaRuntimeWithResolveMobileSessionTerminalCommand } from './mobile-terminal-startup'
import type { WorktreeStartupLaunch } from '../../../../shared/worktree/launch-types'
import type { TuiAgent } from '../../../../shared/tui-agent'
import type { SleepingAgentLaunchConfig } from '../../../../shared/agent-session-resume'
import type {
AgentProviderSessionMetadata,
SleepingAgentLaunchConfig
} from '../../../../shared/agent-session-resume'
import type { SpawnIntent } from '../../../../shared/host-spawn-admission'
import type { AgentLaunchRuntime } from '../../../../shared/agent-launch-runtime'
import type {
RuntimeMobileSessionCreateTerminalResult,
@@ -34,6 +38,8 @@ export type OwnedMobileTerminalCreateOptions = {
targetGroupId?: string
supportsSplitGroupPlacement?: boolean
launchConfig?: SleepingAgentLaunchConfig
resumeProviderSession?: AgentProviderSessionMetadata
spawnIntent?: SpawnIntent
launchRuntime?: AgentLaunchRuntime
signal?: AbortSignal
refuseSleptWorktree?: boolean
@@ -112,6 +118,10 @@ export abstract class OrcaRuntimeWithCreateRuntimeOwnedMobileSessionTerminal ext
env: opts.env,
envToDelete: opts.envToDelete,
...(opts.launchConfig ? { launchConfig: opts.launchConfig } : {}),
...(opts.resumeProviderSession
? { resumeProviderSession: opts.resumeProviderSession }
: {}),
...(opts.spawnIntent ? { spawnIntent: opts.spawnIntent } : {}),
...(opts.launchAgent ? { launchAgent: opts.launchAgent } : {}),
...(opts.launchRuntime ? { launchRuntime: opts.launchRuntime } : {}),
...(opts.viewMode ? { viewMode: opts.viewMode } : {}),
@@ -2,9 +2,22 @@ import { OrcaRuntimeWithRunCreateMobileSessionTerminal } from './mobile-terminal
import type { TerminalWorkspaceLaunchScope } from '../../runtime-legacy-worker-terminal-recovery-types'
import type { WorktreeStartupLaunch } from '../../../../shared/worktree/launch-types'
import type { TuiAgent } from '../../../../shared/tui-agent'
import type { SleepingAgentLaunchConfig } from '../../../../shared/agent-session-resume'
import {
isResumableTuiAgent,
normalizeAgentProviderSession,
type AgentProviderSessionMetadata,
type SleepingAgentLaunchConfig,
type SleepingAgentSessionRecord
} from '../../../../shared/agent-session-resume'
import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection'
import { buildAgentStartupPlan } from '../../../../shared/tui-agent-startup'
import {
buildAgentResumeStartupPlan,
buildAgentStartupPlan
} from '../../../../shared/tui-agent-startup'
import {
resolveTuiAgentLaunchArgs,
resolveTuiAgentLaunchEnv
} from '../../../../shared/tui-agent-launch-defaults'
import { resolveAgentStartupPlanInputs } from '../../../../shared/agent-startup-plan-inputs'
import {
agentLaunchQuotingShell,
@@ -33,7 +46,71 @@ export type MobileTerminalCommand = {
launchRuntime?: AgentLaunchRuntime
}
export type MobileTerminalResumeCommand = MobileTerminalCommand & {
command: string
resumeProviderSession: AgentProviderSessionMetadata
}
function normalizeOriginConnectionId(connectionId: string | null | undefined): string | null {
return connectionId?.trim() || null
}
export abstract class OrcaRuntimeWithResolveMobileSessionTerminalCommand extends OrcaRuntimeWithRunCreateMobileSessionTerminal {
/** The `--resume` launch for a pane's recorded agent session, as the host window's cold restore builds it. */
protected resolveMobileSessionResumeCommand(
workspace: TerminalWorkspaceLaunchScope,
record: SleepingAgentSessionRecord
): MobileTerminalResumeCommand | null {
const providerSession = normalizeAgentProviderSession(record.providerSession)
if (!this.store || !providerSession || !isResumableTuiAgent(record.agent)) {
return null
}
// Why: a session id names a transcript on the machine that captured it; elsewhere it cannot resume.
if (
record.connectionId !== undefined &&
normalizeOriginConnectionId(record.connectionId) !==
normalizeOriginConnectionId(workspace.connectionId)
) {
return null
}
const settings = this.store.getSettings()
if (!isTuiAgentEnabled(record.agent, settings.disabledTuiAgents)) {
return null
}
const launchRuntime = this.resolveAgentLaunchRuntime(workspace, record.agent)
const { launchConfig } = record
const startupPlan = buildAgentResumeStartupPlan({
agent: record.agent,
providerSession,
cmdOverrides: settings.agentCmdOverrides ?? {},
agentArgs: launchConfig
? launchConfig.agentArgs
: resolveTuiAgentLaunchArgs(record.agent, settings.agentDefaultArgs),
agentEnv: launchConfig
? launchConfig.agentEnv
: resolveTuiAgentLaunchEnv(record.agent, settings.agentDefaultEnv),
...(launchConfig?.agentCommand ? { agentCommand: launchConfig.agentCommand } : {}),
...(launchConfig?.ompResumeFilePath
? { ompResumeFilePath: launchConfig.ompResumeFilePath }
: {}),
platform: launchRuntime.platform,
shell: agentLaunchQuotingShell(launchRuntime),
isRemote: Boolean(workspace.connectionId)
})
if (!startupPlan) {
return null
}
return {
command: startupPlan.launchCommand,
env: startupPlan.env,
launchConfig: startupPlan.launchConfig,
launchAgent: record.agent,
startupCommandDelivery: startupPlan.startupCommandDelivery,
launchRuntime,
resumeProviderSession: providerSession
}
}
protected async resolveMobileSessionTerminalCommand(
workspace: TerminalWorkspaceLaunchScope,
opts: MobileTerminalCommandOptions