Skip to content

Session History shows OpenCode, Grok, OMP and Pi chats as chats and refuses a second terminal writer - #27183

Merged
brennanb2025 merged 23 commits into
mainfrom
brennanb2025/native-chat-history-all-agents
Oct 12, 2026
Merged

brennanb2025 merged 23 commits into
mainfrom
brennanb2025/native-chat-history-all-agents

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 12 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1205 $\color{#cf222e}{\Huge{\mathbf{−}}}$​124 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1081
Prod 27 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​791 $\color{#cf222e}{\Huge{\mathbf{−}}}$​209 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​582

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:

  • Your chat's conversation opens your chat. In Session History (the desktop list and search), opening an OpenCode, Grok, OMP or Pi conversation that a chat owns opens the chat. Copy-resume and delete are hidden, as for Claude and Codex.
    • The row keeps the title the agent's CLI saved, the same title its terminal conversations show (Pi saves none, so it shows the first prompt). The chat tab keeps its own name, and renaming the tab doesn't change Session History, just as for terminals.
    • Claude and Codex rows are unchanged: they still show the chat's name.
  • A terminal resume of a chat's conversation is refused. A resume Orca launches for such a conversation (from Session History, a session tab, mobile, or orca terminal send) gets the same "this conversation is open in a chat" refusal Claude and Codex already get.
    • This includes resumes that name no conversation: opencode -c, grok -c, a bare grok --resume, omp -c, a bare omp --resume, pi -c and a bare pi -r.
    • They are refused while any saved chat of that agent exists, because they could pick the chat's conversation. That is the existing rule for claude -c.
  • Mobile builds that can't open these agents' chats no longer see these conversations. Tapping one there would have reopened it in a terminal, as a second writer. Mobile hides them, as clients without chat support already do.
  • Search results no longer offer a terminal resume command for a conversation a chat owns, for any agent, Claude and Codex included.
    • For example, orca CLI search no longer prints resume: claude --resume <id> for a chat's conversation.
    • Opening the result opens the chat.
  • The resume check reads a command line the way a shell would, for every agent, Claude and Codex included.
    • It splits text into commands at &&, ||, ;, |, & 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.
    • It removes quotes the way a shell does, so --session="<id>" reads as the id.
    • An agent counts only as a command's program: the first word after VAR=x prefixes, a path, or env, exec, command, sudo, nohup, time, npx, bunx, pnpm dlx or wsl --.
    • A name anywhere else is an argument or prompt text. So "Fix the Pi --resume flag", sent while a Pi chat exists, is no longer refused.
    • Resume ids compare ignoring case.
    • What this changes for Claude and Codex:
      • Newly refused: a resume of the owned conversation with a separator glued on (claude -r <owned>;), a quoted value after =, or the id in another case.
      • Also newly refused: claude -r <other> ; claude -r <owned>. Main read only the first mention.
      • Newly allowed, because the agent is not the program: echo claude -r <owned>, an agent run through a wrapper not in the list above (xargs, an option value such as sudo -u bob), or nested in bash -c '…'.
      • Still refused: cd ~/src/codex && claude --resume <owned>.
      • Still allowed: claude ; echo --resume <id>.
  • "Resume in New CLI" now works for Grok and Pi chats too. It opens a copy of the conversation in a terminal and leaves the chat alone, as for Claude and Codex (grok --resume <id> --fork-session, pi --fork <session file>). It shows in the Session History row and the chat tab's right-click menu.
    • OpenCode's interactive CLI and OMP have no way to open a copy, so their chats keep only Resume (which opens the chat). A follow-up will get the copy from the running chat instead.
  • Unchanged here: "Resume in New Native Chat" (turning a terminal conversation into a chat) is still Claude and Codex only. Grok and OMP get it in Resume in New Native Chat for Grok and OMP terminal conversations #28049, stacked on this PR; OpenCode and Pi follow.

Mechanism:

  • Each agent's registration says how its chats own Session History rows. Every agent registered for native chat carries a required sessionHistory entry:

    • which history rows it owns;
    • the row id for a chat's conversation;
    • how to read a terminal resume of it.

    Shared code reads these entries instead of checking agent names.

    Agent Rows it owns Row id Resume forms it reads
    Claude claude the session id --resume/-r, --continue/-c; --fork-session is allowed
    Codex codex the thread id resume; codex fork is allowed
    OpenCode opencode and opencode2 the ses_ id --session/-s
    Grok grok the id --resume/-r/--load; a value that isn't an id is a title lookup, so it's refused
    OMP omp the id in the session file's header --resume/-r/--session, which also accept an id prefix or a file path; a bare --resume opens a picker, so it's refused
    Pi pi the id in the session file's name (Pi's chat stores the file path) --session with an id, prefix or path, and --session-id (reopens that id when it exists); a bare -r/--resume opens a picker, so it's refused

    For the four new agents, --continue/-c names no conversation and fork flags are allowed.

  • Adoption reads the registration too. The registration's transcriptImport says how to find a conversation the agent's own CLI recorded.

    • Claude and Codex fill it in; the others leave it empty.
    • The host refuses to adopt for an agent without one, as before.
    • This moves the Claude/Codex branches out of the shared adoption code.
  • 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 send carries typed commands.

Alternatives considered:

  • Publishing per-agent "adopts transcripts" and "owns history" flags to clients. This was the first version (Native chat: answer 'can this agent do X' from each agent's registration #27078) and is dropped. Each flag's only reader could already answer from what it had, and a new field must be reconciled between old and new clients and hosts.
  • Refusing only where Orca builds the command. That misses older mobile clients and typed commands.
  • Leaving these agents unprotected. That leaves the second-writer bug in place.

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".
OpenCode row keeps its CLI title after a tab rename

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.
Pi row menu with Resume in New CLI

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).
Pi chat still answers after the copy

Grok: "Resume in New CLI" is offered and opened grok --resume <id> --fork-session.
Grok row menu with Resume in New CLI

OMP: no "Resume in New CLI" (its CLI can't open a copy).
OMP row menu without Resume in New CLI

CLI copies, checked outside the app with throwaway homes: pi --fork and grok --resume <id> --fork-session each 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

  • Unit tests per agent:
    • Owned rows and search hits open the chat; Claude and Codex rows show the chat's name, the other four keep their CLI's title.
    • "Resume in New CLI" is offered for Claude, Codex, Grok and Pi chats and not for OpenCode or OMP, and every copy command Orca builds passes the resume check.
    • Each resume form is refused or allowed as intended.
    • Every command Orca's own builders produce for an owned row is refused, in posix, PowerShell and cmd, with or without a custom agent command, for all seven history row kinds including Claude and Codex.
    • Shell-reading tests for every agent cover glued and quoted separators, quoted = values, command position, and prompt text.
    • The check reads text in one pass. 16 MB takes about 0.5–3 s on this machine, comparable to main.
    • Rows are visible per client kind.
    • No two agents claim the same history row kind.
    • An agent with no transcript importer is refused before any adoption work.
  • Claude/Codex:
    • Every existing test assertion is kept.
    • The only commands main refused that this branch allows are the "newly allowed" forms listed under What Changed.
  • Checks:
    • The full local static-analysis and typecheck run (24 checks) passes on cdd7221.
    • The targeted ai-vault, runtime, tab-menu and right-sidebar suites pass.
    • The only local failures are tests that need node:sqlite, which this machine's test runtime lacks. They fail the same way on main.
  • I manually tested these changes locally
  • Automated tests added/updated

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

Known limits, deferred:

  • The check doesn't catch hand-typed command forms it can't parse:
    • clustered or repeated short flags (opencode -cs <id>);
    • an agent run through a wrapper not in the list, or nested in bash -c '…', $( … ) or backticks;
    • a backslash-escaped space in the program's path (/My\ Apps/claude);
    • cmd.exe's ;, which is an argument there but a separator here;
    • the stored 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.
  • Pre-existing, and the same for Claude and Codex:
    • An owned row's details panel still labels its action "Resume in Worktree", though clicking it focuses the chat.
    • The CLI prints the raw agent_session_conflict when it refuses.
  • A chat created on OpenCode 1.x and migrated to 2.x shows as two rows with the same name.
  • After a Pi fork or a failed restore, the chat still owns the old conversation, as Claude and Codex chats do today.

Other notes:

  • Remote/SSH: ownership is decided on the host that runs the chat, and no new message crosses between client and host.
    • An older desktop paired with a newer host also gets these agents' chat-owned rows marked as belonging to the chat. That's because the older desktop reports that it can show these agents' chats.
    • Main's row code acts on that mark for any agent, so it should open the chat. This is read from code, not run.
  • Mobile: current mobile can't open these agents' chats from history, so their owned rows are hidden there. Making mobile open chats from Session History (for Claude and Codex too) is a separate follow-up.

Checklist

  • 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)

…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.
…ity-record' into brennanb2025/native-chat-history-all-agents
@brennanb2025

Copy link
Copy Markdown
Contributor Author

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:

  • Session History shows the chat. An OpenCode/Grok/OMP/Pi conversation a chat owns shows with the chat's name. Opening it opens the chat, and copy-resume and delete are hidden.
  • Terminal resumes of it are refused. Any terminal resume Orca launches for an owned conversation is refused, including "most recent" resumes like opencode -c while a chat of that agent exists. Claude/Codex work the same way.
  • Older mobile hides these rows. Mobile builds that can't open these chats no longer see such rows, rather than starting a second writer.
  • Search hits drop the resume command. Search hits for an owned conversation no longer carry a terminal resume command, for any agent.
  • The resume check reads command lines shell-style. It splits at &&, ;, | and line breaks. For Claude/Codex:
    • A resume within one command that main refused is still refused.
    • A resume flag sitting in a different command from the program is no longer read as one. For example, claude ; echo --resume X was refused and is now allowed.

Fixed during review:

  • Round 1:
    • Hid owned rows from clients that can't open that agent's chat; the first version showed them to current mobile, where a tap failed with a raw error.
    • Dropped the resume command from owned search hits.
    • Made the new agents' flag matching case-sensitive, so rg … src/pi -C 3 is no longer refused.
    • Added tests that feed Orca's own resume-command builders into the check.
  • Readiness:
    • The check took seconds on large terminal send text; it is now linear (16 MB in about 1–2 s).
    • Right-clicking these agents' chat tabs ran a pointless history scan; that is gone.
  • Narrow rounds 3–4: replaced a span rule with "split into shell commands, then each agent reads from its first mention". That fixed a Claude command main refused that the span version allowed, false refusals of real forks, and a bare carriage return not ending a command.
  • End: merged Native chat: answer 'can this agent do X' from each agent's registration #27078, which includes current main, with no conflicts.

Deferred:

  • Hand-typed command forms not parsed:
    • Glued or clustered short flags such as opencode -cs <id>, and repeated flags.
    • A resume nested in $( … ) after another resume, or a continuation mark used for a non-matching shell. Main allows both too.
    • cmd.exe's ;, which is an argument there.
  • Possible false refusal: claude --model pi -r X while a Pi chat exists.
  • Duplicate rows: an OpenCode 1.x chat migrated to 2.x shows two rows with the same name.
  • Old conversations stay owned: after a Pi fork or a failed restore, the old conversation stays owned. This matches Claude/Codex.
  • Pre-existing for every agent, seen in QA:
    • The details panel says "Resume in Worktree" on owned rows (it opens the chat).
    • The CLI prints the raw agent_session_conflict.
    • Naming a chat creates an extra OpenCode conversation in the app folder.
  • Follow-up: mobile opening chats from Session History.

Verified:

  • QA on a hidden macOS build (207f0d6) with real OpenCode 2.0.21 and a local fake model:
    • The owned row and search hit open the chat.
    • opencode --session <owned> and opencode -c are refused, and no OpenCode process starts.
    • A plain terminal conversation still resumes in a terminal.
    • Screenshots are in the PR body.
  • Fuzz against main's parser (300,000 Claude/Codex commands): no resume inside a single command that main refused is allowed now.
  • Every command Orca builds for an owned row of each agent is refused, on posix, PowerShell and cmd.
  • Local checks: pnpm tc:node, pnpm tc:web, all 24 orca-ci-checks, and the targeted tests pass at a357a6b.

Not verified:

  • Grok/OMP/Pi live. Running real Grok writes the real Grok home, and the QA machine's Pi is too old for chat. They are covered by unit tests. Grok's id match and Pi's grammar are from code and docs, not observed.
  • Windows/Linux/SSH live.
  • Mobile on a device.
  • This PR's full CI suite only runs on PRs into main, so it runs once Native chat: answer 'can this agent do X' from each agent's registration #27078 merges and this PR retargets main.

… 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.
@brennanb2025
brennanb2025 changed the base branch from brennanb2025/native-chat-capability-record to main October 12, 2026 06:42
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.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Live Electron QA at f4ce1d9: every agent, all rows pass

Hidden 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.

Check Result
A chat per agent, one exchange, tab renamed to "<Agent> QA chat" Pass, all 6
Session History row titled with the chat's name; menu has no Copy Resume Command or Delete Pass, all 6
Opening the row switches to the chat tab; no terminal or new tab is created Pass, all 6
Searching for the chat's word finds the chat, with no resume command (resumeCommand: null) Pass, all 6
Terminal resume of the chat's conversation is refused and starts nothing Pass: opencode --session, opencode2 --standalone --session, grok --resume, omp --resume, pi --session and each bare -c. Agent process count stayed the same.
Forms this PR now catches Refused: opencode -s <id>;echo done, --session="<id>", nohup pi --session <id>
Prompt-like text while a Pi chat exists echo Fix the Pi --resume flag is accepted and only echoes
Plain terminal OpenCode conversation (control) A normal row with Resume, Copy Resume Command and Delete; a terminal resume is allowed
Claude and Codex unchanged Chat-owned rows as above, terminal resume refused. "Resume in New Native Chat" on a plain Claude conversation opens a chat that remembers it.
History id vs the chat's own id Equal for Grok, OMP, OpenCode, Claude and Codex. Pi's row uses the id from the session file's name, as designed.

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):

  • "Resume in Worktree" on a plain OpenCode 2 row types opencode2 --standalone …. A Homebrew OpenCode 2.0.14 install only has opencode, so that fails with command not found. The command comes from tui-agent-config.ts, which this PR doesn't touch.
  • The refusal shows the raw agent_session_conflict text.
  • Each chat's automatic naming adds a "Name this chat from the user's first request…" Claude row to Session History.

