fix(session-history): refuse desktop resumes of chat-owned conversations (#28229)

* fix(session-history): check host ownership before desktop resume

* fix(session-history): preserve resume behavior across hosts

* test: keep only tests that pin user-visible behavior
This commit is contained in:
Brennan Benson
2026-10-13 22:41:08 -07:00
committed by GitHub
parent a0de43aa1f
commit d14b7ca2b5
14 changed files with 321 additions and 61 deletions
@@ -200,6 +200,35 @@ describe('runtime AI Vault session scanner', () => {
})
).rejects.toThrow('Invalid aiVault.prepareSessionResume response')
})
it('emits the desktop sentinel only for this exact missing RPC', async () => {
mocks.callRuntimeEnvironment.mockResolvedValueOnce({
ok: false,
error: { code: 'method_not_found', message: 'Unknown method: aiVault.prepareSessionResume' }
})
await expect(
prepareRuntimeAiVaultSessionResume('/user-data', 'env-1', {
agent: 'claude',
filePath: '/sessions/a.jsonl',
codexHome: null,
executionHostId: 'runtime:env-1'
})
).rejects.toThrow('aiVault.prepareSessionResume: method_not_found')
mocks.callRuntimeEnvironment.mockResolvedValueOnce({
ok: false,
error: { code: 'method_not_found', message: 'Unknown method: terminal.send' }
})
await expect(
prepareRuntimeAiVaultSessionResume('/user-data', 'env-1', {
agent: 'claude',
filePath: '/sessions/a.jsonl',
codexHome: null,
executionHostId: 'runtime:env-1'
})
).rejects.toThrow('Unknown method: terminal.send')
})
})
function result(
@@ -149,6 +149,13 @@ export async function prepareRuntimeAiVaultSessionResume(
args
)
if (response.ok !== true) {
if (
response.error.code === 'method_not_found' &&
response.error.message === 'Unknown method: aiVault.prepareSessionResume'
) {
// Why: Electron drops custom error fields; this exact message preserves the host's verdict.
throw new Error('aiVault.prepareSessionResume: method_not_found')
}
throw new Error(response.error.message)
}
const parsed = aiVaultPrepareSessionResumeResultSchema.safeParse(response.result)
+44
View File
@@ -0,0 +1,44 @@
import { afterEach, expect, it, vi } from 'vitest'
import { setStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry'
import {
installOwnership,
PROVIDER_SESSION
} from '../ai-vault/structured-session-ownership.test-support'
import { prepareAiVaultSessionResume } from './ai-vault-resume'
vi.mock('electron', () => ({ ipcMain: { handle: vi.fn() } }))
afterEach(() => setStructuredAgentSessionHost(null))
it('refuses the native owner while preserving forks and other execution hosts', async () => {
installOwnership({ handle: { transport: 'acp', agent: 'omp', nativeId: PROVIDER_SESSION } })
const local = {
agent: 'omp' as const,
sessionId: PROVIDER_SESSION,
filePath: '/sessions/conversation.jsonl',
codexHome: null,
executionHostId: 'local' as const
}
const prepareSessionResume = vi.fn().mockResolvedValue({ useRealCodexHome: false })
const prepareRuntimeSessionResume = vi.fn().mockResolvedValue({ useRealCodexHome: false })
const options = { prepareSessionResume, prepareRuntimeSessionResume }
await expect(prepareAiVaultSessionResume(local, options)).rejects.toThrow(
'agent_session_conflict'
)
expect(prepareSessionResume).not.toHaveBeenCalled()
await expect(prepareAiVaultSessionResume({ ...local, fork: true }, options)).resolves.toEqual({
useRealCodexHome: false
})
const paired = { ...local, executionHostId: 'runtime:paired' as const }
await expect(prepareAiVaultSessionResume(paired, options)).resolves.toEqual({
useRealCodexHome: false
})
expect(prepareRuntimeSessionResume).toHaveBeenCalledWith('paired', paired)
await expect(
prepareAiVaultSessionResume({ ...local, executionHostId: 'ssh:server' }, options)
).resolves.toEqual({ useRealCodexHome: false })
const wsl = { ...local, filePath: String.raw`\\wsl.localhost\Ubuntu\home\qa\session.jsonl` }
await expect(prepareAiVaultSessionResume(wsl, options)).resolves.toEqual({
useRealCodexHome: false
})
expect(prepareSessionResume).toHaveBeenCalledWith(wsl)
})
+10 -2
View File
@@ -5,7 +5,9 @@ import type {
AiVaultSessionResumePreparation
} from '../../shared/ai-vault-resume-preparation'
import { isUnresolvedOwnerHostId, parseExecutionHostId } from '../../shared/execution-host'
import { isWslUncPath } from '../../shared/wsl-paths'
import { assertLegacyAiVaultResumeAllowed } from '../ai-vault/structured-session-ownership'
import { tryEnsureStructuredAgentSessionHostForResume } from '../runtime/structured-agent-session-host-refusal'
export type AiVaultResumeHandlerOptions = {
ensureStructuredSessionOwnership?: () => Promise<void>
@@ -26,8 +28,6 @@ export async function prepareAiVaultSessionResume(
args: AiVaultPrepareSessionResumeArgs,
options: AiVaultResumeHandlerOptions
): Promise<AiVaultPrepareSessionResumeResult> {
await options.ensureStructuredSessionOwnership?.()
assertLegacyAiVaultResumeAllowed(args)
const executionHost = parseExecutionHostId(args.executionHostId)
if (executionHost?.kind === 'runtime') {
if (!options.prepareRuntimeSessionResume || isUnresolvedOwnerHostId(executionHost.id)) {
@@ -39,5 +39,13 @@ export async function prepareAiVaultSessionResume(
if (executionHost?.kind === 'ssh') {
return { useRealCodexHome: false }
}
if (!isWslUncPath(args.filePath)) {
if (options.ensureStructuredSessionOwnership) {
await (args.codexHome === null && !args.fork
? tryEnsureStructuredAgentSessionHostForResume(options.ensureStructuredSessionOwnership)
: options.ensureStructuredSessionOwnership())
}
assertLegacyAiVaultResumeAllowed(args)
}
return options.prepareSessionResume?.(args) ?? { useRealCodexHome: false }
}
+12 -5
View File
@@ -12,13 +12,17 @@ import { conditionalAiVaultListResult } from '../../../ai-vault/session-list-fin
import { restampAiVaultListResult } from '../../../ai-vault/session-list-results'
import type { AiVaultPrepareSessionResumeArgs } from '../../../../shared/ai-vault-resume-preparation'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
import { isWslUncPath } from '../../../../shared/wsl-paths'
import { describeAiVaultScanError } from '../../../../shared/ai-vault-scan-error-message'
import {
assertLegacyAiVaultResumeAllowed,
projectStructuredAiVaultSessions,
searchWithStructuredOwners
} from '../../../ai-vault/structured-session-ownership'
import { ensureStructuredAgentSessionHostUnlessRefused } from '../../structured-agent-session-host-refusal'
import {
ensureStructuredAgentSessionHostUnlessRefused,
tryEnsureStructuredAgentSessionHostForResume
} from '../../structured-agent-session-host-refusal'
import {
clientOpensStructuredChatFromHistory,
supportsStructuredAgentSessions
@@ -133,10 +137,13 @@ export const AI_VAULT_METHODS = [
// client-provided runtime/SSH stamp escape that host boundary.
executionHostId: LOCAL_EXECUTION_HOST_ID
}
await ensureStructuredAgentSessionHostUnlessRefused(() =>
runtime.ensureStructuredAgentSessionHost()
)
assertLegacyAiVaultResumeAllowed(args)
if (!isWslUncPath(args.filePath)) {
const ensureHost = () => runtime.ensureStructuredAgentSessionHost()
await (args.codexHome === null && !args.fork
? tryEnsureStructuredAgentSessionHostForResume(ensureHost)
: ensureStructuredAgentSessionHostUnlessRefused(ensureHost))
assertLegacyAiVaultResumeAllowed(args)
}
return runtime.prepareAiVaultSessionResume(args)
}
})
@@ -38,6 +38,25 @@ export async function ensureStructuredAgentSessionHostUnlessRefused(
}
}
/** Chat-host setup is bookkeeping for a terminal resume; an existing owner is checked afterward. */
export async function tryEnsureStructuredAgentSessionHostForResume(
ensureHost: () => Promise<unknown>
): Promise<void> {
try {
await ensureStructuredAgentSessionHostUnlessRefused(ensureHost)
} catch (error) {
const code = isAgentSessionRefusalError(error)
? error.refusal.code
: error instanceof Error
? error.message
: null
if (code === 'agent_session_conflict' || code === 'agent_session_ownership_unknown') {
throw error
}
console.warn('[ai-vault] native chat host setup failed during terminal resume:', error)
}
}
/** Set when the journal database would not open; cleared by a later install. The install retries
* on the next call, so a refusal that can clear does. */
export function recordStructuredAgentSessionHostInstallRefusal(
@@ -1,5 +1,5 @@
import { translate } from '@/i18n/i18n'
import { compactIpcErrorMessage } from '@/lib/ipc-error'
import { isAiVaultOwnershipRefusal } from '@/lib/ai-vault-resume-refusal'
import type { AiVaultSession } from '../../../../shared/ai-vault-types'
import { getAgentForkArgv, isResumableTuiAgent } from '../../../../shared/agent-session-resume'
import { getAiVaultAgentProviderSession } from '@/lib/ai-vault-resume-command'
@@ -29,8 +29,7 @@ export function aiVaultSessionCliForkWorktreeId(
* the fork as a second writer, so that refusal asks for the update instead of naming a code. */
export function describeAiVaultCliForkFailure(message: string): string {
// The desktop preparation's refusal arrives inside Electron's IPC wrapper.
const code = compactIpcErrorMessage(message)
return code === 'agent_session_conflict' || code === 'agent_session_ownership_unknown'
return isAiVaultOwnershipRefusal(message)
? translate(
'auto.components.right.sidebar.AiVaultPanel.resumeInNewCliHostTooOld',
'Update Orca on the host that runs this chat to resume it in a new CLI.'
@@ -9,6 +9,7 @@ const mocks = vi.hoisted(() => ({
buildAiVaultResumeStartupForWorktree: vi.fn(),
prepareAiVaultSessionForFork: vi.fn(),
prepareAiVaultSessionForResume: vi.fn(),
prepareAiVaultSessionForTerminalResume: vi.fn(),
launchAiVaultSessionInNewTab: vi.fn(),
activateAiVaultResumeWorkspace: vi.fn(),
toastError: vi.fn(),
@@ -25,6 +26,7 @@ vi.mock('@/lib/ai-vault-resume-command', () => ({
vi.mock('@/lib/ai-vault-session-resume-preparation', () => ({
prepareAiVaultSessionForFork: mocks.prepareAiVaultSessionForFork,
prepareAiVaultSessionForResume: mocks.prepareAiVaultSessionForResume,
prepareAiVaultSessionForTerminalResume: mocks.prepareAiVaultSessionForTerminalResume,
dropDeletedSshResumeCwd: async (session: unknown) => session
}))
vi.mock('@/lib/launch-ai-vault-session', () => ({
@@ -154,3 +156,21 @@ describe('Resume in New CLI launch', () => {
expect(mocks.launchAiVaultSessionInNewTab).not.toHaveBeenCalled()
})
})
it('refuses the stale OMP row before creating a second terminal writer', async () => {
vi.clearAllMocks()
mocks.prepareAiVaultSessionForTerminalResume.mockRejectedValue(
new Error(
"Error invoking remote method 'aiVault:prepareSessionResume': Error: agent_session_conflict"
)
)
renderActions().handleResume(
{ ...owned, agent: 'omp', structuredSession: undefined },
'worktree-1'
)
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledOnce())
expect(mocks.toastError).toHaveBeenCalledWith(
'This OMP conversation is open in a chat. Refresh Session History to open it.'
)
expect(mocks.launchAiVaultSessionInNewTab).not.toHaveBeenCalled()
})
@@ -13,7 +13,8 @@ import type { AiVaultAgent, AiVaultSession } from '../../../../shared/ai-vault-t
import {
dropDeletedSshResumeCwd,
prepareAiVaultSessionForFork,
prepareAiVaultSessionForResume
prepareAiVaultSessionForResume,
prepareAiVaultSessionForTerminalResume
} from '@/lib/ai-vault-session-resume-preparation'
import type { Worktree } from '../../../../shared/worktree/types'
import { translate } from '@/i18n/i18n'
@@ -25,6 +26,7 @@ import type { AgentSessionContinuationRequest } from '@/lib/agent-session-contin
import { activateAiVaultStructuredSession } from '@/lib/activate-ai-vault-structured-session'
import { adoptsCliConversations } from '../../../../shared/structured-agent-cli-conversation-adoption'
import { newAgentLaunchRequestId } from '@/lib/agent-launch-request-id'
import { describeAiVaultResumeFailure } from '@/lib/ai-vault-resume-refusal'
import {
activateAiVaultResumeWorkspace,
resumeAiVaultSessionInNewChat
@@ -86,7 +88,7 @@ export function useAiVaultSessionLaunchActions({
)
)
} catch (error) {
notifyAiVaultSessionPreparationFailure(error)
notifyAiVaultSessionPreparationFailure(error, session.agent)
}
},
[buildResumeCommand]
@@ -150,7 +152,9 @@ export function useAiVaultSessionLaunchActions({
}
showQueuedToast()
})
.catch((error: unknown) => notifyAiVaultSessionPreparationFailure(error, describeFailure))
.catch((error: unknown) =>
notifyAiVaultSessionPreparationFailure(error, session.agent, describeFailure)
)
},
[activeWorktree?.id, activeWorktreeId, targetState]
)
@@ -161,10 +165,14 @@ export function useAiVaultSessionLaunchActions({
void activateAiVaultStructuredSession(session)
return
}
launchInTerminal(session, targetWorktreeId, (worktreeId) =>
prepareAiVaultSessionForResume(session)
.then(dropDeletedSshResumeCwd)
.then((preparedSession) => buildResumeStartup(preparedSession, worktreeId))
launchInTerminal(
session,
targetWorktreeId,
(worktreeId) =>
prepareAiVaultSessionForTerminalResume(session)
.then(dropDeletedSshResumeCwd)
.then((preparedSession) => buildResumeStartup(preparedSession, worktreeId)),
(message) => describeAiVaultResumeFailure(new Error(message), session.agent)
)
},
[buildResumeStartup, launchInTerminal]
@@ -277,16 +285,11 @@ export function useAiVaultSessionLaunchActions({
function notifyAiVaultSessionPreparationFailure(
error: unknown,
agent: AiVaultAgent,
describeFailure: (message: string) => string = (message) => message
): void {
toast.error(
error instanceof Error
? describeFailure(error.message)
: translate(
'auto.components.right.sidebar.AiVaultPanel.prepareSessionResumeFailed',
'Could not prepare this session for resume.'
)
)
const described = error instanceof Error ? new Error(describeFailure(error.message)) : error
toast.error(describeAiVaultResumeFailure(described, agent))
}
function resolveAiVaultSessionLaunchTargetOrNotify(
@@ -18,12 +18,12 @@ import {
getAiVaultAgentProviderSession
} from '@/lib/ai-vault-resume-command'
import { launchAiVaultSessionInNewTab } from '@/lib/launch-ai-vault-session'
import { aiVaultSessionNeedsResumePreparation } from '@/lib/ai-vault-session-resume-preparation'
import { prepareAiVaultTerminalResumeRequest } from '@/lib/ai-vault-session-resume-preparation'
import { describeAiVaultResumeFailure } from '@/lib/ai-vault-resume-refusal'
import { useAppStore } from '@/store'
import { resolveDropZone } from './tab-drop-zone'
import type { TabDropZone } from './useTabDragSplit'
import { translate } from '@/i18n/i18n'
import type { AiVaultPrepareSessionResumeResult } from '../../../../shared/ai-vault-resume-preparation'
import { activateAiVaultStructuredSession } from '@/lib/activate-ai-vault-structured-session'
type PaneDropTarget = {
@@ -222,23 +222,22 @@ export default function AiVaultSessionDropLayer({
)
)
}
const preparation =
payload.sessionFilePath &&
payload.sessionExecutionHostId &&
payload.codexHome !== undefined &&
aiVaultSessionNeedsResumePreparation({
agent: payload.agent,
codexHome: payload.codexHome,
executionHostId: payload.sessionExecutionHostId
})
? window.api.aiVault.prepareSessionResume({
agent: payload.agent,
sessionId: payload.sessionId,
filePath: payload.sessionFilePath,
executionHostId: payload.sessionExecutionHostId,
codexHome: payload.codexHome
})
: Promise.resolve<AiVaultPrepareSessionResumeResult>({ useRealCodexHome: false })
if (!payload.sessionFilePath || !payload.sessionExecutionHostId) {
toast.error(
translate(
'auto.components.tab.group.AiVaultSessionDropLayer.oldPayload',
'This session was dragged from an older Orca window. Resume it from Session History instead.'
)
)
return true
}
const preparation = prepareAiVaultTerminalResumeRequest({
agent: payload.agent,
sessionId: payload.sessionId,
filePath: payload.sessionFilePath,
executionHostId: payload.sessionExecutionHostId,
codexHome: payload.codexHome ?? null
})
void preparation
.then((result) => {
const startup = result.useRealCodexHome
@@ -282,7 +281,9 @@ export default function AiVaultSessionDropLayer({
void launchResult.runtimeLaunch.then((outcome) => {
if (outcome.status === 'failed') {
toast.error(
outcome.message ||
(outcome.message
? describeAiVaultResumeFailure(new Error(outcome.message), payload.agent)
: null) ||
translate(
'auto.lib.launch.agent.in.new.tab.11cce5cc77',
'Could not launch {{value0}} in a new terminal.',
@@ -298,14 +299,7 @@ export default function AiVaultSessionDropLayer({
showQueuedToast()
})
.catch((error: unknown) => {
toast.error(
error instanceof Error
? error.message
: translate(
'auto.components.right.sidebar.AiVaultPanel.prepareSessionResumeFailed',
'Could not prepare this session for resume.'
)
)
toast.error(describeAiVaultResumeFailure(error, payload.agent))
})
return true
},
+6 -1
View File
@@ -1220,6 +1220,10 @@
}
}
}
},
"aiVaultResumeRefusal": {
"openInChat": "This {{value0}} conversation is open in a chat. Refresh Session History to open it.",
"ownershipUnknown": "Orca could not confirm who owns this {{value0}} conversation. Refresh Session History and try again."
}
},
"hooks": {
@@ -3660,7 +3664,8 @@
"sessionQueued": "Session queued",
"openSupportedWorkspace": "Open a workspace before resuming a session.",
"sessionHostMismatchUnsupported": "This session belongs to a different host. Drop it onto a workspace on the same host.",
"localSessionSshWorkspaceUnsupported": "This session's history is stored on this machine, so it can't resume in an SSH workspace. Drop it onto a local workspace instead."
"localSessionSshWorkspaceUnsupported": "This session's history is stored on this machine, so it can't resume in an SSH workspace. Drop it onto a local workspace instead.",
"oldPayload": "This session was dragged from an older Orca window. Resume it from Session History instead."
},
"TabGroupDropOverlay": {
"paneColumnLabel": "New split"
@@ -0,0 +1,34 @@
import { aiVaultAgentLabel, type AiVaultAgent } from '../../../shared/ai-vault-types'
import { translate } from '@/i18n/i18n'
import { compactIpcErrorMessage } from './ipc-error'
export function isAiVaultOwnershipRefusal(message: string): boolean {
const code = compactIpcErrorMessage(message)
return code === 'agent_session_conflict' || code === 'agent_session_ownership_unknown'
}
export function describeAiVaultResumeFailure(error: unknown, agent: AiVaultAgent): string {
const message = error instanceof Error ? error.message : null
const code = message ? compactIpcErrorMessage(message) : null
if (code === 'agent_session_conflict') {
return translate(
'auto.lib.aiVaultResumeRefusal.openInChat',
'This {{value0}} conversation is open in a chat. Refresh Session History to open it.',
{ value0: aiVaultAgentLabel(agent) }
)
}
if (code === 'agent_session_ownership_unknown') {
return translate(
'auto.lib.aiVaultResumeRefusal.ownershipUnknown',
'Orca could not confirm who owns this {{value0}} conversation. Refresh Session History and try again.',
{ value0: aiVaultAgentLabel(agent) }
)
}
return (
message ??
translate(
'auto.components.right.sidebar.AiVaultPanel.prepareSessionResumeFailed',
'Could not prepare this session for resume.'
)
)
}
@@ -3,7 +3,8 @@ import type { AiVaultSession } from '../../../shared/ai-vault-types'
import {
dropDeletedSshResumeCwd,
prepareAiVaultSessionForFork,
prepareAiVaultSessionForResume
prepareAiVaultSessionForResume,
prepareAiVaultSessionForTerminalResume
} from './ai-vault-session-resume-preparation'
afterEach(() => {
@@ -99,6 +100,29 @@ describe('prepareAiVaultSessionForResume', () => {
})
})
describe('ordinary Resume against an older paired host', () => {
it.each([
new Error(
"Error invoking remote method 'aiVault:prepareSessionResume': Error: aiVault.prepareSessionResume: method_not_found"
),
Object.assign(new Error('Unknown method: aiVault.prepareSessionResume'), {
code: 'method_not_found'
})
])('retains neutral resumes but requires managed Codex home preparation', async (error) => {
stubPreparation(vi.fn().mockRejectedValue(error))
const remote = session({ agent: 'claude', executionHostId: 'runtime:paired' })
await expect(prepareAiVaultSessionForTerminalResume(remote)).resolves.toBe(remote)
await expect(
prepareAiVaultSessionForTerminalResume(
session({
executionHostId: 'runtime:paired',
codexHome: '/tmp/orca/codex-runtime-home/home'
})
)
).rejects.toBe(error)
})
})
describe('prepareAiVaultSessionForFork', () => {
// A fork of a chat-owned conversation must run under the selected account, as a resume does.
it('repins a chat-owned per-account session, asking the host as a fork', async () => {
@@ -1,20 +1,83 @@
import type { AiVaultSession } from '../../../shared/ai-vault-types'
import type {
AiVaultPrepareSessionResumeArgs,
AiVaultPrepareSessionResumeResult
} from '../../../shared/ai-vault-resume-preparation'
import {
isLegacySharedCodexHome,
isPerAccountManagedCodexHome
} from '../../../shared/ai-vault-resume-preparation'
import {
getSshTargetIdForExecutionHost,
LOCAL_EXECUTION_HOST_ID
LOCAL_EXECUTION_HOST_ID,
parseExecutionHostId
} from '../../../shared/execution-host'
import { readIpcErrorMessage } from './ipc-error'
import type { AiVaultResumeCommandSession } from './ai-vault-resume-command'
export function prepareAiVaultSessionForResume(session: AiVaultSession): Promise<AiVaultSession> {
return session.structuredSession
return session.structuredSession || !aiVaultSessionNeedsResumePreparation(session)
? Promise.resolve(session)
: prepareAiVaultSessionHome(session, false)
}
export function prepareAiVaultSessionForTerminalResume(
session: AiVaultSession
): Promise<AiVaultSession> {
return prepareAiVaultSessionHome(session, false, true)
}
export function prepareAiVaultSessionResumeRequest(
args: AiVaultPrepareSessionResumeArgs
): Promise<AiVaultPrepareSessionResumeResult> {
return window.api.aiVault.prepareSessionResume(args)
}
export async function prepareAiVaultTerminalResumeRequest(
args: AiVaultPrepareSessionResumeArgs
): Promise<AiVaultPrepareSessionResumeResult> {
const needsHomePreparation = aiVaultSessionNeedsResumePreparation(args)
try {
// Why: this request checks ownership; old desktop resumes kept the recorded home otherwise.
const result = await prepareAiVaultSessionResumeRequest(
needsHomePreparation ? args : { ...args, codexHome: null }
)
if (
typeof result?.useRealCodexHome !== 'boolean' ||
(result.substituteCodexHome !== undefined && typeof result.substituteCodexHome !== 'string')
) {
throw new Error('Invalid aiVault.prepareSessionResume response')
}
return needsHomePreparation ? result : { useRealCodexHome: false }
} catch (error) {
if (
!args.fork &&
parseExecutionHostId(args.executionHostId)?.kind === 'runtime' &&
!needsHomePreparation &&
isMissingPairedResumePreparation(error)
) {
return { useRealCodexHome: false }
}
throw error
}
}
function isMissingPairedResumePreparation(error: unknown): boolean {
if (!(error instanceof Error)) {
return false
}
const desktopPrepareError =
error.message.startsWith("Error invoking remote method 'aiVault:prepareSessionResume':") ||
error.message.startsWith("Error occurred in handler for 'aiVault:prepareSessionResume':")
return (
(desktopPrepareError &&
readIpcErrorMessage(error) === 'aiVault.prepareSessionResume: method_not_found') ||
('code' in error &&
error.code === 'method_not_found' &&
error.message === 'Unknown method: aiVault.prepareSessionResume')
)
}
/** Picks the Codex home a fork of `session` runs under, the same way a resume picks it. */
export function prepareAiVaultSessionForFork(session: AiVaultSession): Promise<AiVaultSession> {
return prepareAiVaultSessionHome(session, true)
@@ -22,19 +85,23 @@ export function prepareAiVaultSessionForFork(session: AiVaultSession): Promise<A
async function prepareAiVaultSessionHome(
session: AiVaultSession,
fork: boolean
fork: boolean,
terminalResume = false
): Promise<AiVaultSession> {
if (!aiVaultSessionNeedsResumePreparation(session)) {
if (fork && !aiVaultSessionNeedsResumePreparation(session)) {
return session
}
const result = await window.api.aiVault.prepareSessionResume({
const args: AiVaultPrepareSessionResumeArgs = {
agent: session.agent,
sessionId: session.sessionId,
filePath: session.filePath,
codexHome: session.codexHome,
executionHostId: session.executionHostId,
...(fork ? { fork: true } : {})
})
}
const result = await (terminalResume
? prepareAiVaultTerminalResumeRequest(args)
: prepareAiVaultSessionResumeRequest(args))
if (result.useRealCodexHome) {
return { ...session, codexHome: null }
}