mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 16:02:38 +00:00
b9ff2a9be8fbe148be04a481ff796a0460867c8b
9032
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6e7782f741 |
refactor(mobile-web): announce bundle ranges on the manifest reply
The manifest reply now names the range grid in an optional rangeBytes, beside chunkBytes, and the status capability is gone. The range method takes exactly the chunk params on that grid instead of a caller length. Both methods share one verified read that returns the six-field header, and the range handler checks the connection again before deflating. Range schemas move into the bundle RPC contract; SHA256_PATTERN is shared from the manifest contract. Shared refusals are tested once over both methods. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
6f5abf4bde |
feat(mobile-web): serve gzipped 384 KiB bundle ranges behind a capability
Adds mobileWeb.bundle.range with its own strict params and result, so shipped chunk readers see no reply change. The host gzips each range at level 6 and sends identity when gzip does not shrink it, sharing the chunk method's read-slot budget and per-asset verification. status.get advertises mobileWeb.bundle.range.v1 beside mobileWeb.bundle.v1, and the method is allowlisted for paired phones. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
1b85be67d8 |
feat(native-chat): notify on every settled structured turn (#22105)
* feat(native-chat): notify on every settled structured turn
A structured chat that finished while you were elsewhere lit the sidebar
but never raised an OS notification, and a notification that did arrive
for one could not open the chat it came from.
Unread and delivery now come out of the single resolveAgentAttention
decision the terminal lane already uses: the structured dispatcher calls
applyAgentAttention instead of applyAgentAttentionUnread, so the same
policy that decides what to light also decides what to deliver, through
the same sound and blocked-permission tail.
Every settled turn notifies, as the CLI lane does. Success says
"finished"; failure and cancellation say "stopped" through the shipped
agentInterrupted flag rather than a second vocabulary. A turn whose
outcome the host never stated stays unknown and lights nothing.
The host now dedupes mobile fan-out by event identity (scope, session,
turn) beside the existing per-workspace burst cooldown, so a completion
two windows both saw reaches the phone once while each window still
decides its own banner. Clicking a structured notification reveals the
chat tab: its pane key's leaf is synthetic, so focusTerminal would hunt
a split-layout leaf that does not exist.
* fix(notifications): spend each mobile gate only when it actually notifies
Two review findings on the structured-chat notification lane, both real.
The mobile event gate consumed its reservation before the per-workspace
burst cooldown ran. Two chats in one workspace share that cooldown key,
so the second chat's completion could burn its event key and then lose
the cooldown to the first chat — never announced, yet permanently marked
as announced, so a later window dispatching it could no longer reach the
phone. The gate now peeks first and records the event at dispatch, which
also keeps a known duplicate from burning the cooldown slot.
A notification id is minted from the status row's stateStartedAt, and the
row re-projects that field as the turn settles: the working episode's
start moves into stateHistory and the settled start takes its place. A
banner raised in the window before that re-projection therefore carried
an id acknowledgement never rebuilt, leaving it on screen for good.
Acknowledgement now collects ids for the row's left episodes too — the
same episodes the unread check beside it already scanned, so the two
halves finally read the same turns. Lane-neutral: the terminal lane
mints its ids the same way and had the same gap.
* fix(notifications): drop the mobile event gate and reveal chats in folder workspaces
The per-event mobile dedupe defended against one completion being
dispatched by several Orca windows. Only one renderer mounts the
structured attention bridge, the completion feed is live-only with no
replay, and any in-process duplicate lands inside the existing 5s
per-workspace burst cooldown, which already collapses mobile and
desktop alike. The gate never acted on a real sequence, so the wire
field, the shared ledger and its tests go; mobile delivery is back to
main's behavior.
A folder workspace id ("folder:<id>") has no "repoId::" prefix, so the
click binding was skipped and clicking a chat notification there did
nothing. The chat route selects its workspace itself through
ui:focusEditorTab, so it now binds without a repoId; the terminal
route is unchanged.
* fix(notifications): retire the banner ids actually dispatched, not ids rebuilt from a moved row
A banner's id is minted from the status row's stateStartedAt at dispatch, and that field moves
afterwards: a completion can outrun the settled re-projection, and a settled structured row is
re-stamped with no history entry by any later journal row (a cancel appends a status note after
the turn settles). Rebuilding ids from the row's episodes at acknowledgement missed the second
case and fanned out up to 21 mobile dismissals per pane for ids never raised.
The shared delivery tail now records each dispatched id per subject; acknowledgement retires
those plus the current-row rebuild it always had. The acknowledgement collector is back to
main's single-field form.
* refactor(notifications): retire announced notifications by subject in main
Main now records, per pane, the ids it actually announced (a desktop banner
shown or a phone alert sent) and an acknowledgement passes the acknowledged
pane keys so main retires all of them. This replaces the renderer-side record
of dispatched ids: main is where the announcement happens, so it records only
real announcements, including phone alerts whose desktop banner focus
suppressed. The id rebuilt from the current row stays as the fallback after
a restart empties the in-memory record.
|
||
|
|
0b6f60ac3b |
fix(worktrees): a listing that predates a create can no longer retire the new workspace (#22311)
* fix(worktrees): a listing that predates a create can no longer retire the new workspace Creating a workspace while another one was being deleted could land the user on the empty Landing screen with the new workspace's Claude launch cancelled. A worktree listing whose scan began before the create finished (the prepared checkout is hidden from listings while it is still locked) was applied after the create returned, and the renderer read the new worktree's absence from that authoritative listing as a deletion: it purged the workspace's tabs, tombstoned the pending structured launch (so the host session was retired the moment it published) and cleared the active selection. Renderer: each provider invocation records the local create sequence when it begins (shared by every caller that coalesces onto it). The merge no longer drops or retires a worktree this client finished creating after that point, and the change-event diff purge defers to what the merge actually dropped. Hydrated rows keep being reaped. Host: a scan a worktree mutation overtook is published as non-authoritative instead of authoritative, and the create paths invalidate scans as soon as the new worktree is listable rather than only at the end of the create. * fix(worktrees): an overtaken scan re-derives the catalog, and the create fence rides every listing A scan a worktree mutation overtook was published as non-authoritative with a failure reason, which lit the sidebar's scan-failed indicator, hid external worktree cards and emptied the visibility dialog after a routine create/delete overlap, and its early return skipped the abort, currency and dual-owner guards every other branch honours. The listing now re-scans a bounded number of times instead (the invalidation already dropped the cache, so it scans afresh or joins a sibling's scan) and past the bound answers non-authoritative through the same guard chain, with no failure reason. The renderer's create-sequence inception is now captured where the provider invocation starts and released only when it settles, so a waiter released early cannot reset it for a later joiner, and the direct-SSH scheduler's refresh -- the only listing on hosts with no scan cache -- now carries it too. A re-created worktree id is re-inserted at the back of the create-record eviction order so the bound holds. * fix(worktrees): the host re-runs a scan a worktree change overtook, for cached and SSH listings alike; the renderer create fence is removed * fix(worktrees): the runtime listing re-runs a scan a worktree change overtook, and a headless host bumps the generation it witnesses * test(runtime): the change-invalidation suite lists through the public runtime method instead of casting to a protected one * fix(runtime): the runtime's own worktree change event bumps the scan generation its listing witnesses A headless host has no window notifier, so a delete (and every other change the runtime announces) never moved the generation the runtime listing re-runs against; a listing the delete overtook published the removed row as authoritative until the next refresh. The bump now rides the runtime's change event, ahead of the send, as the desktop notifier already does. * fix(worktrees): a listing that spends its re-scan bound logs the churn on the host Past the bound the listing answered without a trace, so continuous create/delete churn could not be diagnosed from a host log. One warning names the repo and the number of overtaken scans; a caller that withdrew stays silent. |
||
|
|
4f814c1dd4 |
fix(browser): scope floating browser shortcuts to their own panel (#22361)
* fix(browser): scope floating browser chrome shortcuts to their own panel The floating panel rendered its browser with the default 'focused' scope, so a focused split browser and the floating browser both answered chrome chords. The floating browser now answers only chords from inside its own overlay, and a focused split yields chords whose target sits inside the floating panel. * test(e2e): pin floating browser shortcut scope * test(e2e): reuse split shortcut fixtures in the floating scope spec |
||
|
|
2155673da3 |
fix(usage): price Codex long context per request, not per aggregate (#22360)
* fix(usage): price Codex long context per request, not per aggregate OpenAI bills a request whose prompt exceeds 272K input tokens at the long-context rates for that whole request. Orca applied 272K as a marginal threshold to daily, per-range and per-session token totals, so ordinary days of short requests were billed mostly at long-context rates and the summary disagreed with the model breakdown. Classify each token_count at parse time and carry long-context token counts through every rollup; the estimate bills that subset at the long-context rates and the rest at base. Bump the cache schema to 6 so existing caches rescan and pick up the split. * fix(usage): never class a total-only Codex delta as long context A token_count with only running totals yields the increase since the previous total, which can span several short requests; classing that aggregate as one >272K request would bill short requests at long-context rates. Only a last_token_usage record is one response. Also note that OpenAI's GPT-5.4/5.5 pages say "full session" where 5.6/6 say "full request"; the per-request reading is the stated assumption. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
dfff3915c4 |
fix(browser): scope back/forward/reload/zoom/grab shortcuts to the originating split (#22340)
* fix(browser): scope back/forward/reload/zoom/grab shortcuts to the originating split With two browser panes visible in a split, Back, Forward, Reload, Hard Reload, page zoom and Focus Address Bar fired in every visible pane. Main forwarded these guest chords without the page id, and each split's active pane subscribed. The renderer-side listeners for the same chords were also window-wide per pane, so a key pressed in the toolbar (or in a terminal in another split) reached every active browser pane. Guest-forwarded chords now carry the originating browserPageId; preload admits only well-formed payloads and each pane ignores ids that aren't its own. Toolbar-path listeners use the same focused-split scope Find already uses. The streamed remote pane's history chord moves onto that scoped hook. Cmd/Ctrl+C grab (STA-3319) gets the same scope and no longer arms while a text selection exists outside the browser pane, so copying from the native chat transcript works again. * refactor(browser): simplify split shortcut scoping per review Drop the preload payload admission (main and preload ship together), fold the three inline scope checks into browserChromeShortcutOwnsEvent, and replace the outside-overlay selection check with a plain live-selection rule so Cmd+C copies from surfaces that do not move split focus. * refactor(browser): share one zoom command type and tidy shortcut comments BrowserPageZoomEventDetail and BrowserPageZoomCommand were the same shape; keep one in shared/browser-page-zoom.ts and route guest and local zoom through a single handler. * refactor(browser): narrow the zoom event with instanceof instead of a cast * test(e2e): pin split-scoped browser shortcuts Two browser splits (and a terminal beside a browser) now prove that Back, Forward, Reload, Hard Reload, page zoom, Focus Address Bar, and the element grab chord act only on the split that sent them, from both the guest page and the browser toolbar. A native chat selection proves Cmd/Ctrl+C copies instead of arming grab. Split fixtures move to a shared helper so both specs reuse them. |
||
|
|
e16d9ca597 |
fix(usage): price GPT-6 Sol/Luna, Opus 5.5 and Fable 5.1, and correct GPT-5.6 rates (#22350)
* fix(usage): price GPT-6 Sol/Luna, Opus 5.5 and Fable 5.1, and correct GPT-5.6 rates gpt-6-sol and gpt-6-luna had no pricing entry, so their tokens were left out of the Codex cost total. claude-opus-5-5 and claude-fable-5-1 matched their major's pattern and were billed at Opus 5 ($5/$25) and Fable 5 ($1 cache read) rates. The GPT-5.6 Sol/Terra/Luna rates no longer matched OpenAI's published pricing (Luna was 5x high). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(usage): price gpt-5.2-pro, accept (max)/(ultra) tiers, tighten point-release matching gpt-5.2-pro matched the gpt-5.2 prefix and billed at $1.75/$14 instead of its $21/$168. Codex's `max` and `ultra` reasoning tiers in parenthesized form left the model unpriced. The new fable-5-1/opus-5-5 patterns also accepted a trailing letter, so `claude-fable-5-1m` read as Fable 5.1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
7cb4380179 |
fix(sidebar): hang the subagent chevron in the card gutter (#22331)
A top-level agent with subagents drew its expand chevron inline before the status dot, pushing the dot, icon, and title right of every other agent row. The chevron now hangs in the card's left gutter so all top-level rows share one status column, and leaf rows no longer reserve a spacer. |
||
|
|
c2d9d12b1f |
fix(cli): describe Linear write support (#21830)
Fixes #21829 |
||
|
|
9ece273056 |
fix(native-chat): journal rows name the agent that produced them (#22299)
* feat(native-chat): carry producer linkage on every journal row A journal is the durable record of one agent SESSION, and a session that runs subagents journals their rows into the same timeline with nothing on the row saying which agent wrote it. Add that: a per-row linkage bundle naming the producing agent, its parent, the provider's raw parent reference as provenance, the kind of work, and which run of the agent produced the row. The bundle rides the row BASE, not the body: two nested prompt shapes are strict, so an unknown key on a body makes the whole row parse as malformed. It is deliberately not a schema-version bump either — an unknown `v` makes a row unreadable and latches the host read-only, while an unknown key is ignored, so an older host reads a stamped row and behaves exactly as it does today. One reader predicate interprets absence, by presence and not by truthiness: an id that failed to resolve is still an id, and a truthy test would read it as root and put the child's content back on the parent. The parent-facing status scans — thinking, the running tool call, the latest assistant line and the quoted prompt — now skip rows a subagent produced. The transcript is left unscoped on purpose: it shows every agent's output. No producer stamps anything yet; this is the carrier and the reader. * fix(claude): attribute a subagent's journal rows to the subagent The Claude translator already parsed `parent_tool_use_id` on every envelope and threw it away. It now resolves that reference to the producing agent's canonical task id — never to the reference itself, which names the tool CALL and is re-minted on every resume, so a row stamped with it would split one child into two the moment it resumed. The raw reference is kept beside it as provenance. Resolution is its own module rather than more roster: the roster maintains the spawn-group row a user reads, while this answers, for one frame's parent reference, whether the rows it produces are the session's own agent's, some child's, or nobody's yet. It reads the alias table directly to tell "an announcement named this spawn call" from "this id is simply unknown", which comparing the canonical id against the raw one cannot do when the two match. Where the identity is not final the row waits rather than guessing. A top-level spawn whose `task_started` has not landed is the one case that can still resolve, so its rows are held — bounded at 64, oldest written first — and released when the announcement arrives or when nothing can name the producer any more. Nothing is dropped and nothing is written as the parent's. A release that announces no tasks at all is decided immediately instead of held: nothing stable is ever reachable for its children, and an id that rotates is worse than no id because it is silently wrong rather than visibly absent. Those rows read as the session's own, exactly as they do today. Holding them instead would strand every row that REVISES an earlier one — a tool result would leave its tool row reading "running" for the rest of the turn. The attempt counter moves in exactly one place, the existing reactivation branch where a new spawn alias reopens an entry, and is gated on that observed alias change rather than on the counter, so a late duplicate cannot advance a settled run. The first run carries no attempt at all. The group row keeps its own module's write path, now named there, because it is the one row written from a child's frame that is deliberately the parent's. * test(native-chat): pin producer linkage end to end, and fix the harnesses first Four harnesses in this area silently discarded the append options they were handed, so every assertion about attribution would have passed against `undefined`. Two are fixed here — the journal double behind the real deferred sink, and the Claude subagent translator's sink — and each records the options beside its existing call log rather than on it, so the assertions about call order stay about call order. The read side is pinned first, because linkage correct in the store and never read by the projection is the way this ships looking finished and fixing nothing. The three defects are asserted through the live-turn and projection readers: a parent no longer reads as thinking because its child is reasoning, no longer shows its child's running tool, and no longer quotes its child's prose or prompt. The opposite direction is pinned too — the transcript still renders the child's output, and a parent's own line is never suppressed. Also covered: a resumed child keeps one identity while its spawn call id rotates; a child row arriving before its announcement is held and then written linked rather than dropped; a held row is written under the raw reference when no announcement ever comes; the buffer's bound writes the oldest row rather than losing it; an unresolvable id reads as a child rather than as the parent; a row with no linkage reads as root; the schema version is unchanged; and a strict prompt shape still parses, with a positive control proving that strictness is real and is why linkage rides the row rather than a body. * test(native-chat): give the journal double's cast its SAFETY rationale Editing inside the object literal re-attributes the pre-existing assertion to changed lines, and the changed-code gate requires a line-specific rationale. * fix(claude): name the agent that spawned a nested subagent A grandchild's rows carried an agent id but no parent, and under this journal's semantics an absent parent is not silence — it is the claim that the session's own agent spawned the row's producer. For a task spawned from inside another subagent's sidechain that claim was simply false. The frames from such a task carry exactly one handle: the nested call's tool id. That id was journaled once already, as a tool-use block on the row of the child that made the call, so the child is recoverable from it — but only if something remembers which row carried it. The registry that already tracks which tool calls reached the top-level transcript now records the sidechain ones too, against the reference naming their owner, and the resolver follows that reference to name a row's parent. The reference is recorded, not an identity, and it is resolved through the same path the owner's own rows resolve through, so a parent id always matches the agent id the parent's rows carry however either was settled. A row now persists only once BOTH its producer and its parent are final; a grandchild whose child has not been announced yet waits in the same buffer, and leaves it through the same three doors. * test(claude): pin the streamed-text lane's attribution instead of only capturing it The checkpoint harness was fixed to record the append options, and then nothing asserted them: all five of its tests passed unchanged against an implementation that resolves no producer at all, so the lane's attribution was covered by a capture and no claim. These assert it: a block streamed inside a child carries that child's linkage, the session's own carries no keys at all, a checkpoint is held rather than written while the producing agent is provisional, the flush before settlement writes a held block under the raw reference rather than losing it or filing it as the parent's, and a block's producer is resolved once and kept — every checkpoint rewrites the same row, so a producer that moved would file one agent's prose under two identities. * refactor(journal): name the linkage row fields for their role, not their shape The anti-slop gate rejects "Shape" in a symbol name. These are the linkage fields a render item carries, and the name now matches the sibling helper that builds them. * fix(claude): hold the session's first subagent instead of filing it as the parent A child's first frames can arrive before the `task_started` that names it, and the resolver treated "this release has announced no task" as a settled fact about the CLI. Before its own first announcement every session looks exactly like that, so the FIRST subagent's pre-announcement rows were written straight out as the session's own — the whole defect, for the first child of every session, persisted with no backfill to repair it. A spawn call the session forwarded at top level is positive evidence that an announcement is still expected, so it now outranks the release check. A release that genuinely announces nothing is unchanged: its rows reach the release verdict at settle and still read as root, just written a little later. Also stops a malformed owner chain that loops back from naming an agent its own parent; the depth guard bounded that walk but could not make its answer mean anything, and absence is the truthful claim. * fix(claude): read a tool result as its caller's row, not a child's Every top-level tool call is a forwarded tool id, not just a spawn, so a result frame naming its own call as parent resolved as a child awaiting an announcement that is never coming. The row was parked until the turn settled and the tool sat `running` in the meantime. A frame delivering the result of the very call it names is the caller consuming its own output; only a spawn call ever gets a sidechain. Pins added for that and for the first-subagent hold, and the never-announced case now asserts the row was WRITTEN as root rather than that it carries no agent id, which an absent row also satisfied. * fix(journal): refuse an empty producer id, and drop a bad one without losing the row The reader that scopes a parent's surfaces tests PRESENCE, so `agentId: ''` is present: a row carrying it reads as a subagent's and disappears from its own author's surfaces for good. Neither validator caught it — the wire schema accepted any string, and the persisted-row guard type-checked nothing in the bundle at all, against that file's own stated policy. The wire schema now requires a non-empty id. The persisted side sanitises instead: a bad linkage field is DROPPED and the row is kept. Rejecting there would turn a tightened validator into a whole-store kill switch, and degrading a row to the session's own agent is what every row said before linkage existed. * fix(native-chat): answer the turn activity line for the session's own agent `selectStructuredAgentTurnActivity` is a "what is this agent doing right now" reader and was not scoped by producer. It builds a label set from every tool-call row in the turn — a subagent's included — and both readers below it use that set to suppress a line that repeats it. So a CHILD's tool label could blank the PARENT's activity line: child data deciding the parent's surface. Live, not latent: the provider-activity branch is populated in this lane, and it consults the label set without ever consulting `providerFrame`, which is what the status fallback loop relies on. Scoped once at the top, so both readers share one interpretation point. Renderer and mobile share this function, so both are covered. * fix(journal): stop a lifecycle batch stamping one producer onto N mutations A lifecycle-batch row carries N mutations but stamped linkage at ROW level, so a future mixed-producer batch would silently attribute every mutation to whoever opened it. Both callers are single-producer today, so this was latent. The write path no longer accepts linkage for a batch, which removes the failure mode by construction rather than guarding it. The reducer still READS linkage off a batch row — a row may arrive from a host that writes one — and a genuinely mixed batch would have to stamp per mutation, which nothing needs yet. Chosen over adding a per-mutation field because that would persist a new key forever with no writer and no reader. * docs(journal): say why each unlinked write site is unlinked, and drop two false claims Completes the write-site audit the PR claims. Prompt rows carry no linkage and CANNOT: a prompt arrives through the SDK's permission callback, whose options carry a request id and the tool awaiting approval and no parent reference of any kind — unattributable at that site, not deliberately root. Turn rows are deliberately root and now say so. Two comments justified decisions by mechanisms this store does not have. The linkage docblock cited compaction dropping a start row and a pagination boundary; there is no compaction, and pagination is complete-or-reset. Per-row repetition is still right, for the reason that is actually true: every reader scans back from the tail and stops at the turn. A test carried the same false framing. `claudeFrameParentRef` claimed to read the field by the same rule as `isRootClaudeFrame`; it is deliberately stricter on the empty string. * refactor(claude): write a child's rows through, then correct the attribution Four misattribution paths shared one cause: the lane committed to an attribution verdict at write time and could never revise it. That followed from "the journal has no backfill", which is false — re-appending an `itemId` bumps its revision, the reducer rebuilds linkage from the newest row, and it pins `sequence`/`observedAt` so a correction does not move the bubble. This lane already relied on that twice. So the order inverts. A row whose producer is still provisional is written immediately, stamped with the spawn call's own id, and re-attributed in place when the announcement names it. Bookkeeping no longer gates a user's view of what an agent said. The hold buffer is deleted rather than left as a pass-through. Corrections are bounded and die four ways: the announcement, turn settle, teardown, or the bound. Passing the bound gives up on that producer WHOLESALE — correcting some of a child's rows and not the rest splits one child across two ids, which is worse than correcting none. A correction that would change nothing is dropped rather than burning a revision. The streamed lane loses its producer latch, which pinned the first verdict permanently and is why an announcement one frame later could never reach the row. Every checkpoint rewrites the same identity, so there is one row per block and re-resolving can only revise it; the latch was guarding against a split that cannot happen on this path. A block that stops streaming before its announcement is re-attributed explicitly, since nothing else revisits it, and the announcement is now observed BEFORE the forced flush that would otherwise stamp it a line too early. Also narrows the no-announcements-at-all escape so it no longer swallows a forwarded spawn call. That escape now applies only to a sidechain id no spawn call ever forwarded, where there is genuinely no handle to stamp. * fix(journal): move two test doubles onto the signatures they pin Both failed typecheck while passing at runtime, which is what a test double gets to do: vitest never typechecks them. The sink's lifecycle-batch double still read producer linkage off the batch input after that input stopped carrying any, so it had no property in common with the linkage type. The fence is now all it records, which is what the narrowed contract actually forwards — and what the test beside it already asserts. The row-schema helper returned the whole six-arm `JournalRow` union while every caller reads `body`. It now narrows to the item arm it always builds, so the assertions read it directly rather than through a cast. * fix(claude): resolve a tool result to its real caller, not to the session root A nested tool row could end stuck `running` with its result content dropped. Cause was in the result-frame attribution, not in the correction ledger. A frame delivering the result of the call it names as parent is the CALLER consuming its own output — but the code read "the caller" as "the session's own agent", which is only true when the caller is the root. A call a child made is owned by that child. Collapsing it to root both misattributed the row and made the result's write resolve through a different reference than the call's, so the correction owed to that row was left holding the body it had BEFORE the result landed, and re-attribution then reverted the row. The caller is now resolved through the registry that already records which agent journaled a tool call, so both writes to one row resolve through the same reference and the newest body wins. A settled write also supersedes any correction owed to its row. One `itemId` is legitimately written under two references — `claudeToolIdentity` is keyed on the tool id alone — and a settled write already carries a final verdict, so an outstanding correction could only restamp it from a reference that write did not use. Dropped rather than re-bodied for that reason. Adds the ledger's first unit tests, including the invariant this defect broke: a correction changes a row's attribution and never its content. * fix(claude): keep a correction owed when the sink refuses it A correction went out through the plain append, which discards the queue's admission. Under backpressure the write was refused and `retry` had already dropped the entry, so the obligation died with nothing re-deriving it — the failure class this work exists to refuse. It is self-feeding too: a correction costs a commit on the same serialized writer that carries live rows, so the burst that generates many corrections is what builds the backlog that drops them. It now uses the admission-returning path the sink already exposes, keeps the entry outstanding on a refusal, and lets `abandon` try once more. A refusal there ends it: the row keeps the spawn call's own id, which is usable, and an obligation with no exit is worse than one that settles for less. `settle` also reports what actually happened instead of always claiming it wrote, so publish no longer fires for a write nobody accepted. Also records why the live-turn scans may read the turn record before checking the producer. A turn is the session's unit of work and no producer of a turn-bearing body stamps linkage: Claude's turn rows carry none, Codex has no linkage concept, the compact row passes only a fence, and the stale-turn sweep goes through the lifecycle-batch path, which cannot carry linkage by type. The ordering is safe by construction rather than by accident, and the comment says so, so a future producer knows what it would break. * fix(claude): never read a row naming a parent as the session's own A non-null `parent_tool_use_id` names a child, always. The resolver still had one branch that read such rows as the session's own agent's — a release that had announced no task, where the comment claimed "there is no handle to stamp". There is one: the reference itself. The branch was buying a false attribution to avoid an id nothing joins on, which is the trade already reversed once for forwarded spawn calls, and every reader of this field is a presence test. So the branch goes, and with it the `root` arm of the verdict and the resolver's whole dependency on whether the release announces tasks. Two states remain: linked now, or linked now and owed a correction. The type deleted a stale test double on sight, which is the argument for removing the arm rather than the branch alone. This also closes the severe half of the tool-origin eviction exposure. A spawn id evicted from the bounded top-level set used to flip the release check on and stamp a child's rows as the parent's; with nothing returning root that cannot happen. What remains is a missed correction, which splits one child across two ids — the same end state as passing the correction bound, benign in kind and disclosed. `isForwardedParentTool` stays where it gates PENDINGNESS. It now decides only whether a correction is owed, never whether a row is a child's, so a stale answer costs precision rather than correctness. |
||
|
|
12040f04d0 |
refactor(ai-vault): read Codex's stated subagent parentage instead of a boolean (#22298)
* feat(ai-vault): read Codex's stated subagent parentage
Codex states a spawned thread's parentage in `session_meta`: the parent
thread id, the spawn depth, and the agent's nickname, role and naming
path. Add a reader that keeps all five as a typed record.
Releases disagree about where they state it. Newer ones nest the full
record under `source.subagent.thread_spawn` and copy the parent,
nickname and path onto the payload's own keys; 0.144-0.147 name only the
agent's role there and leave those copies as the sole statement of the
parent. Every field is read independently, so a release that states
three of them is not discarded for omitting the other two, and a
malformed field costs only itself.
* refactor(ai-vault): reject Codex worker transcripts on the parentage record
The scanner collapsed Codex's whole spawn record to a yes/no to decide
whether a rollout belonged in Agent Session History. The parse state now
holds the record itself and derives that decision from its presence, so
the parent thread id, depth and agent name survive the scan instead of
being thrown away at the point they are read.
Two behaviour notes. A release that states only `source: { subagent:
'review' }` is now recognised as a spawned thread; the previous check
required that key to be an object, so such a transcript would have shown
up in the user's own history on a release that states no `thread_source`
alongside it. And a transcript that states `thread_source: 'user'` is
still treated as the user's own even if a subagent source sits beside
it, unchanged from before.
* fix(ai-vault): an unreadable subagent source is not a spawn statement
Detection with no stated thread_source accepted any value under
`source.subagent` that was not undefined/null, so `subagent: false` (or 0,
or "") would have read as a spawn and hidden the user's own thread from
Agent Session History. Every release spells a subagent source as either the
spawn record or the agent's role, so readability is the gate: a value that
is neither states nothing. Letting a worker transcript through is visible
and recoverable; dropping a user's session is neither.
* refactor(ai-vault): classify a Codex thread by its source tag, not its role
`source.subagent` is an externally tagged union naming the sort of non-user
thread: a spawn record, but equally a review pass, a compaction, a memory
consolidation, or a labelled `other`. Those are siblings that exist today,
not an older spelling of the spawn record, so reading them is not a legacy
fallback and the tag is not the spawned agent's role — `agent_role` is a
field that exists only inside a spawn record.
Why this is not a tag rename. Why a thread is not the user's own and who
spawned it are two facts, and folding the first into the second made a
compaction read as an agent whose role is "compact". `kind` now carries the
union tag (with `kindLabel` for the free text `other` states) and parentage
stays the join key, so the two can disagree without either being lost. A
transcript is rejected on the classification, never on parentage — which is
also why a forked thread's lineage can never be mistaken for a spawn.
Also read the union faithfully: a tag is a bare string or a single-key
object, and a value that is neither states no tag at all. That keeps a
user's own thread visible on an unreadable value, where the previous
presence test would have hidden it. Adds the documented `agent_type` alias
of `agent_role`, and the payload-level copy of the role that the other three
spawn fields already fell back to.
* fix(ai-vault): hide the machinery Codex runs for itself, not only spawned agents
Codex's `source` is a nested union, and two of its outer tags are not the
user's thread: `subagent` (an agent it spawned, or a review or compaction it
ran) and `internal` (guardian and memory-consolidation machinery). Only the
first was ever read, so an `internal` rollout landed in Agent Session History
as if the user had started it whenever the release omitted `thread_source` —
and that field is optional, absent on 1,315 of 13,137 local rollouts.
Reading the outer tag rather than one hardcoded key also fixes the direction
of the readability rule. The outer tag is the discriminant: nothing but a
non-user source serializes under those keys, so it classifies the thread on
its own, and a kind beneath it that a later release respells no longer leaks
every worker transcript into the user's history. An unreadable value in
`source` itself still states nothing and leaves the thread visible.
Renamed to match what it decides — whether a thread is the user's own, which
was never only about subagents. Every other tag (cli, vscode, exec, mcp,
custom, unknown) is a thread the user started and is now pinned as such.
* refactor(ai-vault): read only the source tag Codex actually writes
Backs out the reader for Codex's other non-user `source` tag. It has zero
records across 13,137 local rollouts, and the threads it would name state a
`thread_source` unconditionally on the path that creates them, so they are
already classified. Reading a shape that has never been observed, for a case
already covered, is speculation — unlike the bare-string subagent tag, which
has 16 real records behind it.
The nested-union reading stays: the outer tag is still what classifies the
thread, so a kind beneath it that a later release respells cannot leak a
worker transcript into the user's history, and the tags the user's own
threads carry stay pinned as visible.
* Revert "refactor(ai-vault): read only the source tag Codex actually writes"
This reverts commit
|
||
|
|
4c696a1e2a |
fix(agent-status): a structured session with live child work reads as working (#22295)
* fix(agent-status): a structured session with live child work reads as working An idle native-chat session whose subagent was still running showed a green check in the sidebar, the collapsed worktree pill, and worktree ps, while a terminal Claude session in the same situation showed working. The two lanes folded child work into the parent's status with different code: the hook listener did, the structured lane did not. Both lanes now share one child-work liveness vocabulary and one lead-status fold. Live agent work makes a settled lead working; shells and monitors alone make it monitoring. The structured lane derives liveness from the background task list already on the wire, in both its readers, so the sidebar, the CLI, the dashboard and mobile agree. The Claude task-kind table is one shared file covering both the hook inventory and SDK stream names, and the renderer bridge reuses the shared child-work projection instead of carrying its own copy. * fix(agent-status): a blocked or out-of-contact subagent still holds its session working Child-work liveness retired an agent-kind child on any state but working/monitoring, while the shell beside it stayed live on everything except done/idle. A subagent waiting on a permission prompt, or one whose host lost contact, therefore counted for less than a backgrounded sleep and let the session read done. Both kinds now share the settlement rule `resolveAgentChildWorkFreshness` already reads rows by: only an explicit done/idle retires child work. Also keep empty task labels out of the shared background-task projection candidate, so a host that publishes `name: ''` cannot beat the child-row fallbacks. * test(agent-status): pin the widened hook-inventory agent names, and correct two stale claims The hook inventory now classifies through the shared kind table, which also maps the SDK stream's `local_agent` / `local_subagent`. Nothing pinned that widening, so add cases for all four agent names — including `teammate`, whose pane state stays `done` under the #8825 idle-squat rule. Two comments the fold made false: - the teardown marker rule's comment claimed it could not disagree with what the UI calls working; it is deliberately lead-only, so now it says that and why; - the agent-status store reference still described the structured row's `state` as the deleted `structuredAgentSessionStatusState`, and omitted the `workingMode` the ingest now writes. * fix(agent-status): the state clock restarts when monitoring becomes a real turn `stateStartedAt` carried forward whenever the prior `state` matched, which was sound while `state` meant "a turn is running". Now that it folds in child work, an idle lead watching a `sleep 3600` publishes `working`/`monitoring`; the user's prompt 45 minutes later keeps `state: 'working'`, so the row inherited the watch loop's clock and read "Working for 45m" the instant the turn began. Monitoring is its own displayed label (`worktree-card-compact-agent-row.tsx:40`), so the continuity key is now the whole published work identity — state AND workingMode — in both writers. Also record two facts the code stated wrongly: the structured lane's `interrupted: false` is inert (a projected session status has no interrupted member) rather than a decision, and the child-work liveness rule's escape hatch is the roster's session lifetime, not a settled state. * fix(agent-status): a workflow is watch work, and child work dates itself Two defects the fold introduced. `isAgentChildWorkKind` counted `workflow` as agent work, so a structured session whose only live task was a backgrounded `local_workflow` published a full working spinner while the children projection — which admits `kind === 'agent'` only — rendered nothing to expand, and the same workflow in a terminal pane showed the monitoring badge instead. The repo already decides this: `isClaudeSubagentTask` excludes workflows by name, and MATERIALIZED_TASK_KINDS leaves "the backgrounded shell command and the workflow" to the non-agent owner. The predicate is now `kind === 'agent'`, and the three sites that restated the same test route through it, so a new kind is decided in one place instead of three that merely agree. `evidenceObservedAt` dated every row by `summary.updatedAt`, the journal's last activity. The journal cannot date child work: its clock stopped when the lead's turn did, so a genuinely live roster aged past the 30-minute staleness window and mobile's dot decayed a running session to idle. The fold now reports whether child work alone holds the row open, and only then does the host's observation clock stand in — keeping "a restart's republish is not new evidence" for lead turns. * fix(agent-status): the sidebar dates child work the same way the host does `fromChildWork` reached the host ingest but not the renderer bridge, so after ~30 minutes of live child work with no journal activity the sidebar's row aged into staleness while `worktree ps` and mobile stayed fresh — two writers for one session answering differently, which is the defect this PR exists to remove. For a remote host the client's own receipt time is also the more honest clock, since the journal stamp is the host's and is never comparable against this machine's now. |
||
|
|
18696bd875 |
fix(native-chat): hide delete for structured history (#22106)
* fix(native-chat): hide delete for structured history * test(native-chat): fix duplicate session action suite |
||
|
|
60c43695e5 |
feat(agent-launch): report the pane a terminal launch created (#22108)
* feat(agent-launch): report the pane a terminal launch created A `term_*` handle is a main-side mapping the renderer cannot resolve (terminal-handle-links.ts:309), so a client that draws its own tabs had no way to name the tab it had just asked `agent.launch` to build. The runtime already mints that pane, bakes it into the PTY's environment and hands it to its own reveal; the surface factory then dropped it on the floor. Carry it through as `paneKey` on the terminal outcome. Identity, not placement: where the pane goes — which group, what order, whether it takes focus — stays with whichever client is drawing, and nothing here rides the wire for it. One field rather than a tabId/leafId pair, because the key already holds both and two copies of one fact can disagree. Absent when this launch minted no pane: a reused terminal was already running, and a worktree-create startup terminal is built by the create, which reports only a handle. Naming the wrong surface is worse than naming none. Optional on the wire and optional on the read side. Mobile parses the receipt with a loose object and is deliberately mode-blind, so it ignores the field; the persisted-row guard checks it when present and accepts a row written before it existed, because a read rule stricter than the write side turns one odd row into a refused replay. * fix(agent-launch): retain startup terminal pane identity |
||
|
|
9ae7a3de14 |
test(agent-launch): pin per-call-site behaviour of the launch funnel (#22116)
A regression net for the twelve production call sites of launchAgentInNewTab, written before the agent.launch migration rewrites the funnel's internals. Each call site is characterized as a profile — the argument object it builds — and fed through the REAL funnel, so the assertions are about the observable contract rather than about internals the migration will move. A census re-anchors every profile to its call site by source markers, so a caller that changes what it passes fails loudly instead of drifting out of coverage. Dimensions pinned per call site: resolved surface (terminal / structured / host-published), built command and argv, the permission-bypass flag inside the arguments string, prompt transport (argv / post-ready paste / environment), submit bit, delivery-promise exposure, group placement, activation, tab-bar order, initial cwd, and telemetry launch source. |
||
|
|
ba742a86bb |
fix(linux): release orphaned processes when their owner exits (#22247)
* fix(linux): release orphaned processes when their owner exits * fix(linux): handle inhibitor errors until streams close --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> |
||
|
|
632ae1320b |
fix(daemon): reap terminal descendants during shutdown (#22232)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> |
||
|
|
da1c322b00 |
feat(mobile): one build-time switch picks native or OTA, default native (OTA phase E1) (#22193)
* feat(mobile): one build-time constant decides native or OTA, default native EXPO_PUBLIC_MOBILE_SHELL is read in exactly one place, mobileShellBuildKind in preferences.ts. Expo's babel preset inlines a literal process.env member expression at build time, so a release bundle carries the answer as a constant and anything but the exact string 'ota' — unset, empty, a typo — is native. Every default build is therefore the native app, unchanged. mobileWebShellFlagCanBeOn now answers __DEV__ or an OTA build, so the ability to mount the page comes from the build and never from storage: a native binary installed over an OTA one, same bundle id and same data container, still refuses a stored 'true' without reading the key. An unset key reads on only in an OTA build; a development build keeps its opt-in, and a stored 'false' wins everywhere so the Troubleshoot toggle can switch an OTA build back to native. That toggle now mounts wherever the flag can be on, which is the only way back to the native screens in an OTA build, and its label names the build kind rather than saying "(dev)". The bundle probe row beside it stays development-only: it fetches. The flag census gains two rules — one module reads the switch, in the member form Expo inlines and not the bracket form, and one named function answers the build kind — and the build-kind fence now lists the Troubleshoot route that asks it. Docblocks that said a store build can never mount the shell now say it mounts only when built for OTA. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * ci(mobile): one workflow input picks the shell, and no input means native Both release workflows gain a `shell` workflow_dispatch choice, options native and ota, default native, and hand it to the step that bundles the JavaScript as EXPO_PUBLIC_MOBILE_SHELL. That is the Gradle assembleRelease step on Android and the fastlane build_and_upload step on iOS; nothing else in either file sets it. A tag push and a schedule carry no inputs at all, so `inputs.shell || 'native'` yields native for them — the first OTA release is a dispatch with one field changed, and every other run is the app we ship today. Each build step prints the value it is about to build with, read back from the same variable rather than from a second copy of the expression, so a run's log cannot claim a shell the build did not use. The new contract test evaluates that expression rather than matching its text: absent, empty and 'native' all resolve to native, 'ota' to ota, and any expression shape it cannot evaluate is a failure rather than a pass. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * build: the desktop packages the real page, and the placeholder is retired build:mobile-web now runs the app builder and app verifier, and both take their output root from MOBILE_WEB_BUNDLE_DIR in the packaging guard rather than each carrying a constant of their own — one definition of where the bundle lives, so a drift cannot leave electron-builder's beforePack looking at an empty directory while the builder reports a tree it wrote elsewhere. build:mobile-web:app is gone; it was the same two commands. src/mobile-web/ and its two scripts go with it. What the app builder shared with them is split into three modules named for what they hold rather than for the bundle that used to own them: mobile-web-bundle-manifest.mjs (content types, the canonical asset serialization, buildId, hashed assets, the protocol window and the manifest write), script-entry-detection.mjs (isDirectInvocation, whose two failure modes are Windows paths and symlinked entries), and mobile-web-source-line-endings.mjs (the CRLF guard, now with a required directory rather than a default pointing at the deleted tree). The two suites that only needed *a* valid tree on disk — the beforePack guard and the packaged-bundle guard — build one from mobile-web-bundle-fixture-tree instead of bundling the whole mobile graph. It goes through the same manifest writer the page does, so a manifest shape change still reaches them. Also retired: the placeholder's tsconfig project and its typecheck lane, its knip entry, its electron-builder exclusion and .gitattributes pins, and the app-bundle test that asserted the shims stayed out of a builder that no longer exists. pr.yml's page job builds the same bundle the package job ships. Inert for native phones: they never fetch it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(config): one import of node:fs/promises in the entry-detection suite The changed-code quality gate's focused plugins read the two as a duplicate import; the readFile line was left over from the split. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs: the comments that still describe the retired placeholder bundle The web entry said it was built by `build:mobile-web:app` into out/mobile-web-app and shipped by nothing. That script, that directory and that fact are all gone: it is built by `build:mobile-web` into the packaged bundle dir, and a phone mounts it only when the binary was built with EXPO_PUBLIC_MOBILE_SHELL=ota. Two Windows cache keys explained themselves by naming src/mobile-web and "the two bundle builders"; config/** now covers the builder, the verifier and the manifest writer, and the spike's key no longer waits on a Phase C flip that has happened. The keys themselves are unchanged. Three scratch directories in the app-bundle suites and one in the verifier still spelled the retired output root. Renamed to mobile-web, which is what the build writes; they are temp subdirectory names and nothing reads them. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
b57facc5bc |
Fix stale listings when search query changes (#22173)
* fix(renderer): prevent stale listings when query changes Bundle files and truncated state with the request key that produced them. Only display a listing when its request key matches the current query, preventing previous results from leaking through before the new request completes. * fix(renderer): associate loading state with request key When a query changes, there's a gap between the render and the effect that starts the new request. During this gap, the listing becomes empty (because it's scoped to the request key), but the loading indicator was stale. To fix this, track loading per request and also report true if this render is about to start a new request — so the empty listing correctly appears as "loading" rather than "no results". |
||
|
|
90d363afc9 |
fix(renderer): contain Monaco initialization failures (#21555)
* fix(renderer): add defensive error handling for Monaco editor crashes Analyzed 34 crash reports for v1.4.205 released 2026-09-17. Identified and added defensive fixes for React error boundary crashes in Monaco editor setup. - Error: ReferenceError: thũs is not defined - Location: Monaco editor initialization (editor.api2 bundle) - Platforms: Linux, Windows, macOS - Root cause: Undefined variable in Monaco setup or language registration - Status: Added try-catch to prevent cascade crash - Pattern: Cascading process deaths (network service + GPU service) - Platforms: Primarily Windows - Root cause: Infrastructure/concurrent process failure (not code defect) - Status: Documented, requires Electron/Chrome infrastructure review - Pattern: Renderer memory grows to 851MB on low-RAM Windows systems - Root cause: Memory exhaustion on systems with <2GB free RAM - Status: Existing memory monitoring detected; needs leak investigation - Status: Requires minidump analysis with source maps 1. Added try-catch to Monaco editor mount callback (use-monaco-editor-mount.ts) - Catches errors during editor initialization - Logs file path and error for better diagnostics - Prevents crash cascade to React error boundary 2. Added try-catch to Monaco language registration (monaco-setup.ts) - Catches errors during Vue/Svelte/Astro/Nim language registration - Logs failures without crashing Monaco setup - Allows app to continue even if optional features fail - Analyzed 34 crash reports across 3 categories - Examined crash dumps, diagnostics, and memory profiles - Reviewed Monaco setup and editor component code - Checked git history for recent changes - Crash breadcrumbs (memory, process state, user actions) - Process metrics (heap, private memory, system memory) - Component stacks (React error boundaries) - Exit codes and system signals - Error silently continues instead of crashing: Users get degraded experience instead of app crash, can still use editor in most cases - May hide underlying issues: Errors are logged for crash reports, but won't be surfaced as prominently - Type checking: pnpm tc:renderer (passed) - Changes preserve existing error reporting through crash breadcrumbs - Defensive coding only adds try-catch, no behavior change for success path * fix(renderer): keep Monaco mount failures inside error boundary * fix(renderer): isolate Monaco setup failures * fix(renderer): contain Monaco mount failures at the editor surface The try/catch around the onMount body did the opposite of containment: React already routed that throw to the page boundary, so swallowing it left a half-wired editor and hid the crash from the reporting pipeline. It also never saw the reported failure, which is raised inside @monaco-editor/react's own create effect before onMount runs. Revert the hook to main and wrap the editor element in RecoverableRenderErrorBoundary instead, so either throw degrades the file pane only, still files a crash report, and retries by remounting on the existing pane+path key. * refactor(renderer): drive Monaco setup steps from one guarded table Ten near-identical guarded calls, each repeating its own function name as a label, become one [label, step] table run by a single loop. Same behaviour: an optional registration that throws is logged and the rest still run. loader.config and the editor model registry stay unguarded — they are load-bearing, so catching there would only move the failure later. * fix(renderer): breadcrumb swallowed Monaco setup-step failures A guarded registration that throws was console-only, so a lost language or behaviour guard never reached crash reports. Record a breadcrumb so the containment stays visible in the field. Claude-Session: ab8ff806-4870-4ea8-bbf5-bbd123b1166e --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> |
||
|
|
e47ef8cc28 |
feat(mobile): the shell tells a page which optional capabilities it has (OTA phase D, C8.1) (#22141)
* fix(mobile): publish page-route pairs the strict host schema accepts (OTA phase D, C8.1) `routeViewOf` handed the manifest's own route entries to the host as `pageRouteGrants`. The phone reads a manifest route loosely, so an entry arrives carrying whatever field the desktop that wrote it knew about, and `BridgePageRouteGrantsSchema` is `.strict()`: one unread key refuses the pairs, `createBridgeHost` refuses the route with them, and the page gets no `init` at all rather than losing one field. Fixed before any route carries an optional grant (ruling 37.4), so the manifest field the next commits add costs an installed shell nothing. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * chore: drop the closure and bundle probe scripts from the tree Scratch measurements for C8.1 (which route closures reach the HTML preview, and what the preview render rig costs to bundle with a client provider). They belong outside the repository and were swept in by the previous commit's `git add -A`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): a manifest route may declare optional grants (OTA phase D, C8.1) Design B of design-ota-c8-1.md, ruling 37. `MobileWebBundleRouteSchema` grows `optionalGrants` under the required lane's own grammar, with the 16-name ceiling applied over the union of the two lists rather than to each. Serving a route still reads `grants` alone, so a capability a screen cannot work without stays required and takes the route native; a session's granted list is `[...grants, ...optionalGrants]` narrowed to what this shell implements, from one helper that both `grantsForRoute` and the `pageRouteGrants` publish read. The ruling's compatibility rationale is corrected in place. `z.looseObject` passes unknown members through rather than dropping them (measured, zod 4.4.3), so a shell older than the field still receives the key; what it lacks is a policy that reads one. What makes the lane safe against such a shell is therefore the previous commit's publish fix, not the reader. BRIDGE_PROTOCOL_VERSION stays 1. No new notify, verb or frame field. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): name the shell's cancelled-navigation behaviour as a grant (OTA phase D, C8.1) `externalNavigation` joins `MOBILE_WEB_SHELL_GRANTS` beside `screencastBinary` and `haptics`, declared in `cancelled-navigation-target.ts` because that is the module holding the rule which acts on it. A third token that is neither a verb nor a notify: the page posts nothing to make a cancelled top-frame navigation happen, so this list is the only thing that can tell a page whether a tap inside the sealed HTML-preview frame escapes at all. A constant and not a platform read (ruling 37.1): both engines dispatch the event, `ios/MobileWebShellView.swift:481` and Android's `MobileWebShellView.kt:382`, so an app build carries the behaviour on both or on neither. The policy census grows the half that was only pinned by the verb table: the implemented set is that table plus exactly three non-verb tokens, each read off the module that declares it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): the bundle builder carries a route's optional grants (OTA phase D, C8.1) `resolveMobileWebPageRoutes` maps each declaration member by member, so a field the declaration grows reaches a phone only once the map names it: until now `optionalGrants` would have been dropped in silence and every route would have declared nothing optional. Omitted when the route declares none, because absent and empty are the same answer to a shell. The declaration suite grows the rule rather than a row: the map carries the lane through and writes no key without one, and the lane is held to the manifest's own grammar and to the ceiling over the union of the two lists. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): the HTML preview hides its links on a shell that cannot open one (OTA phase D, C8.1) The session route declares `externalNavigation` on the optional lane, and the preview asks for it before it renders an artifact's links as links. Ruling 37.2's three readings are what "hide" means here, and removing `href` is what delivers all three at once: `a:any-link` stops matching, so the UA stylesheet stops underlining, the element leaves the tab order, and there is no dead anchor a tap does nothing on. The text the author wrote stays where it was, the artifact paints, and the Preview/Source toggle is untouched. Done with the browser's own parser rather than over the source text: an `href` inside a comment or a `<template>` is text to a browser, and a pass that rewrote either would be editing the artifact instead of its links. The frame also loses `allow-top-navigation-by-user-activation` on that path, so a link the pass somehow missed is refused by the browsing context as well. One route, measured rather than assumed: the design said two, and the file preview route's closure does not reach the HTML preview at all - it renders `MobileFilePreviewScreen`. The new closure census derives that list from the hook's callers. The render rig grows the case on both engines and the readings it needs, and `mobile-web-app-preview-frame-readings.mjs` is split out of it at the readings/arms boundary, because the two were over the 600-line cap together. Two engine findings are recorded in the rig: an `<a>` with no `href` still answers `tabIndex` 0 on both, so focusability is asked by focusing; and WebKit computes `cursor: auto` for a real link, so that reading is pinned where it discriminates and its blindness pinned where it does not. The hop-coverage census now reads the effective set, because that is what the running rule compares. Inert today: the session route is the only declarer and an opener into every other route. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the preview's hidden link path where the unit suite can reach it (OTA phase D, C8.1) The mobile suite runs in a `node` environment whose resolver has no `.web` precedence, so `MobileHtmlPreview.web.tsx`'s import of the grant hook lands on the native sibling, which answers yes unconditionally. That is why the existing component suite still measured the granted frame without knowing a grant exists, and it means the hidden path had no coverage in the sharded `test` job, where the render rig is skipped for want of the bundler's dependencies. So the wiring gets its own file with the module replaced: that the component asks, and that both the frame's sandbox and the document it is handed follow the one answer. happy-dom rather than the suite default, because the inerting pass parses with the browser's own `DOMParser`. `String(node.type)` rather than a literal comparison: `node.type` is `ElementType`, which overlaps a real intrinsic tag and not the host strings these mocks render, so `=== 'Pressable'` is a no-overlap error under `tsconfig.test.json` and the tests-typecheck ratchet reds on it. Also replaces a `Reflect.get` the anti-slop gate refuses with an `in` check. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the session page closure at 4,362 for C8.1's three modules Measured on both sides with `mobileWebAppRouteClosure(SESSION_ROUTE)` at base `841d06a969` with all five postinstall generators run first, and the two `local` lists diffed rather than the total inferred: 4,359 -> 4,362 modules, 1,017 -> 1,020 local. All three are local source modules and none is vendored: the page's read of `init.grants.native`, the pass that turns an artifact's links back into text without the grant, and the module declaring the token beside the rule that acts on it - reached both by that hook and by `page-route-policy.ts`. The `bridge-caps.ts` it imports was already in this closure, and the hook's native sibling is replaced rather than joined. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): allowlist the preview's grant sibling among the .web.* overrides `mobile-web-app-web-overrides.test.mjs` pins the allowlist against the `.web.*` files on disk, so a new web sibling reds it until the file says why the page needs one. Red before: `expected [ …(36) ] to deeply equal [ …(37) ]`, naming `src/components/use-html-preview-link-grant.web.ts`. The preview's own entry is corrected with it: its reason said `allow-top-navigation-by-user-activation` is granted, and that token is now conditional on the shell answering that it can open such a navigation. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): the hidden-link render case waits on the frame's own reading (round 1) CI's chromium arm timed out at the full 240 s on this case alone while the WebKit sibling passed in 1.5 s and it passed 26/26 locally. The cause is the third arm: it tapped the granted link and waited through `expectNavigation: 'main-frame'`, and `waitForRecordedNavigation` has no bound but the case's own timeout. Under CI load the click missed its 2 s actionability window, no navigation was ever recorded, and the arm sat in that wait until vitest gave up - `recorded []`, with the frame attached only at 38.9 s. Three arms sharing one budget is what made this the case to find it. The arm is dropped rather than its wait lengthened or retried. Every verdict left is a reading the frame itself publishes: the anchors its document holds, the style the engine computed for one, whether focus lands on it, and now whether the tap this arm made landed at all - `actError` is asserted null, so a click that never reached its target is no longer the same three zeros as a tap that did nothing. Nothing is lost. The tap's outcome on a granted shell is the next case, on these same counters from this same rig and with a budget of its own, which is the presence precondition this file already uses elsewhere for the same reason. The WebKit sibling's discriminating reads are untouched. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): the inert-link pass changes nothing an engine renders but the links (round 2) Round 2's ruling: the hidden-link path may change nothing about the artifact's rendering except that links are not links. A parse and a reserialise is not free of that by default, and all four findings reproduced on Chromium 147 and WebKit 26.4. A same-document fragment link is kept. It starts no navigation at all, so it goes on working inside the sealed frame whatever the shell can do, and taking it away would be degradation over a capability it never needed - an artifact's own table of contents is the case. Its `target` still goes, because a fragment aimed at another frame is a navigation rather than a scroll, and `href=""` is not a fragment: it resolves to the frame's own URL. Links inside `template.content` are reached, recursively. `<template shadowrootmode>` is a declarative shadow root the frame's parser attaches and renders, and `querySelectorAll` does not walk into template content, so those links arrived live inside a sandbox that refuses their navigation - the dead anchor ruling 37.2 forbids. Measured: `parseFromString` attaches no such root on either engine or in happy-dom, so the pass can reach them. The leading newline of a `pre`, `listing` or `textarea` is written back. A parser drops one after the start tag and the serialiser is specified to put it back; measured, neither engine's does, so a round trip lost a blank line from every such block. The doctype is carried whole, and the reason is corrected from the one the finding gave. It cannot move this frame between layout modes: a `srcdoc` document takes its mode from its embedder, and measured, a quirks doctype, the bare name and no doctype at all all read `CSS1Compat` inside the frame. What rewriting it does is change the document the author wrote for no reason, with `document.doctype` observable beside a Source tab showing the original. The render case pins `compatMode` as the blind reading it is and reads the frame's own doctype identifiers as the one that discriminates. Option B was not available: the frame has no `allow-scripts` and inherits `script-src 'self'`, so nothing runs inside it and there is no injection to carry the work. Also drops a vacuous half of the affordance test. `renderSource()` is called with no argument, so the markup a Source view shows is the caller's own closure and asserting it equals the fixture passed whatever the component did. What the component decides is whether the rewritten frame stays mounted underneath, and that is what is read now. `mobile-web-app-preview-arm-driver.mjs` is split out of the render rig at the boundary the readings module already names - the rig holds what each case claims, the driver how an arm is driven, the readings what it reports - since the three were over the 600-line cap together. No max-lines disable or bump. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): a fragment link is a frame navigation in this preview, so it is inerted too (round 3) pullfrog is right, reproduced on both engines before believing it. Round 2 kept `#`-prefixed hrefs on the theory that they are same-document scrolls. In this frame they are not: the document's URL is `about:srcdoc` while its base URL is inherited from the embedder, so `#section` resolves against the shell's own URL and the destination differs from the document's by more than a fragment - which makes activating it a frame navigation, and the shipped `frame-src 'none'` refuses it. Measured under the shipped policy, one tap, with something to scroll: Chromium 147 scrollY 0, frame becomes chrome-error://chromewebdata/, artifact gone, embedder reports frame-src <origin>/preview WebKit 26.4 scrollY 0, frame stays about:srcdoc and intact, same report So the destruction is Chromium-only but the absence of a scroll is not: there was no working affordance to carve out for, and the carve-out left a live link that destroys the preview - worse than the inert text it was meant to avoid. Both sandbox values behave the same, so this is the base URL and the policy rather than the sandbox. The same tap does the same thing on the granted path, where this pass does not run, so an artifact's internal links have never worked in the preview. That is not this change's to fix; it is recorded in `followup-html-preview-fragment-links.md`, and the render case reads the granted arm's violation as its presence precondition so the behaviour is pinned rather than merely known. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
0232c03c43 |
Refactor editor header file rename to breadcrumb morph UI (#21265)
* Refactor editor header file rename to a breadcrumb morph UI - Display full breadcrumb (repo name + parent dirs) during rename for context - Separate basename field from extension suffix to clarify what users edit - Replace blur-to-commit with explicit confirm/cancel buttons - Auto-attach extension to basename; respect explicitly typed extensions - Add comprehensive tests for rename scenarios and edge cases * Fix markdown rename: drop blur-commit, handle IME, track active file - Blur no longer commits, preventing accidental renames when focus moves - IME composition keys properly handled for CJK input support - File switches during rename now cancel the operation - Extension display improved to show final filename - Comprehensive test coverage added for edge cases * Simplify markdown rename to inline field without breadcrumb or buttons Replaces breadcrumb-morph rename with confirm/cancel buttons with a simple inline field accepting full filenames. Commits on Enter, blur, or Escape to cancel, matching tab bar and file explorer behavior. Adds renameCancelledRef to prevent blur-commit after Escape. Removes unused i18n strings for buttons and simplifies state by dropping extension pinning and breadcrumb display. * Handle blur race when file changes during rename When switching files mid-rename, React may deliver the old input's blur event after the new file renders, causing a stale rename commit. Mark the rename as cancelled when the active file changes, and add test coverage verifying stale blur events are ignored. * Test blur-race condition in hook unit test Move blur-commit-after-file-change test from EditorPanelHeaderPath integration tests to useEditorHeaderFileRename unit test. Tests the blur-handling logic at the hook level where it belongs. |
||
|
|
9d4039b8c5 |
Lower filter chip contrast to indicate read-only status (#21751)
* fix(cmd-j): lower filter chip contrast to indicate read-only status The scope is seeded from the sidebar, not chosen in the command palette, so the chips should read as metadata rather than action pills offering to undo an action the user never took. - Redesign chips with reduced visual weight: no border, no background color - Add "Scoped to" prefix and muted text color to clarify metadata role - Separate chips with subtle dots instead of relying on spacing - Hide dismiss icon (X) until hover/focus to further de-emphasize the action * refactor(cmd-j): add visual icon to filter indicator - Add ListFilter icon anchor for better discoverability - Improve filter chip button styling with better hover/focus states - Move label to screen reader only for cleaner interface * refactor(cmd-j): adjust filter chip appearance and i18n Refine filter chip styling with adjusted height, padding, and colors to de-emphasize read-only state. Add "scoped to" label and complete internationalization coverage across all locales. * improve filter row alignement |
||
|
|
c3c4b4ec02 |
fix(browser): notify state on offscreen page navigation commit (#21703)
* fix(browser): notify guest state on offscreen page navigation commit Offscreen pages have no renderer to publish their row, so the navigation commit is the only moment paired clients learn the new URL. Every failure path already announces; the success path was missing this notification, preventing background links from mirroring. Extend the E2E test with helpers to verify background links mirror before surfacing as panes, reading both client and host state to isolate failure causes. * test(e2e): validate host tab response in link-open routing Replace unsafe type assertion with runtime validation of the response structure. Add defensive checks to ensure tabs exist and have the expected shape before processing, improving test robustness when the remote host response is incomplete or malformed. |
||
|
|
8fb0ba671b | fix(ssh): keep the generation floor when a target is removed and recreated (#22146) | ||
|
|
7650abe224 |
fix(macos): tell the user when Orca's terminal service can't read their folder, and walk them through the fix (#21923)
* fix(macos): tell the user when Orca's terminal service can't read their folder
On macOS, a terminal daemon that survived an app update can be refused access to
a workspace under Documents, Desktop, or Downloads while the Orca app itself can
still read it. Terminals opened there die with "Operation not permitted" and
nothing on screen explains why. The daemon has reported `cwdReadableByDaemon` on
every create since #18043 and main has emitted `daemon_pty_cwd_denied` on proven
divergence since then; the field data says 1,438 users hit it in 21 days. What
was missing was the notice.
The verdict itself moves off `access()`. A grant-less probe on an affected
machine showed a TCC mode where `access(R_OK|X_OK)` passes on `~/Documents` and
`opendir` still fails, so the check now does what a shell listing its cwd does:
`opendirSync`, one `readSync`, `closeSync`. Only EPERM/EACCES reads as denial —
a missing path, a non-directory, or an unexpected error still reads as readable,
so a non-permission failure can never masquerade as one. The same probe is what
the app side compares with, through one oracle shared by the telemetry emitter
and the notice, so the spawn path reads the directory once.
Proven divergence now also records evidence in main: one entry, keyed by the
daemon's pid, start time and launch nonce, carrying an opaque digest of that
identity and the folder class. No path leaves main. The existing focus-time
`macTccAttribution` poll carries it to the renderer, which raises a second toast
latched per daemon scope: dismissed stays dismissed, and a restart mints a new
identity so the poll returns null and the toast clears with no post-restart
probe. If the replacement daemon is denied too, about 31% of cases, the next
spawn re-records under the new scope and the notice returns, now with the
re-allow sentence doing the work.
No new IPC channel, no daemon protocol field, no polling change, and nothing new
on the spawn path beyond one `opendir`. `daemon_folder_access_notice` counts
shown, dismissed and open_manage_sessions against `daemon_pty_cwd_denied` as the
denominator; `shown` is emitted from main the first time a scope leaves the IPC
handler, so the renderer carries no telemetry plumbing for it.
* fix(macos): clear folder-access evidence only when the same folder class reads back
A readable spawn in ~/code said nothing about a Documents denial but was
hiding the notice; retire the evidence only when the daemon reads a folder
of the class it was denied on.
* fix(macos): say what a terminal-service restart actually does
The Manage Sessions restart confirmation still described the product as it was
before agents resumed themselves: it promised panes showing "Process exited"
that the user reopens by hand, and mentioned legacy-protocol sessions nobody
outside the daemon code can act on. Open terminals and agents come back on
their own now, so the old copy made a routine remedy sound like data loss.
It also called the thing a "daemon". The same restart is about to be offered
from a user-facing fix dialog, so both surfaces now say "terminal service", and
the confirm button is just "Restart".
The new body adds the one fact the old one never stated: terminals on remote
hosts are not affected. Translations of the two changed strings are dropped so
the five non-English locales fall back to English rather than keep showing copy
that is now wrong.
* feat(macos): give the denied-folder notice a fix the user can follow
The folder-access toast told the user their terminal service could not read
Documents and then handed them a paragraph: restart from Manage Sessions, and
if that does not work, re-allow Orca in System Settings. Both halves were
guesses. Roughly a third of restarts do not fix it, and the user had no way to
know which case they were in before spending every open terminal on finding
out.
Main can now answer that. `daemon-folder-access-probe.ts` forks a short-lived
child of the app binary the same way the daemon itself is forked, runs one
opendir/readdir/closedir against the denied path, and prints a single JSON
line. macOS attributes a TCC grant to the process that forked the child, so a
child of the app running now answers exactly the question the running daemon
cannot: would a replacement daemon get in? The child goes through the shared
child-process wrapper, never a shell, with a 3s deadline, a 1KB output cap and
an environment scrubbed to PATH/HOME/TMPDIR. Every failure — timeout, bad
output, spawn error — reads as `unknown`, never as a verdict.
That answer rides out as `restartWillHelp` on the evidence the existing
focus-time poll already carries, and the toast becomes a title and two buttons:
Fix… and Not now. Fix opens a dialog with the two real steps. When the grant is
already in place, step one is shown as done and Restart is live. When it is
not, step one is open and Restart is disabled until it completes — which it
does by itself, because the poll re-probes while the answer is still no, and
returning from System Settings is the moment that lands. An unanswered probe
never accuses the user of a missing grant; it leaves both steps open.
Restart calls the management API directly rather than stacking the Manage
Sessions confirmation on top, since the dialog already states the consequence.
Success replaces the steps with a done line and takes the toast down; failure
says so inline and leaves the button usable.
System Settings opens through the existing developer-permissions pane opener,
which takes an id rather than a URL, with Files and Folders added to it. The
event's action enum now also counts fix_opened, settings_opened,
restart_clicked and — emitted from main when a replacement daemon's first spawn
lands in the folder class the previous one was denied on — whether the restart
actually worked.
* fix(macos): let the folder-access notice return after a poll that read no daemon
A daemon identity reads as null during any reconnect blip, and the poll reports that as
"no mismatch". The notice dismissed itself and then never showed again for that daemon,
because the once-per-daemon latch still held its scope. Only "Not now" should latch.
* fix(macos): say what the folder-access notice costs the user
One line read like a stray warning. The toast now says who is blocked and what fails,
and still leaves the steps to the fix dialog.
* fix(macos): give the folder-access toast one action and the X, like every other toast
"Fix" is the only button; the X dismisses. Sonner fires onDismiss for programmatic
dismissals too, so the post-restart takedown now goes through the store and the hook,
and only a user's X is counted as dismissed.
* fix(macos): keep the fix dialog's steps a checklist and put the one action in the footer
Buttons inside each step made the list look like a form, and a footer Close duplicated
the X. The footer now carries the active step's action, with a ghost Cancel; a probe
that could not answer says so under step 1 instead of showing a check.
* fix(macos): let the checklist show the fix landed instead of saying so
A hedged sentence addressed to the user read like chat. On success both steps check
off and the footer offers Done; the unanswered-probe helper is a status, not advice.
* chore(i18n): drop the fix dialog's unused close key
* Revert "chore(i18n): drop the fix dialog's unused close key"
This reverts commit
|
||
|
|
eb92222e7f |
feat: support Antigravity as supervised worker (#21705)
* feat: add supervised Antigravity worker support * fix: address Antigravity worker review findings * fix: stabilize Antigravity readiness detection * fix: allow Antigravity resume footer after readiness * fix(antigravity): make agy reach worker_done as a supervised worker Three defects each blocked `orchestration worker-start --agent antigravity --worktree new-child` at the agent_readiness stage. 1. Readiness never fired. The composer check required the trimmed line to be exactly one character, but agy 1.2.7 launches in accept-edits mode and paints it into the caret row (`> Accept-edits mode: ...`). Widened narrowly to a bare `>` or `> <name> mode:`; matching any `> <text>` would make every menu dialog read as ready, since they all prefix their highlighted row the same way. 2. No trust artifact for agy. Added markAntigravityWorkspaceTrusted, writing ~/.gemini/antigravity-cli/settings.json under `trustedWorkspaces` — verified empirically against agy 1.2.7, and distinct from the Gemini CLI's trustedFolders.json, which agy does not consult. Trust is exact-path and not inherited by subdirectories, so each child worktree needs its own entry. 3. The orchestration path skipped the preset. Orca has two trust dispatch chains: the renderer's preflightAgentTrust and the main-process markLocalWorktreeTrusted. worker-start only takes the second, which matched cursor/copilot/codex and fell through for antigravity, so the trust write never happened while renderer-side tests passed. Verified live end to end: the dispatch settles `succeeded` with worker_done carrying the right task and dispatch ids, and the worktree is appended to agy's settings with sibling keys untouched. Known gap: remote-agent-trust-presets.ts has no antigravity branch. The SSH artifact path is unverified, so agy over SSH still stalls at agent_readiness. Recorded in a comment there rather than guessed at. * fix(antigravity): wire trust preset through preload safely * fix: preserve Antigravity readiness across transcript tails --------- Co-authored-by: Neil <neil@stably.ai> Co-authored-by: LielinaH <lielinah@gmail.com> |
||
|
|
0677271709 |
fix(orchestration): reap leaked worker terminals via process-incarnation fallback — stops an unbounded PTY/process leak on Remote Server (OOM / cgroup PID exhaustion) (#18790)
* fix(orchestration): remint live handle from process incarnation on worker release When a durable terminal handle goes stale (rendererGraphEpoch fence), inspectWorkerTerminal re-mints a live handle via resolveTerminalHandleByProcessIncarnation + matchesProcessIncarnation so release/stop/read act on the still-running PTY instead of reporting missing and leaking the agent process tree. - keep main shared host-scope re-exports; add matchesProcessIncarnation - wire observation.terminalHandle through control/stop/release - rebuild release-completion on main structured paths - on missing/unattached + provably exited: settleDead fence first, then same-incarnation settleWorker fall back (archive may block settleDead mid-request); settle before recovery defer * fix(orchestration): derive SSH host scope from the reminted handle; reuse fresh-request recovery guidance for structured workers Addresses two open CodeRabbit review comments on PR #18790. inspectWorkerTerminal read the dispatch authority with the stale durable terminalHandle, so after a remint the lookup resolved nowhere and currentHostScope was always undefined — an SSH worker with no liveness verdict and no persisted host_scope got classified from terminal.connected instead of unverifiable. It now reads the same effectiveHandle every other observation in the function uses. stopStructuredWorkerForRelease told the caller to repeat the release with the same --retry-request, which only replays the stale release_unknown receipt and made a structured-worker close failure permanently unretryable. It now sources releaseUnknownRecovery from worker-release-completion so the fresh-request-ID guidance lives in one place. Pre-commit lint-staged (oxlint + oxfmt) run manually: clean. * test(orchestration): exercise incarnation recovery through runtime paths * test(orchestration): pin the incarnation read scenario to the reminted terminal The read scenario only asserted that the call resolved, so it documented nothing about which handle the read reached. Assert that the handle readTerminal received resolves to the registered pane and incarnation, so the scenario proves the read went through the reminted terminal instead of passing on the incarnation fence's throw. * refactor(orchestration): drop redundant incarnation prefix check; require liveTerminalHandle * feat: add freebuff as a first-class TUI agent (#42) <!-- orca-pr-loc --> <!-- Programmatic LoC summary. Do not edit by hand; rewritten on every commit. --> | | Files | Added | Deleted | Net | | :--- | ---: | ---: | ---: | ---: | | Test | 0 | 0 | 0 | 0 | | Prod | 28 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$37 | 0 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$37 | <!-- /orca-pr-loc --> ## ELI5 Add Freebuff (`freebuff`) as a recognized first-class TUI coding agent in Orca alongside Codebuff and other supported agents. ## What Changed - Registered `freebuff` across shared TUI agent definitions, configuration catalogs, display names, and telemetry schemas. - Added agent icons, favicons, status mappings, and mobile asset references for Freebuff. - Added localization strings across supported language packs (`en`, `es`, `fr`, `ja`, `ko`, `zh`) and updated locale translation policy. - Documented Freebuff CLI in README agent table (`npm i -g freebuff`). ## Why Freebuff is a CLI coding agent twin of Codebuff (`npm i -g freebuff`). Adding it to the catalog enables users to launch worktrees, run automated sessions, and pick Freebuff directly within Orca. ## Linked Issue N/A ## Visual Proof `N/A` - Catalog registration and metadata definition for CLI agent launch; UI rendering uses existing TUI agent picker and status components. ## Testing - Verified TypeScript contracts, schemas, and catalog configurations. - Tested CLI detection / agent picker integration locally on Linux (`worktree create --agent freebuff`). ## AI Disclosure Assisted by AI coding tooling. ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why (including ELI5) - [x] Before/after screenshots or videos attached for UI changes, or `N/A` with reason - [x] Self-reviewed for correctness, security, and performance - [x] Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A) --------- Co-authored-by: Lesley Murfin <lesley@revivebusiness.ca> * test(orchestration): erase method overloads in worker reap fixtures * test: document worker fixture type boundaries * test: simplify worker fixture typing --------- Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> Co-authored-by: svc-orca[bot] <313947298+svc-orca[bot]@users.noreply.github.com> Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
88f2f01061 |
fix(daemon): escape the terminal daemon into its own systemd scope so a service restart no longer kills every live PTY (#19430)
* fix(daemon): escape the terminal daemon into its own systemd scope so a service restart no longer kills every live PTY Root cause: daemon-launched-child.ts forks the detached terminal daemon with detached: true, which escapes the POSIX process group (setsid) but never the systemd cgroup. Every PTY the daemon owns is itself an undetached direct child of the daemon (native-pty-spawn.ts). Under a combined systemd unit (Type=simple, KillMode=mixed, per docs/reference/headless-linux-server.md), a systemctl restart/stop SIGKILLs every process still in the cgroup at the stop timeout -- the daemon and every live terminal -- even though the codebase already has a fully-built adoption/reattachment path for a surviving daemon (orcad-entry.ts's refreshRestoredOrchestrationAuthority + reconcileLegacyWorkerTerminals, gated on daemonOwnsFreshPersistentPtys()). That path never fires today because the daemon never survives long enough. Fix: when systemd is actually supervising the process and the OS user has a reachable systemd --user manager (isDurableDaemonScopeSupported(), Linux only), launch the daemon via systemd-run --user --scope so it lands in a cgroup that is a sibling of the service unit's cgroup, not a descendant of it. A systemctl restart of the combined unit then never reaches it. Any failure of the scoped launch (no reachable bus, D-Bus policy rejection, etc.) falls back transparently to the existing plain fork() launch, so every platform/environment without this capability is unaffected. The daemon self-detects its own resulting cgroup scope via /proc/self/cgroup (detectOwnCgroupScopeUnit()) rather than trusting the launcher's intent, and publishes it as cgroupUnit in its pid record and orcad's health/readiness payload (health.terminalDaemon.cgroupUnit), so a running deployment can be observed to confirm the fix actually engaged. No new session registry is added: the existing daemon pid-record + adoption protocol (publishDaemonPidFile, daemon-pid-record-quarantine.ts's dead-record reclaim, refreshRestoredOrchestrationAuthority) already implements durable, crash-safe reattachment for a surviving daemon -- it was simply never exercised against a full unit restart before now. Proven via a systemd-in-Docker recovery test: a live PTY session's shell process, its daemon, and the daemon's cgroup scope were all confirmed unchanged across a real systemctl restart of a Type=simple/KillMode=mixed unit, while the main process pid changed (confirming the unit actually restarted) and the new process's health payload recognized the surviving daemon as adopted and live. A fresh write into the same PTY post-restart reached the same running shell. Ordinary terminal create/work/release and the #18789/#18790 worker-release reap-fix regression tests are unaffected. Fixes stablyai/orca#19408 * fix(daemon): probe the real per-UID XDG_RUNTIME_DIR before trusting the process's own env isDurableDaemonScopeSupported()/buildDurableDaemonScopeCommand() trusted the current process's own XDG_RUNTIME_DIR env var first, falling back to /run/user/<uid> only when that var was unset entirely. On mtl-02, orca-serve@factory.service's RuntimeDirectory= hardening directive makes systemd export XDG_RUNTIME_DIR=/run/orca_serve/factory into the unit's process -- a private scratch dir that shares the env var's name but has nothing to do with the user session bus. /proc/<pid>/environ on that host confirmed exactly that path plus DBUS_SESSION_BUS_ADDRESS=disabled:, while the real bus was reachable the whole time at /run/user/985 (confirmed via systemctl --user is-system-running with that dir exported by hand). The probe treated the hardened override as authoritative, found no bus socket there, and reported unsupported on every launch -- so the cgroup-escape fix from #19408/#19430 never actually engaged on real hardware, even though tonight's factory deployment picked it up. Fix: resolveUserRuntimeDir() now always tries the conventional /run/user/<uid> path first (computed independently via getuid(), never trusted from env), checking for a genuinely connectable bus socket via statSync(...).isSocket() rather than a bare existsSync. It falls back to the process's own XDG_RUNTIME_DIR only when that canonical path has no reachable bus -- covering hosts that legitimately have no /run/user/<uid> at all but do have a working bus wherever their own environment points. buildDurableDaemonScopeCommand() now explicitly sets XDG_RUNTIME_DIR to whichever path this resolution picked, rather than inheriting the spread env's (possibly hardened-wrong) value. Both isDurableDaemonScopeSupported() and buildDurableDaemonScopeCommand() gained an injectable canonicalRuntimeDir parameter (defaulting to the real computed path) so tests can exercise the hardened-override scenario deterministically with a real, connectable AF_UNIX socket fixture instead of the live host's actual runtime directory. Docker's stock jrei/systemd-ubuntu test container never had this hardening directive, so this gap was structurally invisible to the container-based verification in #19430 -- only caught against real mtl-02 hardware. * fix(daemon): report the daemon's own pid over the ready handshake, not systemd-run's The launcher used to infer the daemon's identity pid from the immediate spawned child (`child.pid`). On the durable-scope path that child is `systemd-run --user --scope`, not the daemon, so the launcher was asserting an identity it had no authority over. `DaemonReadyIdentity` now carries a required `pid` populated from `process.pid` inside the daemon itself, and `daemon-launched-child.ts` takes `launchedIdentity.pid` from that self-report. Both sides of the `holdDaemonAdoptionLease` pid comparison therefore originate inside the daemon process, which is the idiom this branch already uses for cgroup membership (`detectOwnCgroupScopeUnit` reads `/proc/self/cgroup` rather than trusting what the launcher intended). Note on the reported consequence: `systemd-run --scope` registers its *own* pid on the transient scope unit and then `execvpe()`s the target command -- same pid, no intermediate process -- so adoption did not in fact fail on systemd >= 206 (verified against systemd 255.4-1ubuntu8.17 and current main, `src/run/run.c` `start_transient_scope()`). The fix stands on its own merits: it removes a silent dependency on that exec-vs-fork implementation detail, which a `systemd-run` shim earlier in PATH or any future systemd change would have broken with no diagnostic. `terminateLaunchedDaemonChild` was audited and deliberately left on `child.pid`: for the same execve-preserves-pid reason that pid is either still systemd-run mid-scope-setup (killing it correctly aborts the launch) or already the daemon, so it targets the right process either way. Regression coverage: `daemon-launched-child-identity.test.ts` pins the identity source, and `daemon-ready-identity.test.ts` gains pid-validation cases. Ready-message fixtures across the `daemon-init-*` suites were updated for the now-mandatory field. Addresses: https://github.com/stablyai/orca/pull/19430#discussion_r3953722704 https://github.com/stablyai/orca/pull/19430#discussion_r3954346518 * test(daemon): assert cgroupUnit in the pid-file parse contract `parseDaemonPidFile` returns `cgroupUnit` on every branch as of the durable-scope commit on this branch, but five exhaustive `toEqual` assertions in daemon-health.test.ts still described the pre-scope shape, so they failed on the branch independently of any later change. Adds the field to those expectations. Deliberately not relaxed to `toMatchObject`: asserting the full parsed shape is what makes these tests catch a field silently dropped from the pid-file contract. * refactor(daemon): resolve the canonical user runtime dir at one point The per-UID path cannot change for a live process, so compute it once into a module const instead of threading the same default call through three signatures, and drop the try/catch around a getuid() that cannot throw once it exists. Trims the module prose to the non-obvious facts and corrects the pid-file record comment: an unscoped daemon writes null; only records no daemon wrote are absent. * test(daemon): clean up the cgroup-scope fixtures and assert a verdict The cgroup fixture tracked only the file it wrote, leaking one temp dir per case. Drains both fixture lists with splice so the pop-may-be-undefined guards go away, and replaces a not-throw/typeof-boolean pair with the verdict it was circling: no resolvable runtime dir means unsupported. * refactor(daemon): share the detached child options across both launch paths cwd, detached and stdio were repeated in the fork and systemd-run branches, which left the two comments explaining them hovering over the env block instead. Names them once so each branch carries only its own delta. * refactor(daemon): validate the ready pid like every other field typeof-first narrows the value, so the two 'as number' casts the isSafeInteger check needed disappear and the pid guard reads like the startedAtMs guard below it. * fix(daemon): don't retry the launch unscoped after losing the endpoint race A scoped attempt that lost the endpoint to another daemon was retried unscoped: a second doomed fork, a misleading 'cgroup-scope launch failed' warning, and the same DaemonEndpointUnavailableError the caller was already going to adopt on. Rethrows it instead, since no launch mode can win a race that is already lost. Also drops a private alias for DaemonChildSpawnOptions and the two 'as number' casts on child.pid in the startup-failure cleanup. * fix(daemon): unlink the pid record by the pid the daemon published The record holds the daemon's self-reported pid, so match on that rather than on the immediate child's, which is the systemd-run wrapper's until it execs. * fix(daemon): route the scope launch through the child-process chokepoint The two files this PR added imported `node:child_process` directly, which `child-process-import-boundary.test.ts` fails on deterministically: the offender count went 155 -> 157 against a pin of exactly 155. Raising the pin or listing the files is what that test explicitly forbids, and the allowlist's own note says a split "moved the import, it did not add one" -- so the fix is to get both new files off the module and put the count back at 155. - `daemon-cgroup-scope.ts`: the `systemd-run --version` probe now uses `runProcessSync` instead of `execFileSync`, so it gets the shared spawn decisions. Kept synchronous deliberately: `launchDaemonChild` attaches the readiness listener in the same tick it is called, and an await before the spawn moves the child past that tick. A non-zero exit is data rather than a throw here, so the verdict now checks `code === 0 && !timedOut`. - `daemon-launched-child-spawn.ts`: the scoped launch uses `spawnProcess`, and the long-standing unscoped launch keeps `fork` semantics through a new `forkProcess`. - `src/shared/child-process/fork-process.ts`: the fork arm of the chokepoint. `spawnProcess` cannot express a Node child with an IPC channel started from a module path under an overridden `execPath`, and the existing launch tests are written against `fork`'s contract, so a spawn rewrite would have changed module resolution, `execPath` and `execArgv` at once. It passes `windowsHide: true` -- the flag every other call site in that directory sets, reachable via an assertion because `ForkOptions` omits it -- which keeps `windows-console-visibility.test.ts` at its pin of 65 too. Both ratchets pass with both pins and both allowlists untouched. Docs: `orcad-operations.md` and `headless-linux-server.md` still described the limitation this PR removes as permanent. Both now describe the durable-scope survival path and its preconditions (systemd as PID 1, a reachable user bus / `loginctl enable-linger`, `systemd-run` on PATH), and scope the old text to the unscoped-fallback case, pointing at `health.terminalDaemon.cgroupUnit` as the way to tell the two apart on a running host. * fix(daemon): seal the cgroup capability probe from the host and correct KillMode=mixed docs The capability probe consulted the host's own /run/systemd/system marker and spawned the real systemd-run binary, so the hermetic unit tests could only pass on a systemd host (and fail closed otherwise, even with faked bus sockets). - Thread systemdBootPath and runVersionProbe as test seams through isDurableDaemonScopeSupported, defaulting to the real boot marker and systemd-run --version probe in production. - Narrow the injected probe to the ProcessResult slice it consumes. - Cover: no-systemd-boot, non-zero probe exit, and probe-timeout cases. - Correct KillMode=mixed semantics in the docs: the cgroup-wide SIGKILL fires the instant the main process exits, not after TimeoutStopSec; document the Docker-container caveat and add KillMode=mixed to the multi-service template. * fix(daemon): satisfy assertion checks in scoped launch * fix(daemon): satisfy anti-slop and console guards * test(serve): update shutdown docs assertions for daemon scope * fix(daemon): migrate adopted legacy scopes * docs: qualify restart safety by daemon scope * docs(daemon): qualify Upgrade restart prose with durable scope caveat Align the Upgrade section in docs/reference/headless-linux-server.md with the earlier preservation section and docs/reference/orcad-operations.md: a service restart terminates live processes only when running under the unscoped fallback, and stops should be treated as destructive unless health.terminalDaemon.cgroupUnit names an orca-daemon-*.scope. Update the shutdown workflow test assertion in config/scripts/headless-serve-shutdown-workflow.test.mjs to match. * fix(daemon): harden legacy scope migration --------- Co-authored-by: Lesley Murfin <260182349+LesleyMurfin@users.noreply.github.com> Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
6ce7208b98 |
fix(sidebar): stop a workspace with a structured chat reading as asleep (#22098)
* fix(sidebar): stop a workspace with a structured chat reading as asleep A workspace whose only surface is a structured native chat showed the sleeping moon, lost its pull-request glyph to it, and would vanish entirely under the hide-sleeping filter. hasActiveWorkspaceActivity asked three terminal-shaped questions: a tab in tabsByWorktree with a live PTY, a browser tab, or a fresh non-done agent-status row. A structured chat answers none of them. Its tab lives in unifiedTabsByWorktree, so the PTY term never sees it, and an idle session projects state 'done', which is exactly what isFreshNonDoneAgentStatus refuses. Both chats finishing their turn was enough to draw the moon. Add a fourth term keyed on the chat EXISTING. Not on a live provider child: that child is held only while the chat's pane is visible and is evicted 15s after it is not, so keying on it would flip the glyph on every worktree switch and report a process recycle the user never sees. The transcript, and the session's ability to take the next send, outlive the child. The term goes in the shared predicate, not the card, because the moon, the hide-sleeping filter and the Cmd+J palette all read it and must not disagree about which workspaces are asleep. Supporting moves, no behaviour change: the projection sits beside its siblings in visible-worktree-activity-inputs, and buildVisibleWorktreeOptionsFromState moves to its own module, which leaves the filter a pure function of its options and keeps both files under the 300-line cap without raising it. * fix(sidebar): keep structured chats visible across workspace surfaces * refactor(sidebar): keep jump palette inputs below lint limit |
||
|
|
15472cd4c6 |
feat(native-chat): keep restart recovery available in status bar (#21397)
* feat(native-chat): keep restart recovery available in status bar * fix(native-chat): source the restart offer from the host and retire it on recovery Closing the reconnect dialog spent the durable recovery offer, so looking around before deciding lost the recovery for good. The offer now survives a close, and the status bar carries it — but a durable offer needs a way to die, and it only had a reconnect, an explicit dismiss, and a 24h expiry. The claim's launch-scoped lifecycle moves into its own collaborator, which splits what the host ADVERTISES from the evidence it holds. A resume-capable hold that hands a marked chat its provider child back is the recovery the offer existed to perform, so it stops being advertised and stops being written back at quit, while the marker stays valid evidence — a user who reopened a chat can still ask the agent to carry on. Teardown re-derives the snoozed offer rather than round-tripping raw markers, and this teardown's own witness now outranks the stale claim for the same chat instead of being overwritten by it, which was silently persisting an old turn id and making the next launch refuse the chat that was actually mid-turn. On the renderer the candidate list gets its own producer against agentSession.restartResumable, so the status entry and the dialog read one host-owned answer instead of the dialog pushing its local state at a sibling. The entry re-reads the host before reopening, so a reopened list can never name a chat the host would now refuse; dialog open becomes the external one-shot request rather than a flag mirrored into render state, which is what let a reopen replay the launch answer and re-offer chats already reconnected. Dismiss all is quiet rather than destructive, saves the preference like every other exit, and reports a write the host never confirmed instead of trapping the dialog open. * fix(native-chat): keep a durable offer a launch never read, and settle the one a continuation spent Teardown replaced the recovery capsule with whatever this launch still owed, and a launch that never read the offer owes nothing — so a quit after a failed first read, a disabled flag, or a window that never mounted deleted a recovery the user was never shown. The write-back now distinguishes "claimed and still owed" from "never claimed": the first is re-derived as before, the second carries forward verbatim, because nothing revealed those sessions and the predicate would refuse every one for want of a journal nobody opened. Reconnect and continue spent the same claims Reconnect does but never shrank the offer, leaving the status bar counting chats the host had already handed back and sending the user to an entry that re-reads, finds nothing and does nothing. * fix(native-chat): stop a teardown answering for an offer it could not read Two ways the write-back deleted a durable recovery offer nobody had seen. A take that FAILED left the claim holding an empty list and reporting that this launch had answered for the offer. The markers were still on disk, unread and unknowable, and teardown then overwrote them with its own empty list. It now writes nothing at all unless it has a witness of its own. `owed()` read "has the capsule been touched" where it meant "did anything here LOOK at the offer" — and its own write-back read counted. Teardown is retried when a phase fails, so the second attempt re-derived carried markers against a session map eviction had already emptied, refused every one, and wiped what the first attempt had just carried forward. The flag is now set only by the paths that actually read or act on the offer. The mock guard for the carry could not fail: it indexed the session it claimed nothing had revealed, so re-deriving passed and the verbatim carry was never the reason it went green. It now runs against no indexed session, which is what an unread offer looks like. Also drops the `Not now` row from the preference table, where it was paired with a dismiss method it no longer calls, and asserts the same thing where the snooze is already covered. Splits the marker predicate's journal reader out of the resume host, which was at its line ceiling. * fix(native-chat): clear the corrupt recovery capsule the take refused A capsule whose contents no longer parse made take() throw before it ever reached the clear, so the bad file survived every launch. Nothing else rewrites it now that a teardown owing nothing readable declines to write, and the freshness filter runs after the parse, so the 24h window could not release it either: one corrupt file refused recovery forever. Clear it inside the same transaction that failed to read it, then rethrow, so the poison dies on the next launch while callers still see why the take failed. A clear that fails is swallowed rather than allowed to mask the parse error. Refusing to expose partial candidates is unchanged, and a read that fails for any other reason still writes nothing. * feat(native-chat): make resume the one restart action, and make it actually resume The restart prompt offered two actions: "Reconnect all", which reattached and sent nothing — exactly what opening the chat already does — and "Reconnect and continue", which reattached and asked the agent to carry on. The vacuous one is gone, the "Not now" button and the info popover with it, and the feature is now called resume throughout. "Don't ask again (resume automatically)" now runs the action the button runs: the launch calls agentSession.restartContinue instead of agentSession.restartResume, so the preference means what it says. Several comments asserted the opposite as a structural guarantee and are corrected. agentSession.restartResume stays: no in-app caller is left, but it is a published wire method a non-desktop or older client can call. * fix(native-chat): label the resume button with the number of chats selected The button read "Resume all" whenever every chat happened to be ticked, which described the selection rather than the action. It always acted on the selected chats only. Now it always names that count, with a singular variant so one chat does not read "1 chats". * refactor(native-chat): drop the reconnect vocabulary the resume action left behind Resuming became one action — reattach and ask the agent to carry on — so the notification helpers no longer need to be told which action they are reporting. Every caller passed `continue`; the `reconnect` branch, its helper and its catalog keys are gone. The dialog and the launch path had grown two copies of the same call: same RPC, same response shape, same announce-and-settle. That now lives once in the store module that owns the offer, which also takes the dismiss call, leaving the modal presentational. The two copies had drifted — only the dialog's caught a malformed payload — and the unified one keeps the defensive reading. No behaviour change. `agentSession.restartResume` stays: it is a published wire method even though nothing in the app calls it. * refactor(native-chat): derive the resume selection instead of intersecting it The modal's selection was intersected back against the host's candidate list before every action, as a guard against naming a chat the host never offered. That guard could never fire: the selection was already derived from that same list, so the intersection was the identity. The array of chosen ids is now the derived value and the lookup set falls out of it, which makes the property structural rather than checked. The helper had no other caller and is gone, along with its three tests. Three tests mocked the resume response in the shape the old API returned. Two never reached that branch at all; the third only passed because the unreadable shape happened to exercise the malformed-payload path. All three now use the real shape, and the malformed-payload behaviour — report an unconfirmed delivery, leave the offer standing — gets a test that says so. Also: the candidate reader took two trailing optional parameters, so one caller passed a placeholder `false` to reach the second; they are an options object now. `isFolderWorkspaceId` had no caller outside its own module and is no longer exported. `RestartActionOutcome` only ever describes a continuation row, so it is named for that. `dismissAll` set a busy flag that nothing could render, since it closes the dialog first. Several comments repeated an argument already made in the module they point at. Settings: the automatic-resume description is one sentence again. No behaviour change. * fix(native-chat): make restart recovery explicitly durable * fix(native-chat): preserve dismissal fence across new interruptions |
||
|
|
0bbaadafa4 |
Clear website annotations after successful delivery (#22060)
* Clear website annotations after prompt delivery Capture annotation snapshot at send time and selectively remove only the captured objects when delivered. Preserves edits and additions made during in-flight delivery. * rm design doc * Clear only delivered browser page annotations Annotations are now explicitly passed to the clear handler, allowing it to remove only delivered annotations by identity. This preserves any annotations edited or added after delivery began. * Distinguish delivered vs user-cleared annotations Add removeDeliveredBrowserPageAnnotations to remove only delivered annotations while preserving concurrent user edits. Simplify clearBrowserPageAnnotations to clear all annotations for a page when user explicitly clears. |
||
|
|
cd59678394 |
refactor(agent-launch): assemble host startup-plan inputs in one resolver (#22082)
* refactor(agent-launch): assemble host startup-plan inputs in one resolver buildAgentStartupPlan was already one shared implementation, but every host re-derived its argument object by hand from the same four settings (agentCmdOverrides, agentDefaultArgs, agentDefaultEnv, terminalWindowsShell), and the copies had drifted. resolveAgentStartupPlanInputs owns that assembly. What genuinely varies per launch stays a parameter: the host (platform, isRemote), a requested shell, the per-launch agentArgs override, and the picked session options. Fixes a live divergence on the agent.launch path: orca-runtime-create-agent-session passed sessionOptions without sessionOptionsOverrideAgentArgs, so a configured `--model` in agentDefaultArgs reached argv alongside the picked model and won on argv order, while the same launch through worktree.create honored the pick. The plan also reported no applied sessionOptions, so the chat surface could not name the model the user chose. Migrates the four host sites; the eleven renderer sites are unmigrated and still assemble their own inputs. * fix(agent-launch): preserve picked options in draft launches * test(agent-launch): assert draft option precedence |
||
|
|
6dc00702d2 |
refactor(floating-workspace): launch the default agent through the shared launcher (#21390)
* refactor(floating-workspace): launch the default agent through the shared launcher The floating workspace titlebar agent button drove tab startup itself: it built its own `buildAgentStartupPlan`, created the tab, queued the startup command and rebuilt the tab-bar order by hand. That is a second copy of what `launchAgentInNewTab` already does for every other "start an agent here" button, so a launch-point change had two places to land. The button now calls `launchAgentInNewTab` and keeps only its own placement: selecting the tab inside the floating panel's unified group and focusing it. `launchAgentInNewTab` gains an optional `activate` so a caller that places the tab itself can keep the new terminal out of the global selection. The floating panel needs this — activating would move the main window's active tab to a tab it does not show — and it matches the other floating tab creators, which already pass `activate: false` to `createTab` and select via `activateTab`. Two behaviours change, both fixes: - tab-bar order now goes through `persistAgentLaunchTabOrder`, which reconciles editor and browser tabs. The hand-rolled loop rebuilt order from terminal tabs only, dropping the floating workspace's markdown and browser tabs. - the startup plan now carries the resolved Windows shell, so argument quoting matches the shell the PTY actually gets. * test(agent-launch): pin the floating button as a launch funnel caller The census exists so a new launchAgentInNewTab caller is a deliberate act. This entry is a bypass converging, not a bypass appearing: the button previously hand-rolled the helper's terminal arm against queueTabStartupCommand. * fix(agent-launch): preserve floating terminal launch boundaries * fix(agent-launch): honour the chat-view default in the floating workspace The floating launch button now routes through the shared launcher, so it should inherit the same launch policy as every other caller. The previous review pass added a `workspaceKind === 'floating'` guard to `decideInitialAgentTabViewMode`, which silently dropped `openAgentTabsInChatByDefault` (and the user's model and effort preferences) for that one button. That guard was not justified. `canToggleNativeChat` has no workspace-kind gate, so a floating terminal can already be switched into the chat view by hand, and `TerminalPaneNativeChatPortal` mounts into the pane's own container — the panel already renders it. Refusing the setting at launch while the same view sits one click away in the same panel is an inconsistency, not an invariant. Structured sessions stay out of the floating workspace on the pre-existing blocker in `resolveStructuredNativeChatSupport`: those open an `agent-session` tab, and the floating panel renders no such surface. * test(agent-launch): record why floating routes to the terminal-backed chat lane * refactor(floating-workspace): record why the launch does not take the global selection * refactor(agent-launch): lift launch execution-context resolution into its own module |
||
|
|
35fe67b610 |
fix(perf): measure terminal latency with presented CI frames (#22096)
* fix(perf): present benchmark frames only on isolated CI display * fix(perf): wait for the benchmark page before presenting its window * docs(perf): record full scale pass with unchanged latency budgets * test(perf): document and verify the isolated display exception |
||
|
|
da982a4eb0 |
fix(native-chat): navigate to open history sessions (#21283)
* fix(native-chat): navigate to open history sessions * test(native-chat): provide structured session predicate |
||
|
|
73b726c64f |
refactor: use generic VirtualizedList in conflict review (#22092)
Replace SourceControlVirtualFileList with a reusable VirtualizedList component, and update related constants and test IDs to reflect the generic nature of the component. This extracts the virtualization logic to a shared utility that can be used across different features. |
||
|
|
6ee7e9511b |
fix(wsl): preserve OpenCode agent variant in guest
Preserves the selected OpenCode variant across WSLENV so WSL status detection remains correct when native and WSL installations coexist. |
||
|
|
7047cdc0d4 |
refactor: virtualize artifacts list with reusable component (#22061)
Extract SourceControlVirtualFileList to a generic VirtualizedList component and apply windowing to the artifacts table for efficient rendering of large lists. Add aria-setsize and aria-posinset announcements to windowed rows when opted in. |
||
|
|
297cfe0cf3 |
fix(usage): price GPT-6 Astra, and declare when the Codex cost total omits a model (#22073)
* feat(usage): price GPT-6 Astra token usage gpt-6-astra was missing from MODEL_PRICING, so normalizeModelForPricing returned null and estimateCostUsd dropped every event on that model from the total. Stats & Usage showed ~$0 for hundreds of millions of tokens with no unpriced indicator, since hasInferredPricing only covers a missing model name, not a missing table entry. Rates are the published ones: $10 input / $1 cached input / $50 output per 1M, with the >272K long-context tier at 2x input and cache and 1.5x output, which the existing tier fields already express. Fixes #22005 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(usage): say when the Codex cost total omits an unpriced model A daily row whose model has no `MODEL_PRICING` entry gets a null cost, and `buildSummary` simply skips it. As long as one other row is priced, `hasAnyBillableCost` is true, so the Codex card prints a confident dollar figure that silently leaves those tokens out. That is how GPT-6 Astra usage read as near-$0 before the entry landed, and it is how the next unpriced model will read too. `hasInferredPricing` does not cover this: it only fires when a rollout has no model name at all, and its label ("inferred pricing") describes a guess, not an omission. So the summary now carries `hasUnpricedModels`, set when a row has a model name and no price, and the estimated-cost card appends "• excludes unpriced models" — the same bullet-suffix idiom the breakdown rows already use for "• inferred pricing". The number stays; it stops claiming to be the whole bill. * fix(usage): caveat the Overview total too, and only when a remainder exists Review of #22073 found the Codex caveat stopped at the Codex tab. The Overview tab prints a combined total across providers and already has a "- some model prices are unavailable" line, but `hasPartialCost` only noticed a provider whose whole cost was null. A Codex range with one unpriced model among priced ones kept a real number, so the line stayed hidden and the Overview repeated the same confident, incomplete figure. `UsageProviderOverview` now carries `hasPartialCost` — set from `hasUnpricedModels` for Codex, false for the providers that cannot yet report it — and the reduction ORs it in. No new string. Second, the Codex card could read "n/a • excludes unpriced models" when nothing at all was priced. "Excludes" promises a remainder, and there was none. The suffix now also requires a non-null total; that case is still declared, on the Overview, through the null-cost path. `unpricedCostLabel` becomes `costCardLabel`, since it holds the plain label whenever there is nothing to qualify. --------- Co-authored-by: Alfred212121 <58665898+Alfred212121@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1a3f4e88c0 |
fix(opencode): support v2 plugins under plain executable name
Supports OpenCode 2 installed as opencode, including plugin loading and quick-command submission. |
||
|
|
9fea4d1ade |
fix(rate-limits): keep polling Claude usage for Fable accounts during live sessions (#22071)
* fix(rate-limits): keep polling Claude usage for Fable accounts during live sessions The statusline feed carries the 5-hour and 7-day windows but never the Fable weekly window. Each live post rewrote the whole provider snapshot with a fresh updatedAt, and the automated OAuth poll skipped whenever that snapshot was under five minutes old. Posts arrive every fifteen seconds while an agent works, so for accounts with a Fable quota the meter froze until the session idled. The skip now applies only when the live feed covers every window the poll would return, i.e. when the account has no Fable window. Accounts that have one keep the normal fifteen-minute cadence. A live post also flipped the snapshot to ok, which dropped the 429 Retry-After and let the next poll land inside the throttle window. The live snapshot now carries retryAtMs forward and the Retry-After check no longer depends on the error status. Fixes STA-8066. * fix(rate-limits): carry a 429's Retry-After through the live-fresh short-circuit resolveClaudeFetchApply returns the live snapshot verbatim when a poll fails while the statusline feed is fresh, so the Retry-After the 429 just reported never reached the poll gate and every cycle re-hit the throttle. Copy retryAtMs onto the kept snapshot. |
||
|
|
72a1b148c1 |
fix(settings): make terminal theme selection override Ghostty colors (#22069)
* fix(settings): clear terminal overrides when selecting a theme * test(settings): cover light terminal theme override reset |
||
|
|
d60043787b |
feat(agent-launch): carry the launch inputs the host cannot derive (#22037)
* feat(agent-launch): carry the launch inputs the host cannot derive Desktop's launch call sites cannot move onto `agent.launch` while the wire drops inputs they depend on. This adds the three the host genuinely cannot work out for itself, and deliberately adds nothing the host can. - `agentArgs` — the host read only `settings.agentDefaultArgs`, so a saved launch recipe's arguments had no way across. Tri-state is preserved: `null` is "no arguments", absent is "use the settings default". - `cwd` — `TerminalCreateOptions.cwd` already reached the spawn, but nothing on the wire filled it. It also decides the route: only a terminal can start somewhere other than its workspace, so the host now feeds it to `requiresTuiLaunchCommand` and downgrades with `tui_launch_command` rather than running a structured session in the wrong directory. - `launchSource` — telemetry, and the only member of the `agent_started` triple the host cannot derive; `agent_kind` and `request_kind` are computed host-side. Typed `z.string()`, not the closed enum: params are validated by the HOST, so a closed arm set would let an older host refuse a newer client's launch over a label. Attribution must not gate a user action. Not added, because the host already derives them: `launchPlatform` (`getAgentLaunchPlatformForWorkspace`, from the same connectionId/path/ projectRuntime the renderer uses) and `startupCommandDelivery` (a pure function of the agent inside `buildAgentStartupPlan`). Fingerprint: `agentArgs` and `cwd` are in — they change what the call does, so a retry carrying different ones must conflict rather than replay. `launchSource` is out — two buttons producing the same launch are one operation, and folding it in would refuse an honest re-attributed retry. A caller sending none of the new fields digests exactly as before, because the canonicalizer drops undefined keys, so launches admitted by an older build still replay across the upgrade. Arguments reaching a structured route are ignored by an existing deliberate decision (the Agent SDK and app-server version their option sets separately from the interactive CLI), so the host reports it in `warning` instead of overriding the user's preference on the strength of a field that is not evidence about the surface. * fix(agent-launch): forward create-target launch inputs |
||
|
|
2739246058 |
feat(native-chat): light the unread indicators when a structured chat finishes (#21924)
* feat(native-chat): light the unread indicators when a structured chat finishes A structured native chat had no attention producer. The PTY lane reaches the unread markers through use-notification-dispatch, whose liveness reads PTY state and whose admission requires terminal panes, so a structured session — which runs on the execution host with no renderer PTY — could finish a turn with nothing lighting anywhere. A backgrounded chat was the worst case: with no mounted pane there was no reader to notice at all. The host derives the completion, because only the host can. The journal keeps committing whether or not a renderer holds a reader, so the new feed observes each commit at StructuredAgentSessionClientDelivery.publishJournal and emits on every running -> settled transition. That edge runs after the subscriber loop and independent of it, which is exactly why a chat nobody is watching can still complete. It is a separate capability-gated stream rather than a field on the status summary: the summary carries no turn identity and no outcome, and is re-broadcast on every status change, so folding a completion into it would make every status consumer a completion consumer. ONLY `success` LIGHTS ANYTHING. Outcome is A0's provider verdict and is never inferred: a turn the host merely watched stop carries no outcome and produces no event, because absent means UNKNOWN. `completed` alone proves nothing — a provider reports its own API error as a finished turn — so the host emits nothing for it and the renderer filters again on the way in. RECOVERY IS LIVE-ONLY. Nothing is retained, queued or replayed on either side. A subscriber learns what settles while it is subscribed and nothing else; on reconnect it re-opens an empty stream and whatever landed during the gap is gone. A retained completion would be a durable "unread is owed" obligation with nothing to retire it, and a reconnect would then light the dot for work the user already read. Tests on both sides pin this so a later refactor cannot quietly turn it into catch-up. The dot itself reuses the neutral policy in attention/agent-attention-policy and #21274's structured surface adapter, so suppression, acknowledgement and addressing keep exactly one implementation and the surface key is never omitted to evade a check. No second suppression rule is introduced. OS delivery is deliberately not wired: this calls applyAgentAttentionUnread, not applyAgentAttention. Also narrows the completion feed's journal dependency to the newest-turn reader it actually uses, and adds journal.newestTurn() beside the existing activeTurnId() on the one shared by-sequence scan rather than a second scan. * test(cross-version): register the turn-completion subscribe on the wire manifest The cross-version gate asserts the structured surface's method list by name and count, so an additive method has to be declared there deliberately. Adding the entry makes the suite call it in both skew directions and stubs the host side, which is the statement the gate exists to force. * fix(native-chat): rebaseline completion feed after rewinds |
||
|
|
dc8cf30554 |
fix(native-chat): end a structured turn when the agent reports it failed (#22047)
* fix(native-chat): end a structured turn when the provider reports it failed (#22044) A turn reads as working while its durable turn row says `running`, and only two events could write a terminal row: the provider's turn-completed notification and the provider process going away. A provider error that ends a turn is neither, so the row stayed `running` with nothing re-deriving it, and the chat counted "Working for N" for the life of the session. Codex reports such a failure as an `error` notification naming the turn it ended, with `willRetry` distinguishing it from a stream error it is about to retry. That frame now settles the turn it names. Claude's CLI reports the same through its session-state frame, whose `idle` arm the SDK documents as the authoritative turn-over signal; that now settles the open turn too. Codex's `thread/status/changed` deliberately settles no open turn: the app server clears `running` on every error, including ones it reports as not affecting turn status, so a turn still open there is still running. What it does settle is a send whose dispatch was never answered — a timed-out dispatch is recorded as unverified delivery, reads as work still owed, and nothing in a live session retired it. Retiring it never makes the send re-deliverable. Splits the codex notification translator so the file stays inside its line budget. * fix(codex): defer idle dispatch release until turn end * fix(claude): enable session state lifecycle events |
||
|
|
f7955e81ff |
feat(conflict-review): virtualize large conflict file trees (#21920)
Implement windowing for the conflict review file tree using SourceControlVirtualFileList to efficiently handle large merge conflicts. Add comprehensive tests for virtualization behavior including scrolling, collapsing, and dynamic updates. |
||
|
|
91e6e1f355 |
fix(native-chat): collapse a finished turn to its answer (#22029)
* fix(native-chat): collapse a finished turn to its answer A finished turn's "Worked for N" row hid the turn's tool runs and nothing else. Every sentence the agent said on the way to its answer stayed in the transcript, so the resting state of a long chat was the narration, not the reply — one 16m 56s review turn left 21 assistant messages and roughly seven screens of scrolling behind a control that reads as if it had put the work away. The fold's unit is now the turn. A settled turn draws its prompt, its duration, and its answer; the narration and activity that produced it sit behind the caret. The answer is the turn's last assistant row that renders prose — derived, because the journal carries no marker saying which message is the reply. Collapsed stays derived rather than stored: nothing closes the disclosure when a turn ends, it arrives closed because the turn gained a duration. A running turn therefore folds nothing and the reader watches the work as it happens, which is what already happened and is now stated rather than inherited. Rows that outlive the turn that started them stay outside the fold — a spawn roster and a background task are often the only record of how that work ended. So does the reader's own message, question receipts, and the turn's diff rollup. A turn that produced no prose folds whole, its status row standing as the anchor. Two presentation changes come with it, both about the opened view: - A settled run's header was a call count followed by a monospace list of tool names and arguments. It is now one sentence in the transcript's own type — "Read 7 files, ran 17 commands, and searched 4 times" — built on the tool-category vocabulary that already picks the row's glyph, so the words and the icon cannot claim different things. A run of one command keeps that command as its header. - A tool call now owns its result instead of standing beside a separate `Result` row, so an opened run lists the work rather than twice as many rows half of which say `Result`. Output is one more click. Pairing is positional — a result answers the most recent unanswered call — because result blocks carry no call identifier to match on. Command previews also lose the `/bin/zsh -lc "…"` wrapper they all opened with. The unwrap happens inside `summarizeToolInput`, before truncation, because the clip at 80 characters removes the closing quote that proves the wrapper; one site fixes the header, the rows, and the running label. Measured on a real session journal at 1200x900: the turn above goes from 6,300px across 51 rows to 452px across 2, the whole session from 8,151px to 2,138px, and the same turn opened from 18,540px to 11,131px. The fold derivation lives in `src/shared` so the mobile transcript can read the same rule; wiring mobile's list to it is not part of this change. * fix(native-chat): preserve FIFO tool result pairing * fix(native-chat): keep tools collapsed when opening turn * test(native-chat): clarify independent tool disclosures |
||
|
|
c49b8cd534 |
Revert "fix(native-chat): stop a subagent's output speaking for the agent tha…" (#22058)
This reverts commit
|