Grok chat's row: titled with the chat's name; no Copy Resume Command or Delete:
Grok owned row menu

Opening the OMP chat's row switches to the chat tab:
OMP row opens the chat

Searching a Pi chat's word finds the chat, with no resume command:
Pi search hit is the chat

Control: a plain terminal OpenCode conversation keeps Resume, Copy Resume Command and Delete:
plain OpenCode row

Claude unchanged: "Resume in New Native Chat" on a plain Claude conversation opens a chat that remembers it:
Claude resume in native chat

@brennanb2025
brennanb2025 marked this pull request as ready for review October 12, 2026 17:07
@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

Walkthrough

The change adds shared CLI conversation contracts and shell parsing utilities. Agent runtime registrations now provide session-history behavior, and selected agents also provide transcript import. AI Vault ownership checks use registered agent behavior to match history rows and detect terminal resume commands. Session and search projections now apply client chat-opening policies. The renderer updates fork eligibility, history switching, and chat-title handling for supported agents.


Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to cdd72

A terminal command that runs through a shell, such as bash -c "pi --session ...", can still resume a conversation that a chat already owns. Two writers could then edit the same conversation. Close this gap, or explicitly accept it, 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 46.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 60 functions across 38 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks (4 passed)
Check name Status Explanation
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.
Title check Passed The title clearly summarizes the primary change: Session History treats OpenCode, Grok, OMP, and Pi chats as chat-owned and blocks conflicting terminal writers.
Description check Passed The description is detailed and follows the required structure. It explains the user impact, implementation, rationale, testing, visual evidence, compatibility considerations, and known limitations. T…

  • 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.

