Third and final shape for this branch, and the only one of the three that holds.
Keeping both maps re-poisons the epoch one cycle later: the consumer here is
also the publisher, so the history's current is the renderer's own lifetime
epoch, and recording the reveal's fresh epoch retires it. The next chat the
renderer publishes is then dropped — this bug again, one close later. Deleting
both loses the guard that stops a frame issued before the close landing after
it and stranding a row nothing republishes.
So: clear the history, keep the cursor. The mainstream path keeps its history
as a tombstone because there the epochs belong to a remote publisher; that
reasoning does not carry to a path that publishes under its own.
Each of the three variants now fails a different test.
Correcting the retraction handling again. Clearing its cursors was more than the
bug needed and cost a guard: the host mints a fresh epoch when it rebuilds a
pruned entry, so a republication is never gated by the retained cursor, while
dropping it left an inventory response issued before the close free to land
afterwards and strand a chat row for a worktree the host no longer publishes.
Skip only the recording. The mainstream path keeps its epoch history for the
same reason, as a tombstone fence.
The test that justified the stronger clearing asserted a host behaviour that
does not exist — a rebuilt entry republishing under the renderer's epoch with a
restarted counter. It now uses what publishStructuredAgentSessionTab actually
mints for a pruned entry, and a new case covers the frame that would strand.
Also fences the reveal's inventory refresh on the sync generation, which every
other caller that applies an inventory already does: structured chat can be
switched off mid-flight, and the answer would otherwise re-seed a row into a
renderer that just discarded them.
Correcting the previous commit. Skipping a retraction frame outright stopped it
pruning the mirrored rows, so a worktree the host no longer publishes would
have kept a chat on screen with nothing behind it. Apply the frame as before
and clear its cursors instead of recording them, which is what the mainstream
session-tabs path does.
The unpublished sentinel keeps its cursor now too: it is skipped rather than
cleared, so a stale frame arriving late is still fenced. Adds the case the
earlier version would have broken.
Manual QA: the host answered reveal with ok:true and republished the tab, and
the chat still did not reopen — only a renderer reload brought it back.
The renderer publishes under one epoch string for its whole lifetime, and a
frame recorded under a different lineage retires that epoch permanently with
nothing to un-retire it. The Resume click asks for an inventory first, and a
worktree the host holds no entry for answers with the none/v0 sentinel; the
structured path recorded it, retiring the renderer's own epoch, so the tab the
reveal published a moment later was dropped. A reload minted a new epoch,
which is why reloading appeared to fix it.
A frame that carries no publication is not a later publication to fence
against. Treat the sentinel and a removal frame as a cursor reset, the way the
mainstream session-tabs path already clears its tracking — its comment names
this exact hazard: recording that sentinel would retire the host epoch and
reject the next live frame.
Pre-existing, and it swallows an ordinary new-tab launch on an empty worktree
too; the reveal is what turned a silent invisibility into a visible failure.
It was swept into the previous commit by a broad `git add` while a diagnostic
ran in this worktree. It asserts the current renderer-sync defect as expected
behaviour, so it would fail the moment that defect is fixed.
The in-flight guard releases when the activation settles, so an await that
never settles holds the row for the life of the process. The capability probe
was the one call in the chain not raced against a deadline: on a cache hit it
awaits a promise an earlier probe created, which may carry no deadline of its
own. Race it like the two calls around it.
A version block can name either side — evaluateRuntimeCompat reports
client-too-old as well as host-too-old — so a message that blamed the host
pointed half of those at the wrong machine. Name the remedy instead of the
machine, which is true for every case that reaches it.
Manual QA found the reveal never ran: the inventory refresh that precedes it
is an optimization, but its failure returned early with 'not available yet,
retry in a moment' — reinstating the dead end this PR removes, one step
earlier. A failed refresh now falls through to the reveal, which is the repair
and does not need the refresh to have worked.
The click can chain a refresh, a capability probe, a reveal and a second
refresh, each with its own timeout, while nothing on the row says it is
working. A per-session in-flight guard keeps an impatient second click from
running the whole sequence again and landing its own toast.
Also drops an unreachable owner scope: the snapshot apply discards any
worktree whose execution host is not local before it reads one, so naming a
remote scope there described a synchronisation that cannot happen.
Dropping an Agent Session History row onto a pane activated the tab by id and,
on a miss, raised the very toast this PR exists to remove — so the same row
answered a click and a drop differently, and the drop kept the advice that can
never come true. The structured branch never used the drop pane, so routing it
through the shared activation loses nothing and gains the reveal.
The helper only ever read one field, so its parameter narrows to that field and
the drag payload satisfies it directly. A source ratchet holds both entry points
to the reveal-capable path, since a mounted drag harness does not exist for this
layer and what regresses is a call site, not a rendering.
The capability probe reaches assertRuntimeStatusCompatible, which throws a
runtime_compat_block error. Treating that as unreachable told a user with an
out-of-date host to retry, which is the one thing that cannot help. Branch on
isRuntimeCompatBlockError the way remote-agent-session-launch already does for
the same probe.
Also adds the refusal-code case a previous commit claimed and did not deliver:
nothing drove a structured_agent_session_unsupported reply through the reveal
client, which is the branch that commit existed to add. Corrects a doc comment
that reveal made wrong: attach is no longer the only call that builds the host.
The host raises two refusals here and they mean opposite things to a user: it
holds no such record, or it holds one no adapter of its own can open. The
client collapsed both into 'this chat is no longer on this host', which is a
eulogy for a chat still sitting on disk. Read the refusal code, and fold the
host-side case in with the too-old host under one honest message, since the
remedy for both is the same.
Adds the coverage the readiness pass found missing: the host's reveal answer
itself (workspace and provider from the record, both refusals, an unreadable
journal, a live session), and the activation branches for a host that cannot
open the chat and for one that never answered.
The capability gate read the LOCAL runtime's advertised capabilities while the
call went to the host that owns the workspace, which for a paired workspace is
a different build. On desktop the renderer and its local host are always the
same build, so the gate passed unconditionally and proved nothing about the
host being called: an older paired host still received the unknown method and
its method_not_found was reported to the user as 'this chat is no longer on
this host'. The cache it read also starts empty and resets to empty when
status.get fails, so 'not fetched yet' and 'unsupported' were the same value.
Gate on the environment that will answer, the way agentSession.close already
does, and skip the round trip entirely for a local host. Reveal now reports
four outcomes instead of a boolean, so a host that is merely too old is not
reported as a chat that is gone, and a host we could not reach keeps the
retryable message.
Also syncs the localization catalog: the 'gone' key shipped without an en.json
entry, which reddens static analysis and verify while typecheck stays green.
Both sides add to the same additive lists: the runtime capability list, the
structured method list, and the cross-version manifest. Kept both, and bumped
the structured method count ratchet to 19 for reveal plus the status feed.
The adhoc mac and dev-channel Windows builds vet the requested ref by
mirroring every branch and tag of this repo into a scratch bare repo and
proving the commit is reachable. Both runner disks are case-insensitive,
and the repo now has two branches differing only in casing, so the files
backend refuses the fetch outright — the whole job dies before checkout.
reftable keys refs in a table rather than as file paths, so both refs
store and every ref stays in the reachability set.
* fix(hooks): register the Claude hook script directly on Windows (#18875)
The Windows Claude Code lifecycle hook was registered as
`powershell.exe -NoProfile -EncodedCommand <...>` whose entire decoded payload
was a `Test-Path` and a call to `~/.orca/agent-hooks/claude-hook.cmd`. Every
hook event paid a full PowerShell start-up to reach a script that exits at its
first `ORCA_PANE_KEY` guard, so sessions outside Orca paid it to do nothing.
Register the script path itself instead, with `|| echo {}` for the
neutral-JSON-when-missing contract (#14818). Measured on Windows 11, invoked as
Claude Code invokes it (`printf payload | bash -c -l "<command>"`):
idle (n=12) baseline 177ms | before 471ms | after 213ms
10-way conc (n=40) -- | before 656ms | after 296ms
p95 under load -- | before 696ms | after 337ms
It also drops an interpreter from the chain the hook's timeout kill must tear
down. Killing the hook does not kill its PowerShell grandchild, which still
holds the stdout handle the agent reads to EOF -- measured, EOF arrived 352ms
AFTER the kill, when the orphan exited by itself. msys2 creates children
suspended and resumes them after, so a kill landing in that window strands one
that never exits and EOF never comes; that is the reported frozen session.
The encoded launcher stays as the fallback for profile paths the shells cannot
carry bare (space, `%`, `^`, `&`, non-ASCII) and for hosts where Git Bash is not
resolvable, because PowerShell 5.1 rejects `||`. Every other agent's hook is
untouched, as is the remote/SSH path.
Not adopted from the report: `cmd.exe /d /c <path>` (MSYS rewrites the `/c`
under Git Bash -- measured, the invocation fails), and raising the 10s timeout
(the orphan survives the kill regardless; the fast path puts the hook 30x under
the budget so the kill effectively stops firing).
* fix(build): list the new hook launcher modules in the CLI tsconfig project
config/tsconfig.cli.json enumerates its files explicitly, so the two new
imports reached by src/main/claude/hook-settings.ts failed tc:cli with TS6307.
src/main/git-bash.ts pulls in only node:fs, node:path and a shared constant,
so it adds nothing heavy to the CLI project.
* fix(hooks): address review of the direct Windows Claude hook launcher
- Make the Windows hook suites host-independent. A box with a cmd.exe AutoRun
(HKCU\...\Command Processor\AutoRun) failed them at HEAD too: the tests
redirect USERPROFILE, the AutoRun target vanishes, and MSYS spawns a .cmd
without /d so AutoRun runs and lands on the hook's stderr. Seed an empty
target, including under the deliberately-absent profile.
- Note in managed-hook-stdin-lifecycle why the "missing managed script" case no
longer exercises the fallback for the direct shape (it carries an absolute
path, so a redirected profile changes nothing); that path is covered live in
windows-direct-cmd-hook-command.test.ts.
- Keep the direct shape off UNC profiles: WINDOWS_CMD_SAFE_PATH admits them, but
//server/share/... is not a command cmd.exe reliably starts.
- Correct the comments: `|| echo {}` also fires when cmd.exe itself exits
non-zero (failing AutoRun), printing {} twice. The encoded launcher exited 1
on that same box, so neither shape is clean there.
- Test the contract that replaced runtime %USERPROFILE% resolution (STA-3348): a
stale absolute path reports not_installed and is rewritten on install.
- Record the standing unmeasured assumption in windows-edr-posture.md: `||` does
not parse in Windows PowerShell 5.1, so a compat consumer that hosts hook
strings there would fail closed. Measure before widening to another agent.
- Trim the launcher comments per AGENTS.md; the numbers live in the doc.
* test(win32): register the new Windows-gated hook test in the CI lane
win32-test-lane-registration guards against exactly this: a Windows-gated file
that self-skips on ubuntu and reports success, so it runs on no machine. The new
windows-direct-cmd-hook-command.test.ts needs both entries — WINDOWS_PACKAGE_TESTS
decides whether package_windows runs for a diff, and the workflow argv decides
whether the file runs once that job started.
* test(win32): remove the hook temp tree through the retrying helper
windows-lane-tree-removal-boundary scans exactly the specs in the Windows CI
lane, so registering windows-direct-cmd-hook-command.test.ts subjected it to the
rule: cmd.exe and bash have just exited in that tree, and a raw recursive rm
throws EPERM on Windows while their handles drain, turning a green spec into a
lane failure. Use removeTreeSync, which carries the repo's maxRetries policy.
---------
Co-authored-by: Orca Worker <orca-worker@localhost>
* fix(relay): abandon a client accept once the phone hangs up; jitter the control lease
The accept runs several serialized Postgres calls behind the contended
cell-inventory lock, and phones bound their dial. Finishing that work for a
phone that had already left acquired (and leaked for 90s) an activity lease and
then failed at bind with host_data_reservation_already_bound. Check the client
socket between the DB steps and unwind what was taken, reporting the stage on
orca_relay_client_accept_abandoned.
Jitter the control lease grant so a cohort that reconnected in the same minute
(a cell recreate dumps hundreds at once) walks apart instead of rebinding
together every cycle.
On the phone, treat a probe session that enters 'reconnecting' as a failed
probe: it is the direct client's own backoff after a dead-LAN 1006, and waiting
it out held the supervisor's operation mutex for the full 12s bound.
* perf(relay): lengthen the control lease to 6h
The lease bounds how long a host lingers on a cell after a missed drain, and
rebinding it is the only passive rebalancing we have, so it stays finite. 6h
keeps both properties while cutting control-activation traffic on the contended
cell-inventory lock ~6x. The relay JWT (5 min, refreshed by the desktop) and the
75s silence watchdog are enforced separately, so the longer grant authorizes
nothing extra. The jitter widens with it, to +/-30 min.
* fix(relay): let one flap recover the direct probe; correct the leak window
'reconnecting' is published on any socket close, so rejecting on it outright
turned a single access-point flap into a booked direct failure and a 60s
cooldown. Give the first 'reconnecting' a 2s grace in which a 'connected'
transition still resolves; a dead LAN still fails in ~2s rather than holding the
supervisor's operation mutex for the 12s bound.
The abandoned accept held its activity lease for the 10s attach deadline, not
90s -- the attach timer is armed before bind throws and already unwinds it.
Also cover the assignment-stage check that guards reserveCredential, and drop a
spread assertion the two exact-value assertions above already imply.
* fix(relay): extend the probe grace once on a handshake; pin the lease band top
The redial fires at 500ms but 'connected' waits on the Noise handshake and a
capability RPC, so one 2s window is too tight for real work. A 'handshaking'
transition is evidence the peer answered, so extend the grace once; a stalled
handshake still fails at ~3.5s, far inside the 12s bound.
The longest-lease case only had an upper bound, which a jitter clamped to one
side would satisfy. Pin it to the exact top of the band instead, and assert the
assignment resolve ran so the third-guard test cannot pass vacuously.
* fix(orchestration): typed error codes for dispatch and worker-start refusals
orchestration dispatch (and worker-start, which composes it) surfaced task
not found, task not ready, and inject rejected as the same bare
runtime_error, so an agent reading the receipt could not choose between
creating the task, waiting on dependencies, or picking another terminal.
Add task_not_found (data.taskId), task_not_ready (data.status,
data.unmetDependencies), and inject_rejected (data.terminal, data.reason),
each carrying data.nextSteps so every shipped CLI already prints the
recovery. worker-start's not-ready refusal moves from task_not_startable
to task_not_ready with the same detail. runtime_error stays for genuinely
unexpected failures.
Proven red-first from RpcDispatcher through the CLI's own failure
formatting, plus an SSH bridge test that the host CLI's typed refusal
relays unchanged.
* test(orchestration): load CLI formatter at runtime in the dispatch-code test
The composite node typecheck (config/tsconfig.node.json without
--composite false, as CI runs it) rejects a static import of src/cli from
a main test with TS6307. Load the formatter and error class dynamically
behind narrow structural types, as the CLI/runtime boundary test does.
* fix(orchestration): keep task_not_startable and split the CLI-format proof
Review on #18902:
- Drop task_not_ready. worker-start already published task_not_startable
for a not-ready Task, so renaming it would change an existing receipt
value under old clients. dispatch now emits task_not_startable too (it was
a bare runtime_error before, so this is purely additive), with the new
data.status / data.unmetDependencies / data.nextSteps.
- Move the refusal receipts (code, message, data) into
src/shared/orchestration-dispatch-refusal-contract.ts so the runtime
emits them and the CLI test formats the identical envelope. The RPC test
under src/main asserts toEqual against the contract; the new
src/cli/orchestration-dispatch-refusal-format.test.ts feeds those same
receipts to formatCliError / reportCliError. Neither tsconfig widens and
the composite typecheck CI runs is clean.
* fix(orchestration): keep published refusal messages and type the DB claim guards
Codex review of #18902:
- Every call site keeps the exact message it published on main
("Task not found: <id>", "only a ready Task can start.", "cannot retry
from Dispatch"); the shared contract now takes the message per site and
only owns the code and data. Baseline strings are pinned as literals.
- createDispatchContext's own missing/non-ready guards, including the
atomic-claim loser, now emit the same typed receipt instead of a bare
Error, so a dispatch that races a status change no longer flattens to
runtime_error. Covered by a dispatcher-level race test.
- Invalid --retry-of keeps task_not_startable but now carries status,
unmetDependencies, retryOf, and a retry-specific next step.
- Dependency recovery text distinguishes waiting on running deps from
retrying/unblocking failed ones.
- CLI test adds an unknown-code case so the old-client claim rests on an
assertion, not a comment; SSH test asserts exact stdout.
- Guide table narrowed to the covered preflight cases; occupancy stays
runtime_error and is named as such.
Operator record for the 2026-09-04 relay reconnect incident and the Roll 1
same-cap cell image roll (complete 2026-09-05, selector gen 148), plus the
follow-up checklist, roadmap, and the Roll 2 implementation plan.
Docs only; split out of #18565 so the record merges independently of the code.
The flat-table redesign in #16532 dropped the sort UI, orphaning
AutomationListSortHeader, nextAutomationListSort and the whole
AutomationListViewItem layer. Wire them back to the rendered list.
Name and Last run become interactive header cells again; the other six
columns stay plain text. Sorting now spans local and external rows as
one list, so the panel renders per-row components from a single sorted
collection instead of two independent sections.
Two model fixes fall out of that:
- View items key on the host-qualified row key, not the bare automation
ID. The old builder predated automation-list-row-identity, so under
All hosts two authorities returning the same ID collapsed in the sort
tie-break.
- sortAutomationListViewItems takes the locale as a parameter instead of
reading getIntlLocale(). A hidden global read is invisible to a
dependency array, and the list result is memoized.
Keyboard traversal and focus recovery now read the sorted order, so
arrow navigation matches what is on screen. The dead unified filter is
removed in favor of the live row/entry filters the page already used.
Clicking Resume on a chat-UI row could only reveal an already-open tab. If the
chat had been closed, or this process had never published it, the click re-read
an inventory that did not contain it and toasted "Retry in a moment" — advice
that could never come true, because nothing republishes an unpublished tab. The
legacy `claude --resume` fallback is deliberately refused for structured-owned
rows, so the row had no way back at all.
`close` already keeps the record and the journal on disk so a session can be
attached again, and the hold path already resurrects one in full. What was
missing was the tab: `restoreReadableSessions` is latched to run once, at
startup, so nothing could ask for a single session later.
Adds `agentSession.reveal`. The host looks up its own record, restores the
session readable, and republishes the tab through the same call
`agentSession.create` uses. Deliberately narrow:
- It takes no hold. A provider child exists because a surface asked, and the
chat pane asks when it binds.
- A journal it cannot read is not a refusal. A chat whose journal predates the
SQLite store restores to nothing here, but attach still recovers it, so the
tab is published and the pane's hold finishes the job.
- Workspace and provider come from the record, never the client, so a session
id alone cannot aim the publication at another workspace.
Claude and Codex both, by construction: eligibility is `adapterSupportsRecord`,
which the router answers from the record's own provider.
Gated on a new advertised capability rather than probing for method_not_found,
matching agent-session.structured.hold.v1 — absence is visible during
negotiation instead of by calling.
An `agentSession.create` the host could not confirm — it committed the session but
could not publish its tab, and answered `agent_session_operation_unknown` — was
rejected with a bare `Error` carrying a `code`. Nothing in the type said "unknown",
so the verdict lived only in the code string, and the shared transport matcher was
still free to re-read that error's *message*: an unknown refusal whose text ends in
a definitive token (`Owner check failed: method_not_found`) classified as definitive,
which is exactly the answer that permits a legacy sibling terminal.
Make the class the verdict. `StructuredAgentSessionCreateUnknownOutcomeError` is a
sibling of `StructuredAgentSessionCreateRefusalError`, not a subclass, so the nine
existing `instanceof` consumers keep reading "refusal" as "you may fall back" with
zero edits, and an unknown outcome flows down the lost-reply path instead —
replaying the same envelope, re-publishing the tab the host failed to publish, and
parking as visibility-unknown rather than creating anything. Classification now
short-circuits on our own classes, so a message we wrote can never invert the
verdict we already reached.
Adds an end-to-end guard that drives the real classifier through
`startStructuredAgentLaunch`: an unknown outcome opens zero legacy terminals, a
definitive refusal opens exactly one. Ablating the branch turns that green suite red
with `['legacy-terminal']` — the duplicate session the guard exists to prevent.
Co-authored-by: Merge Sim <sim@local>
A chat whose journal is still the pre-SQLite `log.jsonl` opened empty and indistinguishable from one created seconds ago. It now carries one status row naming the transcript still on disk and saying to send a message to continue, and read restore no longer drops such sessions — an unpublished chat had its tab pruned from persisted state, leaving nowhere for the message to appear.
The notice survives a crash between the epoch commit and its append (re-offered while the epoch holds nothing) and stays out of a journal the same open just repaired, where it would have retired the unreconcilable_prefix marker and permanently ended provider-history recovery.
No importer: the history is explained, not replayed. Nothing reads the remnant beyond its existence, and nothing moves or deletes it.
* fix(native-chat): publish structured session status from the host
The sidebar learned whether a structured chat was mid-turn by replaying
the session journal in the renderer, through a reader whose lifetime was
tied to the chat pane. Hiding the pane stopped the reader before the
turn's settlement arrived, so the row stayed on "working" until the chat
was reopened. The same coupling meant a tab never opened this session
showed no status at all, and a reloaded renderer lost every settled row.
The host owns the journal, so it now projects each session's status once
per journal publication and fans the changes out on one stream per client
(`agentSession.subscribeStatus`). The projection survives eviction of an
idle session's provider child and is republished when readable sessions
are restored. The renderer bridge subscribes to that feed per runtime
target and never opens a transcript reader; the observation hook is gone.
Additive wire surface behind the existing structured capability; old
hosts reject the method and the renderer retries, showing no status.
* fix(native-chat): negotiate the status feed and stop losing a change on subscribe
The status stream is additive to a surface that already shipped, so a host
advertising agent-session.structured.v1 can still answer subscribeStatus with
method_not_found. Every renderer error path reconnected, so a remote host one
release behind got a relay round-trip every 5s and no sidebar status at all.
Give the method its own capability and probe it before subscribing; a failed
probe still retries, an absent capability does not.
Re-projecting on subscribe also wrote straight into the shared cache, so a
second client could pin the first to a stale summary. Route those diffs
through publish() before the arriving subscriber is registered.
* fix(native-chat): bound the status prompt, merge snapshots, and prove the unread path
One status frame carries every retained session and a send admits 256 KB per
prompt, so ~16 large-prompt sessions could push the snapshot past the 4 MB
outbound guard and into the retry loop. Bound latestPrompt to the same
200-char single-line preview every other agent-status row already carries.
A snapshot also replaced the cached map wholesale, so the empty first frame
from a restarting host retracted every row before restore republished them.
Merge instead; the tab map, not this feed, decides which sessions are listed.
Tests: the hidden-pane claim now sits at the host, where a journal with no
transcript subscriber is driven from running to idle; the RPC test reads a
real projection instead of its own stub.
* fix(native-chat): merge the duplicated status-event type import
* test(native-chat): pin the restart status publication, and log the unsupported host
Startup restore indexes a readable session and publishes its status, which is
what puts a never-reopened tab back in the sidebar. Only an Electron screenshot
covered that wiring; a sitting status subscriber now pins it directly.
The terminal "host too old" branch was silent, so a mixed-version report showed
an empty sidebar with nothing in the log to explain it.
---------
Co-authored-by: Merge Sim <sim@local>
* feat(native-chat): model Codex MCP and web-search items instead of leaking opcodes
Codex's app-server sends 19 thread-item types; the structured translator handled
six. The rest fell through to a generic gray `codex · item:<type>` row, even
though the disposition table's own comment says it exists so a new item type
cannot leak like that — the table had one entry.
Give `mcpToolCall` and `webSearch` real tool-call bodies, and chrome `sleep`,
which carries only a duration and renders as nothing in Codex's own TUI.
`subAgentActivity` and `collabAgentToolCall` deliberately keep their generic
rows. They arrive in real sessions today and are currently the only visible
sign a subagent is running; hiding them before the subagent UI lands would
render minutes of work as an idle turn. Tests pin that they stay visible.
MCP tool names pass through verbatim when they contain `:`, `.`, `/` or `__`,
so `mcp__server__tool` survives instead of being title-cased into nonsense.
* fix(native-chat): keep Codex MCP tool identity and web-search results on the row
Four fixes to the Codex MCP / web-search item bodies:
- Drop the title-casing display name. `get_forecast` became `Get Forecast`,
which no longer matches the raw snake_case identifiers that the diff
renderer, question parsers, and tool-input previews dispatch on, and does not
match how the Claude lane or the sibling `shell`/`apply_patch`/`web_search`
bodies name a tool. The row name is now `server/tool` verbatim, the bare
`tool` when no server is given, and `mcp` when the item names no tool at all.
Server-qualifying also stops an MCP tool that happens to be called
`apply_patch` from hijacking the diff renderer.
- Pass the MCP call's own `arguments` as the tool input instead of wrapping it
in `{server, tool, arguments}`. Row-label derivation only reads top-level
keys, so the wrapper degraded every MCP row to a truncated raw JSON blob.
A non-object `arguments` stays addressable under a key rather than being
dropped; an absent one becomes null, which labels as empty rather than `{}`.
- Carry a web search's `results` as the call output, bounded like every other
inline payload and omitted when there are none. They were being dropped
entirely, which showed less than the generic fallback row it replaced.
- No streaming branches were added for these two item types: the Codex delta
stream is a closed set of six methods that neither can reach, so such
branches would be unreachable.
* fix(native-chat): label Codex web searches and argument-less MCP calls
A row label is derived from top-level `input` keys only, so a webSearch
whose detail lives inside `action` — an opened page, an in-page find, or
a bare `other` — fell through to the raw JSON of the whole input, as did
the empty `query` Codex leaves on a completed search. Hoist the action's
`url`, `pattern` and `type` beside the query, keep the full `action`
object so the expanded detail loses nothing, and emit no input at all for
the start frame.
An MCP tool that takes no arguments sends `arguments: {}`, which passed
straight through and labelled the row a literal `{}`; treat it as absent
so the row reads as a bare `server/tool`.
Split the durable-identity half of the item translator into
`codex-thread-item-identity.ts`, re-exported so every existing import is
unchanged, to keep both files under the max-lines cap.
---------
Co-authored-by: Merge Sim <sim@local>
* fix: retain favicons across same-origin navigations
Move favicon clearing from did-start-loading to did-start-navigation and
only clear when origin changes. Chromium re-announces favicons only when
the icon URL list changes, so clearing on every load orphans same-origin
navigations. Extract favicon URL validation into a shared module.
* fix: drop favicon on cross-origin redirects
When a same-origin navigation redirects to a different origin, the favicon should be cleared to prevent stale icons from displaying the wrong site's identity.