Repository navigation
Session History shows OpenCode, Grok, OMP and Pi chats as chats and refuses a second terminal writer - #27183
Conversation
…he agent registration Claude and Codex's transcript importers and Session History ownership (row id, resume-command grammar) move onto their runtime registrations as required-nullable fields; every other agent writes null. agentSession.agents publishes the derived transcriptAdoption/sessionHistory flags, and a client reads a host that omits them (or lists nothing) as the old build's answer. Strict parity: only Claude and Codex answer yes anywhere.
…w agents, fork offer from fork argv Review round 1: the duplicate-adoption check and Session History ownership read one index keyed by the registration's row id (an importer now requires sessionHistory); resume-command binaries derive from rowAgents through the TUI agent config; the Resume in New CLI offer asks getAgentForkArgv instead of naming Claude and Codex. Strict parity: only Claude and Codex answer yes.
An OpenCode, Grok, OMP or Pi native chat's conversation listed in Session History as a plain terminal row, and resuming it ran the agent's CLI as a second writer to the conversation the chat was still using. Each agent's registration now fills `sessionHistory`, so those rows show as the chat (its name, opening the chat) and Orca refuses a terminal resume of an owned conversation, as it already did for Claude and Codex. - OpenCode lists under `opencode` and `opencode2` (2.x) rows by its ses_ id; `-s/--session`, `-c/--continue` resume, `--fork` does not. - Grok: `-r/--resume/--load` and `-c`; a non-UUID value names a title, so it is read as target-less; `--fork-session` does not resume. - OMP: `-r/--resume/--session` take an id, any prefix of it, or the session file's path; bare `--resume` and `-c` are target-less. - Pi: rows are listed by the session file name's id while the chat holds the file's path; `--session <id|prefix|path>`, `-c`, `-r` (picker). - An optional `resumeTargetMatches` on the registration covers prefix and path targets; it defaults to exact equality. - Each agent now reads every token naming its own binary, so another agent's name earlier in a command (`claude --model pi`, `cd ~/src/codex && claude --resume X`) no longer hides a resume. For Claude and Codex this only ever refuses more than before. - A renamed OpenCode chat tab names its `opencode2` row too.
…t a resume command - The list and search projection now asks, per owning agent, whether the requesting client opens that agent's chat. Current mobile renders only Claude and Codex chats, so it no longer receives OpenCode/Grok/OMP/Pi rows owned by a chat (a tap there opened an empty terminal and showed agent_session_conflict). Claude and Codex keep their rule: any client that reads structured sessions. - An owned search hit opens its chat, so it no longer carries a terminal resume command (all agents; `orca` CLI search stopped printing one for a chat). - OpenCode/Grok/OMP/Pi flags match case-sensitively, as their CLIs read them, so `rg foo src/pi -C 3` is not `pi -c`. Claude and Codex still fold case. - A test feeds every resume command Orca builds for an owned row of each agent (Session History, live-tab argv; posix, PowerShell, cmd) into the guard.
…h no move - Each agent binary mention in a command now reads its arguments only up to that agent's next mention, which reads the rest, so every token is parsed once per agent. A 200 KB text naming agents throughout took 9.4 s on the host's main process; it now takes about 90 ms. Every refusal before still holds: a later mention still reads its own resume. - Right-clicking a native chat tab looks up its Session History row only when the agent's CLI can fork a conversation, the one move a chat tab offers, so OpenCode, Grok, OMP and Pi chat tabs run no list or rescan for nothing. Claude and Codex are unchanged.
The guard tokenizes the text once and splits it into shell commands at &&, ||, ;, |, & and newlines (a newline after a line-continuation mark stays inside its command). In each command, each agent reads its arguments from its first binary token to the command's end; a later token naming it is an argument. A `claude` path inside a Claude command no longer ends that command, so `--session-id <owned>` still cancels a fork after it, and a binary-named path between a resume and its fork flag keeps the fork a fork. Binary names are looked up once per token, which keeps a 16 MB send to about 2 s. Against main's parser, a Claude/Codex command is now allowed only when main refused it for a flag in a later shell command (`claude ; echo --resume X`).
A PTY's Enter, and many orca terminal send texts, end lines with \r. The guard now treats \r and \r\n as line breaks like \n, including after a line continuation mark, so a later line's resume is read as its own command.
…hat-capability-record
…ity-record' into brennanb2025/native-chat-history-all-agents
Review status: ready for your review (draft, not merged; stacked on #27078)The problem. A conversation you have with OpenCode, Grok, OMP or Pi in Orca's chat view showed up in Session History as a plain terminal row. "Resume" on it ran the agent in a terminal on the same conversation the chat was still using, so two programs wrote to one conversation. Claude and Codex chats were already protected, but that protection was written only for those two. This is the item deferred from #26981. What changes for users:
Fixed during review:
Deferred:
Verified:
Not verified:
|
… flags agentSession.agents no longer publishes transcriptAdoption or sessionHistory, so there is no new field for older and newer peers to reconcile. Their only consumers were renderer gates that could answer from what they already have: "Resume in New Native Chat" eligibility goes back to main's rule (adopting a CLI conversation is Claude/Codex-only, and the host still refuses any other agent from its registration's transcriptImport). Removes the AgentSessionDeclaredCapabilities split, the older-host decoder shim and the generic host-flag reader.
"Resume in New CLI" and the chat-tab menu read the agent name again instead of building a synthetic provider session to ask the fork builder, and the chat-tab lookup loses its host Session History flag gate. Only Claude and Codex CLIs fork, so only their chat tabs look up a row, exactly as on main: no new list requests.
…on union - listStructuredProviderSessionOwnership reads each record's registration for its Session History row id; the callback parameter and its one-function wrapper module are gone (no import cycle: checked value and type imports). - StructuredAgentCliConversations is a plain type: transcriptImport is nullable and sessionHistory is required, since every registered agent now owns its rows. The "importer needs history" union and its null checks go. - A test pins that no two registrations claim the same Session History row agent, since the owner lookup takes the first match.
Separators glued to a word (`opencode -s id; echo`, `cd /x&&claude -r id`) now end the command, and quotes are removed inside a word, so `--session="id"` reads as the id. Applies to every agent, Claude and Codex included. The acp-and-pi allowed() helper now asserts.
terminal.send checks agent prompts too, and agentPrompt does not keep the text from a shell (it falls back to a plain PTY write), so prompt text like "Fix the Pi --resume flag" was refused. The resume check now reads each command's program: its first word past VAR=x prefixes, a path, and env/exec/command/sudo/npx/bunx/pnpm dlx. Every command Orca's builders produce, Claude and Codex included and with a user's command override, is still refused.
…d option reader The default target match now ignores case (ids are UUIDs or ses_ ids), so Grok's override, which only repeated it, is gone. Claude folds its flags and reads them with resumeInvocationFromOptions instead of repeating the `=` handling.
A repeated regex group overflowed the stack on a 16 MB word. A sticky-regex scanner reads words, quoted runs and separators in one pass; an unclosed quote is searched for once per quote kind.
Live Electron QA at f4ce1d9: every agent, all rows passHidden macOS build of this PR, with the real OpenCode (2.0.14), Grok, OMP, Pi, Claude and Codex CLIs, each on its own existing sign-in. A separate agent drove every click.
The agents' real config files were hashed before and after. Only Grok's and Pi's own sign-in files changed: each agent refreshed its own token. Found along the way, not from this PR (the same on main):
Grok chat's row: titled with the chat's name; no Copy Resume Command or Delete: Opening the OMP chat's row switches to the chat tab: Searching a Pi chat's word finds the chat, with no resume command: Control: a plain terminal OpenCode conversation keeps Resume, Copy Resume Command and Delete: Claude unchanged: "Resume in New Native Chat" on a plain Claude conversation opens a chat that remembers it: |
📝 Walkthrough
Priority: ➖ Normal Merge Risk: 🟡 Moderate · up to A terminal command that runs through a shell, such as Pre-merge checks ·
|
…t rows Session History shows these agents' chat conversations under the title their CLI saved, as it does for their terminal conversations; the chat tab's name and renames stay on the tab. Claude and Codex rows keep the chat's name, as on main.
A Grok or Pi chat's Session History row and tab menu now offer "Resume in New CLI", like Claude and Codex: the CLI opens a copy of the conversation (grok --resume <id> --fork-session, pi --fork <file>) and the chat keeps its own. OpenCode's interactive CLI and OMP have no way to open a copy, so their chats keep only Resume.
…built A Pi chat found by search on a remote Orca server has no session file path, so its copy command can't be built; the row no longer offers the button there.
`pi --session-id <id>` reopens that session when it exists, so it is a second writer like --session.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟠 Major · Handle resume commands hidden behind shell… · structured-session-ownership.ts:254-259
src/main/ai-vault/structured-session-ownership.ts:254-259
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftHandle resume commands hidden behind shell interpreters.
The terminal entrypoints pass caller-supplied commands to this guard. For
bash -lc 'pi --session /path/to/owned.jsonl',shellCommandskeeps the quoted script as one word.commandProgramIndextherefore selectsbash, notpi. No invocation is found, so the guard returns before checking ownership. A shell can then resume the owned Pi session as a second terminal writer.Parse supported interpreter payloads such as
-cand-lcrecursively. If the payload cannot be inspected safely, refuse the command instead of treating it as having no resume invocation.Source: Learnings
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
c581a18e-771e-4c0e-b931-7f803a749484
📒 Files selected for processing (15)
src/main/ai-vault/structured-session-ownership-acp-and-pi.test.tssrc/main/ai-vault/structured-session-ownership-client-audience.test.tssrc/main/ai-vault/structured-session-ownership.tssrc/main/pi/pi-session-history.tssrc/renderer/src/components/right-sidebar/AiVaultSessionRow.structured-title.test.tsxsrc/renderer/src/components/right-sidebar/ai-vault-session-cli-fork.test.tssrc/renderer/src/components/right-sidebar/ai-vault-session-cli-fork.tssrc/renderer/src/components/right-sidebar/ai-vault-session-surface-switch.test.tssrc/renderer/src/components/right-sidebar/ai-vault-session-surface-switch.tssrc/renderer/src/components/right-sidebar/use-ai-vault-session-display-title.tssrc/renderer/src/components/tab-bar/tab-session-history-switch.test.tssrc/renderer/src/components/tab-bar/tab-session-history-switch.tssrc/shared/agent-resume-argv.tssrc/shared/structured-chat-row-name.tssrc/shared/tui-agent-fork-startup.test.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.
…efuses a second terminal writer (stablyai#27183) * refactor(chat): answer transcript adoption and Session History from the agent registration Claude and Codex's transcript importers and Session History ownership (row id, resume-command grammar) move onto their runtime registrations as required-nullable fields; every other agent writes null. agentSession.agents publishes the derived transcriptAdoption/sessionHistory flags, and a client reads a host that omits them (or lists nothing) as the old build's answer. Strict parity: only Claude and Codex answer yes anywhere. * refactor(chat): one Session History ownership index, binaries from row agents, fork offer from fork argv Review round 1: the duplicate-adoption check and Session History ownership read one index keyed by the registration's row id (an importer now requires sessionHistory); resume-command binaries derive from rowAgents through the TUI agent config; the Resume in New CLI offer asks getAgentForkArgv instead of naming Claude and Codex. Strict parity: only Claude and Codex answer yes. * feat(chat): Session History owns OpenCode, Grok, OMP and Pi native chats An OpenCode, Grok, OMP or Pi native chat's conversation listed in Session History as a plain terminal row, and resuming it ran the agent's CLI as a second writer to the conversation the chat was still using. Each agent's registration now fills `sessionHistory`, so those rows show as the chat (its name, opening the chat) and Orca refuses a terminal resume of an owned conversation, as it already did for Claude and Codex. - OpenCode lists under `opencode` and `opencode2` (2.x) rows by its ses_ id; `-s/--session`, `-c/--continue` resume, `--fork` does not. - Grok: `-r/--resume/--load` and `-c`; a non-UUID value names a title, so it is read as target-less; `--fork-session` does not resume. - OMP: `-r/--resume/--session` take an id, any prefix of it, or the session file's path; bare `--resume` and `-c` are target-less. - Pi: rows are listed by the session file name's id while the chat holds the file's path; `--session <id|prefix|path>`, `-c`, `-r` (picker). - An optional `resumeTargetMatches` on the registration covers prefix and path targets; it defaults to exact equality. - Each agent now reads every token naming its own binary, so another agent's name earlier in a command (`claude --model pi`, `cd ~/src/codex && claude --resume X`) no longer hides a resume. For Claude and Codex this only ever refuses more than before. - A renamed OpenCode chat tab names its `opencode2` row too. * fix(chat): show owned rows only to clients that open the chat, without a resume command - The list and search projection now asks, per owning agent, whether the requesting client opens that agent's chat. Current mobile renders only Claude and Codex chats, so it no longer receives OpenCode/Grok/OMP/Pi rows owned by a chat (a tap there opened an empty terminal and showed agent_session_conflict). Claude and Codex keep their rule: any client that reads structured sessions. - An owned search hit opens its chat, so it no longer carries a terminal resume command (all agents; `orca` CLI search stopped printing one for a chat). - OpenCode/Grok/OMP/Pi flags match case-sensitively, as their CLIs read them, so `rg foo src/pi -C 3` is not `pi -c`. Claude and Codex still fold case. - A test feeds every resume command Orca builds for an owned row of each agent (Session History, live-tab argv; posix, PowerShell, cmd) into the guard. * fix(chat): keep the resume guard linear and skip chat-tab lookups with no move - Each agent binary mention in a command now reads its arguments only up to that agent's next mention, which reads the rest, so every token is parsed once per agent. A 200 KB text naming agents throughout took 9.4 s on the host's main process; it now takes about 90 ms. Every refusal before still holds: a later mention still reads its own resume. - Right-clicking a native chat tab looks up its Session History row only when the agent's CLI can fork a conversation, the one move a chat tab offers, so OpenCode, Grok, OMP and Pi chat tabs run no list or rescan for nothing. Claude and Codex are unchanged. * fix(chat): read each shell command once in the resume guard The guard tokenizes the text once and splits it into shell commands at &&, ||, ;, |, & and newlines (a newline after a line-continuation mark stays inside its command). In each command, each agent reads its arguments from its first binary token to the command's end; a later token naming it is an argument. A `claude` path inside a Claude command no longer ends that command, so `--session-id <owned>` still cancels a fork after it, and a binary-named path between a resume and its fork flag keeps the fork a fork. Binary names are looked up once per token, which keeps a 16 MB send to about 2 s. Against main's parser, a Claude/Codex command is now allowed only when main refused it for a flag in a later shell command (`claude ; echo --resume X`). * fix(chat): end a resume-guard command at a bare carriage return A PTY's Enter, and many orca terminal send texts, end lines with \r. The guard now treats \r and \r\n as line breaks like \n, including after a line continuation mark, so a later line's resume is read as its own command. * refactor(chat): drop the transcript-adoption and Session History wire flags agentSession.agents no longer publishes transcriptAdoption or sessionHistory, so there is no new field for older and newer peers to reconcile. Their only consumers were renderer gates that could answer from what they already have: "Resume in New Native Chat" eligibility goes back to main's rule (adopting a CLI conversation is Claude/Codex-only, and the host still refuses any other agent from its registration's transcriptImport). Removes the AgentSessionDeclaredCapabilities split, the older-host decoder shim and the generic host-flag reader. * refactor(chat): keep main's Claude/Codex rule for the CLI fork offer "Resume in New CLI" and the chat-tab menu read the agent name again instead of building a synthetic provider session to ask the fork builder, and the chat-tab lookup loses its host Session History flag gate. Only Claude and Codex CLIs fork, so only their chat tabs look up a row, exactly as on main: no new list requests. * refactor(chat): one registration-aware ownership index, no registration union - listStructuredProviderSessionOwnership reads each record's registration for its Session History row id; the callback parameter and its one-function wrapper module are gone (no import cycle: checked value and type imports). - StructuredAgentCliConversations is a plain type: transcriptImport is nullable and sessionHistory is required, since every registered agent now owns its rows. The "importer needs history" union and its null checks go. - A test pins that no two registrations claim the same Session History row agent, since the owner lookup takes the first match. * fix(chat): read resume commands into words as a shell does Separators glued to a word (`opencode -s id; echo`, `cd /x&&claude -r id`) now end the command, and quotes are removed inside a word, so `--session="id"` reads as the id. Applies to every agent, Claude and Codex included. The acp-and-pi allowed() helper now asserts. * fix(chat): count an agent's name only where a shell would run it terminal.send checks agent prompts too, and agentPrompt does not keep the text from a shell (it falls back to a plain PTY write), so prompt text like "Fix the Pi --resume flag" was refused. The resume check now reads each command's program: its first word past VAR=x prefixes, a path, and env/exec/command/sudo/npx/bunx/pnpm dlx. Every command Orca's builders produce, Claude and Codex included and with a user's command override, is still refused. * refactor(chat): compare resume ids in any case; Claude uses the shared option reader The default target match now ignores case (ids are UUIDs or ses_ ids), so Grok's override, which only repeated it, is gone. Claude folds its flags and reads them with resumeInvocationFromOptions instead of repeating the `=` handling. * perf(chat): read terminal text in one linear pass A repeated regex group overflowed the stack on a 16 MB word. A sticky-regex scanner reads words, quoted runs and separators in one pass; an unclosed quote is searched for once per quote kind. * refactor(chat): read a command's resume only when its program is an agent * fix(chat): read an agent run under nohup, time or wsl as the command's program * chore: restore pnpm-lock.yaml * fix(chat): keep the CLI's own title on OpenCode, Grok, OMP and Pi chat rows Session History shows these agents' chat conversations under the title their CLI saved, as it does for their terminal conversations; the chat tab's name and renames stay on the tab. Claude and Codex rows keep the chat's name, as on main. * feat(chat): Resume in New CLI for Grok and Pi chats A Grok or Pi chat's Session History row and tab menu now offer "Resume in New CLI", like Claude and Codex: the CLI opens a copy of the conversation (grok --resume <id> --fork-session, pi --fork <file>) and the chat keeps its own. OpenCode's interactive CLI and OMP have no way to open a copy, so their chats keep only Resume. * fix(chat): offer Resume in New CLI only when the copy command can be built A Pi chat found by search on a remote Orca server has no session file path, so its copy command can't be built; the row no longer offers the button there. * fix(chat): refuse pi --session-id of a conversation a chat owns `pi --session-id <id>` reopens that session when it exists, so it is a second writer like --session. (cherry picked from commit e83e40e)
…efuses a second terminal writer (#27183) * refactor(chat): answer transcript adoption and Session History from the agent registration Claude and Codex's transcript importers and Session History ownership (row id, resume-command grammar) move onto their runtime registrations as required-nullable fields; every other agent writes null. agentSession.agents publishes the derived transcriptAdoption/sessionHistory flags, and a client reads a host that omits them (or lists nothing) as the old build's answer. Strict parity: only Claude and Codex answer yes anywhere. * refactor(chat): one Session History ownership index, binaries from row agents, fork offer from fork argv Review round 1: the duplicate-adoption check and Session History ownership read one index keyed by the registration's row id (an importer now requires sessionHistory); resume-command binaries derive from rowAgents through the TUI agent config; the Resume in New CLI offer asks getAgentForkArgv instead of naming Claude and Codex. Strict parity: only Claude and Codex answer yes. * feat(chat): Session History owns OpenCode, Grok, OMP and Pi native chats An OpenCode, Grok, OMP or Pi native chat's conversation listed in Session History as a plain terminal row, and resuming it ran the agent's CLI as a second writer to the conversation the chat was still using. Each agent's registration now fills `sessionHistory`, so those rows show as the chat (its name, opening the chat) and Orca refuses a terminal resume of an owned conversation, as it already did for Claude and Codex. - OpenCode lists under `opencode` and `opencode2` (2.x) rows by its ses_ id; `-s/--session`, `-c/--continue` resume, `--fork` does not. - Grok: `-r/--resume/--load` and `-c`; a non-UUID value names a title, so it is read as target-less; `--fork-session` does not resume. - OMP: `-r/--resume/--session` take an id, any prefix of it, or the session file's path; bare `--resume` and `-c` are target-less. - Pi: rows are listed by the session file name's id while the chat holds the file's path; `--session <id|prefix|path>`, `-c`, `-r` (picker). - An optional `resumeTargetMatches` on the registration covers prefix and path targets; it defaults to exact equality. - Each agent now reads every token naming its own binary, so another agent's name earlier in a command (`claude --model pi`, `cd ~/src/codex && claude --resume X`) no longer hides a resume. For Claude and Codex this only ever refuses more than before. - A renamed OpenCode chat tab names its `opencode2` row too. * fix(chat): show owned rows only to clients that open the chat, without a resume command - The list and search projection now asks, per owning agent, whether the requesting client opens that agent's chat. Current mobile renders only Claude and Codex chats, so it no longer receives OpenCode/Grok/OMP/Pi rows owned by a chat (a tap there opened an empty terminal and showed agent_session_conflict). Claude and Codex keep their rule: any client that reads structured sessions. - An owned search hit opens its chat, so it no longer carries a terminal resume command (all agents; `orca` CLI search stopped printing one for a chat). - OpenCode/Grok/OMP/Pi flags match case-sensitively, as their CLIs read them, so `rg foo src/pi -C 3` is not `pi -c`. Claude and Codex still fold case. - A test feeds every resume command Orca builds for an owned row of each agent (Session History, live-tab argv; posix, PowerShell, cmd) into the guard. * fix(chat): keep the resume guard linear and skip chat-tab lookups with no move - Each agent binary mention in a command now reads its arguments only up to that agent's next mention, which reads the rest, so every token is parsed once per agent. A 200 KB text naming agents throughout took 9.4 s on the host's main process; it now takes about 90 ms. Every refusal before still holds: a later mention still reads its own resume. - Right-clicking a native chat tab looks up its Session History row only when the agent's CLI can fork a conversation, the one move a chat tab offers, so OpenCode, Grok, OMP and Pi chat tabs run no list or rescan for nothing. Claude and Codex are unchanged. * fix(chat): read each shell command once in the resume guard The guard tokenizes the text once and splits it into shell commands at &&, ||, ;, |, & and newlines (a newline after a line-continuation mark stays inside its command). In each command, each agent reads its arguments from its first binary token to the command's end; a later token naming it is an argument. A `claude` path inside a Claude command no longer ends that command, so `--session-id <owned>` still cancels a fork after it, and a binary-named path between a resume and its fork flag keeps the fork a fork. Binary names are looked up once per token, which keeps a 16 MB send to about 2 s. Against main's parser, a Claude/Codex command is now allowed only when main refused it for a flag in a later shell command (`claude ; echo --resume X`). * fix(chat): end a resume-guard command at a bare carriage return A PTY's Enter, and many orca terminal send texts, end lines with \r. The guard now treats \r and \r\n as line breaks like \n, including after a line continuation mark, so a later line's resume is read as its own command. * refactor(chat): drop the transcript-adoption and Session History wire flags agentSession.agents no longer publishes transcriptAdoption or sessionHistory, so there is no new field for older and newer peers to reconcile. Their only consumers were renderer gates that could answer from what they already have: "Resume in New Native Chat" eligibility goes back to main's rule (adopting a CLI conversation is Claude/Codex-only, and the host still refuses any other agent from its registration's transcriptImport). Removes the AgentSessionDeclaredCapabilities split, the older-host decoder shim and the generic host-flag reader. * refactor(chat): keep main's Claude/Codex rule for the CLI fork offer "Resume in New CLI" and the chat-tab menu read the agent name again instead of building a synthetic provider session to ask the fork builder, and the chat-tab lookup loses its host Session History flag gate. Only Claude and Codex CLIs fork, so only their chat tabs look up a row, exactly as on main: no new list requests. * refactor(chat): one registration-aware ownership index, no registration union - listStructuredProviderSessionOwnership reads each record's registration for its Session History row id; the callback parameter and its one-function wrapper module are gone (no import cycle: checked value and type imports). - StructuredAgentCliConversations is a plain type: transcriptImport is nullable and sessionHistory is required, since every registered agent now owns its rows. The "importer needs history" union and its null checks go. - A test pins that no two registrations claim the same Session History row agent, since the owner lookup takes the first match. * fix(chat): read resume commands into words as a shell does Separators glued to a word (`opencode -s id; echo`, `cd /x&&claude -r id`) now end the command, and quotes are removed inside a word, so `--session="id"` reads as the id. Applies to every agent, Claude and Codex included. The acp-and-pi allowed() helper now asserts. * fix(chat): count an agent's name only where a shell would run it terminal.send checks agent prompts too, and agentPrompt does not keep the text from a shell (it falls back to a plain PTY write), so prompt text like "Fix the Pi --resume flag" was refused. The resume check now reads each command's program: its first word past VAR=x prefixes, a path, and env/exec/command/sudo/npx/bunx/pnpm dlx. Every command Orca's builders produce, Claude and Codex included and with a user's command override, is still refused. * refactor(chat): compare resume ids in any case; Claude uses the shared option reader The default target match now ignores case (ids are UUIDs or ses_ ids), so Grok's override, which only repeated it, is gone. Claude folds its flags and reads them with resumeInvocationFromOptions instead of repeating the `=` handling. * perf(chat): read terminal text in one linear pass A repeated regex group overflowed the stack on a 16 MB word. A sticky-regex scanner reads words, quoted runs and separators in one pass; an unclosed quote is searched for once per quote kind. * refactor(chat): read a command's resume only when its program is an agent * fix(chat): read an agent run under nohup, time or wsl as the command's program * chore: restore pnpm-lock.yaml * fix(chat): keep the CLI's own title on OpenCode, Grok, OMP and Pi chat rows Session History shows these agents' chat conversations under the title their CLI saved, as it does for their terminal conversations; the chat tab's name and renames stay on the tab. Claude and Codex rows keep the chat's name, as on main. * feat(chat): Resume in New CLI for Grok and Pi chats A Grok or Pi chat's Session History row and tab menu now offer "Resume in New CLI", like Claude and Codex: the CLI opens a copy of the conversation (grok --resume <id> --fork-session, pi --fork <file>) and the chat keeps its own. OpenCode's interactive CLI and OMP have no way to open a copy, so their chats keep only Resume. * fix(chat): offer Resume in New CLI only when the copy command can be built A Pi chat found by search on a remote Orca server has no session file path, so its copy command can't be built; the row no longer offers the button there. * fix(chat): refuse pi --session-id of a conversation a chat owns `pi --session-id <id>` reopens that session when it exists, so it is a second writer like --session. (cherry picked from commit e83e40e)





ELI5
When you talk to OpenCode, Grok, OMP or Pi in Orca's chat view, the conversation still shows up in Session History as a plain terminal conversation, not as your chat. Clicking resume on it runs the agent in a terminal (for example
opencode --session <id>) on the same conversation your chat is still using, so two programs write to one conversation at once. Claude and Codex chats already avoid this. This PR gives the other four agents the same behaviour.What Changed
User-facing:
orca terminal send) gets the same "this conversation is open in a chat" refusal Claude and Codex already get.opencode -c,grok -c, a baregrok --resume,omp -c, a bareomp --resume,pi -cand a barepi -r.claude -c.orcaCLI search no longer printsresume: claude --resume <id>for a chat's conversation.&&,||,;,|,&and line breaks, also when they touch a word (opencode -s <id>; echo,cd /x&&claude -r <id>), but never inside quotes. A line ending in\, a backtick or^continues onto the next line.--session="<id>"reads as the id.VAR=xprefixes, a path, orenv,exec,command,sudo,nohup,time,npx,bunx,pnpm dlxorwsl --.claude -r <owned>;), a quoted value after=, or the id in another case.claude -r <other> ; claude -r <owned>. Main read only the first mention.echo claude -r <owned>, an agent run through a wrapper not in the list above (xargs, an option value such assudo -u bob), or nested inbash -c '…'.cd ~/src/codex && claude --resume <owned>.claude ; echo --resume <id>.grok --resume <id> --fork-session,pi --fork <session file>). It shows in the Session History row and the chat tab's right-click menu.Mechanism:
Each agent's registration says how its chats own Session History rows. Every agent registered for native chat carries a required
sessionHistoryentry:Shared code reads these entries instead of checking agent names.
claude--resume/-r,--continue/-c;--fork-sessionis allowedcodexresume;codex forkis allowedopencodeandopencode2ses_id--session/-sgrok--resume/-r/--load; a value that isn't an id is a title lookup, so it's refusedomp--resume/-r/--session, which also accept an id prefix or a file path; a bare--resumeopens a picker, so it's refusedpi--sessionwith an id, prefix or path, and--session-id(reopens that id when it exists); a bare-r/--resumeopens a picker, so it's refusedFor the four new agents,
--continue/-cnames no conversation and fork flags are allowed.Adoption reads the registration too. The registration's
transcriptImportsays how to find a conversation the agent's own CLI recorded.One ownership index. The duplicate-adoption check and Session History both read one list of which chat owns which conversation. Each row id comes from the owning agent's registration. A test pins that no two registrations claim the same kind of history row.
Per-client row visibility. Session History decides per row whether the requesting client can open that agent's chat, reusing the existing "does this client render this agent" check. Claude and Codex rows behave as before for every client.
Nothing new crosses the wire. Clients and remote hosts exchange the same messages as before; ownership is worked out on the host that runs the chat.
Why
This is the third bug of one kind: a feature written for Claude and Codex never reached the newer agents. The earlier two were orchestration workers (#26776) and the floating-workspace folder (#26981).
Each agent's registration now answers "which history rows does this agent's chat own". A future agent gets this behaviour by filling in its own entry, instead of someone hunting down agent-name checks spread through shared code.
Matching the command text is still needed because mobile, including older mobile builds, sends resumes as plain text, and
orca terminal sendcarries typed commands.Alternatives considered:
Linked Issue
N/A: maintainer follow-up. It is a deferred item from #26981 (floating-workspace chats of every agent). It replaces #27078, whose registration changes it now includes in slimmer form.
Visual Proof
QA on a hidden, isolated macOS test build (window code at cdd7221, background process at 216ba66), clicked by a separate test agent. OpenCode, Grok, OMP and Pi ran with isolated homes and a local fake model; Claude and Codex were the real signed-in CLIs.
OpenCode: the chat tab is renamed "Renamed OC tab", but its Session History row keeps OpenCode's own title ("QA chat", from the fake model). Opening the row opens the chat, and there is no "Resume in New CLI".

Pi: the row keeps Pi's title (its first prompt) and offers "Resume in New CLI", which ran

pi --fork <session file>in a new terminal.After the copy, the chat still answers into its own conversation; the copy is a second Pi row (same title, since both start with the same prompt).

Grok: "Resume in New CLI" is offered and opened

grok --resume <id> --fork-session.OMP: no "Resume in New CLI" (its CLI can't open a copy).

CLI copies, checked outside the app with throwaway homes:
pi --forkandgrok --resume <id> --fork-sessioneach created a new session and left the original byte-identical.Claude and Codex (control): rows still show the chat's name and offer "Resume in New CLI".
Seen in QA, not changed here: new rows and message counts appear only after Refresh (existing indexing behaviour), and a CLI copy of a Pi chat shows the same title as the chat's row.
Testing
=values, command position, and prompt text.node:sqlite, which this machine's test runtime lacks. They fail the same way on main.Agent skill upstream boundary
Notes
Known limits, deferred:
opencode -cs <id>);bash -c '…',$( … )or backticks;/My\ Apps/claude);;, which is an argument there but a separator here;cmd /d /s /c "cd /d ""C:/x"" && opencode --session ""<id>"""form. This one predates this PR, and Claude and Codex have the same gap.agent_session_conflictwhen it refuses.Other notes:
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)