…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.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟠 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 lift

Handle 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', shellCommands keeps the quoted script as one word. commandProgramIndex therefore selects bash, not pi. 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 -c and -lc recursively. 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
📥 Commits

Reviewing files that changed from the base of the PR and between f4ce1d9 and cdd7221.

📒 Files selected for processing (15)
  • src/main/ai-vault/structured-session-ownership-acp-and-pi.test.ts
  • src/main/ai-vault/structured-session-ownership-client-audience.test.ts
  • src/main/ai-vault/structured-session-ownership.ts
  • src/main/pi/pi-session-history.ts
  • src/renderer/src/components/right-sidebar/AiVaultSessionRow.structured-title.test.tsx
  • src/renderer/src/components/right-sidebar/ai-vault-session-cli-fork.test.ts
  • src/renderer/src/components/right-sidebar/ai-vault-session-cli-fork.ts
  • src/renderer/src/components/right-sidebar/ai-vault-session-surface-switch.test.ts
  • src/renderer/src/components/right-sidebar/ai-vault-session-surface-switch.ts
  • src/renderer/src/components/right-sidebar/use-ai-vault-session-display-title.ts
  • src/renderer/src/components/tab-bar/tab-session-history-switch.test.ts
  • src/renderer/src/components/tab-bar/tab-session-history-switch.ts
  • src/shared/agent-resume-argv.ts
  • src/shared/structured-chat-row-name.ts
  • src/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.

@brennanb2025
brennanb2025 merged commit e83e40e into main Oct 12, 2026
35 checks passed
registered2nd pushed a commit to registered2nd/orca that referenced this pull request Oct 14, 2026
…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)
brennanb2025 added a commit that referenced this pull request Oct 15, 2026
…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)
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