mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 16:02:38 +00:00
db66ab4ac83b60301f08bbfa206ac741a4ad56d4
2023
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5868fdc9e3 |
feat(native-chat): report Codex background tasks in the chat strip (#19346)
* feat(native-chat): report Codex background tasks in the chat strip The background-tasks strip works for Claude only; a structured Codex session shows nothing in it. Feed it from the Codex app-server stream. The strip stands for work that OUTLIVED a turn, which is what the monitoring header, Claude's foreground suppression, and the conversation command gate all already assume. Codex has no `is_backgrounded` flag, so that fact is derived from the turn boundary: a `subAgentActivity` child or a primary-thread `commandExecution` becomes visible once the turn it belongs to completes and it is still unsettled. `turn/completed` only reveals a task here, never settles one — measured on `codex app-server` 0.153.4, a spawn_agent child reported `completed` 95.8s after its parent turn ended. Only a child's own activity kind settles it. Codex exposes no honest stop: `turn/interrupt` on a child ends its turn without emitting a terminal activity item and leaves its shell running. So the state carries a new optional `supportsStopAll: false`, the strip hides a control that could not act, and the blocked-command message asks the user to wait rather than to press a button that does not exist. * refactor(codex): move session teardown out of the structured adapter Merging main crossed the 300-line cap on `codex-structured-session-adapter.ts`: the rewind backend (#19235) and this branch's close-time strip clear both landed in it. The four close paths move verbatim into `codex-structured-session-teardown.ts`, where they funnel through one `settled` helper instead of repeating the notification-retry and background-task cleanup at each call site. No ratchet bump. Also normalize a background task's description once at receipt rather than on every projection; the roster is re-projected on each observed frame. * fix(codex): drop the shell row the journal already settles A `commandExecution` still `inProgress` when its turn ends was reported as a `command` task. But `settleCodexJournalTurn` writes exactly those items to the journal as `state: 'failed'` on `turn/completed` and forgets them, so the strip row would have claimed a shell was still running at the same instant Orca recorded that it was not — two surfaces contradicting each other about the same process. A subagent is the opposite case and stays: the roster pointedly does not sweep at a turn boundary, because children measurably outlive it. That leaves the producer making exactly one claim — these spawn_agent children are still live after their turn — which the durable roster row corroborates. * fix(native-chat): track Codex background execution lifetimes * fix(native-chat): keep running tool groups from claiming completion * Fix runtime catalog and capability expectation * fix(codex): keep a child's name on the command row that outlives it A child agent's commands stay hidden behind its agent row while the child works. Once the child's turn settles with a command still running, that command surfaces as its own row labelled from the raw command string, so 'long_probe' became "/bin/zsh -lc 'ping -c 300 127.0.0.1 > /dev/null'" at the moment that row was the only remaining signal for the work. Qualify a child's command row with the child's label. Resolved on read, so a label registered after the command still lands, and bounded by the existing description cap so admission accounting stays valid. Primary- thread commands are left unqualified: they have no child to name. --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
0fe132ea29 |
fix(orchestration): file mail from terminals in no Run under an unbound Run (#19696)
* fix(orchestration): file mail from terminals in no Run under an unbound Run #19542 deleted the fallback that filed such mail under the legacy Run, because a live row there makes the schema-skew probe read the database as pre-Runs and replay adoption on the next open. That refusal also broke the first command in the guide: `orca orchestration send --to <handle>` between two plain terminals, which worked in v1.4.198. Restore delivery by filing under `run_unbound`, a Run the probe never matches, created on first use so `run list` shows it only to a user who has such mail. Claude-Session: 1fec75fd-224b-46ab-95fe-d88e0f3d9ff9 * fix(orchestration): create the unbound Run only for a null Run id Claude-Session: 1fec75fd-224b-46ab-95fe-d88e0f3d9ff9 |
||
|
|
d15a6df224 |
Render task checklists with update diffs and a composer progress panel (#19230)
* Render native chat task lists with incremental checklist updates * Keep native chat task-list review plan out of repository root * Render live Codex plan notifications through task checklists * fix(native-chat): keep checklist test fixture within shared boundary * Keep agent task progress in one composer panel * Restore inline task checklists and historical update diffs --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
1c1cb7115a |
fix(orchestration): unify structured-session status across worktree ps and sidebar (#19217)
* fix(orchestration): stop worktree ps reporting a busy structured session as idle A worktree running a structured Claude or Codex chat read as idle to `orca worktree ps`, while the desktop sidebar showed the same session working. The sidebar was right: the host already projects a status summary for every structured session and publishes it, and the renderer maps it into an agent row. `worktree ps` simply never consumed it, so the agent-facing surface was the blind one. Structured sessions have no PTY, so they reach neither the hook snapshots nor the retained ones that every other row is built from. This reads the summaries the host has already published and applies the same projection the sidebar does — working, attention as blocked, otherwise done — so the CLI and the GUI cannot disagree about one session. Two things worth knowing: The connected-PTY evidence gate had to be skipped for these rows. It exists to drop a row whose PTY is gone, which is the wrong question for a session that never had one; a structured row's liveness evidence is the status feed that produced it. The exemption is keyed on the row being structured, so every PTY-backed row keeps today's behaviour. `RuntimeWorktreeAgentRow` needed no change. It was already a non-PTY shape — paneKey, state, agentType, and no ptyId, connected or writable — so a structured row fits without inventing a fake terminal coordinate. The pane key is the DERIVED one the renderer already publishes, never the orchestration bearer handle or the minted worker pane key: both are credentials, since `orchestration check` is identity-gated and accepts a caller-supplied pane key. `orca terminal list` is deliberately untouched, and a test pins that. Adding rows there breaks real consumers: mobile mounts a terminal WebView per row that can never receive a frame, a `connected`-keyed refresh check goes permanently true and pins shipped clients to a fast cadence with no exit, and the plugin projection has no field that can carry `writable: false`. Every safe consumer of a terminal summary checks `ptyId`; the breaking ones key off `connected` or mere row presence, which no added field can qualify. An honest partial-listing count there is a separate change. * fix(runtime): report only live structured sessions in worktree ps The status feed's `published` map is a broadcast cache, not a roster. It deliberately never retracts — an evicted idle session is still idle, and a reloading renderer must not lose every settled row — so enumerating it lists every session the host has ever opened, and eviction's `forget-session` step deletes the session from the live map while touching nothing else. Reading it as a roster made `worktree ps` report a closed chat forever. The sharp edge is a chat closed while an approval was pending: a deliberate close does not settle a pending prompt, so the retained summary stays `attention`, maps to a `blocked` row, and merges the worktree to `permission` for the whole 30-minute freshness window — on the CLI and on the mobile sidebar it backs. The poller now answers from the sessions the host still holds, intersecting the live map with the retained projections. `subscribe()` and its snapshot are untouched: retention there is the point. Gating on the live session set rather than the visible tabs keeps a headless orchestration worker listed, which is what the agent-facing surface is for. Also folds out two things the enumerator left behind: the working/attention/idle to working/blocked/done mapping now lives once in the shared projection module instead of once per process, which is what actually enforces "the CLI and the GUI cannot disagree"; and the structured row source no longer builds a write-only `payload` behind an `as` cast that compensated for nothing. The structured source construction moves to its own module to keep runtime-worktree-agent-rows.ts clear of the 300-line cap. * test(runtime): execute the structured-host call site in worktree ps Both structured-row suites called attachRuntimeWorktreeAgentRows directly with summaries they built themselves, so nothing ever ran getWorktreePs's own `getStructuredAgentSessionHost()?.liveSessionStatusSummaries()`. That file carries `@ts-nocheck`, so renaming the accessor was green in typecheck and in the suite, while `orca worktree ps` and mobile's 3s poll would throw a TypeError for every user — the `?.` optional-chains the host, not the method. Swapping the call back to a whole-cache read was equally invisible: the liveness suite injects feed.liveSessionSummaries() itself, and the string-match guard only needs the identifier to appear somewhere in the file. Drives the real runtime with a stub host over a real status feed that has published two sessions and forgotten one, asserting the live session's row reaches ps output, that the live accessor is the one called, and that the `?? []` fallback still returns a page with no host installed. The stub is typed against the real host, so a class-side rename reddens tc here. * refactor(runtime): admit agent sources before worktree row projection * fix(runtime): preserve host-authoritative structured status * Fix structured host session activity lifecycle --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
72befaf360 |
feat(native-chat): show Claude subagent activity on the shared carrier (#18806)
* feat(native-chat): show Claude subagent activity on the shared carrier Claude's `message:system:task_*` frames are classified `status-chrome` and reach the transcript as nothing at all, so a turn that spawns subagents renders as an idle turn. The journal translator now reads them into the shared subagent-group carrier — no new UI, and the frames stay `status-chrome` so nothing prints a raw opcode row. `local_agent`, `local_workflow` and `local_bash` tasks share that channel and all carry a `tool_use_id`, so `task_type` is the discriminator and a backgrounded `sleep 20` stays out of the roster; `subagent_type` covers releases that predate `task_type`. `skip_transcript` tasks never render, `is_backgrounded` children survive the turn-end sweep, and a resumed task re-announced under a fresh tool id is aliased onto its `task_id` rather than duplicated. A child still reported as working when the turn — or the session — ends becomes `unverifiable`: contact was lost, which is not evidence it exited. * fix(native-chat): stop the Claude subagent roster dropping its own rows The roster published under the same coalescing key it appends the row with, and the sink queue replaces any queued operation sharing a key regardless of kind: once a write was in flight, each new append evicted the pending publish and the next publish evicted that append, so the body never reached the journal and `lastSerialized` had already moved past it. Publish now takes the sink's own slot, as the Codex streams do. A tombstoned row could never come back: the non-batch item-row builder derived its revision from `items` alone, so a re-add was built at revision 1 against a tombstone at 2 and the reducer discarded it forever. It now takes the same `max(items, tombstones)` the batch builder already used — reachable here because an announcement that reveals a `local_bash` task empties and tombstones the group row that a genuine subagent later in the turn reuses. `settleTurn` swept whatever group the key named at the time it ran, so children rostered before any turn key existed were never swept, and a turn whose result never arrives was left working forever. The ending turn's key is now an argument, a superseding turn start settles the turn it replaces, and every turn end also sweeps the outside-turn group. Teardown without an `ended` event, and eviction past the group bound, both lose contact instead of stranding a row at `working`. Label ordinals are a high-water mark now: releasing one on a re-label handed the next child an ordinal that was already on screen. * fix(native-chat): bound subagent-group blocks on every wire that carries one Adding a fifth arm to `NativeChatBlock` made every consumer that assumed four wrong. Two of them ended in `return block`, so they compiled while handing a roster straight through: the mobile RPC sanitizer shipped it unclipped past both mobile char caps, and the legacy transcript import stored an untrusted roster unbounded. Both now clip each label and cap the entry count the way they bound their other blocks. The remaining three sites did not compile at all. The worker transcript payload and the live-session benchmark get real arms rather than casts — a cast would have turned the transcript one into a third silent passthrough inside the wire byte budget — and the CLI worker output renders a roster with its shared summary instead of `[image omitted]`. The mobile sanitizer moves to a sibling module beside the image-block one: the file sat exactly on the max-lines bound, and the block bounds are a self-contained concern with their own caps. Also caps the roster's `subagent_type` label fallback, which reached the journal uncapped, and covers the new block type in the schema audit. * test(native-chat): cover the capped subagent_type label The roster stores the frame's label verbatim, so the cap on the `subagent_type` fallback is the only thing bounding it. * fix(native-chat): type the roster fixture so the suite typechecks The mobile-cap test built its entries with an inferred `state: string`, which is not a `NativeChatSubagentState` — the only typecheck failure on the branch. * fix(native-chat): stop child traffic rostering an id Claude never announced `observeChildActivity` minted a provisional row for any `parent_tool_use_id` outside the excluded set. An id that was never announced is never excluded, so a nested Task, a workflow child, or a grandchild parented to a tool id inside the sidechain each produced a permanently unlabelled `subagent` row that could only ever end `unverifiable`. The bounded exclusion set cannot cover an id no frame ever declared, and in a long session it can forget a genuine exclusion. Track instead whether this CLI announces tasks at all — set by ANY `task_started`, including one the subagent filter rejects. Once it has, an undeclared child is provably not a new subagent, so no row is created. The provisional path now serves only releases that announce no task frames, which is what its comment already said it was for. The label-ordinal test moves to an announcement-driven removal, the scenario that path now actually reaches; it still fails if `remove` releases the ordinal. * fix(native-chat): outrank the tombstone when building one too `buildJournalTombstoneRow` still built its revision from `items` alone, leaving it asymmetric with the item builder. It is correct today only because `upsertItem` clears the tombstone whenever a re-add wins — an invariant that lives in the reducer and was not pinned. Apply the same `Math.max`, and pin the invariant so the reducer cannot drop it silently. * fix(native-chat): stop an unrelated turn end settling an outside-turn child `settleTurn` swept the `outside-turn` group on every turn end, so a child Claude announced while no turn was live — a frame trailing the previous turn's result, or one that arrives before the first turn starts — was marked `unverifiable` by the next, unrelated turn ending. That state is terminal and latches, so the `task_updated: completed` that followed was discarded: loss of contact was recorded as the child's outcome on evidence that was never about that child. A turn end now sweeps exactly the group its key names. `outside-turn` belongs to no turn, so only an end with no key of its own reaches it, and what no turn end reaches `settleSession` does — reliably, since teardown without an `ended` event also routes through it. The cost is a child outside every turn showing `working` a little longer; the alternative prints a wrong outcome that nothing can revise. Also pins that a subagent announced after a task the filter rejected still rosters: the announcement path was never what the child-traffic gate closes. * fix(native-chat): bound a subagent entry's id, not just its label Every site that bounds a `subagent-group` block clipped the label and handed the id through whole. From the Claude producer the id is bounded upstream, but the legacy transcript import reads an untrusted file, so an oversized id survived into the journal and then out to every wire that replays it — 64 entries of it, since only the entry count was capped. Each site now clips the id with the helper it already uses for its other bounded fields: the journal's inline-text bound on import, the transcript payload's metadata clip, and the mobile char cap (renamed, since it is no longer a label-only cap). * fix(native-chat): surface an adverse subagent outcome in the fallback sentence The roster row's plain-text stand-in counted only `working`, so a fan-out whose children all latched `unverifiable` (or `failed`, or `stopped`) rendered as "Ran 3 subagents" — a completion claim. Mobile and paired web have no roster renderer, so that write-time-frozen sentence is the entire row there, and collapsing `unverifiable` into something that reads like success is exactly what the SSH execution boundary forbids. It now appends the worst adverse count, worst-first across failed/stopped/ unverifiable, and shows it even while siblings still work — matching the Codex lane's shared `subagentGroupFallbackText` verbatim so collapsing the two copies later is a deletion, not a behaviour change. Also bounds the provisional entry id. `observeChildActivity` wrote the `parent_tool_use_id` straight into the entry's durable id with no length cap, while the announced path already rejects an over-long id via `claudeTaskId`. Both now share `isBoundedClaudeTaskId`, and the provisional path rejects rather than truncates, as the announced one does. * fix(native-chat): stop a subagent label ordinal and a clipped roster key colliding - claimLabel probes the labels the group actually rendered instead of a per-base counter, so a generated `Audit 2` cannot duplicate a provider's own `Audit 2`. - Bound `NativeChatSubagentEntry.id` with a head plus a digest of the whole id at every site that bounds it. The id is the roster key: a prefix clip merged two distinct children onto one entry. - Correct a stale journal-reducer test comment: tombstone cleanup is a map-state invariant, no longer load-bearing for revision ordering. * fix(claude): merge duplicate unhandled-provider-frame imports * fix(claude): preserve subagent lifecycle and bounded invocation identity --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
852495d35c |
fix(native-chat): unify launch routing and support structured worker placement (#19431)
* fix(orchestration): let worker-start actually produce a structured chat `orchestration.workerStart` reads the user's "open agent tabs in chat" default, but two placement checks downgraded a structured-preferring worker to a PTY terminal agent for the two flags a routine dispatch always passes: --worktree new-child / new-top-level -> worktree_creation --model / --effort -> launch_preferences so in practice a structured worker never happened. launch_preferences was stale. PR #19040 gave AgentSessionAttachParams `options` and added resolveStructuredLaunchSeedOptions, which narrows a saved selection to exactly `model` and `effort` — the two ids both structured providers accept as strings. --model/--effort now go through that same narrowing (extracted as narrowStructuredLaunchSeedOptions) and seed the worker's session instead of forcing a terminal. An option set that narrows to nothing resolves to undefined, never `{}`, which would fail the record's bounded-string guard under a code that is not a wire refusal and strand the launch with no fallback. worktree_creation was a consequence of createWorkerWorktree creating agent-first: its startup terminal WAS the worker, so the structured branch below it was unreachable for any new worktree. A structured worker now creates the worktree with no startup agent and creates its session for the worktree afterwards — the order the renderer's own structured worktree create already uses. Because the executing host can only answer agentSession.createSupport for a workspace that exists, that verdict moved after creation: a refusal (WSL, and the rest) becomes a terminal agent in the worktree just created, never a failed start. --on and --terminal still downgrade, with their reasons intact, and every remaining downgrade still states itself in the mode receipt. The wait-for-setup gate is preserved explicitly. A PTY worker got it for free — agent-first creation sequences the agent's startup command behind the setup runner, so tui-idle could not arrive until setup exited. A structured session has no startup command to sequence, so the gate is now awaited directly, bounded by the start's own timeout. Split out worker-worktree-creation.ts and worker-start-agent-placement.ts rather than growing two files that were both pinned at the max-lines cap. * refactor(native-chat): make shared feasibility authoritative for launch routing * Type the structured setup gate's absent blocked reason so the wait union stays property-typed The type-aware audit rejected the blocked-reason template literal: narrowing the wait union with an 'in' check left the field typed unknown. Declaring that a structured setup gate never carries a blocked reason restores the direct read. --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
2558bf1b17 |
perf: count command-line escapes without regex match arrays (#19499)
* perf: count command-line escapes without regex match arrays * test(windows): pin command-line budget counting against the regex oracle Covers every BMP code unit, astral and lone-surrogate adjacency, trailing backslashes, %VAR% and carets, plus randomized quote-heavy command lines. * perf(windows): count command-line escapes by seeking, not scanning Counting every character regressed the shape this estimator actually guards: a multi-KB WSL script with almost no escapes went 25-38x slower on Windows. Seek escapes with indexOf so the cost tracks their count, and hand the rest to a plain scan once they are dense enough to pay for it. --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
e373c89536 |
perf: cache update timestamps for Linear and Jira result sorting (#19473)
* perf: cache update timestamps for Linear and Jira result sorting * perf(issues): build updatedAt key map without an intermediate tuple array --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
063c1caa6f |
perf: reuse AI-vault path matchers and query and sort keys (#19469)
* perf: reuse AI-vault path matchers and query and sort keys * test(ai-vault): add cross-platform path and sort-key differentials --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
4a502d2e19 |
perf: index terminal neighbor order and remaining membership (#19453)
* perf: index terminal neighbor order and remaining membership * test(session): prove indexed terminal close picks the pre-index neighbor --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
f242c99af4 |
perf: scope activation inventory to the owning host and workspace (#19447)
* perf: scope activation inventory to the owning host and workspace * fix(activation): keep an unscoped census fallback when the owning host is unnameable Scoping the activation inventory made resolveActivationPtyListScope throw for paired-runtime workspaces and made a detached relay reject the scoped list, and both collapse to a 'blocked' gate. 'blocked' skips the sleeping-agent resume and the caller's reseed, so an SSH target on the bounded offline floor lost its initial pane and peer workspaces stopped resuming. Fall back to the unscoped inventory that shipped in exactly those two cases; the scoped fast path still covers local, folder and attached-SSH workspaces. Also OR the host-reported worktreeId with the id-prefix match instead of preferring it, because a relay seeds worktreeId from the host's own ORCA_WORKTREE_ID and a session dropped from the census is one the gate forks a second writer onto. * test(activation): update forkbomb fakes to the scoped session.tabs.list shape The gate now asks the host for one workspace's snapshot instead of the whole session.tabs.listAll inventory and refuses an answer that does not name its scope, so the old snapshots-array fakes made it block instead of resume. --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
44eb95fc6b |
perf: bound tool pairing and avoid unused attribution arrays (#19468)
* perf: bound tool pairing and avoid unused attribution arrays * test(native-chat): cover mobile pair-limit shapes and group allocation tests --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
c3a415487b |
perf(terminal): avoid quadratic status-marker scans in output bursts (#19373)
* perf(terminal): reuse forward OSC status terminator searches * test(terminal): pin cached OSC terminator reuse across BEL frames Document that forward match reuse requires a monotonic search offset and cover a distant ST held across many intervening BEL frames. |
||
|
|
d872a5a8b8 |
perf: avoid repeated pending SOCKS payload copies (#19439)
* perf: avoid repeated pending SOCKS payload copies * test(browser): cover pipelined SOCKS request-tail ordering in pending buffer Also document takeBuffer's view-not-copy contract and restore node: import order. --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
6a9c5d8ce3 |
perf: replay pending chat asks with a FIFO cursor (#19496)
* perf: replay pending chat asks with a FIFO cursor * perf(native-chat): track the pending-ask FIFO with counters instead of a queue --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
96216601bc |
perf: retire exhausted buckets from host-balanced listings (#19493)
* perf: retire exhausted buckets from host-balanced listings * test(listing): guard the bucket-retirement invariant and starvation fix Document why in-place compaction keeps bucket[round] defined and preserves first-appearance host order, and cover cap-exact filling plus small-host representation so a drifting retirement predicate cannot emit blank rows. --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
2c5cd845a2 |
perf: append to owned project source lists without repeated copying (#19465)
* perf: append to owned project source lists without repeated copying * docs(projects): record draft-ownership rule for in-place project merge --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
df8a43724f |
perf: index document history workspace and path identities (#19461)
* perf: index document history workspace and path identities * docs(browser): note doc-history dedupe must mirror location equality --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
86fe80da25 |
perf: preserve store state on unchanged document titles (#19459)
* perf: preserve store state on unchanged document titles * fix(browser): compare every doc-history field before skipping a title refresh A hand-listed title check would silently swallow any field added to WorkspaceDocHistoryEntry later. Cover the over-cap trim path too. --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
e80fae0c4d |
Summarize turn file changes and preserve resolved prompt receipts (#19229)
* feat(chat): summarize turn changes and retain resolution receipts * fix(chat): defer turn diff details and localize resolution times * fix: complete approval projection test fixture * Align turn diff disclosure chevron and indent details --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
bba68b1bdd |
fix(pi): finish the dialog-wait signal on every surface (#19533)
* fix(pi): carry modal waits to mobile and stop losing the dialog close Follow-ups to #18836, from its readiness review. - Paint pi's `!` needs-input state marker while a dialog is open, so the 80ms spinner frame stops repainting a working title over a mid-turn wait. Mobile and the CLI read the title, so they saw `working` where the desktop already showed `waiting`. - Keep the assistant reply that lands while a dialog is open. The modal guard cleared tool fields and the `message_end` capture with them, so a turn ending under a dialog left the preview on the previous message. - Report `ui_prompt_end` even when `ctx.isIdle()` throws on a runner the modal itself invalidated; the lost post stranded the pane on `waiting`. - Declare the `esbuild` the runtime smoke tool imports. * fix(pi): hold the needs-input marker until the dialog actually closes From review of the previous commit. - Settling under an open dialog no longer retires the marker. stopAnimation painted the plain title unconditionally, so agent_settled, a resolved agent_end, or an idle auto_compaction_end erased it mid-dialog — and because that also cleared the timer, the close then painted the plain title again and the wait was lost for good. - Track the dialog as a boolean, not a depth counter. Pi does its own nesting accounting and emits one pair per stack, which is what the status extension already assumes; two files disagreeing on that would have let an inner close release the outer wait. - Reset the flag on agent_start in both extensions. A turn cannot begin under a dialog holding input focus, so it is the one boundary that can recover a close that never arrived instead of pinning the pane forever. - Leave OMP to its approval events: it reports waits through those already, and painting the marker there too would put title and hook in disagreement. * fix(pi): do not ring the completion bell for a dialog that lost its close From review of the previous commit. - Report working, not done, when ui_prompt_end's isIdle() throws. done is not cosmetic: it reaches dispatchCompletion and fires the pane's finished notification, so a turn that is still running would announce itself. The real done still arrives from agent_end/agent_settled. - Keep the idle-maintenance frame cap accruing while a dialog holds the title, so a dialog left open cannot suspend the guard that stops a compaction spinner whose end event never came. - Guard the dialog handlers against a ctx without ui. The source is generated and untypechecked, and pi does not document the ctx it passes these two events; a TypeError there would surface on every dialog. * fix(pi): let a turn still complete after a dialog loses its runner From review of the previous commit. - Re-arm the completion report when ui_prompt_end's isIdle() throws. The fallback posts working, but the finished turn had already reported its end, so nothing further would ever fire and an idle pane sat spinning. - Count dialog depth in both extensions instead of trusting pi to emit one pair per stack. The guarantee is undocumented, and if it ever does emit a pair per dialog, an inner close would release the wait the outer dialog still holds. A counter costs nothing and drops the dependency. * fix(pi): decide a dialog close from turn state, not from a guess From review of the previous commit. - Fall back to agentEndReported when ctx.isIdle is unavailable or throws. The previous guess of working stranded the common case — a dialog opened at idle — because no later event was coming to correct it, and the agentEndReported re-arm it relied on could not fire either. A turn that already reported its end is not still running, and that is knowledge this process holds without needing ctx at all. - Only suppress spinner frames once the marker is actually painted. Pi may pass a ctx with no ui, and freezing the title on its last working frame is the opposite of what the marker is for. - Gate the titlebar dialog handlers on the OMP runtime too, not just the installed kind: a bare-shell OMP launch runs inside a pi-kind pane, and the status extension already defers there. Extracted that check so both extensions share it rather than carrying two copies. * fix(pi): treat a pane that never ran a turn as idle, not busy From review of the previous commit. - Track turn-in-flight separately from agentEndReported. That flag also dedupes the completion post, so it starts false on a pane that has not run a turn — which read as still-running and left a dialog opened before the first prompt spinning forever. - Retry the marker paint on each dialog open instead of only the outermost, so an outer ctx without ui cannot decide the whole nested stack goes unmarked. - Fall back to the opening ctx when the close carries no ui. Nothing else clears the needs-input marker, so the pane would have kept asking for attention until the next turn. * fix(pi): keep a dying dialog ctx from stranding the needs-input marker The close path paints through the ctx captured at open time, which is the one a session-switching modal is most likely to have invalidated. Guard both paint sites so a throw cannot reject the handler and leave the title on the needs-input marker, and make local turn state the floor for the status extension's idleness verdict instead of a fallback. * fix(pi): hold the dialog wait against pi's own title writes and lost closes Reviewed against real Pi 0.85.1 source rather than inference: - ctx.ui is a getter that calls assertActive() and throws once a session- replacing dialog invalidates the runner, so optional chaining never screened it out and the probe sat outside the try. A throw landed after the depth decrement but before markerPainted cleared, stranding the needs-input marker until the next turn. - Pi writes the same terminal title from its own writers with no event we observe, so the marker is now re-asserted rather than merely not overwritten, on a slow timer that outlives the spinner and its cap. - resetExtensionUI drops an open dialog without resolving its promise, so a replaced or reloaded session never emits the matching ui_prompt_end. Both extensions now release the wait on session_start and shutdown. * fix(pi): build the title inside the guard, not as an argument to it paintTitle caught the setTitle throw but not the two calls one argument to its left: pi.getSessionName() asserts runner liveness the same way ctx.ui does, and process.cwd() throws ENOENT once the worktree is unlinked under a live pane. Four of the six call sites are timer callbacks, where an escape is an uncaught exception and pi exits(1) through its own handler — so the cwd route was reachable today. paintTitle now takes a builder and runs it inside the existing try. * fix(pi): let only the pane-owning process assert the needs-input marker The spinner is harmlessly per-process, but the marker is status the pane reports, and child agents inherit ORCA_PANE_KEY. Gate the two dialog handlers on a PID claim, mirroring ORCA_PI_STATUS_OWNED in the status hook. |
||
|
|
12f53da542 |
Remove settled-worker automatic resume and hibernation fences (#19544)
* Remove settled-worker automatic resume and hibernation fences * test: retirement rollback case follows the no-fence policy Case 4 seeded and asserted automaticResumeBlockedBy, which this branch deletes. A rolled-back settled worker is now an ordinary done record that wake clears as passive evidence, same as any finished agent pane. * chore(i18n): regenerate the runtime-required catalog for the contrast floor strings * test(orchestration): give the stopping-worker guard fixtures a Run |
||
|
|
98fdbc4ade | fix(orchestration): file federated worker mail under the coordinator Run (#19542) | ||
|
|
53852c9ca4 |
feat(terminal): make the contrast floor user-configurable (#10754) (#18126)
* feat(terminal): make the contrast floor user-configurable (#10754) The xterm minimumContrastRatio floor was hardcoded (3 on dark backgrounds, 4.5 on light) and applied to every pane with no way out, so TUIs that use deliberately low contrast were rewritten: Powerline separators drawn in the neighbouring segment's background became visible seams, and dimmed secondary text lost its hierarchy. Adds an optional `terminalMinimumContrastRatio` setting under Settings -> Terminal -> Rendering. Blank keeps today's automatic, background-luminance gated floor; 1 disables correction entirely (matching VS Code's documented `terminal.integrated.minimumContrastRatio` and iTerm2's off-by-default Minimum Contrast); values are clamped to xterm's 1-21 range. The floor is resolved in one place, so live panes, the Appearance preview and the dashboard terminal preview all follow it, and the existing value-gated write still avoids clearing xterm's contrast cache on no-op re-applies. The clamp also lives at the persistence boundary that every writer crosses, so a hand-edited profile or CLI write can never hand xterm a non-finite option. Mobile mirrors the desktop gate, so the resolved floor travels with the terminal theme payload as a new optional field; hosts that omit it leave older and newer clients on the luminance gate. Fixes #10754. Co-authored-by: Nyanako <44753291+Nanako0129@users.noreply.github.com> * fix(terminal): refresh mobile payload fixture and clarify contrast target * feat(terminal): make contrast controls intent-based with custom tuning --------- Co-authored-by: Nyanako <44753291+Nanako0129@users.noreply.github.com> Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> |
||
|
|
6c85e33197 |
perf: lazily index case-insensitive Windows environment keys (#19483)
* perf: lazily index case-insensitive Windows environment keys * test(windows): pin env expansion fallback against the per-miss lookup oracle Adds zero-enumeration, first-case-variant-wins, prototype-chain and 4,000-case randomized differential coverage, and groups the new cases under their describe. --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
ed881889c4 |
fix(terminal): fold-safe CAN/SUB and double-ESC handling in partial-escape tail (#19521)
`extractPartialEscapeTail` broke its own fold invariant
(extract(a + b) === extract(extract(a) + b)) in the oscEsc/stringEsc states,
so a PTY read that split there produced a different pending tail than the
same bytes delivered whole — the tail snapshots append after a restore.
Two causes, both in the "ESC did not terminate the string" branch:
- CAN/SUB were routed through `stateAfterEscByte`, which maps them back to
`esc` instead of aborting to ground. `extractPartialEscapeTail('\x1bPx\x1b\x18X0abc')`
returned '\x1b\x18X0abc'; the chunk-split fold returned ''.
- A second ESC opened its new sequence at `i - 1` rather than at itself.
`extractPartialEscapeTail('\x1b] \x1b\x1b^')` returned '\x1b\x1b^' whole but
'\x1b^' folded. The fold was right — xterm starts the sequence at the second ESC.
The existing fuzz only asserted the fold as `advance(extract(pending), chunk)`,
which is a tautology because every PENDINGS entry is already a tail. Replaced
with a sweep that re-splits the combined stream at every code-unit boundary,
and extended the alphabet (NUL, 0x20 intermediate, CJK) and SEQUENCES with
CAN/SUB and doubled-ESC-inside-string cases. A 1.25M-split fold fuzz over a
VT alphabet goes from 421 failures to 0.
|
||
|
|
5b111ae607 |
perf: memoize ancestry when selecting foreground agents (#19502)
* perf: memoize ancestry when selecting foreground agents * perf(foreground): scope the ancestry memo to its ancestor and process snapshot --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
ef6ad22431 |
perf(native-chat): preserve historical tool rows while streaming (#19364)
* perf(native-chat): preserve historical tool rows while streaming * perf(native-chat): short-circuit identical rows and lock producer immutability Most folded rows come back as the input object, so compare identity before scanning fields and blocks. Add a regression test for the invariant the reuse cache depends on: ordering and folding never rewrite producer-owned messages or blocks, which reused rows alias. |
||
|
|
36d209f515 |
Verify failure causality in PR checks fix prompt before making changes (#19435)
* Update PR checks fix prompt to verify failure causality before fixing Revise the prompt to classify failures as caused by this branch, not caused, or uncertain before making changes. Only proceed autonomously for confirmed failures; ask the user for guidance on uncertain or unrelated issues to avoid fixing failures that weren't caused by the branch. * Update PR checks fix prompt to verify failure causality before fixing - Emphasize investigation phase by reframing prompt: "Investigate" rather than "Fix" - Extend untrusted-data warning to all investigation sources (repository files, commit messages, diffs, CI output) - Add test verifying injection safety: malicious input confined to JSON payloads, never as prompt instructions * Refactor buildFixChecksPrompt test to focus on field mapping The wrapper's only responsibility is renaming mobile PR fields onto the shared prompt builder. Remove assertions about prompt wording, which are already covered by the builder's own test suite. Simplify the test to verify the field mapping contract and nothing else. |
||
|
|
b52d777c95 |
perf: index prior memberships during project identity succession (#19463)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> |
||
|
|
894fbc8698 |
perf: normalize only retained browser history candidates (#19460)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> |
||
|
|
83167f08ff |
perf: index VM feature restoration and precompute sorting identities (#19457)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> |
||
|
|
990674f7e3 |
perf: sum omitted workspace sizes without intermediate objects (#19491)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> |
||
|
|
d0969a4917 |
perf: parse Git history headers without splitting commit bodies (#19486)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> |
||
|
|
7dd6373e2c |
perf: skip sorting when all final automation runs fit (#19485)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> |
||
|
|
5d0a45bb92 |
perf: use counted membership for worker transcript roster twins (#19484)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> |
||
|
|
fb9d08f5f9 |
perf: index project table option and iteration order (#19476)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> |
||
|
|
a278d84a4e |
fix(pi): show input modals as waiting instead of working (#18836)
* fix(pi): show input modals as waiting instead of working * test(pi): verify real input dialogs through Electron CDP --------- Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
da836faeef |
fix: preserve terminal retirement proof across renderer publications (#19002)
* fix: preserve terminal retirement proof across renderer publications * refactor: share the live-surface filter between retirement proof preservation and projection The publication projection already dropped proofs whose surface is live; reuse that as one helper instead of a second inline scan. * fix: emit stored retirement proofs from host-authored snapshot writes Three callers built a snapshot, stored it, then emitted the pre-store object. Storing grafts on the preserved proofs, so those frames carried the stored snapshotVersion without the proofs; subscribers dedupe on version and never saw them. * fix: send terminal retirement proofs once per stream and fence them by occupant Proofs are pinned per worktree for the host's lifetime, so every snapshot publication — including a 50ms title tick — re-shipped up to 64 proofs (~17 KB on realistic ids) to every paired client. Negotiate session-tabs.retirement-proof-delta.v1: the host projects each session-tabs stream to send a proof only the first time that stream carries it, and a capable renderer keeps the union in a ledger keyed by (environment, worktree) with the same 64-entry bound and the same live-surface drop rule as the host, reset on removed frames and on a new connection generation. Legacy clients keep receiving the full list; CLI and mobile do not advertise the capability. Also inherit worktreeInstanceId onto identity-less host writes so a host write between two renderer occupants can no longer launder one occupant's proofs into the next. * fix: keep an empty proof delta distinguishable from a proof-less host A negotiated stream now sends retiredTerminalSurfaces: [] when nothing is new instead of omitting the field. Absence is the host's "I hold no proofs" signal — which is also what a recreated worktree's fresh host entry publishes — so the client ledger forgets on absence and a successor occupant never inherits its predecessor's proofs, even when the removed frame was missed. * test: pin ledger visibility against a legacy full-list host An old host sends the full proof list whenever it holds any and omits the field when it holds none. Prove the new client ledger shows exactly what a legacy client would see across that sequence, so forgetting on absence is verified not to regress the mixed-version case. |
||
|
|
c1e15c4008 |
feat(native-chat): read a tool batch as a group (#19372)
* feat(native-chat): read a tool batch as a group A run of several tool calls collapsed to one joined string: names and arguments run together, separated by a middle dot that also occurs inside `browser.open` and `tools/read`, with the overflow cut mid-token. Opened, the member rows sat flush with the header and with the message content around them, so the batch had no visible end. Two presentation changes, no new derivation: - Each member gets its own bounded pill in the collapsed header, carrying its own category glyph, so the boundary between calls is a shape rather than a character. Pills wrap instead of truncating, and members past the summary cap are counted in `+N more` rather than dropped silently. - Opened members are indented under the header, which is what marks where the run ends. `toolRunSummaryMembers` keeps the run's leading calls apart instead of pre-joining them; `summarizeToolRun` now derives its string from it, so mobile's header is byte-identical and the two cannot disagree about which calls speak for a run. Two existing behaviours are pinned by test rather than changed, both being naming decisions rather than layout ones: the header still prints the raw `mcp__linear__list_issues` while the row beneath prints the split name, and a call carrying only a `url` still falls through to a JSON preview clipped at 28 characters. * fix(native-chat): bundle hidden tool count copy * fix(native-chat): drop the filled pill for a glyph-led member list Rendered in the app, the filled chips were wrong twice over. `bg-accent` is reserved for hover/active row backgrounds, and the only full-strength use of it in native chat is on payload and diff surfaces — so each member read as a shrunken content block, and a run became the loudest thing in the transcript. Worse, `flex-wrap` degenerated: at a 297px pane each member is 274-288px, so every one took its own line, the header grew 24px to 72px, and the `5x` count centred against the block landed beside the second member as though it counted that call alone. The glyph already marks where a member starts, so the fill was carrying no information the icon wasn't. Members are now inline, glyph-led, and separated by spacing; the list stays one line and truncates as a whole, as it did before this branch. `+N more` moves outside the truncating span so the count of what is not shown survives a pane too narrow to print the list. Members carry `data-tool-run-member` rather than being found by their fill. * fix(native-chat): let the run summary size to its content `flex-1` on the truncating member list made it claim the header's slack, so `+N more` was pushed to the far right edge with a gap between it and the last member it counts. Without it the span still shrinks and truncates — `min-w-0` plus the default shrink is what drives the ellipsis, which is how the header worked before this branch — and the count now sits directly after the list at every width. * fix(native-chat): separate run-header members with real whitespace An `ml-3` margin marks the boundary on screen but is invisible to a copied selection and to the button's accessible name, so the header read `ls -latools/read`. Adds a space text node between members and trims the margin to pay for its width. `+N more` also picks up the hover transition every other header segment already had. --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
9f044031fc |
fix(native-chat): render compaction notices, plan documents, and images (#19228)
* fix(native-chat): render compaction notices, plan documents, and images * fix(native-chat): avoid repeating notice text in details * fix(native-chat): journal canonical and legacy compaction events * test: add digest to native chat notice payload fixture * chore(native-chat): drop the planning doc from the PR --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
d0506bf5de |
feat(native-chat): add execution details and tool row identity (#19226)
* feat(native-chat): annotate tool rows with execution and source details * fix(native-chat): require explicit MCP identity for tool annotations * test: add required state to MCP projection fixture --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
6a47d2831f |
fix(native-chat): scope composer file drops to the pane that received them (#19328)
* fix(native-chat): scope composer file drops to the pane that received them A native OS file drop resolving to `target: 'composer'` carried no pane identity, so the window-wide payload was attached by every mounted composer. Because inactive chat tabs stay mounted (hidden), one drop populated every chat pane's attachment cache, and those chips replayed whenever the user returned to a tab they never dropped into. The workspace-creation composer and chat composers also leaked into each other, since neither could tell which surface actually received the drop. Composer drops now carry a `scopeKey` the way a terminal drop carries its tab and pane leaf id: the composer publishes its pane key as `data-composer-scope-key`, the preload harvests it during the composedPath walk, and each composer attaches only its own. The workspace composer's last-wins ownership stack now claims unscoped payloads only. * test(native-chat): supersede the bug-asserting drop repro with the scoping test The repro that landed on main asserts the pre-fix behavior (a drop reaching every mounted composer), so it fails once drops are scoped to the pane that received them. Its scoping cases now live in native-chat-composer-drop-scope.test.tsx, which keeps its editor-target control case verbatim and adds coverage for unscoped composers and a scope key published inside the drop-target marker. * test(native-chat): cover workspace composer drop isolation * fix(native-chat): authorize external attachment paths before preview --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
5cefb440bf |
fix(native-chat): stop an unanswered host from reading as one that refuses structured chat (#19321)
* fix(native-chat): stop an unanswered host from reading as one that refuses structured chat `readLocalRuntimeCapabilities()` returned `[]` both before the first status probe landed and after one failed, so "not asked yet" and "host says no" were the same value. Every structured-chat launch route consumed it, and an unprobed host was routed to legacy chat exactly as a refusing one is. Keep the two apart: the cache holds `null` until a probe succeeds, a failed probe leaves it `null` rather than emptying it, and the launch route names the case with its own blocker instead of borrowing `runtime-capability`. No routing outcome changes — both cases still decline structured chat. The point is that the reason is now truthful, which is what the routing work needs to build on: once a launch can target a runtime peer, capabilities come from that host, and an unanswered remote must not be indistinguishable from one that refuses. `hostCapabilities` on the launch route stays local-only at every call site; a per-target resolver replaces it when the route learns to reach a peer. * test: cover unknown runtime capability lifecycle and launch fallback --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
ce4a3a4186 |
feat(chat): add structured session rewind backend (#19235)
* feat(chat): add structured session rewind backend * fix(chat): make interrupted session rewinds recover safely * fix(native-chat): negotiate rewind runtime capability * fix(native-chat): consolidate remaining adapter imports --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
9fed61e5c2 |
Persist agents sidebar search visibility as pairing-local preference (#19313)
* Persist agents sidebar search field visibility as pairing-local preferen - Add `agentsShowSearch` to workspace UI state with default on - Include in pairing-local fields so preference syncs across clients - Convert search from menu action to checkbox menu item for explicit toggle - Update activity thread options menu to reflect checkbox state - Add localization strings across all supported languages - Update RPC schemas and preference persistence layer - Includes readiness validation reports confirming feature is clean * rm review * fix documentation |
||
|
|
5857357fcf |
feat(relay): log the region probe and name the assigned cell (#19307)
* feat(relay): log the region probe and name the assigned cell A desktop silently pinned itself to a far relay region for a day and every phone connect paid the round trip. Nothing in the desktop logs said which regions were probed, what they measured, why one was rejected, or which cell the host landed on, so the only way to diagnose it was a bench harness. The resolver now emits one line per outcome. A refresh carries every region's probe origins, the discarded warm-up, the kept samples, the minimum, the spread, and a verdict, then the chosen region or no-hint with the reason it withheld one. Cache hits, diagnostic overrides, and a director that cannot list its regions each get their own line so a quiet run is never ambiguous. Self-heal logs the cached region, the best measured region, the assigned cell's round trip, and whether it kept or deleted the cache. Only a refresh reports a catalog failure; a self-heal never chose a region, so a line saying it withheld a hint would be a lie. Relay status now carries the assigned cell so the pairing panel can name it. The field is optional because an offline host holds no assignment and the web client answers from a stub that never has one. Splitting catalog fetching out of the preference module keeps both files inside the line budget without a lint disable. * fix(relay): drop the assigned cell from statuses not served on it The origin pool publishes offline while it still holds the assignment it is about to rotate, so the panel kept naming a cell nothing was served from. The same class of bug hid a second instance: the coordinator republishes registered right after the broker announces its cell, and that republish carried no cell, blanking the value moments after it was set. The cell would never have reached the panel in the real flow. Deriving the cell from the status at each publisher removes both. The rule lives beside the status type because it defines when the optional field is populated, and the coordinator reads the owned broker's endpoint rather than trusting a call site to remember to pass it. * i18n: add the relay cell label to the English catalog * test(relay): audit the relocated region catalog fetch call site * fix(relay): report a self-heal whose catalog request failed instead of staying silent |
||
|
|
da4da8e60a |
fix(agent-status): stop a stale self-authored agent title from faking a pending question (#19237)
* fix(agent-status): stop a stale self-authored agent title from faking a pending question A workspace card could show the amber "agent is asking you something" icon while every pane sat idle and its only agent row read `done`. Orca injects its own `<Agent> - action required` OSC title when a hook reports blocked/waiting, then classifies that same title back as evidence. Two gaps let that one-shot string outlive the state it described: - The pane-id sets that suppress the title heuristic were built only from FRESH rows, so once a row aged past AGENT_STATUS_STALE_AFTER_MS the pane stopped suppressing its own title and `permission` — which outranks `done` — decided the indicator. Pane identity is not a liveness fact, so it is now tracked separately and never expires. Stale rows suppress `permission` only; a working spinner re-renders, so its stale-row fallback is preserved. - The hook-driven tab-title write compared the resolved title against the pane's layout slot (`titlesByLeafId`, which only a mounted pane updates) while writing `tab.title`. Once those slots diverged, `done` resolved to a title equal to the pane slot, the no-op guard skipped the write, and the tab kept the stale label. The guard now compares against the slot it actually overwrites. All three status surfaces read the same suppression inputs, so all three showed it: the workspace card, the terminal tab glyph, and the cmd-J palette dot. Tests: each fix has a regression test that fails without it (the sidebar, tab-bar and palette tests all go red from a single ablation of the stale-set lookup). * fix(agent-status): preserve native permissions and cover palette fallbacks --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
0252fe5c36 |
feat(native-chat): show Codex subagent activity instead of opcode rows (#18773)
* feat(native-chat): show Codex subagent activity instead of opcode rows
Codex spawns subagents and reports their lifecycle, but Orca rendered only
gray `codex · item:subAgentActivity` opcode rows. Build the real display: one
summary row per spawn group with a live working count and token usage.
State is accumulated from `subAgentActivity.kind` alone. A live probe against
app-server 0.152.1 showed `agentsStates` arrives empty even in a real subagent
run, and that every activity item is delivered twice (item/started and
item/completed), so every transition is idempotent and terminal states latch.
Children never receive `thread/started`, so there is no nickname, role, or
depth to read; the row labels from the trailing segment of `agentPath`.
Two sweeps keep a row from claiming work forever: the parent turn's terminal
event settles still-running children, and session start marks a pre-restart
roster unverifiable rather than exited, since Codex resume replays no
non-message items and no event can ever settle them.
The roster rides a new NativeChatBlock variant paired with a plain-text twin.
A journal item kind could not be used: that union is closed, and an unknown
kind parses as malformed, which is the corrupt-journal class that can hide the
chat tab. Block types are explicitly admissible when unknown, so an older
client drops the block and renders the sentence.
MessageRow moves out of NativeChatMessageList to keep both files under the
max-lines budget without a disable.
* feat(native-chat): give the subagent summary row its bot glyph
The row led with a glyph that swapped on state — a check once every child
completed, a group icon otherwise — so a group appeared to change identity
the moment it settled. Per the approved mock, the glyph names the category
and never moves: state is carried by the status dot and the tone of the
words beside it.
Use lucide `bot`, the same glyph the individual `subAgentActivity` rows take
in the eight-category vocabulary, so the summary reads as their parent. Slot
and glyph are the mock's 16px/14px, muted by default, and the svg is
`aria-hidden` — the headline is what a screen reader announces, so the icon
never stands alone.
* fix(native-chat): correct the Codex subagent roster's build, journal write, and failure reporting
* Restore the exhaustive block handling that adding `subagent-group` to
`NativeChatBlock` broke. `formatWorkerTranscriptMessage` and `boundBlock`
both fell through to `image-ref` field access, so `tsc -p` failed for the
CLI and node projects and `build:cli` could not emit. Both now guard on
`image-ref` explicitly and give the roster block its own branch.
* Stop the roster's publish from evicting its own append. The sink queue
coalesces by `coalescingKey` alone with no op-kind check, so passing the
append's key to `tryPublish` spliced the queued append out and the row
never reached the journal — permanently, since `lastSerialized` was
already set. `tryPublish()` now takes no argument, matching every other
call site. The regression test's fake sink honours the key, which the
previous fake did not.
* Keep `collabAgentToolCall` substantive. Only the MultiAgentV2 path emits
`subAgentActivity`, so a V1 turn has no roster row; suppressing its collab
tool calls too would have left a V1 fan-out showing nothing at all.
* Surface a settled failure while siblings still work. The summary now
reports the worst adverse outcome independently of the group verdict, so
the row shows `3 working +1 failed` with a failed-coloured dot instead of
a neutral pulsing dot. The plain-text twin names it too.
* Treat `/morpheus` as a child. Only `/root` is the turn itself; the old
segment-count test silently dropped a valid single-segment agent.
* Refresh token-usage recency on update so an active thread is not evicted
as the oldest entry, and scope the `agentsStates` comment to the V2 path.
* fix(native-chat): stop the subagent roster announcing a new duration every second
The roster row is an `aria-live="polite"` region and it contains the elapsed
clock, which reticks once a second for as long as the fan-out runs. A screen
reader therefore reads out a fresh duration every second, burying the state
changes the live region exists to report — the headline, the verdict, and the
`+1 failed` alert.
No other live region in the transcript does this. `NativeChatToolRun`'s live
button holds only the active tool label, and in `NativeChatWorkingStatus` the
variant that shows a duration is precisely the one with no `aria-live`.
Hide the clock from the accessibility tree only while it is moving. Once the
group settles the duration is fixed, so it stays readable and costs no
announcements.
* fix(native-chat): retry a refused roster publish, and stop two wrong readings
Four defects from a third review pass over the Codex subagent roster.
`write()` set `lastSerialized` before the append and rolled it back only when
the APPEND was refused. A refused PUBLISH left it set, so an identical replay
short-circuited and the revision was never published again. The repo's own
pattern is the opposite: `codex-structured-item-streams.ts` advances
`checkpointLengths` only once the append AND the publish are both accepted.
Roll back on either half.
That alone did not cover the sweep, which is the LAST event a group ever gets:
its `changed` guard skips the write on a retry because every child has already
latched, stranding the settled roster's final revision. Write when the previous
attempt was refused part-way, too.
`formatWorkerTranscriptMessage` read `block.agents` as its exhaustive fallback.
The journal schema deliberately admits block types this build does not know and
`client.call` casts the RPC result instead of validating it, so a newer remote
host's block reached that line and threw `agents is not iterable`, taking down
the whole `worker read`. It printed a harmless `[image omitted]` before. Match
`subagent-group` explicitly and degrade the unknown case.
The elapsed clock measured to `now` whenever no child carried a terminal
timestamp. That is exactly the roster restored from the journal after the host
died: the reconciler latches `unverifiable` without a `settledAt`, so a child
that ran four seconds reported the time since the crash as its run length, on a
row that is not even counting. Show no duration when none is known.
Also restores package.json to origin/main: the merge had deleted one of main's
two duplicate `bench:terminal-partial-escape-tail` keys. Behaviour-preserving
(JSON is last-wins and the deleted line was the dead one), but unrelated to this
PR and better left to its own change. No gate rejects duplicate JSON keys.
The new refusal tests also cover the append-side rollback, which had none.
* fix(native-chat): stop the subagent roster vanishing from every settled turn
`NativeChatToolRun` bailed out for a completed turn whose activity disclosure
is collapsed before it reached the branch that draws a roster-only run. That
guard exists to push TOOL activity behind the turn-status disclosure, and it
fires on exactly the shape a spawn group has: a roster message carries no tool
blocks, so `selectActiveToolCall` returns null and `isSettled` is true, while
the list passes `expandOverride={expandedTurnIds.has(turnKey)}` — false until
the reader opens that turn — and `activeTurnIsWorking={false}`.
That is the default state of every finished turn in the transcript, so the one
compact row this feature exists to leave behind ("Ran 3 subagents") disappeared
the moment its turn ended. Worse, `MessageRow` counts a spawn group as
renderable specifically so the row survives, then rendered a wrapper around a
component that returned null — the empty ghost bubble its own guard is written
to prevent.
Order the roster branch before the disclosure guard. A roster has no tool
activity to hide, and the guard's reasoning ("a failed child command looked
like the whole response was still running") does not reach it. Runs that do
carry tool blocks still fall through to the guard unchanged, and in practice a
roster never shares a message with them: it is its own `role: 'system'` journal
row and `isToolOnlyMessage` is false for it, so `foldToolMessages` never merges
tool blocks into it.
Also drop childless groups when building the rows, so `subagentRows.length`
stays an honest test of "something will draw" — the roster-only branch returns
a margin-bearing wrapper on the strength of it, and a group with no children
renders null.
Both tests fail with their fix reverted; the existing NativeChatToolRun suite
still passes, so the completed-turn disclosure behaviour is unchanged.
* test(native-chat): cover the subagent roster at the message-list level
Every defect this feature has shipped so far lived in the assembly between
rows, and the row-level suites kept passing through all of them. Loop 4's
regression — a settled roster swallowed by the completed-turn disclosure —
was found by reading the code, not by a test, and an independent visual-proof
run observed the same symptom in the real UI and routed around it rather than
reporting it. `NativeChatToolRun` rendered alone is handed `expandOverride`
and `activeTurnIsWorking` by the test author, so it agrees with whatever the
caller was assumed to pass.
Drive the real component instead. The roster is its own `role: 'system'`
journal row carrying the producer's two blocks (structured + plain-text twin),
so what reaches the DOM depends on `foldToolMessages`, the turn-key mapping
and the disclosure state `NativeChatMessageList` owns — none of which a row
test exercises.
Three cases, on one assembled transcript that holds tool calls AND a roster:
- a settled turn with activity collapsed, the resting state of the whole
transcript, still shows the row (fails with loop 4's reorder reverted);
- tool activity stays behind that disclosure and appears only on expand,
and expanding draws no second roster (fails with the guard removed);
- a working turn reads as a live spawn.
The first also pins that the plain-text twin is dropped rather than printed
beside the row it stands in for.
Timestamps are explicit and ascending: the list re-sorts by (timestamp, id),
so rows sharing a millisecond tie-break alphabetically and the user turn can
sort last, stranding the roster outside its own turn and reconciling live
children to `unverifiable`.
No production code changed.
* fix(native-chat): make "counts as renderable" and "actually draws" agree for a spawn group
`MessageRow` counts any `subagent-group` block as renderable, but
`NativeChatSubagentRun` renders null for a childless roster. A group with
`agents: []` therefore mounted a row that drew nothing — an empty div that still
costs the transcript one `gap-5` slot. The Codex producer never writes one (every
`write()` call site operates on a group that already holds an entry), but the
block schema admits `agents: []` with no `.min(1)`, and the wire is where such a
shape would arrive.
Narrow `subagentGroupBlocks` — whose only production caller IS that renderable
check — to the groups that will draw, behind a named `isRenderableSubagentGroup`
that `NativeChatToolRun` now shares in place of its own copy of the predicate, so
the two guards cannot drift apart again. A childless group carrying its
plain-text twin now prints the twin, which is what the twin is for; a bare one
skips the row entirely.
Also correct four comments that had stopped describing the code:
- the roster header called `agentsStates` "always empty", contradicting the
probe note in `codex-subagent-activity.ts` — it is empty on the MultiAgentV2
path that emits these items, and the V1 path does populate it;
- `tokensByThread` was documented "retained UNCONDITIONALLY" while
`handleTokenUsage` LRU-caps it 65 lines below;
- the sweep is not "the LAST event a group ever gets": neither `settleTurn` nor
`settleSession` removes the group, so a later `thread/tokenUsage/updated`
naming a swept child still writes it. The retry condition is right; only its
stated reason was wrong;
- the `subAgentActivity` classification is not reached "for every event — and
every one of them arrives twice". `handleSubagentItem` intercepts those items
before `items.handle`, so the live path never consults the catalog;
`restoreThread` replays them straight through, and is the real consumer.
Comment-only apart from the childless-group guard.
* fix(cli): stop `worker read` printing the subagent roster sentence twice
The producer ALWAYS writes a roster block beside a plain-text twin carrying the
same sentence, for clients that cannot draw the block. The renderer honours that
contract from one side — it draws the block and drops the twin. The CLI honoured
neither side: it printed the twin as prose AND rendered the block as
`[subagents] <same sentence>`, so a real roster message read
[system] Ran 2 subagents (1 failed)
[subagents] Ran 2 subagents (1 failed)
Take the mirror of the renderer's rule, which is the cleaner half for a text
client: the twin IS the sentence, so print it and drop the block it stands in
for. A block that arrives WITHOUT its twin — a shape the wire admits and no
producer writes — still stands in for itself, because dropping it
unconditionally would lose the roster entirely. Either way the sentence prints
exactly once, off the same `subagentGroupFallbackText` helper both sides use.
Unreachable through `readWorkerTranscript` today, whose provider rollout decoder
never emits a `subagent-group` block — but the formatter is the CLI's contract
for any transcript source, and the shape is already producible.
The test pinned a TWIN-LESS group, a body `codexSubagentGroupBody` never writes:
it asserted the exact double-print this fixes was correct output, and would have
blessed either behaviour. Rebuild the fixture as the producer's real two-block
row, with the sentence taken from the shared helper rather than hardcoded so it
cannot drift, and assert the sentence appears exactly once. The twin-less shape
keeps a test of its own, labelled as the wire-only fallback it is.
Also record why `settleTurn` keys on the RAW `turnId` while `groupFor` remaps
off-primary activity onto the primary's active turn. The asymmetry is
load-bearing, not an oversight: were `settleTurn` to remap, a child thread
ending its own turn would sweep the parent group and settle every still-working
sibling to `unverifiable`. The lookup missing is the intended no-op.
* fix(native-chat): add the subagent roster's localization keys and narrow its twin filters
The roster row called 16 `components.native-chat.subagents.*` keys that were
never added to the catalog, failing the localization gate. Synced en.json; the
English strings are the component's own inline fallbacks, so nothing renders
differently.
Also tightens the twin/block handoff on both readers. The renderer dropped
every text block once a roster was present, which is safe only because Codex
writes a roster as its own message — the block is provider-agnostic, so a lane
folding prose in beside one would have lost it on desktop while mobile kept it.
And both readers decided "the twin is already printing" by recomputing the
sentence and comparing bytes, which a roster from a newer build never matches:
its unknown state normalizes to `unverifiable` here, so the CLI printed the
roster twice with two different verdicts. Both now recognize a twin by shape.
* test(native-chat): pin the roster twin recognizer against prose
Both readers use it to decide the twin is already printing, so a false positive
eats a message's real prose and a false negative prints the roster twice.
* docs(codex): restore the roster's evictionated trigger to its KNOWN LIMITATION
The previous rewrite dropped both triggers the old comment named and kept only
the restart one, but eviction is the reachable half: `groupFor` caps `groups` at
MAX_CODEX_SUBAGENT_GROUPS and drops the oldest-INSERTED entry (it returns an
existing group without re-inserting, so this is not LRU), which can evict a
still-live group in-process. The row identity is keyed on the group id alone, so
the next activity item rebuilds that row from one child — the same N-to-1
rewrite, with no restart, and with the sweep skipped so the children never latch
`unverifiable`. Also softens "every real turn id is freshly minted" to the
provider assumption it is: turn ids are read verbatim off provider frames and
nothing in this repo mints or asserts them.
* docs(codex): justify the subagent wire notes from the live probe alone
The roster and disposition comments explained themselves in terms of a
provider-internal path taxonomy rather than anything this repo can observe.
Restate them from the evidence Orca actually has: the live app-server probe
saw `agentsStates` arrive empty, so nothing reads it; and `collabAgentToolCall`
stays substantive because nothing guarantees a session reports subagent work as
`subAgentActivity` at all — one that only emits the collab tool call gets no
roster row, and suppressing that too would leave its fan-out blank.
Same behaviour, same tests; comments and one test name only.
* fix(native-chat): stop the roster's durable twin from claiming live subagents
The spawn-group row is written once and revised in place, but the row itself
is durable and replayed on every reconnect. Its plain-text twin — the only
thing a client that cannot draw the block ever sees — froze a live count into
that row: `Kicked off 4 subagents — 2 working`. The desktop renderer never
shows it, and reconciles the block's `working` to `unverifiable` outside the
live turn. A text-only reader does neither. When the writing process dies
mid-flight the turn-end sweep never runs, so the sentence keeps asserting two
running children forever, with nothing left that could re-check them. That is
the collapse `docs/reference/ssh-execution-boundary.md` forbids: loss of
contact reported as a live state.
Fix it at the source rather than per client: the durable sentence now states
only what survives its process — that the group was spawned, plus whatever
outcome had latched. `Kicked off` vs `Ran` stays, because it reports whether an
outcome was recorded at write time; saying `Ran` while children were in flight
would assert they exited, the same error inverted. The adverse count stays so a
failing fan-out still reads as failing. Reconciliation stays in the renderer,
where the block still needs it.
The twin recognizer keeps matching the legacy `— N working` shape: journals
already hold those sentences and their rows replay forever, so dropping the
branch would print every one of them twice, once as the block and once as prose
the reader meant to drop.
Also align the two functions that read `agentPath`. The root check compared the
raw string while the label normalized separators, so `/root/` was both the turn
itself and a child of it — a phantom row labelled `root` inflating the group by
one. Compare normalized segments instead, keeping `/morpheus` a child. And a
trailing segment with nothing visible in it survives the empty-segment filter
and would draw a nameless row, so it now reads as no label and falls back to the
placeholder.
* fix(codex): key the subagent label collision ordinal on what the row draws
`codexSubagentLabel` tested the trailing segment trimmed but returned it
untrimmed, and `claimLabel` keys its collision ordinal on that string. Two
children at `/root/read` and `/root/ read ` therefore both drew as `read`
with no ordinal — the one thing the ordinal exists to prevent. Return the
trimmed segment so labels that render identically collide.
Also correct the legacy-clause note on the twin recognizer. It claimed shipped
journals hold the old `— N working` sentence; the feature is unreleased, so the
only journals holding one are dev worktrees of this branch. The branch still
earns its place — those rows replay too, and it adds no false-positive surface
the bare shape does not already carry — but the stated reason was wrong.
* test(native-chat): retire the subagent-visibility guards now the roster renders
Two tests from the sibling item-coverage PR asserted that subagent items stay
on the generic gray row, explicitly gated on "until a real renderer exists".
This branch is that renderer, so both guards fire on merge — the handoff they
were written to mark rather than a regression.
They now pin the other side of it: subAgentActivity is suppressed because the
spawn-group roster renders it, and collabAgentToolCall deliberately stays
visible, since nothing guarantees a session reports subagent work as
subAgentActivity at all.
Git merged both files without conflict; only running the suite surfaced this.
* fix(native-chat): let a subagent swept at turn end still report what it did
The turn-end sweep marks still-running children `unverifiable`, and the
producer latched on any state that was not `working` — so `unverifiable`
latched too. A subagent that outlived its turn then reported `completed`, the
latch refused it, and a child that finished successfully read as one we never
saw finish, permanently.
One predicate was doing two jobs. `isTerminalSubagentState` is right for
counting — `unverifiable` is not working — and wrong for latching, because
`unverifiable` records that we stopped being able to see the child, not what
it did. Split them: a child's own verdict latches, the sweep's guess does not.
The reverse stays refused. Nothing returns to `working` once we have given up
on it, so a straggler progress tick cannot re-light a settled row.
Neither the latch nor the sweep was wrong alone, and both were tested; the
defect lived only in their interaction, and only when a subagent outlives its
turn — which the probe that drove this design never produced, because the
parent it captured waited on its child.
* fix: drop the @pnpm/exe lockfile drift a merge staged
`git add -A` swept up the pnpm-lock.yaml mutation that every pnpm invocation
leaves in this repo. Nineteen lines, thirteen of them @pnpm/exe, and it fails
sixteen unrelated CI checks — native smoke, typecheck, packaging, xterm patch
sync — none of which name the lockfile.
* fix(native-chat): restore the item fall-through an inline dropped
Inlining the subagent routing helper lost its null check: the roster returning
null means it did not claim the item, and the translator must keep looking.
Returning unconditionally once any thread item parsed swallowed every ordinary
item — twelve settlement tests, none of them about subagents.
* fix(orchestration): rebind the subagent block arm to the renamed bound state
Main renamed clipMetadata's second parameter from a warnings set to a
TranscriptBoundState. The subagent-group arm still passed `warnings`, and git
merged both sides without a conflict because the lines never overlapped — the
rename and the new arm are in different hunks. Typecheck was the only thing
that could catch it, and did.
* fix(codex): publish the turn tail for a subagent item the roster claims
Main's #19055 added a `subAgentActivity` arm to the provider activity table,
which is reached only through `publishActivity`. The roster's admission returned
above that call, so every `subAgentActivity` item bypassed it and a fan-out that
reports nothing else left the turn tail stuck on the previous frame's text.
`publishActivity` already no-ops on a refused admission and on a non-primary
thread, so routing the roster's admission through it is safe.
Also corrects a docstring the frames extraction copy-pasted onto
`settleOversizedNotification`.
* fix(native-chat): bound the subagent roster on every boundary that carries it
The spawn-group arm was the one collection in the worker-transcript payload with
no cap, and the one block type mobile's `sanitizeBlock` forwarded verbatim. The
producer's `MAX_CODEX_SUBAGENTS_PER_GROUP` does not reach either boundary: the
journal schema declares no maximum on `agents`, and a remote host may run a build
with a different cap. Both transports now cap the roster and bound `id`, `label`
and the open `state` string; `label` and `id` also take the standard inline bound
on the journal write path, where every other provider string already does.
A token count is now persisted onto its entry at write time. `write` rebuilt
`tokens` from the LRU-capped thread map on every write, so an eviction silently
retracted a count the durable row had already shown.
Adds the first coverage of the three roster caps, including the group eviction
that rewrites a row from N children down to one.
* fix(native-chat): keep the roster drawn beside tool calls and its clock honest
The roster-only escape is keyed on `blocks.length === 0`, so a spawn group
sharing its message with tool-call blocks fell through to the settled-turn guard,
which returned bare null and took the roster with it — the exact regression the
escape above was written to avoid, after the message row had already counted the
group as renderable. Unreachable for Codex today; the block type is deliberately
provider-agnostic, so it is live for the Claude lane.
The elapsed clock also froze at a sibling's timestamp on a partial sweep: in a
group where one child completed and another is unaccounted for, the ended turn
left `working === 0` with the completed child's `settledAt`, and the row showed
that child's duration as the group's run length. No clock is drawn while any
child is `unverifiable` with no terminal timestamp.
* perf(native-chat): bound the roster's provider strings without digesting them
`boundInlineText` computes a sha256 and a Buffer BEFORE it checks the length,
so the roster paid two digests per child on every write even when nothing was
truncated — and `write()` runs on every claimed activity item (each delivered
twice) and again from `handleTokenUsage`, which streams. A same-process A/B over
a 64-child group: 76.5 us/write before, 2.0 us/write after (plain, unbounded row
is 1.2 us).
The cap changes with the mechanism. 16 KB is the tool-output bound; both readers
of this row already clip the same fields to 512, so the producer was admitting
~2 MB per durable roster row for consumers to throw ~97% of away. One
`MAX_SUBAGENT_FIELD_CHARS` now serves the producer and both readers, and the
marker is an ellipsis rather than the tool-output truncation sentence — `id` is
the roster key and the renderer's React key.
Also raises the orchestration arm's per-group bound from 20 to the producer's
64, matching the mobile arm: a 21-64 child group is routinely producible here,
so that arm clipped children and warned while its sibling clipped none. The
slice and warning stay as the transport's own defence against a remote host with
a larger cap.
* fix(orchestration): suppress one roster block per twin, not all of them
`hasTwin` was a single boolean over the whole message, so a message carrying two
`subagent-group` blocks and one plain-text twin printed one sentence and dropped
the second roster with no marker. Count the twins and claim one per group
instead. Not reachable from this branch's producer, which writes one group per
journal item, but the surrounding reasoning is explicitly about wire shapes the
producer never writes and this is the adjacent one it missed.
* fix(native-chat): loop-3 fixes to the Codex subagent worklog
Five defects loop 2's own fixes introduced.
Twin claiming was order-blind: the count-based claim silenced whichever
roster block came first, so a lone twin belonging to a LATER group erased
an earlier group's roster and printed the later sentence twice. Exact-text
claims are now settled for every group before any leftover twin is claimed
by position; the positional fallback stays for a newer build's frozen twin,
which can never equal a recomputed sentence.
`boundSubagentField` sliced UTF-16 units and could leave a lone high
surrogate in a durable row, and the clip removed exactly the tail that told
two children apart — `id` is the renderer's React key and `claimLabel`
writes its repeat ordinal at the end. It now backs off a split pair and
reserves the child index inside the bound, so both readers' re-clip cannot
cut the disambiguator off again.
`MAX_SUBAGENT_FIELD_CHARS`'s doc claimed a `groupId` bound the producer
never applies; the doc now says so and why. The worker-transcript metadata
cap is a separate literal again: it governs message ids, turn ids, tool-call
names and image urls, so a roster-motivated change must not move it.
* fix(native-chat): never infer a lost subagent from a turn boundary
QA drove a real Codex session with three live `spawn_agent` children and sent
a mid-turn correction. The roster row immediately read "Ran 3 subagents /
3 unverifiable" with no clock, while all three were still running — they
reported `completed` 57-87s after that turn ended.
Both sites rested on the same false premise: that a turn ending means no
event will ever settle a child. Children outlive their turn and keep
reporting into the same group.
- Renderer: drop `reconcileSubagentRoster`. Nothing plumbed to the component
distinguishes a row written by a dead host from a turn that merely ended —
journal render items carry no epoch, and a new epoch deletes the rows of the
one it supersedes — so the row now draws the state the journal recorded.
Under-claiming beats over-claiming.
- Main: stop sweeping on `turn/completed`. That sweep wrote `unverifiable`
into the DURABLE journal, which mobile reads with no reconciliation.
`turn/completed` is Codex's only turn-end notification, so an abort cannot
be told apart from a clean finish; the safe default is not to sweep.
`settleSession` — the provider actually being gone — is unchanged and is now
the only sweep. `unverifiable` stays non-latching so a late verdict still lands.
* test(native-chat): pin the roster at the seam the QA defect came from
The mid-turn correction opens a new turn, so the fan-out's row stops being
the current turn and the list hands the roster `activeTurnIsWorking={false}`.
Asserted through the list, not the component, because that prop is what
carried the wrong claim.
* fix(native-chat): settle a roster the dying host never got to sweep
`settleSession` only fires when the provider goes away while this process is
alive. If the host itself dies, nothing sweeps and nothing reconciles on
restore, so a `subagent-group` row persisted as `working` claimed live children
forever — the mirror of the defect the previous commit fixed, and the same
`ssh-execution-boundary.md` violation in the other direction.
Reconciled host-side, at journal open, not in the renderer: mobile shows only
the durable text twin and reconciles nothing, so a renderer-only fix would
leave it claiming live children indefinitely. Opening the journal is also the
one moment a host can honestly say the previous writer is gone.
- `staleSubagentRosterRevisions` rewrites every child still reading `working`
to `unverifiable` and regenerates the twin from the same summary, so the
block and the sentence cannot disagree.
- No terminal timestamp: the child stopped being observable at an unknown
moment, and stamping the reopen would report the downtime as its run length.
- Revises in place under the parsed identity, so a reopen upserts the row
rather than appending a duplicate, and a second reopen writes nothing.
- Skipped on a corrupt load: that journal is still owed a rebuild from provider
history, and content past the repair's free sequence retires the demand.
Reconciles journal ROWS, not roster state — the producer's in-process group map
is untouched, so the roster's known seeding limitation is unchanged, as is
`canReplaceSubagentState`: `unverifiable` still does not latch.
---------
Co-authored-by: Merge Sim <sim@local>
|
||
|
|
bffdad9f05 |
fix(native-chat): make structured chat tabs renameable (#19153)
* fix(native-chat): let a structured chat tab be renamed Renaming a native chat tab accepted the text and silently did nothing: setTabCustomTitle only scanned terminal tabs and only bridged to unified tabs whose contentType was 'terminal', so the agent-session tab it was keyed to never matched. Any label that did land was then re-nulled by the next host snapshot, which preserved color/createdAt/isPinned but not customLabel. Also routes both placeholder sites through one helper so a Claude chat stops falling back to 'Codex Chat'. * test(native-chat): cover structured chat tab rename and label fallback * chore: drop the local @pnpm/exe lockfile artifact Swept in accidentally; running pnpm here adds @pnpm/exe to the root lockfile, which fails CI's frozen-lockfile guard. * fix(native-chat): reach the rename shortcut and tab color too Review found the first fix covered only the context-menu path. The tab.rename shortcut gated on activeTabType === 'terminal', so on a structured chat tab it stayed the silent no-op this branch set out to fix. setTabColor carried the identical terminal-only lookup one function below the one that was fixed. Both lookups now share one resolver instead of two copies. * fix(native-chat): stop unknown agents reading as Codex, cover the terminal path Review found the placeholder helper encoded "unknown means Codex": its signature accepts null/undefined and Tab.agentSessionAgent is the open AgentType, so the first caller passing a Tab would label gemini or grok as "Codex Chat". Routed through the shared agent-name table instead. Also adds the missing regression test that a terminal rename still resolves through its entityId now that both rename and color share one resolver, and a guard on a test that passed with the fix reverted. * fix(native-chat): degrade instead of throwing on a null tab title A stacked branch can publish title: null when a conversation name is cleared. The wire type says string, so this consumer trusted it and threw inside the store patch that applies the snapshot. Fall back to the placeholder — the producer bug is fixed separately, but a consumer of wire data should not crash on a contract violation. * fix(native-chat): rename the focused structured tab, not a background terminal * fix(native-chat): cycle terminals from the structured tab, not a stale terminal --------- Co-authored-by: Merge Sim <sim@local> |