Repository navigation
fix(mobile): a session resumed from the phone's history shows its conversation before the first new turn (#10923) - #30283
Conversation
…versation before the first new turn (#10923)
📝 Walkthrough
Priority: ⬇️ Low Merge Risk: 🟡 Moderate · up to
|
There was a problem hiding this comment.
Actionable comments posted: 3
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
3313d3a8-df55-4468-aa95-867c2b1edc47
📒 Files selected for processing: 10
mobile/src/session/ai-vault-resume-launch.test.tsmobile/src/session/ai-vault-resume-launch.tssrc/main/ipc/pty/pane/resume-launch-identity.tssrc/main/runtime/mobile-session-resume-identity.test.tssrc/main/runtime/rpc/methods/session-tabs.tssrc/main/runtime/terminal/launch/mobile-terminal-create.tssrc/shared/rpc-contract/session-tabs-create-resume-session-param.test.tssrc/shared/rpc-contract/session-tabs-schemas-params.tssrc/shared/rpc-contract/terminal-unary-params.tstests/e2e/headless-serve-mobile-native-chat-identity.spec.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.
| const resumeProviderSession = getAiVaultAgentProviderSession({ | ||
| ...args.session, | ||
| ...(resumeFilePath ? { filePath: resumeFilePath } : {}) | ||
| }) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
#!/bin/bash
# Locate the native-chat reader and its transcript-path fallback.
rg -n -C 6 -g '*.ts' 'nativeChat\.readSession|readNativeChatSession' src/mainRepository: stablyai/orca
Length of output: 12967
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- changed builder ---'
nl -ba mobile/src/session/ai-vault-resume-launch.ts | sed -n '1,180p'
printf '%s\n' '--- relevant symbols and path consumers ---'
rg -n -C 5 -g '*.ts' -g '*.d.ts' \
-e 'getAiVaultAgentProviderSession' \
-e 'AgentProviderSessionMetadata' \
-e 'transcriptPath' \
-e 'resumeProviderSession' \
-e 'readNativeChatTranscriptTail' \
-e 'resolve.*Transcript' \
src mobile | sed -n '1,360p'
printf '%s\n' '--- native chat method ---'
nl -ba src/main/runtime/rpc/methods/native-chat.ts | sed -n '1,125p'Repository: stablyai/orca
Length of output: 42334
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- helper declaration ---'
rg -n -F -- 'export function getAiVaultAgentProviderSession' src mobile
helper=$(rg -l -F -- 'export function getAiVaultAgentProviderSession' src mobile | head -n 1)
nl -ba "$helper" | sed -n '1,220p'
printf '%s\n' '--- metadata contract ---'
meta=$(rg -l -F -- 'export type AgentProviderSessionMetadata' src mobile | head -n 1)
nl -ba "$meta" | sed -n '1,180p'
printf '%s\n' '--- execution-host path mapping ---'
pathfile=$(rg -l -F -- 'export function nativeChatTranscriptPathOnExecutionHost' src mobile | head -n 1)
nl -ba "$pathfile" | sed -n '1,220p'
printf '%s\n' '--- tail reader and transcript resolution ---'
reader=$(rg -l -F -- 'export async function readNativeChatTranscriptTail' src/main | head -n 1)
nl -ba "$reader" | sed -n '1,320p'Repository: stablyai/orca
Length of output: 32025
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- resolver references ---'
rg -n -C 8 -g '*.ts' -g '*.d.ts' \
-e 'export async function resolveSessionFilePath' \
-e 'function resolveSessionFilePath' \
-e 'resolveSessionFilePath\(' \
src/main/native-chat src/main | sed -n '1,360p'
printf '%s\n' '--- resolver files ---'
rg --files src/main | rg 'session-file-resolver|transcript.*resolver|native-chat.*session'Repository: stablyai/orca
Length of output: 41524
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- resolver implementation ---'
nl -ba src/main/native-chat/session-file-resolver.ts | sed -n '1,300p'
printf '%s\n' '--- relevant resolver tests ---'
nl -ba src/main/native-chat/session-file-resolver.test.ts | sed -n '120,310p'Repository: stablyai/orca
Length of output: 24298
Preserve Claude’s normalized transcript path in the seeded identity.
getAiVaultAgentProviderSession drops filePath for Claude and returns only the session ID. The reader then falls back to an ID-based lookup, which misses Claude transcripts whose filename differs from session_id. Preserve the path for Claude. Codex already has an ID-suffix lookup.
Suggested fix
if (session.agent === 'antigravity' || session.agent === 'cursor') {
return { key: 'conversation_id', id: session.sessionId }
}
+ if (session.agent === 'claude' && session.filePath) {
+ return { key: 'session_id', id: session.sessionId, transcriptPath: session.filePath }
+ }
if (session.agent === 'pi' || session.agent === 'prime-agent') {| const pty = this.getLivePtyForHandle(created.tab.terminal)?.pty | ||
| // Why: an SSH pane's status rows belong to the relay on that host. | ||
| if (!pty?.paneKey || pty.connectionId) { | ||
| return | ||
| } | ||
| seedResumeLaunchIdentity({ | ||
| paneKey: pty.paneKey, | ||
| launchAgent: opts.launchAgent, | ||
| resumeProviderSession: opts.resumeProviderSession, | ||
| worktreeId: pty.worktreeId, |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '110,140p' src/main/runtime/terminal/launch/mobile-terminal-create.ts
sed -n '30,95p' src/main/ipc/pty/pane/resume-launch-identity.ts
sed -n '65,125p' src/main/native-chat/session-file-resolver.ts
rg -n 'transcriptPath|readNativeChatTranscriptTail|readSession' src/main/native-chat src/main/runtime/rpc/methodsRepository: stablyai/orca
Length of output: 37089
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- createTerminal registrations and schema ---'
rg -n -F --glob '*.ts' -e 'session.tabs.createTerminal' -e 'createTerminal' src/main/runtime/rpc src/main/runtime/terminal src | head -220
printf '%s\n' '--- native-chat method ---'
sed -n '1,190p' src/main/runtime/rpc/methods/native-chat.ts
printf '%s\n' '--- seed normalization and provider metadata ---'
rg -n -F --glob '*.ts' -e 'function normalizeAgentProviderSession' -e 'normalizeAgentProviderSession' -e 'AgentProviderSessionMetadata' src/main | head -120
printf '%s\n' '--- native-chat reader/resolver relevant blocks ---'
sed -n '220,310p' src/main/native-chat/transcript-tail-reader.ts
sed -n '100,190p' src/main/native-chat/session-file-resolver.ts
sed -n '145,215p' src/main/native-chat/host-readable-transcript-path.ts
printf '%s\n' '--- changed file signature and startRun callers ---'
sed -n '1,155p' src/main/runtime/terminal/launch/mobile-terminal-create.tsRepository: stablyai/orca
Length of output: 43414
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- session tabs RPC ---'
nl -ba src/main/runtime/rpc/methods/session-tabs.ts | sed -n '1,145p'
printf '%s\n' '--- native chat RPC ---'
nl -ba src/main/runtime/rpc/methods/native-chat.ts | sed -n '1,125p'
printf '%s\n' '--- resume metadata and normalization ---'
rg -n -F --glob '*.ts' -e 'export function normalizeAgentProviderSession' -e 'normalizeAgentProviderSession =' -e 'AgentProviderSessionMetadata' src/shared src/main | head -100
nl -ba src/shared/agent-session-resume.ts | sed -n '1,200p'
printf '%s\n' '--- createTerminal tests ---'
nl -ba src/main/runtime/rpc/methods/session-tabs.test.ts | sed -n '400,700p'
printf '%s\n' '--- RPC permission definitions and paired-mobile checks ---'
rg -n -F --glob '*.ts' -e 'clientKind' -e 'paired' -e 'permission' -e 'session.tabs.createTerminal' src/main/runtime/rpc src/main | head -180Repository: stablyai/orca
Length of output: 42289
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- provider metadata normalization ---'
nl -ba src/shared/agent-session-resume.ts | sed -n '30,215p'
printf '%s\n' '--- terminal and native-chat schemas ---'
rg -n -F --glob '*.ts' -e 'export const CreateTerminalTab' -e 'CreateTerminalTab =' -e 'export const NativeChatSession' -e 'NativeChatSession =' src/shared
nl -ba src/shared/rpc-contract/session-tabs-schemas-params.ts | sed -n '1,180p'
nl -ba src/shared/rpc-contract/native-chat-params.ts | sed -n '1,130p'
printf '%s\n' '--- stored-path binding ---'
nl -ba src/main/native-chat/ssh-transcript-path.ts | sed -n '1,110p'
printf '%s\n' '--- paired standard permissions ---'
nl -ba src/main/runtime/rpc/rpc-caller-scope.ts | sed -n '1,85p'
nl -ba src/main/runtime/rpc/rpc-method-permission.ts | sed -n '1,80p'Repository: stablyai/orca
Length of output: 28449
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- resume request schema ---'
nl -ba src/shared/rpc-contract/terminal-unary-params.ts | sed -n '1,180p'
printf '%s\n' '--- transcript admission helpers and permitted roots ---'
rg -n -F --glob '*.ts' -e 'admitTranscriptPath' -e 'TranscriptPath' -e 'claudeProjectsDirs' -e 'codexSessionsDirs' -e 'grokSessionsDir' -e 'ompSessionsDir' -e 'resolveSessionFilePath' src/main/native-chat src/main/runtime/rpc
printf '%s\n' '--- structured source admission ---'
nl -ba src/main/native-chat/structured-agent-session-source-admission.ts | sed -n '1,240p'
printf '%s\n' '--- native chat path consumers ---'
nl -ba src/main/native-chat/native-chat-file-provenance.ts | sed -n '1,100p'
nl -ba src/main/native-chat/structured-agent-cli-conversations.ts | sed -n '1,115p'Repository: stablyai/orca
Length of output: 41756
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- exact resume schema ---'
rg -n -F --glob '*.ts' -e 'ResumeProviderSessionParam' src/shared
nl -ba src/shared/rpc-contract/terminal-unary-params.ts | sed -n '300,390p'
printf '%s\n' '--- transcript path admission ---'
nl -ba src/main/native-chat/transcript-path-admission.ts | sed -n '1,240p'
printf '%s\n' '--- resolver imports and root configuration ---'
nl -ba src/main/native-chat/session-file-resolver.ts | sed -n '1,125p'
printf '%s\n' '--- PR diff for relevant files ---'
git diff acc235187f626ca2af6d0fb17f8ec8ec3ae6b227..cafd7c92476f6ecf03c154d48b08ae06513670fe -- src/main/runtime/terminal/launch/mobile-terminal-create.ts src/shared/rpc-contract/terminal-unary-params.ts src/main/native-chat/session-file-resolver.ts src/main/native-chat/ssh-transcript-path.tsRepository: stablyai/orca
Length of output: 15525
Validate the resume transcript path before native-chat resolution.
A paired mobile caller can pass any .jsonl path in resumeProviderSession.transcriptPath. The new seed stores that path on a local pane. nativeChat.readSession then accepts the same path, and resolveSessionFilePath reads it when it exists. A file outside the agent transcript roots can therefore be exposed when its contents parse as the selected agent’s JSONL format.
Add canonical admission against the selected agent’s configured transcript roots before the direct-path branch in resolveSessionFilePath. Preserve the existing SSH and WSL routing for admitted paths.
| }) => { | ||
| test.setTimeout(240_000) | ||
| const fakeDir = mkdtempSync(path.join(os.tmpdir(), 'orca-e2e-10923-')) | ||
| const host = await launchHeadlessPairedRuntimeHost({ pairingScope: 'mobile' }) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Set explicit background mode for this E2E launch.
launchHeadlessPairedRuntimeHost calls electron.launch in tests/e2e/helpers/headless-paired-runtime-host.ts Lines 149-169. Its launch environment sets ORCA_E2E_HEADLESS but not ORCA_BACKGROUND_LAUNCH. Set ORCA_BACKGROUND_LAUNCH: '1' in that helper for this new run. As per coding guidelines, “A raw electron.launch() outside those helpers must pass ORCA_BACKGROUND_LAUNCH: '1'.”
Source: Coding guidelines
ELI5
When you reopen an old Codex (or Claude, OMP, and so on) conversation from the phone's Agent Session History, the phone's chat view said "Can't find this conversation yet" until you sent a new message. The phone knows exactly which conversation it is reopening. It now tells the computer when it opens the terminal, so the chat shows the saved conversation right away.
What Changed
Before: Session History resumes a conversation by opening a plain shell tab (
session.tabs.createTerminal) and typing the resume command into it (terminal.send). The computer never started the agent itself, so it had no idea which conversation the tab held. Codex only reports its session when its first new turn starts, so the phone's tab list had noagentStatus.providerSessionand native chat had nothing to read.After: the phone adds an optional
resumeProviderSession({ key, id, transcriptPath? }) to thatsession.tabs.createTerminalcall. Once the tab exists, the host writes that session into its agent-status store as an identity-only row for the new pane. This is the same row #29664 records for host-launched resumes. The phone's nextsession.tabs.listcarriesagentTypeandproviderSession, andnativeChat.readSessionreturns the saved conversation. The agent's own first hook replaces the row as usual.Mechanism:
mobile/src/session/ai-vault-resume-launch.ts: the resume launch names its conversation with the sharedgetAiVaultAgentProviderSession(the same rule the desktop's history resume uses) and sends it on the create.src/shared/rpc-contract/session-tabs-schemas-params.ts:CreateTerminalTabaccepts the field, reusingterminal.create's schema (now exported asResumeProviderSessionParam). A malformed value is dropped rather than failing the create, and a chat fork refuses it, as forks refuse every launch field.src/main/runtime/terminal/launch/mobile-terminal-create.ts: after a create run, the host looks up the created pane's live PTY by handle and seeds it throughseedResumeLaunchIdentity(split out ofseedResumeLaunchIdentityFromSpawninresume-launch-identity.ts, so the agent and Pi transcript checks stay in one place). This covers every create path (headless runtime-owned, renderer-backed, Qoder launch-at-create) without changing any of them.Wire compatibility: one new optional request field. An older host strips it (zod
objectis not strict) and behaves as today: the identity arrives with the agent's first hook. An older phone does not send it and also behaves as today. The host publishes no new fields; it publishes the existing optionalagentStatus.providerSessionearlier.Why
The execution host owns agent status in one store, and every reader (phone tab list, native chat,
worktree ps) joins on it. The phone is the only party that knows which conversation it typed into the shell, so it has to say so. Seeding the host store from that, rather than keeping a phone-only memory, fixes every reader at once, including other paired clients. Alternatives considered:commandso the existing spawn-time seed (fix(agents): a resumed agent's chat is readable on the phone before its first new turn #29664) fires. That changes how every agent's history resume is delivered, and old hosts gate command-bearing creates differently (assertLegacyAiVaultResumeCommandAllowed). Too broad for this gap.Linked Issue
Part of #10923: the "agent resumed from the phone's Session History" item still open after #29664 and #30273.
#30072 is not fixed here. That report shows the previous transcript and the model pill, so the phone already knew the session there. Its symptom (Send does nothing) is in the composer's send path, not in a missing identity. The maintainer's questions on that issue are still open.
Visual Proof
N/A. This changes host-published status data, not UI. The phone renders its normal chat once the identity arrives, and the e2e spec checks the phone's wire.
Testing
Reproduced, then fixed, on a real isolated headless
orca servewith a scripted phone client (tests/e2e/headless-serve-mobile-native-chat-identity.spec.ts, new case). The phone pairs, lists the saved Codex conversation through the host's ownaiVault.listSessions(written into the serve's isolated Codex home), builds the launch with the real phone builderbuildMobileAiVaultResumeLaunch, creates the tab, types the command, then readssession.tabs.listandnativeChat.readSession.providerSessionstaysnull(fails).src/main/runtime/mobile-session-resume-identity.test.ts: seeds the created pane once, with launch time 0, and not again on a retried create. Skips a create with no session and skips SSH. The seeding case fails on main.src/shared/rpc-contract/session-tabs-create-resume-session-param.test.ts: the field is accepted, a malformed value is dropped, and a fork refuses it.mobile/src/session/ai-vault-resume-launch.test.ts: the launch names the session (Codex, OMP), and the create sends it.Local: the full static-analysis job steps,
pnpm tc, mobiletypecheck-mobile, the tests-typecheck ratchet, mobilelintandformat:check, mobilesrc/session+src/agent-history+src/test-support/rpc-recording(309 files), and host unit folderssrc/main/ipc/pty,src/main/runtime/terminal*,src/main/runtime/rpc/methods,src/shared/rpc-contract.Verified live: macOS, isolated headless serve and scripted phone (fake Codex binary, real hooks path, real host scan). Covered only by tests: the desktop-window (renderer-backed) create path, Linux, and Windows.
I manually tested these changes locally
Automated tests added/updated, or explained why not below
Review
Codex (gpt-6.1-sol, high), two rounds:
Agent skill upstream boundary
Notes
session.tabs.createTerminalresume. New hosts take it throughterminal.ensureAgentSession, which already seeds.launchAgent, so it gets no seed. That is unchanged.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)