Repository navigation
fix(mobile): keep a Codex pane's chat history after the phone app restarts - #25358
Closed
brennanb2025 wants to merge 7 commits into
Closed
brennanb2025 wants to merge 7 commits into
brennanb2025 wants to merge 7 commits into
Conversation
…ersation On a headless host, a Codex pane idle under its neutral thread title keeps its session in the hook store, yet a fresh session.tabs list gives the phone no agentStatus, so native chat has no transcript to open. Fails on main.
…tarts On a headless host, an idle Codex pane sits under its neutral `<thread> | <project>` title. The status builder treated every non-agent title as the shell reclaiming the pane and returned no status, which also dropped the provider session the hook store still held, so a restarted phone had no transcript to open and showed an empty chat. A neutral (not shell or management) title now relays the pane's conversation identity as a completion-neutral session-boundary carrier. It rides under a private symbol that JSON drops, is handed explicitly from the builder to the projected tab, and the per-audience step consumes it for every client but folds it into agentStatus only for phones, so paired desktops keep receiving exactly what they did before. Live-state rules, shell titles and Claude panes are unchanged. The status-row index moves to its own module to keep the projection under its line limit.
…not go Every tab-bearing session.tabs route through the real dispatcher and JSON gives the identity only to phones; createTerminal and adoptOrphans send no carrier; a carrier-only change sends a runtime subscriber no extra frame; a paired desktop ends exactly as on main; and the phone's resolver opens the conversation from the session-boundary status.
…odex-transcript-7370
…he aggregate routes - The conversation identity carrier now includes the model (and OMP's model switch command) reported on the same hook row as the provider session, so a restarted phone's model picker is seeded like a connected phone's. - A real-runtime test drives session.tabs.listAll and the subscribeAll census after the idle restoration: a phone gets the carrier, a paired runtime gets no status. - Frames with no carrier skip the per-tab copy in the audience step. - Split the builder's two-rule comment into one line per rule.
…odex-transcript-7370
5 of 9 tasks
Contributor
Author
|
Close this older history patch while preserving the idle-terminal conversation problem. The phone should learn the conversation directly from its host, even when the terminal title changes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ELI5
When Codex finishes a turn in a terminal on a headless
orca servehost, the phone forgets which Codex conversation that terminal holds. The same happens to any agent's tab after you rename it. The next time the phone app starts, the pane's Chat says "Start a chat with Codex" and shows no history, even though the server still has the conversation. This PR makes the server keep telling the phone which conversation the pane holds, so the history comes back.What Changed
The problem. On a headless
orca servehost, you start a Codex pane (for exampleorca worktree create --agent codexwith a prompt) and let it finish, with Codex still open. While the phone stays connected, the pane's Chat view shows the transcript. After the phone app restarts, reopening the worktree shows "Start a chat with Codex" with no history. The paired desktop still shows the conversation, the Codex session file is still on the server, and a Claude pane on the same server keeps its history.Why it happened. The phone learns which conversation a terminal pane holds only from the pane's status in the server's session-tab list. On a headless host there is no desktop window feeding that status, so the server builds it from the terminal title and the agent hooks. That builder drops all status, including the conversation id, whenever the terminal title doesn't look like an agent's. The rule was meant for "the shell took the pane back", but it also caught any title it didn't recognise.
When Codex goes idle, its title becomes just the thread name and project, for example
Say hi | my-repo, which Orca doesn't recognise as an agent title. Once Orca confirms Codex is still running and marks the pane idle, the next projection drops the conversation id. That confirmation does not republish to a connected phone, so it keeps its last good frame. A restarted phone asks again and gets a pane with no conversation.Claude's idle title starts with
✳, which Orca recognises, so an un-renamed Claude pane doesn't hit this. A renamed tab of any agent, Claude included, does: the rename outranks the agent's own titles, so the builder sees a title it doesn't recognise.The rule is not Codex-specific. It applies to any pane where the host has positive evidence that an agent is present and idle, a title it doesn't recognise, and a session in its hook store. The desktop survives because it keeps its own copy of the status and remembers the last session it opened.
What you see now.
How it works. The server's hook store still holds the pane's conversation id after the turn. Under a title that isn't an agent's and isn't a shell or management screen, the status builder no longer throws that id away. It still shows no live state there. The builder hands the id on as a "connected and landed idle" status. That status has Orca's existing
sessionBoundaryflag, which every completion consumer (notifications, unread badges, finished timestamps, automations) already ignores. It carries no prompt, tool, question or turn facts and is dated by when the id was observed, so repeated updates are identical.That status is sent to phones only. A paired desktop overwrites its own status row with whatever the server sends. So sending it this status would erase the desktop's own completion time, unread Activity entry, interrupted flag and turn duration. To keep it off the desktop, the server attaches the status to the tab under a private symbol key, which JSON serialisation drops.
The step that already tailors each session-tab frame to its client removes the symbol for every client. It folds the status in only for phones, and only when the tab has no real status. Every response that skips that step serialises exactly as before, now and for any route added later. Those responses include terminal creation and orphan adoption.
Files:
src/main/runtime/mobile-conversation-identity-carrier.ts(new): the private symbol and its helpers, the status builder, and the check that the session belongs to the pane's agent.runtime-mobile-agent-status-builder.ts: relays the id under a neutral title instead of returning nothing.runtime-mobile-session-projection.ts: copies the id onto the freshly built client tab, because that tab is rebuilt field by field.runtime-mobile-session-status-row-lookup.tsbecause the file was at its line limit.rpc/methods/session-tab-agent-status-projection.ts: removes the symbol for every client and folds it in for phones.No phone app, desktop app, RPC parameter, hook store or transcript reader code changed.
Why
Compatibility (clients and servers update independently): no new wire field, parameter or opcode. Only the content sent to phones changes.
Follow-up (in progress): publish the pane's conversation identity (agent, session, model) as its own optional field to every client, independent of status, following the common pattern. That also covers a pre-existing gap: a paired desktop that restarts while such a pane is idle gets no session id, and survives today only from in-memory state. It will replace this PR's phone-only carrier. Draft: #25511 (stacked on this PR).
Known limits, not changed here:
Linked Issue
Fixes #20408 (STA-7370).
Likely the same cause as #13494 (STA-3802), but that report has too little detail to confirm. Not claimed as fixed.
#13629 (STA-3838, Codex goal mode shows only "agent is working") is a separate cause: goal-mode messages are dropped when the transcript is read. That follows up separately, building on #13985 by @OrcaWin.
Visual Proof
Phone QA on an Android emulator against a headless host with a seeded Codex session is in progress. Before and after screenshots will be embedded here.
Testing
Regression test (
orca-runtime-codex-idle-conversation-identity.test.ts). It uses a real hook store and real/hook/codexposts (SessionStart, UserPromptSubmit and Stop carryingsession_idandtranscript_path). A stub terminal whose foreground process iscodexshows the spinner title, then the idleSay hi | my-repotitle. Presence is unverifiable or unknown.session.tabs.listtab had no status.Builder unit tests: neutral, shell and management titles; fresh, aged and leftover sessions; owner and mismatch combinations; and the real builder through the real projection to the phone.
Per-client tests and every route out:
Desktop check: the real desktop session-tab mirror (completion time, finished timestamp, Activity unread, chat duration) behaves exactly as it does with main's frames. Rows the desktop owns are fully preserved.
Fix removed: with the production change reverted, 30 of the new tests fail.
Totals: 14 root test files, 1553 passing (including the full
orca-runtime.test.tssuite), plus the phone'smobile-native-chat-eligibility.test.ts.Checks:
tc:node, the mobile typecheck and its tests-typecheck ratchet, oxlint, the anti-slop lint andcheck:code-quality:changedare clean.tc:web's only error was inrich-markdown-image-insert.test.ts, which this PR doesn't touch; main has since fixed it in fix(test): pass getInsertionRange in the image-insert access test #25351, which is merged in.Not run: real Codex, real-device timing between the live phone and the idle confirmation, Linux and Windows foreground process naming, and remote transcript readability over SSH.
I manually tested these changes locally (phone QA in progress)
Automated tests added/updated, or explained why not below
Review
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
Security: no new data leaves the server except the conversation id the phone already received before the turn ended, and only to phone clients. Cross-platform, SSH, mobile and backwards compatibility are covered above. Performance: one extra object per affected tab per projection.
Author: @BrennanKB5
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)