Skip to content

fix(mobile): a session resumed from the phone's history shows its conversation before the first new turn (#10923) - #30283

Merged
nwparker merged 1 commit into
mainfrom
nwparker/mobile-resume-provider-session
Oct 18, 2026
Merged

nwparker merged 1 commit into
mainfrom
nwparker/mobile-resume-provider-session

Conversation

@nwparker

@nwparker nwparker commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​252 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​251
Prod 6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​89 $\color{#cf222e}{\Huge{\mathbf{−}}}$​14 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​75

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 no agentStatus.providerSession and native chat had nothing to read.

After: the phone adds an optional resumeProviderSession ({ key, id, transcriptPath? }) to that session.tabs.createTerminal call. 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 next session.tabs.list carries agentType and providerSession, and nativeChat.readSession returns 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 shared getAiVaultAgentProviderSession (the same rule the desktop's history resume uses) and sends it on the create.
  • src/shared/rpc-contract/session-tabs-schemas-params.ts: CreateTerminalTab accepts the field, reusing terminal.create's schema (now exported as ResumeProviderSessionParam). 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 through seedResumeLaunchIdentity (split out of seedResumeLaunchIdentityFromSpawn in resume-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.
    • The seed is published with launch time 0. The pane exists only for this resume, so every row already on it is the agent's own, and a retry that reconciles a live pane can never replace that row.
    • SSH panes are skipped, because the relay on that host owns their status.
    • The shell spawn itself does not get the session. That would turn on the spawn-time Codex home selection for a plain shell, while the typed command already chooses its Codex home.

Wire compatibility: one new optional request field. An older host strips it (zod object is 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 optional agentStatus.providerSession earlier.

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:

  • Send the resume as the create's command so 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.
  • Guess the session from the Codex process or the newest rollout file. That breaks with two panes in one folder.

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 serve with 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 own aiVault.listSessions (written into the serve's isolated Codex home), builds the launch with the real phone builder buildMobileAiVaultResumeLaunch, creates the tab, types the command, then reads session.tabs.list and nativeChat.readSession.

    • On main's host: providerSession stays null (fails).
    • With this change: it names the session, and the transcript reads (passes). The two existing cases in the spec still pass.
  • 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, mobile typecheck-mobile, the tests-typecheck ratchet, mobile lint and format:check, mobile src/session + src/agent-history + src/test-support/rpc-recording (309 files), and host unit folders src/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:

  • Fixed: a Qoder resume retried after the 60s create cache could reconcile the live pane and re-seed it, which would replace the agent's report with an identity-only row. The seed now uses launch time 0, so it never replaces a row on its own pane.
  • Second round: no findings.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • SSH: skipped on purpose, because the relay owns those rows. Folder workspaces work the same way, since the seed is keyed by pane.
  • Not covered: a paired desktop or web client's legacy session.tabs.createTerminal resume. New hosts take it through terminal.ensureAgentSession, which already seeds.
  • Pi from the phone still takes the plain-command fallback, which sends no launchAgent, so it gets no seed. That is unchanged.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Mobile resume launch plans now carry provider-session metadata when available. The RPC contract validates that metadata and passes it to mobile terminal creation. Eligible local panes record resume identity after creation. Tests cover metadata parsing, launch planning, identity recording, retries, SSH-backed panes, and an end-to-end Codex resume.

Priority: ⬇️ Low

Merge Risk: 🟡 Moderate · up to cafd7

Some resumed Claude conversations may not display immediately, and a paired client could read a parseable JSONL file outside the intended transcript directories. Resolve those issues before merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 10 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main mobile resume behavior fix: showing the saved conversation before the first new turn.
Description check Passed The description is complete and aligned with the template. It explains the user impact, mechanism, rationale, linked issue, visual proof status, testing, compatibility considerations, review notes, an…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description is complete and aligned with the template. It explains the user impact, mechanism, rationale, linked issue, visual proof status, testing, compatibility considerations, review notes, and checklist results.

  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3313d3a8-df55-4468-aa95-867c2b1edc47
📥 Commits

Reviewing files that changed from the base of the PR and between c30753c and cafd7c9.

📒 Files selected for processing: 10
  • mobile/src/session/ai-vault-resume-launch.test.ts
  • mobile/src/session/ai-vault-resume-launch.ts
  • src/main/ipc/pty/pane/resume-launch-identity.ts
  • src/main/runtime/mobile-session-resume-identity.test.ts
  • src/main/runtime/rpc/methods/session-tabs.ts
  • src/main/runtime/terminal/launch/mobile-terminal-create.ts
  • src/shared/rpc-contract/session-tabs-create-resume-session-param.test.ts
  • src/shared/rpc-contract/session-tabs-schemas-params.ts
  • src/shared/rpc-contract/terminal-unary-params.ts
  • tests/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.

Comment on lines +114 to +117
const resumeProviderSession = getAiVaultAgentProviderSession({
...args.session,
...(resumeFilePath ? { filePath: resumeFilePath } : {})
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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/main

Repository: 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') {

Comment on lines +123 to +132
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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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/methods

Repository: 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.ts

Repository: 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 -180

Repository: 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.ts

Repository: 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' })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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

@nwparker
nwparker merged commit c82cf2b into main Oct 18, 2026
45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant