mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 00:02:10 +00:00
9678dfb9aa884546bb8ecefc0ff2c7f48407f161
11697
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9678dfb9aa |
fix(pi): stop pi-subagents workflows from pinning a pane on working (#22533)
Awaited workflow children announce subagent:async-started but never get a subagent:async-complete, so the #21882 roster kept their ids forever and suppressed every later agent_end. Treat subagent:process-terminal (the runner exiting) as the end signal for tracked runs, with a short grace so runs that report their own completion keep settling through that path. Also accept completion events that carry only runId. Fixes #22527 |
||
|
|
7ea01279cd |
feat(search): bundle ripgrep for local, WSL, and SSH search (#22396)
* feat(search): bundle ripgrep for local, WSL, and SSH search Ship @vscode/ripgrep-universal's prebuilt rg for all six relay platforms in every desktop artifact. Local and WSL searches spawn the bundled binary and drop the git ls-files / git grep fallbacks; SSH deploys upload the remote's binary once per ripgrep version and the relay prefers it over PATH rg. * fix(search): address bundled ripgrep review findings - Key the SSH ripgrep cache on the binary's content hash; a package bump is the only update step - glibc verifier: read arch tokens below the slice root and accept static ELFs (arm64 release blocker) - Ship ripgrep/PCRE2/musl license notices; bundle rg with orcad - Packaged builds never spawn a bare rg; report fd pressure as transient - SSH: install rg before sweep/GC, size-validate installs, back off instead of disabling on launch failure - Scope Dependabot to @vscode/ripgrep-universal; revert unrelated lockfile churn * chore(search): drop bundled-ripgrep reference doc; assert full packaging layout parity * refactor(search): one entry point for spawning the bundled ripgrep Local Quick Open, Quick Open path search, the Explorer name filter, and runtime text search each repeated the same three steps: resolve the bundled command, spread in the WSL distro, spread in the WSL shell expression. Fold that into spawnBundledRipgrep so one place owns the rule that a bare 'rg' must never reach spawn, and simplify the resolver's command/packaged checks. Restore the AGENTS.md ripgrep rule dropped alongside its reference doc in |
||
|
|
edbb2a91e2 | docs: update translated Android APK links to 0.0.50 (#22740) | ||
|
|
1069bb053f |
fix(agent-launch): a cwd at the workspace root no longer forces a terminal (#22729)
"Continue in New Session…" always names a cwd, and both the renderer route input and the host launch-mode decision read any cwd as a custom start directory, so the continuation opened a terminal agent even when chat was the user's default. Both now share one rule: only a cwd outside the workspace root (after normalising slashes, Windows case, WSL aliases and the distro's Linux spelling) requires a terminal. A subdirectory still does, because a structured session cannot start there. Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
5e6fcca0b3 |
fix(native-chat): date a session by its own lifecycle, not its subagents' work (#22520)
* fix(native-chat): date a session by its own agent's rows, not its subagents' The journal reducer's lastActivityAt is the structured status summary's updatedAt, which the status row uses as its completion stamp and acknowledgement clock. It took the max over every journal row, and a session's subagents write into the same journal after its own agent has settled, so an idle parent was re-dated and marked unread on child work. A row now dates the session only when the session's own agent produced it: not a row whose producer linkage names a subagent, and not a subagent roster row (a subagent-group block), which the session writes but revises on every child transition. The roster rule is derived from the row body; no new persisted field. Replay folds through the same rule, so existing journals are re-dated to their own last row on reopen. Claude: a backgrounded subagent emits no child frames, so its re-dating came entirely from roster revisions (task_updated, task_notification) and from the stale-roster revision written when a journal reopens. Codex: the roster is revised on every child token-usage report; child-thread rows carry no producer linkage yet, and read as the session's own until they do. * fix(native-chat): a reopened journal's verdict on stale work does not date the session Reopening a journal settles rows the previous host left live (a working subagent roster, a live background task) to unverifiable. Those revisions were appended at the reopen moment, and a background-task row is the session's own non-roster row, so a crash-restarted session with a live shell was re-dated to the restart although no agent acted. The reconciler now writes each verdict revision with the row's own observed time. That is one rule at the one writer, covering both settle shapes; the render item's observedAt was already pinned to the row's first write, so nothing the transcript shows changes. The live-transition roster exclusion stays: live roster revisions are written by the providers, not here. The `recovered` row flag is not used as the discriminator: the live unexpected-exit settlement also writes recovered rows, and a clock rule keyed on it would stop dating a provider crash the host just observed. * fix(native-chat): date a session by the reducer's attribution of what a row wrote The clock read producer linkage off the raw row. A lifecycle batch names no row-level producer, a tombstone names none, and a revision may name none while the reducer still attributes the item to a subagent, so each of those dated an idle parent. The clock now asks the reducer: after a row applies, whether any item it wrote is the session's own work; before a removal, whether the item it removes was. * fix(native-chat): date a session's status by its own lifecycle edges A subagent writes into its parent's journal and keeps going after the parent settles. Every one of its rows advanced the summary's updatedAt, and the status row re-dated a done parent to it, so an idle parent read as newly finished and unread on each child step. The host now publishes statusStartedAt beside updatedAt: when the session's own agent entered its status, read off edges only it writes. Idle is when its newest turn ended; working is when the running turn was requested, or the earliest send still unanswered; attention is its own oldest pending ask, or a subagent's when that alone holds it. A turn that recovery settled after its host went away ended when that settle was written, so it reads as a completion the user has not seen; it carries no outcome, so no completion event or notification calls it a success. Render items carry recoveredAt, the recovered row's own write time, so nothing new is persisted. The sidebar bridge and the host ingest date the row and the main agent's clock from it whenever the row shows the main agent's own state, and keep their existing rules for a row child work holds open or a summary from an older host. The status feed republishes when the clock moves instead of on every idle row. * revert(native-chat): keep the journal clock over every row The row filter this branch put on the reducer's lastActivityAt decided which rows could date a session: a list of exclusions that each new row kind could slip past. The session's state is now dated by its own lifecycle edges, so the filter, its attribution helper and the backdated reopen verdicts go back to main. lastActivityAt, and the summary's updatedAt it feeds, is again the evidence clock over every row, including a subagent's. * fix(native-chat): keep republishing an idle session its live child work holds open A row held open by live child work is dated by when each reader saw the publish, and mobile decays a working row whose evidence is older than the staleness window. Suppressing row-activity republishes for every dated idle session froze that evidence, so a subagent running more than 30 minutes past its parent's turn made the row read idle on mobile. Only a session nothing holds open stays quiet on row activity now; its state clock is unchanged. * fix(activity): order an agent's timeline by when each state was seen An answered ask returns a settled parent to its own turn's end, so its done repeats the time of the done before the ask. Activity keyed and ordered events by that time: the new done collided with the old one and was dropped, and the row took its state from the newest-dated event, the blocked ask, so a done parent read Blocked and needed attention. Each state switch now records the `updatedAt` it was seen at. Events are keyed and ordered by that, while unread and "Clear completed" still compare the state's own time, so the answer neither re-lights unread nor revives a cleared done. The row's state comes from the pane's own status entry, so a clear that hid the done cannot leave it reading Blocked either. * test(activity): pin the timeline across repeated asks, a clear, and a stale turn Three parts of ordering the timeline by when each state was seen had no test that failed without them: - A second ask moves the answered done into history. Both dones share the turn's end, so only the history entry's own seen time keeps them apart; without it one done collided with the other and the timeline showed two Blocked events in a row. Three asks also exceed the per-pane cap, which must keep the most recently seen events, not the most recently started. - "Clear completed" on an answered row must cut off past the ask, which is dated after the done, or the cleared row stays listed. A done that the user cleared must also stay hidden once a later ask moves it into history. - A stale working row must not read as running just because the pane's own status says working. |
||
|
|
29480cd8cd | fix(skills): recognize symlinked provider skill roots (#22606) | ||
|
|
6ae6ed08bb |
fix(claude): open structured chat without a startup deadline, and make Retry start fresh (#22364)
* fix(claude): open structured chat without a startup deadline, and make Retry start fresh
Publish the Claude session as soon as its process is spawned instead of racing
initialize against a fixed 10s deadline. Prompts sent before startup lands are
held and written in order once it does. An exit or sign-in failure before startup
ends the session with the reason and the CLI's stderr.
A create that failed because the process provably exited now carries
ownerVerdict 'exited', so the client marks the launch failed and Retry mints a
new operation instead of replaying the stored failure.
* fix(native-chat): sending into a chat that failed to start restarts it
* fix(native-chat): a send with no live owner restarts it once
A provider child that timed out or exited hands its lease back, and every
later send was refused agent_session_ownership_unknown. Clients read that
code as "not admitted yet" and resend forever, while only a surface hold
could make a new child, once per mount, with its failure swallowed.
The send now routes to a live owner, otherwise restarts one from the
persisted resume state where resume eligibility allows it (single-flight
per session), otherwise refuses with the new settled
agent_session_owner_unrecoverable. Unverifiable, reserved and handed-off
leases are left alone. The desktop hold now logs its failure.
* test(native-chat): pin the unrecoverable refusal as settled in the outbox
* test(native-chat): pin the release clock after a send restarts an unheld owner
* test: read the sent operation id without a cast
* fix(native-chat): type the send-recovery record lookup as the store returns it
* fix(native-chat): a send ensures its owner before admission, and an unheld owner idles for 30 minutes
* fix(native-chat): a create that throws releases its event sink
A child that dies between spawn and journal attach can still write through
the host's event sink, which attach unbound in onAcquiring and never re-bound
because onAttached never ran. The orchestration released that sink only when
performAttach returned a refusal; a thrown failure (the root-exit path) kept
the sink cached with its queued write, so the next attach's drain barrier and
runtime shutdown's flush waited forever.
Also pins the publish-on-root-exit clause for a start that never proved:
deleting it reddened nothing before.
* fix(native-chat): a resend the journal answers restarts nothing, and a send joining a restart rebases from the fence it replaced
* fix(native-chat): the host learns a Claude start positively, and persists only proven options
A publish-first create used to read the session's options before Claude had
answered initialize. With startup pending that read fell back to the built-in
catalog's default, so `record.options.model` was persisted as `sonnet` for
every user whose CLI default is something else; an owner handoff or a reopen
then replayed `set_model('sonnet')` and silently switched their model.
The adapter now reports `started` once startup facts are applied and saved
options restored. The host keeps a `providerChildPhase` on the session it
owns: a starting child hands over nothing but the saved options as intent,
and the `started` event re-reads the options as fact and persists them through
the same record write a user's option change takes. The status summary carries
`hostExecutionPhase` (optional, wire-safe), and the chat pane says the agent is
still starting instead of showing nothing.
A child whose exit already reached the adapter before acquire returns is no
longer handed over as live; the create fails with the CLI's diagnostic.
* fix(native-chat): a hold and a send that find the owner gone share one restart, and a send the ledger already holds restarts nothing
* fix(native-chat): a failed create answers one refusal shape, stamped once at the boundary
A create whose Claude process was seen to exit answered twice in two shapes:
the first call threw a generic runtime error, and only the replay of the same
operation carried the `ownerVerdict: 'exited'` refusal that lets a client
retry under a new operation. Three sites stamped the verdict and the store
failure path stamped nothing.
The first-hand root exit is now returned as the refusal on the first call,
with the provider's own diagnostic as its message. The verdict is stamped in
one place, at the boundary of the attach, from the durable row the operation
settled to, so every refusal shape answers the same fact and no site can
forget it. The per-site stamps are gone.
* fix(native-chat): a send into a session whose child ended restarts it before admission
A session that published and then lost its Claude child before startup (not
signed in, for one) keeps a released lease and a chat the user can still type
into. The send was refused as ownership-unknown, the outbox parked it as
pending admission, and nothing ever restarted the child: the message sat
there until the user closed and reopened the tab.
A send reaching a session with no provider child now runs the same resume a
surface's first hold runs, before the write is admitted. The resume reserves
a new fence, so that send is answered stale with the published fence and the
client's outbox re-drives under it, as after any fence change. A resume that
fails is not this send's answer; admission reports the lease as it stands.
* chore: restore pnpm-lock.yaml to origin/main (local pnpm rewrote it)
* test(native-chat): pin the pre-handover exit as a failed acquire; stub the status feed in the delivery test
An exit the adapter observes before acquire returns now fails the acquire
with the CLI's diagnostic instead of handing over a dead child; the
published-then-ended path stays pinned by the slow-init startup case. The
delivery test renders the pane, which now activates the host status feed.
* test(native-chat): a same-ID re-hold over the wire joins the one resume, and a replay reopen goes on the idle clock
* test(native-chat): a re-hold that joins a failing resume proves one resume ran
* fix(native-chat): a create whose child was proven gone answers the refusal on the first call
The previous change answered a first-hand root exit as the exited refusal on the
first call, but the common failed start never took that path: when the close
ladder proves the whole tree dead the acquisition error is a plain one, the
store-failure classifier rethrows it, and the client still saw a runtime error
first and the refusal only on replay.
The cleanup that proves the child gone now names such a failure
`AgentSessionAcquisitionExitProvenError`, carrying the provider's diagnostic,
unless it already names its own verdict (a refusal, a typed exit proof, a host
store code). The attach answers both proven-exit kinds as the refusal its replay
gives. How a failed acquisition settles and how it is first answered now live
beside the verdict stamp, in the failed-create module.
* test(native-chat): pin the outbox re-driving a stale-refused send under the resumed fence
A send into a session whose child ended is answered stale once the host has
restarted the child. The outbox keeps that operation queued and blocked, and the
fence change the resume publishes re-drives the same operation under the new
fence; the host admits it.
* fix(native-chat): a child restarted for a send nobody holds is still released
The restart a send runs for a childless session takes no holder, on the premise
that the sending surface already holds one. A one-shot writer holds nothing, so
the child it restarted had no release clock and lived until the app quit. The
write resume now arms the clock when no holder is present, as the first-hold
resume already does. The send-after-failed-start cases also pin that the stale
answer's operation is admitted when re-sent under the new fence, and that two
racing sends restart the child once.
* test(native-chat): pin the picked Claude model across a resume whose child starts on its own default
The started event re-reads and persists what the child reports. A resumed child
answers initialize with its CLI default before the saved pick is restored over
it; the record must hold the pick while starting and after started.
* Revert "fix(native-chat): a child restarted for a send nobody holds is still released"
This reverts commit
|
||
|
|
f0b3f44f10 |
feat(agent-session): let the host own a chat's tab id and let a create reserve it (#22616)
* feat(agent-session): let the host own a chat's tab id and let a create reserve it A structured chat's tab id was derived from its session id by every layer that needed one: the renderer, the host snapshot and the status address each built their own spelling. The join between a conversation and the tab that shows it must be a pointer the host owns, not a derivation each client repeats. The session record now carries surfaceTabId. A create pins it: the tab half of the pane agent.launch reserved, an optional tabId on agentSession.create, or a host-minted UUID. Records written before the field existed are backfilled at open with the string clients derived, in memory at once and on disk with the store's first transaction, so nothing keyed by it (read state, notification ids, worker rows) moves on upgrade. A second record under a held id is refused. Only the record and the two create wires change here. The snapshot still publishes agent-session:<sid> and the renderer still derives its local id; those move in the next two changes. agentSession.create is a strict object, so the field is advertised as a capability a client checks before sending it. * fix(agent-session): record the derived tab id for an unreserved create A create that reserved no tab minted a random UUID that no reader uses: the renderer, status address, worker rows and host-shared read state all still key by structured-agent-session-<sid>. Persisted, that id would move every chat created before readers switch to the recorded one, orphaning its read state and worker rows the way the backfill exists to prevent. An unreserved create now records the derived id, the same rule the backfill applies, so the record always matches the prefix every existing key uses; an opaque mint belongs with the change that moves the last reader. Also: - a chat tab id must be a host tab id on the record, the create wire and in admission, matching what agent.launch already requires of paneKey; a web-surface id would decode as another tab - the stored launch-result guard checks the structured outcome's tabId - comments no longer claim a retry naming another tab conflicts; replay keys on the attach fingerprint and answers with the recorded id (now pinned) - the wire refusal test used a non-hex digest, so the schema refused it for that reason; it now reaches the tab id rule - pin that the reload path refills the id without forcing a save * test(agent-session): correct the tab-id fingerprint comment to match replay |
||
|
|
8f18930c48 | feat(native-chat): add a hover copy button to sent messages (#22721) | ||
|
|
6cda65fe5a |
fix(browser): import empty-value Chromium cookies instead of their domain hash (#22719)
Chromium cookie DB schema 24+ prefixes every decrypted value with
SHA-256(host_key). The strip heuristic required more than 32 bytes, so an
empty-value cookie (plaintext = the hash alone) kept the 32 hash bytes as its
value. Chromium rejected it on load ("Sanitizing cookie failed"), it was staged
for restart, and it was dropped from the jar after restart, after the import
had already cleared the matching existing cookie.
Strip when the prefix equals SHA-256(host_key) exactly, falling back to the
existing heuristic so browsers with any other prefix behave as before.
|
||
|
|
98584332a3 |
fix(native-chat): record which Codex agent produced each journal row (#22532)
* fix(journal): a batch revision restates the producer of each row it revises The reducer rebuilds a row's producer linkage from its NEWEST revision, and absence is a positive claim: no agent id means the session's own agent wrote the row. So any revision written without the stamp hands a subagent's row back to its parent, permanently. Three host paths revise rows they did not write, from the render item they already hold, and all three dropped the stamp: - answering a prompt re-appended the asker's row with the fence only; - dead-generation settlement failed running tool calls and cancelled pending prompts through a lifecycle batch; - stale-session settlement on acquire cancelled lost prompts the same way. The batch path could not carry a producer at all: linkage was removed from the batch row because one row covers N mutations, with a note that a mixed batch would have to stamp per mutation. Dead-generation settlement is such a batch already, and Codex settlement is about to become one. So each item mutation now names its own producer, inline like the row base. A mutation that names none falls back to the row's linkage, which is what a batch read before. Parse sanitizes a bad per-mutation id the same way it does a row's: the field is dropped and the mutation kept. No schema version bump. An older host's mutation validator ignores unknown keys, so it reads a stamped mutation as the session's own, which is exactly what it shows today. Old journals carry no stamp and read as before. Turn revisions still carry nothing: a turn is the session's unit of work, and the live-turn scans rely on a turn row never carrying linkage. The note recording that invariant is updated to the new write sites. * fix(native-chat): attribute a Codex subagent's journal rows to the subagent Codex journals every thread on its app-server connection into the session's journal, and a spawned subagent's items arrive on the child's own thread. None of those rows carried producer linkage, so under the journal's rule that absence means the session's own agent wrote a row, every child's command, message, reasoning, prompt and status row read as the PARENT's: the parent could show its child's running command, its child's reasoning as "thinking", and its child's prose as its own latest line. The Claude lane's model is reused, not reinvented: the same fields and the same absence rule. What differs is how the producer is known. Orca opens exactly one thread per app-server, so any other thread is one Codex spawned. That decides WHETHER a row is a child's from its first frame, announced or not, and the thread id is final at once: it is never re-minted the way a tool-call reference is, so no correction ledger is needed for identity. - agentId: the child thread id, the same id the status side keys a Codex child on. - parentAgentId: the thread whose stream carried the child's `started` activity. Codex emits that item on the spawning agent's own session, so a child that spawned a grandchild is named; the session's own thread is not. Other activity kinds ride whichever agent acted and are not used. - producerKind: 'agent'. - attempt: which run of the child the row's own turn was, counted from the child turns the roster already observes; absent on the first run. Taken from the row's turn rather than the child's latest, so a persistent shell that outlives its turn keeps its run across revisions. - providerParentRef is omitted: a Codex child's frames carry no parent reference of their own beyond the thread id, which is already agentId. One resolver, owned by the roster (which already owns what is known about each child thread), is handed to every writer: items, streams, generic and summary rows, prompts, compactions, goals, and the three settlement batches. The session-end settlement mixes every thread's rows in one batch, so each mutation names its own producer. Turn rows stay unstamped: Codex writes them only for the primary thread. The spawn-group roster row stays unstamped on purpose: a child's frame can trigger its write, but it is the parent's list of its children. The translator's construction moves to a parts module so the translator stays a router under the line cap, and the item streams reuse one append-and-publish helper instead of two copies. Children are never swept at turn end; nothing here changes that. * test(native-chat): pin Codex subagent attribution at every writer and every parent reader Two layers, so a stamp that is correct in the store and never read, or read and never persisted, cannot pass. The readers, through the real path: translator, deferred sink, on-disk journal, snapshot. Each is a defect on main: the parent named its child's running command as its own tool, read its child's reasoning as itself thinking, showed its child's compaction as its activity line, and quoted its child's prose as its latest line (checked after closing and reopening the journal, so the stamp is read back from disk). The transcript still renders the child's rows. The writers, through a sink that records the linkage of every plain append, batch mutation and lifecycle transition: start, streamed checkpoint and completion of one command all restate the child; a row that beats the spawn announcement is still the child's; a grandchild names the child that announced it, while an `interacted` activity names no parent; a follow-up turn is the child's second run, and a shell that outlives its turn keeps its own; the exit batch settles each thread's rows under its own producer and the turn row under none; a child's provider frames, approval and goal rows are its own; nothing is stamped while the session thread is still opening; and the spawn-group row stays the parent's. * test(native-chat): pin linkage forwarding on the sink's lifecycle-transition path A Codex child's goal row is written through a lifecycle transition, so a sink that forwarded only the fence there would file the child's goal as the session's own. * test(native-chat): type the Codex item fixtures as thread items * refactor(journal): keep a row's producer across revisions that name none The reducer took a row's producer linkage from its newest revision, so every writer that revised a row it did not write - a prompt answer, a dead-generation or stale-session settlement, the reopen sweep of stale subagent rosters - had to restate the producer or silently hand a subagent's row to the session's own agent. Three of those writers had been patched to restate it; the next one to forget would reintroduce the bug. Attribution is now fixed by a row's first write. A revision that names no producer keeps the row's existing linkage; one that names any replaces the whole bundle, which is how a provisional stamp is still corrected in place. A row re-created after a tombstone starts with nothing. The reducer runs the same fold on replay, so the kept producer survives a reopen. The three restatements are removed. Per-mutation linkage on lifecycle batches stays: a batch can create a row (a Codex child's prompt, or a child's item settled before any checkpoint landed) and one batch can mix producers. * test(journal): pin producer inheritance in the reducer and across a reopen A revision naming no producer keeps the row's, on the plain item path and in a batch settling a child's row beside the session's own; one naming any replaces the bundle wholesale; a tombstone clears it; a stale revision cannot touch it; and a reopened journal replays it exactly as it was folded live. * refactor(codex): name the translator's writer factory for what it builds * docs(codex): say why a settled row names its producer * test(journal): drop a producer test the stale-revision guards make unreachable The stale revision is dropped whole by two independent guards before the inheritance rule runs, so its producer assertion could never fail; the reducer's own stale-revision tests already cover the drop. Also say what the batch sink does forward: each mutation's own producer. |
||
|
|
5610b11703 |
feat(ipynb): create a .venv when pip is locked out, and show ipykernel setup progress (#22710)
* feat(ipynb): set up ipykernel in a new .venv when pip is locked out, and show install progress in the dialog * fix(ipynb): drop the retired installFailed string from translated catalogs * refactor(ipynb): drive the setup dialog from one setup state; fix review findings - Kernel status now only describes the kernel; a single `setup` object (base, offer, phase, error) drives the dialog, replacing the extra statuses and the externallyManaged/setupError fields. - The picker's 'Create virtual environment…' opens the same dialog; success switches through selectEnvironment, so a running kernel is only replaced once the venv exists. - Async setup results are dropped when the dialog they belong to is gone (tab closed/reopened). - main verifies ipykernel imports after pip, reuses an existing .venv instead of re-running venv over it, and explains failures that printed nothing. - Windows copy command guards the install with if ($?); notebooks at a filesystem root get a correct .venv parent; 'Try again' shows for both retry paths. * fix(ipynb): close the setup prompt when another Python is picked |
||
|
|
1985c1b11e |
fix(native-chat): load older messages automatically before the reader reaches the top (#22541)
* fix(native-chat): load older messages automatically before the reader reaches the top The transcript paged older history only on an upward scroll within 80px of the top, and only when the visible row count had changed since the last request, so readers hit a "Load earlier messages" button and waited. A page that landed only hidden rows left the count unchanged and stalled paging. A sentinel above the first row is now observed by an IntersectionObserver rooted at the transcript scroller with a 600px top margin. The observer is recreated each time a page settles, so it keeps paging while the sentinel stays in range and stops once a prepend pushes it out or history runs out. The virtualizer's existing prepend anchoring keeps the reader's row in place. Both lanes' load calls now reject when a page does not land (error result, rejected read, host reset, exhausted re-anchoring). A rejection stops automatic paging and shows the manual button; a successful retry resumes it. While automatic paging works, the top shows only a polite status line. * fix(native-chat): keep the reader in place when the last older page lands The older-history row sat in the transcript column above the window, so the final page, which takes that row away, removed its height and the column gap from above the window and every row jumped up by that much. The row now sits out of flow, absolutely positioned inside the scroller's existing top padding, so its presence never changes the content height or the window's scroll margin. The sentinel keeps the same place relative to the top of the transcript, so prefetch is unaffected, and the button stays in the padding band above the first row. * fix(native-chat): keep auto-loading older history when a page is abandoned mid-flight A reconnect snapshot, replacement, or hide ends the lane's loading while its read is still outstanding. The auto-load latch stayed held until that read settled, so the recreated observer's report was dropped and paging stalled with no button. Release the latch once the lane reports loading, and have the transcript lane resolve rather than reject when a superseded read fails, so a replaced connection cannot switch the current transcript to the retry button. * fix(native-chat): let each chat lane alone own the in-flight older page The older-history autoload kept its own "page in flight" latch beside each lane's guard. It could not tell a request the lane had not yet reported from one the lane abandoned before rendering it (a reconnect snapshot in the same batch), so a read that never settled could stall paging with no retry button. The transcript lane now dedupes synchronously on the epoch its page is in flight for, so an epoch bump abandons it with no extra reset path; the structured owner already dedupes synchronously. The autoload hook drops its latch and the effect that cleared it. * test(native-chat): pin the structured page result and paging generation reaching the list The structured session test still fed a rejection into loadEarlier, a contract neither lane has any more. Assert the resolved page result and the paging generation the list needs to stop and re-arm auto-loading. |
||
|
|
59d8275996 | fix(i18n): align ko artifacts search keyword with value override (#22711) | ||
|
|
c4e58fac56 |
fix(mobile): restart the streamed browser pane on every return to the app (#22694)
* fix(mobile): restart the streamed browser pane on every return to the app The browser pane stops its screencast when the app leaves the foreground and starts a new one when it comes back, keyed on an `appActive` boolean. When the leave and the return are handled in one React render (the JS thread was held across the whole trip, as a suspended or frozen app is), React applies false-then-true as no change, so the stream effect never re-runs: the old subscription is kept and no new one starts. If the desktop ended that subscription while the phone was away (it evicts a viewer whose socket refuses 90 frames in a row), the pane shows the last frame it had indefinitely, with no error and nothing that would restart it. The pane now keeps `foregroundVisit`: null while the app is away and a new id on each return. A batched leave-and-return still moves it to a new value, so every return starts a fresh stream, and the host's start snapshot repaints the pane. * refactor(mobile): drop a busy reset both stream-effect branches overwrite |
||
|
|
2c2414be57 | Update README downloads badge | ||
|
|
9f7f406b57 |
test(mobile): repin the RPC recording corpus and session closure after #22392 (#22702)
* test(mobile): repin the RPC recording corpus to main after #22392 #22392 pinned baseline to a branch commit ( |
||
|
|
5613c4fe71 |
fix(i18n): add missing translations for artifacts and browsing (#22697)
Adds translations for artifact publishing, remote browser features, SSH workspace routing, browser identity settings, and skills management across all supported languages (Spanish, French, Japanese, Korean, Chinese). |
||
|
|
80e0bee23b |
fix(floating-workspace): keep agent launches from moving the main window's tab (#22603)
* fix(floating-workspace): keep agent launches from moving the main window's tab
Launching an agent from the floating workspace's "+" menu switched the main
window off whatever chat or editor tab it was showing and onto its terminals.
The main window's selection is supposed to move only for the worktree it is
showing: browser and editor tab creation, splits, moves and drops all check
`activeWorktreeId === worktreeId` before touching it. Two places did not:
- `launchAgentInNewTab` called `setActiveTabType('terminal')` without a
worktree, which targets the active worktree whatever worktree the launch
landed in.
- terminal `createTab` wrote the global `activeTabId` for a tab in any
worktree.
Both now follow the store rule. The launch still selects its tab within its
own worktree, which is what the floating panel renders.
The floating titlebar button had side-stepped this with an `activate: false`
opt-out plus manual selection. That opt-out had no other caller and is removed;
the button now launches and focuses like every other entry point.
* fix(tabs): scope the remaining launch surface writes to the launch's worktree
Three more launch paths create a terminal tab and then call
`setActiveTabType('terminal')` without a worktree, which targets whatever
worktree is active when the call runs rather than the one the tab landed in:
- the paired-host agent launch, after the host's asynchronous create
- Session History resume, which can target a worktree the user is not viewing
and activates it only afterwards
- sleeping-agent resume, which the activation gate runs after asynchronous
readiness checks, by which time the user may have moved to another worktree
Each now names its worktree, like the local agent launch. The new tab still
lands selected when the user switches to that worktree.
* test(tabs): pin the paired-host launch scope in its existing web-runtime test
* test(tabs): type the left-worktree resume fixture instead of casting it
* refactor(tabs): require the worktree that setActiveTabType applies to
`setActiveTabType(type, worktreeId?)` quietly fell back to the active
worktree when the caller left the worktree out. A caller acting on a tab in
another worktree (the floating workspace, a background launch, a reveal that
lands after an async step) therefore retyped whatever the main window was
showing. The launch paths fixed earlier in this branch were instances of that;
54 other callers still relied on the fallback.
The worktree is now a required argument (nullable only for the no-active-
worktree case), so every caller states which worktree it means and a new
unscoped call fails to compile. Each call site passes the worktree of the tab
it acts on; where that is by construction the active worktree (shortcuts,
palette, tab strip), the result is unchanged. `activateTabAndFocusPane`
resolves the tab's owning worktree the same way `setActiveTab` does.
End-to-end helpers that drive the store directly pass the active worktree,
which keeps their previous behaviour.
* fix(floating-workspace): let the floating New Terminal activate its own tab
The floating "+" New Terminal created its tab with `activate: false` and then
selected it with `activateTab`, because creating an active tab used to write
the main window's selected tab even for another worktree. `createTab` now
activates a tab only within its own worktree's group unless that worktree is
the one on screen, so the workaround is no longer needed.
Creating the tab active also moves the floating workspace's remembered tab to
the new one; before, it stayed on the previously selected floating tab, which
auto-acknowledge reads to decide which floating agent the user is looking at.
* refactor(floating-workspace): route every floating New Terminal through one creator
The floating "+" New Terminal had stopped deferring activation, but Cmd+T with the
floating panel focused still went through a separate creator that created the tab
inactive and activated it by hand, which left the floating workspace's remembered tab
on the previous tab. Both now call createFloatingWorkspaceTerminalTab, which creates
the tab active in its own group and focuses it.
* docs(tabs): say why an unowned tab id keeps the on-screen worktree scope
|
||
|
|
060743d813 |
fix(mobile): keep the streamed browser pane flipping on slow phones, and stop double taps (#22392)
* fix(mobile): keep the streamed browser pane flipping on slow phones, and stop double taps
Frame pacing. The pane decodes each frame on a hidden layer and flips to it on
onLoad. While one frame decoded, every newer frame re-pointed that same hidden
layer, which cancels the in-flight load. On a phone that decodes a frame
slower than frames arrive (~10/s during page loads, menus, spinners), onLoad
never fired for any of them and the pane sat on an old frame until the page
went still. A decoding layer is now never re-pointed: only the newest frame is
held, and it takes the layer once the decode settles. A 1.5s watchdog frees a
layer whose decode never reports, and a frame the hidden layer already holds
(a blinking caret alternating two frames) flips at once, since an unchanged
source reloads nothing.
Double taps. When browser.mouseClick failed, the pane replayed the tap as
move/down/up. On a timeout the click is still queued on the host, so the
replay landed a second tap on whatever the first one opened. The replay now
runs only when the click definitely did not reach the host.
* test(mobile): re-record the corpus without the timed-out tap replay
The corpus certified the move/down/up replay after a transport-rejected
browser.mouseClick, which the commit before removes. Scoped like #22179:
baseline bumped by editing that one line, then --record.
788 files. Every changed line classified:
- `baseline`: 787 files (786 goldens + pilot-scenarios.json), nothing else.
- matrix-browser.pointer-click-browser.mouseclick-1.json: the
transport-rejection and transport-rejection-no-message partitions of
browser-pointer-click-fallback now send only browser.mouseClick#1. The
refused partitions still replay, unchanged.
* test(mobile): move the session closure pin past #22452's main-agent-status modules
#22452 changed only src/shared, so its CI never ran the page-closure suite; main
now measures 4220 modules (1034 local) against a pin of 4218. This branch adds
nothing to the closure: its own count matches main's.
* docs(mobile): say a re-pointed decoding layer loses its onLoad, as measured on Android
* refactor(mobile): give the streamed browser pane's double buffer one owner
The frame pacing, decode watchdog, layer flip and reset were spread over three
hooks and a helper module, wired back through the stream hook and the pane.
They now live in one plain pacer (browser-frame-pacer.ts) with one timer, and
the pane binds each layer's View/Image straight to it.
Behaviour fixed on the way, each with a failing test first:
- A slow last frame with nothing newer queued was abandoned by the watchdog and
never shown. The decode deadline now only applies when a newer frame waits.
- Any pane re-render re-pointed both layers at the newest frame behind the
pacer's back, so a blinking caret froze. The Image source prop is now only
the mount-time frame; every later source write is the pacer's.
- A frame that failed to decode left its layer marked as holding it, so an
identical frame flipped to an undecoded layer. Giving up on a decode now
clears the layer's source.
- A native onLoad for a source the layer has since moved off could flip early.
The flip now checks nativeEvent.source.uri, which Android and iOS Fabric both
report as the raw source string; RN Web's own load event has none, so the web
flips only through its decode probe.
The session closure pin drops by the two modules this removes.
* refactor(mobile): send the tap's mouseClick directly instead of through a flag
The delivery-unknown check was a mutable flag set inside the request callback.
The click now calls browser.mouseClick itself in a try/catch: a delivered click
returns, a delivery-unknown failure returns without replaying, and a refusal or
null result still replays as move/down/up. Same wire traffic; the corpus
certifies it unchanged.
* fix(mobile): never cut a streamed frame's decode short
The 1.5 s decode watchdog abandoned a slow decode whenever a newer frame was
queued and re-pointed its layer. On an Android emulator under load that is the
original freeze again: noise frames decode in 2-10 s, every abandon starts a
decode the next abandon cuts, Fresco reports the superseded loads (30 stale
onLoads in one run) and the pane showed 11 of 41 applied frames. Without it,
the same run flips every applied frame (16/16, no stale load), and a slow last
frame is shown in every cycle.
Nothing else needs it: with the layer never re-pointed mid-decode, native
answers every load with onLoad or onError, and the web probe's decode()
always settles. The pacer keeps one timer, for the interval.
* fix(mobile): track what each frame layer's Image holds, so no write goes unanswered
The pacer cleared a layer's source when it gave up on a decode (a reset
mid-decode, or a failed decode) while the native Image still held it. The
next identical frame was then written again, which is a native no-op on
Android (ReactImageView.setSource returns on equal sources) and iOS
(ImageShadowNode skips equal requests): no onLoad, no onError, and the pane
stayed frozen until the stream restarted. Returning from the background to
an unchanged page is enough to trigger it.
Each layer now records the source its Image holds and whether that source
has answered (loading, ready, failed). A layer is written only when it is
not loading and only with a different source, so every write gets exactly
one answer. A reset no longer abandons anything; the load under way still
answers for its layer. A frame the hidden layer already holds flips at once
if it decoded and is skipped if it failed.
The pane test's native model now treats a same-source write as a no-op and
answers each change once with the layer's current source; both new cases
(reset mid-decode then the same frame, failed decode then the same frame)
freeze on the previous head.
Also, per review: the pacer no longer touches busy or metadata. The stream
hook creates it and receives each frame as it goes on screen, so metadata
(and with it touch mapping) now follows the visible frame rather than one
still decoding.
* test(mobile): hold the pane test's AppState listener without a type assertion
* fix(mobile): never replay a tap the host answered
A fulfilled browser.mouseClick ran on the host, but a null result still
replayed it as move/down/up. The native bridge always answers { clicked },
while the external-Chromium provider returns agent-browser's `data` as is,
which can be null, so a right-click there was a double tap. Only a refusal
that is not delivery-unknown now replays.
* test(mobile): re-record the corpus for the answered-tap rule and rename its checkpoint
The commit before stops replaying a tap the host answered with a null
result. The seed's checkpoint was named clicked-by-fallback, which several
partitions no longer do, so it is renamed tap-settled in the same record.
Baseline bumped to
|
||
|
|
85642d0d88 |
fix(agent-status): count only agent work in stats, and read a Grok background subagent as working (#22474)
* fix(agent-status): renderer and recorder consumers read the question they mean Since #22295 a row's combined `state` reads `working` both while the lead's turn runs and while a subagent or background shell outlives a settled lead. The lead's own state now rides beside it (`lead`); each consumer in this slice reads the question it actually asks. - Smart sort and the Activity unread badge keep reading the combined state: their classes and rows are what the sidebar shows. Pinned with tests, including a restored `lead.state: 'working'` row that must never read live. - The stats recorder asks "was an agent executing" and now reads a shared derivation (`isAgentExecutionOwed`): the lead's turn, or live agent child work holding a settled lead's row open. A settled lead's background shell no longer accrues "Time agents worked". Old hosts without `lead` fall back to today's read; restored and replayed rows still never open a session. - The `agent.status.changed` plugin event gains `lead` as an optional field through one tested projection; `state` keeps its meaning and restored rows still project to nothing. - A Codex root Stop that follows an inferred interrupt keeps the `cancellation` verdict, as the Claude lane already does at its turn boundary, on both the hook and relay paths. * test(agent-status): pin that a child's approval wait no longer splits the recorded span The recorder's move to the lead fact quietly changed one more story: a Codex child's PermissionRequest turns the combined row waiting while the root's own turn keeps running. The old state read closed the span there and minted a second spawn on resume; the new read keeps one span, because the lead never stopped. Pin it at both boundaries (the shared derivation and the recorder) so the change is deliberate, not incidental. * fix(agent-status): date stats edges by this host's clocks and scope the accrual predicate to stats The recorder dated a start by the producer's mainAgent.stateStartedAt. An SSH host stamps that with its own clock while every stop is stamped locally, so each span gained or lost the clock skew. The same clock also survives a row that briefly lost the fact (an OSC repaint to another state), dating the reopen before the close already sent, and a subagent reopening a monitoring row took the row clock the hook lane pins to the main agent's turn start, re-billing the whole watch-loop window. Edges now use the row clock when the row settles or pauses and the evidence clock otherwise. Rename isAgentExecutionOwed to isAgentTimeAccruing and state that it is the stats question, not a liveness gate: it excludes watch loops, which lifecycle gates must keep treating as live. Note on the plugin schema that mainAgent.stateStartedAt is the execution host's clock. * fix(agent-status): pause agent time while the row waits on the user, whoever asked Time agents worked now accrues only while the combined row reads working. A child's approval or question wait pauses the clock exactly like the main agent's own prompt, and the pause edge is dated by the row's own clock. * refactor(agent-status): read agent time from the combined row and date edges by the row's own state Time agents worked now accrues while the combined row reads working and is not a watch loop. The shared fold emits monitoring only for a settled main agent, and hosts that predate the main agent fact did the same, so this is the same answer on every new-host row without reading mainAgent, and it applies the watch-loop rule to older hosts too instead of billing their monitoring windows. An edge that leaves working is dated by the row's state clock; an edge inside working is dated by the evidence clock. This also stops a live repeat of a hydrated working row (any row without the main agent fact, such as an OSC row) from dating its start at the persisted state clock from the earlier runtime. * fix(grok): read a background subagent as agent work, not a watch loop Grok's end-of-turn Stop lists each in-flight background task with its type (shell, monitor or subagent). Orca filed a running subagent with the shells, so a Grok subagent that outlived the main agent read "Monitoring background tasks" and, with the stats recorder now skipping watch loops, stopped the "Time agents worked" clock. Map shell and subagent entries to the shared child-work kinds and let the shared liveness classifier decide: any live subagent keeps the pane working, a shell alone or an active stop hook stays monitoring, monitors stay excluded. * test(agent-status): cover a waiting child in the fold's every-input accrual check Since the shared fold learned a child's human wait, a waiting child makes the row wait, so it must not accrue agent time whatever the main agent is doing. The exhaustive check now includes that input. |
||
|
|
122b8c25d7 |
fix(terminal): let Linux IMEs keep the candidate key for a preedit they own (#22607)
* fix(terminal): let Linux IMEs keep the candidate key for a preedit they own Sogou on fcitx draws its preedit and candidate list in its own window and opens no Chromium composition session, so every composition-scoped candidate guard is idle when the user presses Space or a candidate digit. The selector reached the PTY as literal text and the commit arrived after it. The marker the input framework does still deliver is the claimed letter keydown - `keyCode 229` / `key 'Process'` over the physical key. A bounded window armed from that claims the next Space or digit, and stands down for a real composition session, for the commit, and on expiry. Co-authored-by: nxiaobai <48705519+nxiaobai@users.noreply.github.com> Co-authored-by: Big-ios <18501866+Big-ios@users.noreply.github.com> Co-authored-by: cporoske <14369468+cporoske@users.noreply.github.com> * fix(terminal): keep the claimed-preedit window open while the IME edits it The window released on any keydown that was not a letter the IME had claimed, but the IME claims the keys that edit and page a preedit too — Backspace, the arrows, `-`/`=` for the next candidate page — and Chromium keeps the original `code` on those 229/Process keydowns. Paging to a later candidate therefore disarmed the window and the digit that picked from that page reached the PTY. A claimed keydown now refreshes the deadline instead of clearing it. Arming still requires a claimed letter, so a bare navigation key with no preedit behind it cannot open the window from cold. * fix(terminal): release the claimed-preedit window when a composition ends An engine that does run a composition session still emits the claimed `keyCode 229` letter keydowns that arm this window, and both its commit and its cancel travel as `insertCompositionText` — the one input type the commit release deliberately ignores. Nothing else closed the window, so a cancelled preedit left it open and the next literal Space was swallowed. Refreshing the deadline on every claimed keydown had widened that tail. `compositionend` now releases it, as `compositionstart` already did. The engines this guard exists for emit neither event, so their path is unchanged. The selector suite drives the state object directly and cannot see a missing listener, so the release wiring gets its own DOM-level suite. * test(terminal): pin a Space-committing composition session end to end The regression the compositionend release fixes needs the whole session to show up: compositionstart fires once, so every letter after the first re-arms the window, the commit and the cancel both travel as insertCompositionText, and the committing Space arrives claimed rather than bare. Only the end of the session closes the window before the user's next literal Space. Fails with the compositionend listener removed. * fix(terminal): end the claimed-preedit window on the selector the IME took A claimed selector means the engine picked the candidate itself, so the round is over. It reads `key: 'Process'`, which the selector predicate cannot see, so this asks the physical code instead. Belt and braces for an engine that opens a composition session and never closes it — a documented defect in at least one shipped input framework. Without a `compositionend` the session-end release never runs, and the refresh added for candidate paging would otherwise hold the window open across the commit and swallow the user's next literal Space. Paging keys still refresh, so a long candidate browse is unaffected. * fix(terminal): let a claimed Enter or Escape end the pick too Japanese and Zhuyin commit with Enter, not Space, and Escape cancels. Both arrived claimed and fell into the refresh branch, so for an engine that opens a composition session and never closes it they held the window across the commit — the same defect the Space case was written to close, reached by a different key. Arrows, `-`/`=`, Backspace and PageUp/PageDown still refresh, because those page the candidate list rather than ending the round. Physical codes rather than `key`: `key` reads `Process` on all of them, and `Digit*` survives AZERTY, Dvorak and Colemak moving the digit row. Also corrects the window comment, which still described the orphan-keyup picking window. Since claimed keys refresh it, it is now a stuck-state valve that only expires when the IME stops reporting. --------- Co-authored-by: nxiaobai <48705519+nxiaobai@users.noreply.github.com> Co-authored-by: Big-ios <18501866+Big-ios@users.noreply.github.com> Co-authored-by: cporoske <14369468+cporoske@users.noreply.github.com> |
||
|
|
ad6cb0e05c |
fix(worktrees): version every catalog publication so a stale listing cannot undo a create (#22507)
* fix(worktrees): version every catalog publication so a stale listing cannot undo a create
A worktree listing is a snapshot from when its scan began. The renderer treated any
authoritative listing that lacked a known worktree as proof of deletion, judged at apply
time against the live store, so a listing delayed past a create reply purged the new
workspace: tabs wiped, selection cleared to the landing, pending structured launch
tombstoned so the host session was closed the moment it published. #22311 re-runs a scan a
mutation overtakes, which covers a bump during the scan but not a reply that is simply
applied late, on the host or in the renderer, or a refresh that joined an older one.
The host now stamps every listing with the catalog version its scan began at (the existing
per-repo scan generation, scoped by a per-process epoch) and every create and remove reply
with the version the mutation produced. Clients keep the newest version applied per repo
and host; a listing older than that is not applied at all, not its rows, not its purge, not
the pre-merge terminal teardown. Coalesced joiners inherit the reply and therefore the rule.
Fields are optional on the wire; older hosts and clients keep today's behavior.
* fix(worktrees): relist after a refused stale listing and stop version churn
- A refused listing can be the only answer a caller gets (a change-event
refresh that joined an older in-flight listing), so fetchWorktrees lists
once more; that listing scans at or past the applied version.
- An equal catalog version keeps the held object, so a no-op listing no
longer writes store state on every refresh.
- Versions the client cannot order are treated as unstamped at ingest.
- worktree.rm takes the repo from its id selector instead of resolving the
worktree a second time, which also stamped nothing for an id two hosts share.
- A removal on one of two hosts sharing a worktree id records its version.
* test(worktrees): pin the scan-generation bump right after git worktree add on every create path
A listing is stamped with the generation its scan began at, so a create must
advance it before any post-add work can yield. Pins the local desktop, SSH and
runtime local create paths.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(worktrees): bump the scan generation right after git worktree remove
A listing is stamped with the generation its scan began at. Removals bumped it
only at the end, after watcher release, push-target cleanup and the metadata
purge, so a listing scanned before the git removal and one scanned after it
could carry the same sequence. Applied out of order, the older one restored the
removed row until the removal reply. Bump right after the git removal on the
desktop local, desktop SSH and runtime paths, as creates already do.
The ordering pins now witness the generation at the first step after the git
mutation rather than at the re-list, so moving a bump past any intervening
await fails them.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(runtime): stop leftover worker-recovery retries from firing into later tests
The legacy worker terminal recovery retry timer re-arms itself and had no
way to end, so a runtime from one aggregator test kept rescanning repo-1
through the shared listing mock during later tests, consuming the listing a
lineage create expected ("Worktree created but not found in listing").
Give the controller a dispose() that cancels pending retries and refuses to
re-arm, and have the runtime test harness dispose every controller it
constructed after each test.
* Revert "fix(runtime): stop leftover worker-recovery retries from firing into later tests"
This reverts commit
|
||
|
|
25d7c21fcb |
feat(native-chat): show context window usage in the composer (#22301)
* refactor(native-chat): move the composer's stop action into its own hook The composer is at its line budget; lifting the stop action out makes room for the context usage ring without changing what Stop does. * feat(native-chat): record the Claude CLI's context window facts on the structured journal A structured Claude session now keeps what the CLI says about its context window on journal rows, so every client reads the same answer and a restart replays it: - Each main-thread assistant response keeps its API usage. A subagent's response measures its own window, so it carries none. - The turn a result settles records the session's window: the largest contextWindow across the result's per-model usage, since side calls to a smaller model report their own smaller window. - After a result and after a compaction boundary the host asks the CLI for its /context breakdown (5s bound) and records the answer on the current or last turn. An answer is dropped when the main conversation moved, a send was accepted, a newer request was issued, or the session was released while it was in flight; a failure or an older CLI leaves the row unchanged. - A compaction boundary or conversation reset records that the used count is unknown until the next response or report, so the pre-compaction size is never shown as current. Every part carries its own host clock, and the reader takes the newest, since a revised turn row keeps its place in the transcript. The persisted validator admits every value the writer can write, including a zero auto-compact threshold: a row replay rejects truncates the journal from that row. * feat(native-chat): show context window usage in the composer A structured Claude chat shows a ring beside send once the journal can state the session's context usage. Hovering shows used/window with a bar and, when the CLI has reported its breakdown, one row per CLI category as a share of the window, largest first. Between reports the ring shows the newest response's usage against the newest window the CLI reported, marked as estimated. Before the CLI has reported any window, and after a compaction or reset until the next response, there is no ring. A terminal-backed chat shows none. * fix(native-chat): measure the context ring against the main thread's model window The result's per-model usage is cumulative across the session and includes subagents and side calls, so the largest window was often not the one the main conversation runs in: after switching from a 1M model to a 200k one, or when a subagent ran on a larger-window model, the ring read against the wrong window after every turn. Pick the entry named by the model that served the newest main-thread response, and among its [1m]/non-[1m] entries the one the result moved; fall back to the largest only when nothing names it. * fix(native-chat): keep the context ring moving through tool-only responses The live estimate lived on assistant message rows, and a response with only tool calls or thinking writes no message row, so the ring froze through long tool loops and stayed hidden after a mid-turn auto-compaction until the next text reply. Record every main-thread response's usage on its turn row instead, once per response, so the selector sees each one. * fix(native-chat): read the ring's window from the model the turn's init names The CLI keys per-model usage by the main loop's model string, [1m] included, and every turn's system/init frame carries that exact string, while a response drops the suffix. Match the init's key first, so a session that switched between the 1M and 200k variants of one model reads the right window; fall back to the newest response's model, then the largest entry. * fix(native-chat): correct the composer's control-order note for the context ring * fix(native-chat): keep a running turn's context facts when the host settles it A turn row now carries the live context estimate while it runs. When the host settles a running row itself (a crashed or stale generation, a close the translator never saw), it rebuilt the record field by field and dropped those facts, so after a crash the ring fell back to an older turn's size, or to a pre-compaction size the dropped reset had superseded. * refactor(native-chat): revise Claude turn rows from the journal so the ring survives a restart The context ring's facts were written to turn rows through an in-memory list of recent turns. A new translator is built on every acquisition, so after a restart or reattach that list was empty and every fact for a turn that was not open was dropped: a /compact as the first action after a restart never cleared the ring and never showed the fresh breakdown. Every Claude turn-row write is now a revision of the row as the bound journal holds it when the write runs. The sink gains a resolved revision that reads the target row and its body at execution; the queue runs one operation at a time, so the read-modify-write cannot interleave, and revisions are never coalesced. Lifecycle writes own the lifecycle fields and context writes own contextUsage; each keeps every other field. Only the open turn is kept in memory. A fact with no open turn lands on the newest turn row, and a report lands on the turn it was requested for. The persisted facts are simplified to a window, which now names the model it was measured for, and a single used part (report, estimate or unknown) that each write replaces. The ring reads the newest turn row carrying each part, and hides an estimate whose model the window was not measured for instead of dividing by another model's window. A turn opening, and a reset, count as activity, so a late report can never land behind a newer turn. Host settlement of a stale running turn now drops only the fields its verdict owns, so context facts and any field a newer build wrote survive it. * fix(native-chat): keep a turn row whose context facts this build cannot read Context facts are validated deeply, so one malformed or future-shaped fact made the whole turn row malformed, and replay truncates the journal from that row on. Replay now drops unreadable facts from a turn row, in item rows and in settlement batches, and keeps the row, the same way it already drops producer linkage it cannot trust. The ring shows nothing for that turn instead of the session losing its history. * test(native-chat): pin that a child exit mid-turn keeps the ring's last size A lifecycle-only revision, the end a turn gets when its child exits without a result, must keep the context facts the row already carries. * perf(native-chat): revise a named Claude turn row by key instead of scanning the journal Every Claude turn-row write walked every reduced journal item to find its row, even when it already knew the row's identity, so a long session paid O(items) per write on the main process. The journal now answers a keyed read, and a context report names its turn by row identity rather than turn id, so only a write made while no turn is open still scans. * fix(native-chat): tell a 1M window from a 200k one of the same model Responses drop the [1m] suffix, so after a switch between the 1M and 200k windows of one model the running turn was measured against the previous turn's window until its result arrived. An estimate now records the turn's init model, which keys the window exactly, and the reader requires the full model id to match. * fix(native-chat): show no ring for a context kind a newer host writes A paired client reads turn rows from the host unvalidated, so a used-count kind this build does not know fell through to the estimate branch and threw reading its missing usage. Only the kinds this build can measure now produce a ring. * fix(native-chat): keep the context ring through plan-mode turns on another model Plan mode can run a turn on a model the turn's init does not name (opusplan upgrades to Opus's 1M window). The estimate then carried only the response's id, which drops [1m], and the exact comparison against the window hid the ring for every plan-mode turn. The estimate now records the response's id beside the init's exact key, and the reader matches the base model only when no exact key was recorded. * refactor(native-chat): pair the context ring's window by model change, not by model id The ring divided the newest response's size by the newest window only when their model ids matched, which meant comparing ids from the init frame, the response, per-model usage keys and canonical ids. Those disagree in plan mode and across 1M and 200k windows of one model. The writer now knows when the model may have changed: after a model or permission-mode write that changes the value, when a restore cannot put the stored model back, and when a main-thread response comes from a different model than the one the window serves (an approved plan). It then marks the size unknown, holds estimates, and asks the CLI for its context report, which states the new model's window. Any new window, from a report or a turn result, releases the hold. The reader compares nothing: a report, or the newest estimate over the newest window. Turn rows no longer store window.model, window.canonicalModel, estimate.model or estimate.responseModel. * fix(native-chat): keep a late context report's window when only its count went stale * test(native-chat): pin that each turn's init lets its result restate the context window * fix(native-chat): open the context card on click and tap * fix(native-chat): wait a beat before a mouse hover opens the context card * fix(native-chat): publish each context write in the operation that makes it A context report answers after the turn's last frame, so a revision that waited for the next frame's publish reached live clients only on the next turn. Context writes now queue their revision and its publication as one operation. * fix(native-chat): write million-token counts with a capital M A lowercase m read as minutes on the context card. * fix(native-chat): keep the context card open while the pointer crosses into it The card closed the moment a mouse left the ring, so the pointer could not cross the gap into the card. Leaving now waits a beat, and entering the card cancels the close. * fix(native-chat): let Escape close the context card without stopping the agent The card keeps focus in the composer, so the Escape that closed it also reached the composer and interrupted the running turn. The composer now skips an Escape an open layer already handled. * fix(native-chat): show the context ring when the chat has not loaded the turn it belongs to The ring read context facts only from the rows the chat had loaded, so a reopened chat whose recent page started after the last turn row, or a live turn longer than the retained window, showed no ring until the next turn. The host now derives the newest context facts from its whole journal with the same selector the chat uses, and returns them on agentSession.options for sessions that write them. The chat prefers each fact its loaded rows carry and takes the host answer for a fact they lack. When a live batch revises a turn row older than the loaded window, the chat asks for options again so that answer stays current. * fix(native-chat): bound context refresh reads and refresh when the turn row is trimmed Each turn-row revision the loaded window missed started its own options read. Those reads share the session's host queue with sends and interrupts, and each asks the CLI for its settings, so a burst could pile reads in front of a user action and discard every answer before it landed. The chat now keeps one options read in flight and at most one behind it. A live turn longer than the retained window also lost its turn row to the trim without asking for a fresh host answer, so the ring fell back to the answer read at turn start until the next response. Trimming a turn row now asks again, like a dropped revision does. * fix(native-chat): show the context ring from the first response, sized from the session's model A new session has no measured window until its first result, so the ring stayed hidden for the whole first turn. The host now keeps the window the applied model's name implies (1M for a [1m] name, unknown for default, 200k otherwise) and writes it beside an estimate when the journal holds no window, or after a model write, until the result or the CLI's report replaces it. * fix(native-chat): imply a context window only from a [1m] model name A bare model name does not fix the window: first-party runs today's opus, sonnet and fable models natively at 1M while a gateway or cloud provider runs them at 200k, and opusplan and haiku run another model in plan mode. Sizing their first response at 200k read the ring about five times too full, so only a [1m] name implies a window now; any other name waits for the result. * fix(native-chat): size the first response from a report taken before any turn A model picked in a chat with no turn yet asks the CLI for its context report, but with no turn row the report's write lands nowhere. Recording it still marked the journal as holding a window, so the first response wrote none and the ring stayed hidden until the turn's result. The report's window now serves as the fallback a response writes while the journal holds no window, and recording a report no longer assumes its write landed. * test(native-chat): move the fake Claude connection out of the structured integration suite The context-report delivery case pushed the suite past the 800-line limit, failing repo-wide lint. The fake child now lives in its own fixture. |
||
|
|
8ddad49ae5 |
fix(opencode): retire a subagent's blocker when the root turn ends (#22604)
A descendant's question is stored under its own sourceSessionID but displayed on the root session it rolls up to. clearAttentionForSession matches only the source, so the root's own idle could never retire it: the blocker outlived the turn that raised it and pinned the pane on "needs input" with nothing left to answer. The lead agent truthfully reports no pending question, because the blocker belongs to a child it does not know about (#22371). Add clearAttentionForTurnEnd, matching the source id or the rolled-up root, and use it only from the idle branch. Scoped to turn end deliberately: a live blocker must still outrank the root going Busy, since a subagent can be waiting on the user while the root keeps working — widening the existing clear broke exactly that test, which is what surfaced the right scope. Shared with the OpenCode family, so mimo-code gets the same fix; both pinned plugin digests move. New test fails without the change: after the root turn ends the last post is still AskUserQuestion instead of SessionIdle. |
||
|
|
b2940a18ff |
fix(file-explorer): toggle folder instantly on name click (#22325)
* fix(file-explorer): toggle folder instantly on name click (#22324) Single-clicking a folder name held back directory expansion for the 500ms double-click window (PR #10867), which reads as latency compared to the chevron and editors like VS Code or Zed. Toggle immediately on the first click (0ms) and skip the second click of a double-click rename so the folder does not flip back under the rename input. Delete the 500ms timer machinery. Fixes #22324 * Fix double-toggle of symlink directories during rename Treat symlinks the same as directories when skipping toggle on rename. Symlink rows remain file-shaped in the UI until activated, so checking only isDirectory would cause a stat and toggle during the rename flow. --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> |
||
|
|
5ba9649058 |
test(terminal): pin Option-composed currency on a punctuation key (#22608)
#21082 widened the kitty composed-text check from ASCII to every printable codepoint, which is what restored French Option+$ -> €. Nothing pinned a composed character on a punctuation code: the Polish cases all sit on `Key*` codes, whose unmodified character the US fallback table also answers, so they pass even when the layout map is the only source of `characterWithoutOption`. Reverting the widening fails this case at kitty flags 1, 3, 5 and 7. Co-authored-by: rasata <93783+rasata@users.noreply.github.com> |
||
|
|
ca75bc4c8d |
fix(orchestration): type a request ahead of pasted dispatch briefs so Claude workers follow them (#22582)
* fix(orchestration): type a request ahead of pasted dispatch briefs so Claude workers follow them Claude Code wraps a bracketed paste in <pasted_content> and tells the model to follow instructions inside it only where the user's own message asks. Orca sent the whole dispatch brief as a bare paste, so Claude workers (Opus 5.5, Sonnet 5) refused it as suspected prompt injection. Every dispatch path now types a short lead line in the same PTY write as the paste frame, the preamble drops shouted rules, and dispatch detection accepts the lead line and pasted_content wrapper. Fixes STA-8200 * refactor(orchestration): tidy dispatch lead-line delivery after review - Share one dispatchPreambleSendOptions() across the four dispatch paths. - Fold every C0 control and DEL out of the typed lead line. - Bound the <pasted_content> tag scan and let compaction return null for non-dispatch prompts, removing the separate detector. - Restore the stay-off-other-channels rule in plain wording. - Test through the real status normalizer and trim duplicated assertions. Refs STA-8200 * test(orchestration): guard coordinator auto-dispatch lead line - Capture send options in the coordinator runtime fake and assert the auto-dispatch send uses dispatchPreambleSendOptions. - Drop the helper test that only restated its literal. - Share DispatchPreambleSendOptions with the coordinator runtime contract. Refs STA-8200 * docs(orchestration): fit the pasted-spec note inside the kernel line budget Refs STA-8200 * docs(orchestration): drop the pasted-spec note from the coordinator guide The typed lead line is the fix; the advisory note cost always-loaded context. Refs STA-8200 * fix(orchestration): type the dispatch lead line only for Claude agents Codex discards typed text that shares a PTY write with a bracketed paste, so the lead line never reached it. Only Claude Code needs the lead to follow a pasted brief, so known non-Claude agents now get the pre-lead bytes and unidentified agents keep the lead in case they are Claude. Refs STA-8200 |
||
|
|
420fcb3e77 | Update README downloads badge | ||
|
|
b4d732685c |
feat(agent-status): combine Codex child work through the shared main-agent status fold (#22475)
* feat(agent-status): combine Codex child work through the shared main-agent status fold * docs(agent-status): correct two comments the waiting child-work arm made stale A child failure reported in place as `blocked` now pins the row `waiting`, not `working`; and no relay ever sent an unfolded `working` beside a waiting child. * fix(agent-status): only a waiting child asks for a human A child's `blocked` state means its task failed (the only producer maps a failed background task to it, and the background-task view labels it "failed"), not that a human must act. Folding it into the waiting arm would surface a failed child as needs-you. It stays live work, as before this series. * docs(agent-status): say a waiting child, not a blocked one, makes the row wait A child's blocked state means it failed; only its waiting state feeds the waiting arm. Two fold comments, a test describe and two parity story names still called the waiting child blocked. * docs(agent-status): name where a child's wait is still lost, and pin the structured lane's real input The doc said the Claude hook lane's rows match Codex and that every lane feeds a child's wait into the fold. Neither holds: Claude keeps the wait in one slot the next main agent event overwrites, the structured lane turns a child's prompt into the main agent's own attention, and Codex drops its roster on a root Stop when it tracks no child transcripts. The parity story now drives the structured lane with the input it actually receives. |
||
|
|
7a4f080086 |
revert: #18790 (orchestration incarnation reap fallback and bundled Freebuff agent) (#22601)
This reverts commit
|
||
|
|
3ea15dd0a2 |
fix(native-chat): keep chats that failed to resume in the status bar and say what to do (#22448)
* fix(native-chat): keep chats that failed to resume in the status bar and say what to do After a restart, a chat whose resume did not carry on was reported only by a four-second toast that named nothing, and the status bar entry vanished because the reattach had already spent the offer. The host now files the outcome as a durable `failed` entry in the recovery capsule, with the refusal code and the prompt the offer quoted, and returns it from the restart-resume RPCs. The renderer shows a "N chats failed to resume" status bar entry, a count-only toast with Show and Dismiss, and keeps the resume dialog open with a status icon per row and a "To resume" line whose action is chosen from the reason. A failure dies on dismiss, on a successful retry, on the user's own send in that chat, or with the marker's 24h expiry. * fix(native-chat): keep the resume dialog unchanged and add failed chats as rows The failure view had replaced the resume dialog's title, checkboxes, preference box, and footer. The dialog is back to main's layout. A chat an earlier resume could not carry on is now an ordinary selectable row there, plus a status icon whose tooltip carries the reason, a dismiss control, and a "To resume" line. Selecting it and pressing Resume retries it; it is pre-selected only when a retry can succeed. Resumed chats leave the list as before. Also stubs the new failure listing on the cross-version wire host fixture, which the restart-resume RPC now reaches. * refactor(native-chat): release a failed-resume record where a send is admitted Keeps the host file at main's size, and only releases the record for a send the controller actually lets through. * fix(native-chat): note a failed restart resume in the chat and derive when it is settled The chat itself now says when Orca could not continue it after a restart, with an error (refused) or warning (unconfirmed) status row, so the failure survives the toast, a dismissed record, and another restart. A recorded failure is current only while the chat's newest user message is the one it had when the failure was filed. Listing re-derives that from the journal and prunes superseded records, replacing the in-memory set and the hook on every send. Failures move to their own optional top-level key in the recovery file, so an older build that rejects unknown entry states keeps reading its offers. A retried failure stays a failure through a rollback or a lapsed lease instead of returning as a pending offer, and the toast's Dismiss names only the chats the host listed as failed. Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): check the older reader against a filed failure before any retry Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): file a failed restart resume against the chat as its attempt ended A failure was filed against the chat's newest user message read at settlement, after every chat in the action had finished. The chat's note asks the user to send a message, and one sent while other chats were still being continued became part of the filed state, so the failure stayed listed after the user had done what it asked. Each chat's newest message is now observed as its own attempt ends. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): let a reply that made a chat ineligible retire its failure When a restart resume reached a chat the user had already replied in, the attempt was refused as no longer eligible, but the failure was filed against that very reply. It then stayed listed as "finished on its own" until the user sent yet another message. An ineligible chat is no longer observed at the attempt, so its failure falls back to the reserved marker and the reply that made it ineligible supersedes it at the next listing. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): offer Retry on a failed resume only when a retry would run When the agent refused Orca's "continue" message with its own reason, the failed row fell to the generic guidance, which offers Retry and pre-selects the chat in the resume dialog. The refused message is already the chat's newest user message, so a retry is never eligible: it did nothing and the same "couldn't be resumed" toast came back. The host now reports whether a retry would run, derived at list time from the same predicate the retry applies to the failure's marker. Where it would not, the row offers Open chat and Dismiss and is not pre-selected. An older host omits the flag and the reason alone decides, as before. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): file only restart failures the user must act on A chat that stopped being resumable between listing and acting (it finished on its own or is waiting on the user) was filed as a failure with no note in the chat. It now just spends the offer. A failed reattach now writes the same in-chat note as a refused continuation, so every filed failure explains itself in the chat. An unconfirmed continuation's failure retires once the chat shows the continuation's own message opened the newest turn. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): clear a failed resume from the status bar once its chat moves on While a failed resume is listed, the restart store watches the host's status feed; when a failed chat's status or latest prompt changes after the list was read, it re-reads the host once. The host still decides whether the failure stands. Nothing is watched while nothing failed. The failure toast now counts only the requested chats the host still lists as failed, keeping the old count for a host that sends no list. Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): pin that an unjournaled continuation never retires its unconfirmed failure Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): stop telling the user to send a message into a chat Orca couldn't reconnect A failed reattach, or a continuation refused because another window or terminal owns the session, now leaves a note saying Orca couldn't reconnect the chat instead of advising a send that would meet the same refusal. The restart list keeps the reason-specific advice. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): keep the failed-chat re-read from undoing an action or missing a reply The status-bar re-read no longer runs while a resume or dismiss is in flight, and its answer is dropped if one settled meanwhile, so a dismissed failure cannot come back. A change to a failed chat already seen always triggers it, whatever the host's timestamp says. A chat whose unconfirmed continuation the host already retired is now reported as resumed instead of saying nothing. Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): pin that no failed-chat re-read runs under a resume in flight Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): typecheck the unjournaled-continuation case against a nullable marker Co-Authored-By: Claude <noreply@anthropic.com> * docs(native-chat): drop the stale "no arguments" note on the restart-offer params The dismiss call now names sessions, so the older comment contradicted the schema below it. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): count unconfirmed resumes apart from refused ones in the action toast The post-action toast said "N chats couldn't be resumed" for chats whose continuation may well have gone out, while the list and the chat itself say Orca couldn't confirm it. That wording invites a duplicate "continue" send. Unconfirmed chats now get their own count, classified by the outcome the host filed, so the toast matches the row it points to. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): stop offering Resume on a failure the host says cannot retry A failed row the host marks unretryable could still be ticked, sending a resume that could only fail again; its checkbox is now disabled and it never joins the action. An older host that omits the flag keeps today's selectable row. The status bar no longer calls a chat "failed to resume" when the host only couldn't confirm the resume, matching the dialog's own wording, and the mixed toast's second line now says "other" so it cannot read as the same chat. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): skip unreadable failure records instead of rejecting the capsule A failure record this build cannot parse (a newer outcome, say) made the whole recovery file unreadable, so a downgraded build listed no restart offers and could not record new teardowns. Failures are advisory: parse each one on its own and drop what does not parse. Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): pin that a failure record with an unreadable marker is skipped The skip-unreadable-failure test only covered an unknown outcome, so going back to the throwing marker parser for failure records still passed. A failure record usually outlives its offer entry, so a newer marker shape can appear only there. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
8ba7f829ac |
feat(ipynb): run notebook cells in a persistent Jupyter kernel (#22581)
* feat(ipynb): run notebook cells in a persistent Jupyter kernel Replaces the fresh-process runner (which silently re-ran every earlier cell) with the user's own ipykernel, driven by a small bundled Python bridge over line-framed JSON. One kernel per open notebook: started on first Run, shut down when its tab closes or Orca exits (stdin EOF), and the kernel's own parent poller reaps it if the bridge dies. The header gains a kernel pill (workspace .venv/.conda recommended, PATH interpreters, Browse), Interrupt/Restart/Run all/Clear all, and a one-time missing-ipykernel dialog with Install. Outputs stream live per cell and are written into the document when the run finishes. * test(ipynb): cover the stalled-interrupt restart offer * refactor(ipynb): disable the kernel pill while settling; merge its classes with cn * refactor(ipynb): tie kernels to their renderer document and simplify the run flow - Main keys kernels per renderer, so a reload, renderer crash or closed window shuts them down, and two windows never share one notebook kernel. One close-driven cleanup replaces the separate exit and start-failure deletions. - The first run uses the nearest workspace env, else the first Python on PATH; the picker no longer opens itself, so its open state stays in the toolbar. Closing the picker brings the missing-ipykernel dialog back instead of dropping the queue, which also keeps a Browse pick's run. - Discovery marks the kernel starting, so a second run during it queues instead of starting a second kernel, and a tab closed mid-discovery no longer leaks one. - Running an nbformat 4.4 notebook gives its cells ids (upgrading to 4.5), so moving a cell mid-run cannot misroute its output. - The death notice drops stderr from before the kernel was ready (the unencrypted-TCP warning). - SSH and non-Python runs toast instead of writing a notice into the cell. - Windows conda envs are named after their folder. * fix(ipynb): install ipykernel into envs without pip uv-created venvs ship without pip, so Install failed with 'No module named pip' there. When pip is missing, bootstrap it with the stdlib's ensurepip and retry. The install moves beside the other interpreter probes, and the copyable install command comes from one helper. * fix(ipynb): address PR review comments on stream errors, old jupyter_client and the Windows venv hint - Swallow stdout/stderr stream errors on the bridge child, as spawnProcess requires, so a broken pipe cannot crash main. - The bridge exits (reporting the death) even when cleanup_resources is missing (jupyter_client < 6.1.5) or raises. - The install-failure hint suggests `py -m venv .venv` on Windows. * feat(ipynb): add Cancel to the missing-ipykernel dialog It does what Esc does: drops the cells waiting on the kernel. * fix(ipynb): recover from a rejected kernel start; quote the install command per shell - Discovery moves into start, so one catch turns a rejected listPythonEnvironments or startKernel into the usual failed start: the session returns to off with the error in the cell, instead of sticking at starting. - The copyable ipykernel command quotes the interpreter only when its path has whitespace, prefixing PowerShell's call operator on Windows. Install itself still spawns without a shell. * fix(ipynb): always shell-quote the copyable ipykernel install command Quote the interpreter path for every path, not only ones with whitespace, so paths with shell metacharacters like & copy as a working command. Single quotes are literal in POSIX shells and PowerShell; embedded quotes are escaped per shell, and PowerShell keeps its & call operator. |
||
|
|
128e97ffca |
fix(relay): drain a same-cap cell over 5 minutes, not 2 (#22584)
* fix(relay): drain a same-cap cell over 5 minutes, not 2 The 2026-09-23 c27 roll drained 2,145 hosts over the 2-minute window, about 18 re-dials/s, while the director re-places roughly 8/s through its single-slot sticky lane. The overflow queued behind slow re-placements and timed out, so /v1/assign returned 503 fleet-wide for about 5 minutes. 5 minutes is the cell's maximum pace window and keeps the remaining 2,650-host cells near lane capacity. The transition wait already outlasts a 5-minute window (17 min). Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010 * test(relay): pin the same-cap drain window contract at 5 minutes Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010 * fix(relay): keep the drain wait at lease plus the 5-minute window The transition wait after a drain was set to the 15-minute migration lease plus the pace window. Widening the window to 5 minutes without moving the wait left 12 minutes for a migration that can hold for 15, so a late-window migration would time the wave out into rollback. Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010 |
||
|
|
795b64b9a6 |
docs(tui-agent-config): correct the OpenCode readiness-budget rationale (#22593)
The comment merged with #22546 claimed ConPTY never forwards DECSET 2004 and that the signal therefore cannot fire on Windows. Verification on two real Windows hosts refuted that: the sequence arrives in order on both ConPTY backends, and the readiness signal fired in every run. The budget was the actual problem — opencode does not enable bracketed paste until ~4.8s and its composer is not ready until ~10s, so the 8s default expired first and the draft was pasted blind. Same fix, accurate reason. Note the claim that seeded this: terminal-agent-paste-bracketing.ts says 2004 "can be lost by remote replay or ConPTY", which is careful and not contradicted here; the absolutism was mine. |
||
|
|
5e3effc32f |
fix(native-chat): show every user message on the message rail, not just loaded ones (#22558)
* fix(native-chat): show every user message on the message rail, not just loaded ones The message rail was built only from transcript rows the renderer had loaded, so any prompt above the loaded page had no tick, and the rail lost ticks when a long live session trimmed its retained window. The host now answers `agentSession.conversationOutline`: every user message in a structured session's journal (item id, creation sequence, a preview cut to 200 characters, image count) plus the journal position it is current through. It is derived from the reduced journal on each request with the same projection the transcript runs, so an entry's id and preview are what the loaded row shows. The reply is bounded like a history page: previews shorten, then drop, and only then do the oldest entries go. The renderer asks only while the pane is visible and older history is unloaded, uses outline entries only for messages older than its loaded window (the window is authoritative for the rest), and falls back to loaded messages while the outline is stale (epoch change, or the window trimmed past what it covers). Selecting a tick with no row pages older history in until the row exists, then uses the existing rail jump. The method is negotiated with `agent-session.conversation-outline.v1`; a client never calls a host that does not advertise it, and any failure leaves the rail on loaded messages. * fix(native-chat): keep a rail jump from the bottom from re-arming follow and cancelling itself A rail jump started by a reader following the end stopped a few pixels above the bottom instead of reaching the message. Paging older history in for a jump always leaves the reader following at the very end, so jumps to unloaded messages hit it every time; a jump to a loaded message from the end did too. The jump scrolls smoothly, and only its landing is marked as the application's own scroll. Its first frames move a pixel or two, still inside the band where a reader event re-arms follow, so the list read the jump leaving the end as the reader arriving at it. The next frame, just outside the band, then read as the reader taking over and rebased the view with an instant write, which cancels the smooth scroll. Re-arming follow now needs the reader to be arriving at the end: an unmarked event that moved the view up never reattaches a detached reader. * test(native-chat): check a rail jump left the end before reading where it landed * fix(native-chat): keep the rail's message list still while a press selects an item With the whole conversation in the rail, its hover list overflows and opens scrolled to the message being read. Clicking an older item did nothing: pressing it focuses it, which turns the hover preview interactive, and that switch re-ran the effect that scrolls the lit row into view and focuses it. The list moved under the pointer between press and release, so the click landed on the list instead of the item, and focus jumped to the lit row. Revealing the lit row now follows the list opening (and its rows shifting), not the switch between hover and interactive. Entering interactive moves focus into the list only when focus is not already on one of its items. * perf(native-chat): keep the rail's outline entries stable while a trimmed window slides A long live session holds a head-trimmed window, so every new row moved the oldest-loaded edge and rebuilt the outline view even when no user message crossed it. The rail then re-merged, re-rendered and re-read the scroll geometry on each new row. The view is now reused while the set of entries older than the edge is unchanged. * fix(native-chat): let a rail jump wait out an older page already loading Scrolling to the top of the loaded window asks for the next older page. A rail jump made while that page was in flight asked again, got the lane's immediate no-op return, read it as a page with no progress, and dropped the click. The jump now waits for the in-flight page to land before deciding. * fix(native-chat): reattach follow when content shrinking clamps a reader onto the end The rule that stops a smooth scroll leaving the end from re-arming follow compared offsets, so it also refused a reader whose offset dropped because settled content folded away beneath them and the browser clamped them onto the end. They sat at the bottom without following, and the next reply grew out of view. Re-arming now requires closing on the end rather than moving down, which still rejects a scroll leaving it. * fix(native-chat): keep the rail hooks' ref writes out of render Both hooks wrote a ref while rendering, which React may replay or discard. The rail's structural-sharing baseline is now recorded after commit, and the history jump calls the lane's page loader from its effect instead of through a render-updated ref. * fix(native-chat): let the latest rail pick win over a jump still paging A jump to an unloaded message keeps paging older history until it lands. A pick made meanwhile lost to it: a loaded message scrolled into view, then the earlier jump finished and pulled the reader away; another unloaded message was ignored. Picking a loaded message now cancels the paging jump, and picking an unloaded one retargets it without asking for a second page. * fix(native-chat): step a rail history jump with a functional update The step that requests the next page wrote the pending jump from the value its effect closed over, so a pick or cancel queued since that commit would be overwritten. * refactor(native-chat): run a rail history jump as one abortable awaited loop The jump through unloaded history was an effect-driven state machine that guessed "no progress" from the message list's identity and could not be cancelled by anything but another rail pick. A diff reveal, "Jump to latest" or the reader scrolling left it paging, and when its page landed it pulled the reader away; a history read that kept failing during a live turn could repeat back to back. Loading an older page now reports how it ended, and a second request while a page is in flight joins it instead of being refused. The jump is an awaited loop that reads the rail from a commit after each page, stops on anything but a page that moved the window, and is aborted by any other navigation, reader input (wheel, touch, scroll keys, scrollbar), a session switch or unmount. The latest pick wins. * fix(native-chat): derive the rail outline from the transcript's own projection The host built the outline from user items alone, while the transcript orders every message by when it was observed, folds tool results into the turn above and then drops harness turns. An imported user row carrying a tool result beside harness text therefore got a rail tick previewing the harness text, and clicking it paged history for a row that never draws. The transcript's order-fold-strip projection now lives in one shared function. The renderer's list projection wraps it with its own tail-row order, and the host runs it over the whole journal and keeps the user rows that draw content, so the outline lists the same messages in the same order. * fix(native-chat): retry a failed rail outline read a few times A failed outline read left the rail on loaded messages until a new gap opened, the pane was shown again or the epoch changed. The client now rejects a failed read (a host without the outline still resolves to nothing, without being called), and the rail retries up to three times with doubling backoff. * test(native-chat): give the rendered-transcript fixture the older-page result contract * perf(native-chat): sort the shared transcript projection without a spread copy * fix(native-chat): let a wheel over the rail cancel a rail jump still paging The rail forwards its wheel to the transcript, so a reader scrolling there is scrolling the transcript. That wheel never reached the scroller's reader-input handlers, so the jump kept paging and later pulled the reader to its target. * fix(native-chat): keep the rail's list open while a picked message pages in Picking a message that is not loaded yet can take several pages of older history. The list closed on the pick, so its busy item was never seen and the click looked ignored. The list now stays open with that item pulsing until the jump lands or is abandoned, and stops revealing the lit row meanwhile so the rows do not move under the pointer. * fix(native-chat): keep the shared transcript projection loadable on mobile The projection moved to src/shared, which mobile's Hermes engine also loads, and switching its sort to toSorted broke the Hermes compatibility guard. Sort a copy made with Array.from instead, as other shared code does. |
||
|
|
5802b54579 |
fix(rate-limits): read OpenCode Go usage with the Go API key (#22551)
* fix(rate-limits): read OpenCode Go usage with the account API key Since OpenCode's console migration (upstream fe51b0b19a, "fix(console): restrict legacy access to Black"), an account with no Black subscription is redirected from the legacy console to /console/login, so Orca's cookie-based workspace lookup returns nothing and the Go bar stays empty. Fetch usage from GET https://opencode.ai/zen/go/v1/usage instead, which authenticates with `Authorization: Bearer <key>` and needs no console session. The key resolves in order: Orca settings override, OPENCODE_API_KEY, then whatever OpenCode itself stored on /connect -- auth.json for 1.x, the credential table for 2.x. The cookie path stays as the fallback so Black/legacy accounts keep working. A 403 EntitlementError now reads as "no OpenCode Go subscription" in the status bar instead of a generic refresh failure (#22257's reporter was misled by exactly that). * fix(rate-limits): prefer OpenCode's stored Go key over OPENCODE_API_KEY OpenCode applies the key saved on /connect after the environment, so the stored key is the one its own Go requests use. OPENCODE_API_KEY is also the Zen provider's variable, so ranking it first could read a key that OpenCode itself is not using for Go. Co-Authored-By: Claude <noreply@anthropic.com> * fix(rate-limits): name the API key when OpenCode Go usage lands on sign-in A redirected usage request arrives as a 200 sign-in page because Electron follows redirects; report it as a rejected key instead of a parse failure. The cookie path's empty workspace lookup is what non-Black accounts now hit after the console migration, so its message points at the API key rather than only the workspace override. Co-Authored-By: Claude <noreply@anthropic.com> * chore(i18n): add the OpenCode Go API key strings to the English catalog Co-Authored-By: Claude <noreply@anthropic.com> * docs(rate-limits): stop calling the credential table an OpenCode 2 marker Verified on two real Windows hosts running OpenCode 1.18.16: the `credential` table exists there too (empty, same columns), so its presence does not identify a 2.x install. Neither host had an `auth.json` at all. The resolution already probes both stores on every version, so only the comments were wrong. Says so now, and records that a 2.x install which never ran the legacy import has no `auth.json` either — which is why both tiers exist. * refactor(shared): move GhosttyImportPreview out of global-settings-types Adding `opencodeGoApiKey` pushed global-settings-types.ts one line past the 300-line ceiling, failing `oxlint` in CI. AGENTS.md forbids a max-lines suppression, so split instead: the Ghostty import preview is a distinct concern that never belonged in the settings-shape file. Re-exported from the original module so no importer changes. 293 code lines now. --------- Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
57bf732a42 |
test(opencode): cover the opencode2 host-env branch and stop inheriting ORCA_OPENCODE_AGENT (#22547)
* test(opencode): pin per-major OpenCode overlay selection on WSL and the relay OpenCode 2 dies with "Duplicate plugin ID" when two plugin files share an id, so which overlay a guest or remote pane is pointed at decides whether the agent starts. Nothing failed if that selection regressed: - the WSL spawn path never asserted which major it asks the guest relay for, and the shared pty-ipc mock had no openCode2HookService at all, so no test could reach the opencode2 branch of buildPtyHostEnv; - requestGuestOpenCodeOverlayDir had no coverage for the v2 guest dir; - PluginOverlayManager had no case for a remote config root that still holds the other major's stale Orca plugin. Tests only; no behavior change. Each new case was mutation-checked against the production line it guards. * test(opencode): stop the plugin contract test inheriting ORCA_OPENCODE_AGENT The generated plugin self-disables when ORCA_OPENCODE_AGENT names a different major, and the contract test saved and restored that variable without ever setting it. Run from a shell that has it — which is any shell inside an Orca OpenCode pane, i.e. how this repo is usually developed — the plugin returned an empty hook set and the contract failed for the wrong reason. Delete it in beforeEach, the way the opencode2 setup test already pins it. Verified the file passes with the variable set to either major and unset; before this it failed for two of the three. |
||
|
|
b0ae7d18a0 |
fix(opencode2): resolve subagent session lineage so child work stops taking over the pane (#22444)
OpenCode 2's plugin adapter unwraps a single-property `{ data }` success schema,
so `ctx.session.get` resolves to the bare session record. The shared lineage
lookup only accepts `result?.data?.id === sessionID`, and OpenCode 2 has no
`session.list` fallback, so `resolveRootSessionID` returned null for every
session and `childState` was permanently null.
With unknown lineage `canFailOpen` is true for attention events, so a subagent's
`permission.asked`/`question.asked` fell through and pinned an un-evictable
blocker keyed to the child's own session id — publishing a subagent as if it
were a root. Observed in hook posts: SessionBusy for a child session id whose
`session_v2` row carries a parent.
Envelope the result in the OC2 client shim so the shared lineage module works
unchanged; OpenCode 1 already receives enveloped results and is untouched.
Also adds `opencode2` to the double-Escape interrupt list, extracted into one
shared helper so the server inference and renderer gate cannot drift. A single
Escape was inferring an interrupt, and Escape is how the Subagents dock closes.
7 of 11 new lineage tests fail without the shim.
|
||
|
|
c4dfd9deef |
fix(claude): resume a native chat from its real latest message (#22395)
* fix(claude): resume a native chat from its real latest message Claude's last-prompt marker names the chain tip, which is often a stop-hook summary or attachment row that --resume-session-at rejects. The branch proof now resolves the marker to the latest main-chain message, and a plain resume re-derives its point from the transcript instead of trusting the stored cursor, resuming by session id alone when the transcript cannot vouch for one. An acquisition release now reads the transcript tail like close and exit do. Co-Authored-By: Claude <noreply@anthropic.com> * fix(claude): advance the durable resume point at every turn end A completed turn now writes the live main-chain message uuid onto the owner's head provider-handle link in place, so a host that dies before its close path runs still resumes from its last completed turn and the chain does not grow per turn. The write is serialized per session, only logged on failure, and close and exit persist after it settles. Co-Authored-By: Claude <noreply@anthropic.com> * fix(claude): retry a failed turn-end resume write and ignore results after exit A turn-end write that failed was never retried when the next turn ended at the same point, because the memo of the last attempted leaf outlived the failure. A result frame delivered after the child's exit could also start a write that landed behind the exit path's transcript-derived cursor, moving the durable point backwards. The failed leaf is now forgotten so the next turn end retries it, and a turn-end write only runs while the session is still the published live owner. The session-id fallback on a plain reopen and a failed durable write on the unexpected-exit path now log why, instead of leaving no trail. * fix(claude): resume a native chat by session id and stop predicting Claude's marker A plain reopen now passes only the session id, so Claude continues from the real end of its own conversation. Orca's saved leaf is its own record of the last completed turn, taken from the live stream at each turn end. It is bookkeeping (the reconciliation anchor), never a resume argument. - Launch resolution resumes by id and checks only the session id; the prior head leaf is carried into the publication link. - Remove the launch-time transcript re-derivation. - Close, unexpected exit, and acquisition release no longer read the transcript. They wait for any in-flight turn-end write, then persist the last completed turn, so a crash mid-turn never saves a half-turn prompt. - The delivery-reconciliation window walks from the file's last main-chain transcript row to the anchor instead of Claude's lagging marker, so a prompt Claude saved just before a crash reconciles as accepted. - Revert the transcript branch graph to main; the terminal handoff readers keep their semantics. - Report Claude rewind as unsupported. Its marker-based proof can never pass on the real binary, and no app screen calls it. Remove the Claude rewind launch, proof, and recovery path. A pending Claude rewind left by an older build is settled as refused on the next attach, which resumes by id; a failed settlement is logged and never blocks the chat. * fix(claude): drop the stale resume-cursor wording from the restart-resume note The restart path resumes Claude by session id alone now; the module comment still described the old resume-at cursor. * refactor(claude): prove the file-tail resume tip inside the branch graph The reconciliation readers proved the transcript tip by re-parsing every line and feeding a synthetic last-prompt row through the graph, doubling parse cost on every reopen and coupling the tail path to the marker's JSON shape. The graph now takes tip: 'file-tail' and tracks the last main-chain row from its own parse; marker mode is unchanged and the no-eligible-tail fallback keeps the exact marker semantics. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
b7a4fee700 |
fix(agents): stop claiming an unconfirmed OpenCode handoff succeeded (#22546)
* fix(opencode): stop claiming a handoff prompt was delivered when it was written blind "Continue in New Session…" to OpenCode reported success even when the prompt never reached the TUI (#22479). The paste-after-ready helper falls back to a blind write when the composer-ready signal never arrives and only the agent process is known to exist; that write was indistinguishable from a real delivery, so the continuation showed its success toast. - pasteDraftWhenAgentReady / pasteDraftToAgentPtyWhenReady report the blind fallback via onUnconfirmedDelivery, plumbed to launchAgentInNewTab as onPromptDeliveryUnconfirmed. - The session continuation hedges instead of claiming success, and both the failure and the hedged notice offer "Copy prompt". - OpenCode gets Codex's 20s composer budget. Both are quiet-window-less signals anchored on DECSET 2004, which ConPTY never forwards, so on Windows that budget is the settle delay before the blind paste. * test(runtime): retarget the 8s startup budget test off OpenCode The main-runtime startup-draft budget test used `opencode` as its stand-in for "an agent without an override", which this branch invalidates by giving OpenCode 20s. It failed with "expected vi.fn() to not be called at all, but actually been called 1 times" — the readiness signal now legitimately arrives inside budget. Point it at `claude`, which still takes the 8s default, and add a companion pinning OpenCode's 20s: a readiness signal at t+10s, past the old default, must now deliver the draft. Removing the override makes that companion fail. |
||
|
|
4064653740 | refactor(browser): remove the unused screenshot-prep visibility helper (#22526) | ||
|
|
f2ac9f29b2 |
fix(browser): let pixel capture hold its own page drawn, without the desktop window (#22534)
* fix(browser): let pixel capture hold its own page drawn, without the desktop window Screenshots were the last browser commands that still borrowed the desktop window: they took the per-page automation-visibility lease, which waits for two desktop-window animation frames (capped at 2 s) and never arrives when the window is minimized or throttled. Only pixel capture actually needs a page drawn — input, scripts, layout, the accessibility tree and PDF all work on a hidden page. Capture now takes a main-owned paint hold: a synchronous, per-page ref-count that tells the renderer one way (no reply awaited) to keep the page drawn and keeps the desktop renderer unthrottled while held. Both Orca's full-page capture and the agent-browser helper's screenshots take it in cdp-screenshot.ts and retry on a bounded schedule until the page answers with a frame; a CDP error fails fast. Deleted: the queue's needsPaint lease, the executeJavaScript acquire path and its two racing 2 s timeouts and late-token cleanup, the renderer's rAF wait and window bridge, the capture commands' own leases, the fixed 300/500 ms settle waits, and the global one-screenshot-at-a-time lock. Rebased onto main after #22528 landed; content identical to the reviewed branch head 7b390ed6a8. * fix(browser): probe for a frame instead of repeating the full capture Retrying a capture resent the caller's full request, so on an already drawn tall page (a full-page capture takes ~0.5 s) the 250 ms retry started a second full beyond-viewport capture while the first was still running. Measured on Electron 43: any later request makes a held page produce a frame, and that frame answers every pending capture with a full, correct image. So the capture is sent once and 1x1 probes follow until it answers; their results are ignored. Also report a detached debugger as detached rather than destroyed, and give the layout-metrics timeout its own "did not respond" message, since that request doesn't need a drawn page. |
||
|
|
8352752e54 |
fix(opencode): give the opencode2 status plugin a distinct id (#22544)
* fix(opencode): fail-open plugin setup and distinct opencode2 plugin id setup() threw when OpenCode 2 probed it without a full context, which the TUI reported as an 'orca-opencode-status' plugin failure. Both plugin files also shared one id while living in the same config dir. * fix(opencode): drop unused oxlint-disable in setup fail-open test * test(opencode): pin distinct plugin ids for the shared global config dir Orca installs both family plugins into one global plugins dir, so a shared plugin id makes OpenCode 2 fail the later one with 'Duplicate plugin ID'. |
||
|
|
80f5aae0f9 |
feat(agent-status): publish the main agent's own state beside the combined row state (#22452)
* feat(agent-status): publish the lead agent's own state beside the combined row state
Every status producer folded the main agent's state together with live child
work into one `state`, so a lead that had finished while a subagent still ran
read `working` and its own state was lost. The row now also carries
`lead: { state, outcome?, stateStartedAt }`, admitted by the one payload
normalizer on the relay wire, IPC and disk, and published from the Claude hook
lane, the structured host ingest and renderer bridge, Grok (now on the shared
fold) and Codex (own combine kept). The persisted child-only boundary flag is
derived from `lead` plus child evidence and no longer written; old rows map
onto `lead` at hydrate. Combined `state` and `workingMode` are unchanged for
every reader; a cross-lane parity table pins that, with the cancelled-turn
watch-loop story recorded as a known divergence.
* fix(agent-status): make Orca's inferred interrupt the primary source of a Claude lead cancellation
Current Claude Code sends no hook at all on a cancel and no is_interrupt on
Stop, so the cancellation enters the lead record from the server's inferred
interrupt and rides into the next real Stop; is_interrupt on a turn boundary
stays as the secondary source for builds that send it. Comments, the store
reference and the parity table say so; no suppression changes.
* docs(agent-status): the child-only boundary comment now describes the persisted shell fact
The old sentence said a hydrated row no longer carries the shell fact, which is
the opposite of the mechanism: claudeRunningNonAgentTask is persisted precisely
so hydration can read it, and only a pre-lead row lacks it — reading as
shell-free, the same assertion its legacy flag made at write time.
* rename the lead fact to mainAgent: the main agent's own state
* docs(agent-status): the inferred cancel comes from Ctrl+C, not Esc
* fix(agent-status): an inferred interrupt keeps an already settled main agent, and the row verdict docs name its inferred source
* fix(agent-status): a child-induced wait publishes the main agent state it displaced
* fix(agent-status): decide child-held Claude rows from the saved main agent fact
Restart seeds the Claude main agent from the row's saved mainAgent whenever it
settled and no shell held the row, instead of re-deriving a child-only shape.
OSC cannot settle or repaint a row child agents hold open, including a row
waiting on a child's permission prompt. A sticky child permission prompt still
records the main agent's own progress, and OSC repaints and inferred answers
keep the shell fact beside the main agent they preserve.
* fix(agent-status): keep a finished turn's main agent verdict and clock with that turn
A Claude SessionStart restarts the main agent's clock instead of inheriting the
previous session's last Stop. A Grok idle prompt or session end, and a late
Codex root Stop after an inferred cancel, restate the same finished turn, so
they keep its recorded verdict; only a new turn clears it.
* test(agent-status): publish the Grok verdict restatement past the late-event window
* docs(agent-status): describe hydrate seeding and the OSC refusal from the saved main agent fact
* fix(agent-status): push a held child permission row when its main agent changes
* fix(agent-status): keep the shell fact on a held child permission row so restart does not settle it
* docs(agent-status): note the held child permission row carries the shell fact and is pushed
* fix(agent-status): pair the Claude shell fact with the main agent at the one row-build point
Every non-hook rewrite (terminal-title repaint, inferred answer, held child
permission) had to re-carry the shell fact beside `mainAgent`, and each one that
forgot let a restart settle a row while a shell still ran. The row builder now
pairs the fact once: a listener event restates it, any other write keeps it only
while `mainAgent` is unchanged. Restart seeds a settled main agent only when the
row says no shell ran, and legacy child-only rows map to that explicitly.
A held child permission now also accepts the main agent event's background
evidence, as it already accepts its `mainAgent`, so the child's drain no longer
settles a row a shell still holds. The renderer keeps a previous `mainAgent`
only for writers that never carry one, so a hook row without it matches the
host snapshot.
* test(agent-status): pin that restart never seeds a main agent from a row silent about its shell
* docs(agent-status): the row builder pairs the shell fact with the main agent, and restart seeds only on an explicit no-shell
* test(agent-status): name the legacy-row case parameter for what it holds
* docs(agent-status): name which rows carry the main agent fact
|
||
|
|
7216d5af25 |
test(runtime): isolate per-repo worktree scan expiry from the shared timer queue (#22575)
The TTL case was advancing every fake timer, so a scan an earlier test had left scheduled was counted as this repo's rescan. |
||
|
|
d4386763d5 |
fix(opencode-usage): merge a migrated session's two rows per column (#22550)
A session that lived through OpenCode 2's V1 import has a row in both `session` and `session_v2`, and neither is complete. #22391 resolved the pair by ranking whole rows on one number — total token count, ties to `session_v2` — which let that number decide everything else on the row. Three consequences, each reproduced against that PR's own fixtures: - A recorded cost could be zeroed. `session_v2` wins on tokens while carrying `cost = 0`, and row parsing maps a zero cost to `null`, so a legacy row's $12.50 disappeared. Cost is re-derived by the same lossy reduce as the tokens, but only the tokens were guarded. - The token comparison decided metadata. A legacy row with more tokens supplied a stale pre-migration directory, and a legacy row with a NULL model erased the model `session_v2` had — 23 of 234 shared ids on a real migrated database have a model only on the v2 side. - Winner-takes-all is per row, so a legacy row holding the input tokens and a v2 row holding the cache reads reported one of them as zero. Metadata now comes from the generation OpenCode still writes, with older generations filling only its NULLs; usage columns take a per-column MAX. Both rows aggregate the same assistant messages, and the import can only drop messages, never invent them, so each column's MAX is a tighter lower bound on the truth than either row and can never exceed it. The relation stays exactly one row per id — the highest-priority generation holding it — every column stays `columnExists`-guarded with a SQL fallback, and a database with a single session table builds the same SQL it did before. Cache schema version 4 -> 5 so existing caches rescan. |
||
|
|
800d33e5c9 |
feat: name runtime machines (#22094)
* feat: name runtime machines
* fix: preserve pairing address optionality
* fix(cli): keep host and environment listings local
Listing paired servers read each one's machine name by dialing it, so both listings made a network
round trip per server and waited out a timeout on any that were offline. They answer from this
machine's own pairing store; `orca host name --environment <name>` reads one server's name.
* fix(settings): caption the machine name paired devices actually receive
The caption read the runtime's published name once, when the pane opened, so saving an override
left it naming the old computer while phones already showed the new one. It now re-reads whenever
the saved override changes; the settings write lands in the main process before the store publishes
it, so that read already sees the new name. The name is interpolated rather than baked into the
fallback, and the caption, label and placeholder are in the English catalog.
* refactor(settings): normalize the machine name in one place
The trim and length rules for `machineName` were spelled out separately at the
renderer IPC (trim + 255), the settings load path (trim only, no cap), the RPC
schema (zod trim + 255) and the runtime reader (trim). A hand-edited or legacy
profile could therefore load a longer name than any writer accepts.
`src/shared/machine-name.ts` now owns `MACHINE_NAME_MAX_LENGTH` and
`normalizeMachineName`, and every writer and the load path use it. The RPC
schema keeps rejecting over-long names but derives its cap from the constant,
and the runtime settings controller normalizes an RPC write before storing it.
* fix(runtime): detect the machine name once and label handoffs with it
Every runtime constructed in a process (the app, plus each one a test builds)
ran its own `scutil` lookup. The friendly name is a property of the host, so the
lookup is now a single shared promise; construction still never blocks on it,
and a rejected lookup can no longer surface as an unhandled rejection.
The structured-chat handoff banner ("Agent is open in terminal on X") named this
host with the bare `os.hostname()` while paired devices saw the published name.
The transport now reads the same `RuntimeMachineName`, through a getter so a
rename in Settings is reflected without rebuilding the transport.
* fix(cli): print the name the runtime publishes and keep its envelope
`orca host name --name X` printed `undefined`: `settings.update` replies with
`{ settings }`, but the handler read a bare `machineName` off the reply, and the
test fixture mirrored the wrong shape so it passed. After a write the command
now re-reads `status.get` and prints what the runtime publishes, so a blank
`--name` prints the detected name it returned to rather than an empty string.
The read path wrapped a possibly routed answer in a local envelope, stamping
`_meta.runtimeId: "local"` on a reply from another server. It now returns the
`status.get` envelope itself, and an unreachable runtime is reported as the
usual error instead of an invented "unknown" name.
`environment list` had gained machine-name and platform columns that no caller
populated, so every row printed "platform unknown"; the columns are removed.
* refactor(settings): give the machine name field its own component
The caption under the field re-read runtime status every time the saved value
changed, relying on a comment about write ordering to show the new name. A saved
override already is what paired devices see, so the hook now derives the caption
from it and asks the runtime only for the detected name; a stale status read can
no longer show the previous name.
`MobileMachineNameField` owns the store read, the published-name hook and the
debounced input, so `MobilePairingSetupSection` returns to its prop shape and
the pass-through `MobilePanePairingOutput` wrapper is gone. Paired-device
revocation moves into `useMobilePairedDeviceRevocation`, which keeps
`MobilePane` within its line budget with an extraction that carries behavior.
The web client mounts this pane too, but its settings store kept the name
locally where nothing published it. `machineName` now rides the existing
runtime-backed settings sync so the field renames the paired runtime.
* refactor(settings): normalize the machine name at the store boundary
Every writer (desktop IPC, web RPC, CLI) reaches the store through
updateSettings, which already normalizes the other free-text settings
there. Trim and bound the machine name in that one place instead of at
two upstream edges, so a future main-process writer is covered too.
* test(settings): pin machine-name routing and detection, and make the field searchable
The shared machine-name lookup test spawned the real `scutil` twice and compared the answers, so a
slow runner could time one spawn out to the hostname and fail. It now mocks the subprocess, proves
the hostname answers until the one shared lookup lands, and that a second runtime does not spawn
again.
`host name` is no longer pinned local, but only the explicit `--environment` route was covered; an
ambient `ORCA_ENVIRONMENT` now has its own test so the pin cannot silently grow back.
The Machine name field is added to the Mobile pane's search catalog at the tail, keeping every
existing row's tie-break index.
* fix(runtime): wait for the machine-name lookup before publishing status
A status read answered in the first few milliseconds after launch published the bare
hostname because the friendly-name lookup had not landed yet, and a caption fetched in
that window never corrected itself. RuntimeMachineName now exposes the settled lookup
as a promise, and both status publishers (the status.get RPC and the desktop
runtime:getStatus IPC) await it before reading. Construction, listen, and every other
method stay unblocked; the worst case is one wait of at most a second on the first read.
* fix(cli): refuse to rename a runtime that does not publish a machine name
An older Orca runtime rejects the unknown settings field with a bare invalid_params, so
'orca host name --name' routed at one failed with no explanation. The runtime that does
not publish machineName on status cannot store one either, so the CLI reads status first
and refuses with incompatible_runtime and a message that says to update that host,
before writing anything.
* fix(ipc): introduce this desktop to remote hosts by its machine name
When this desktop connected to a remote workspace host it announced itself under a
hostname captured once at module load, so a renamed machine kept its old name on every
other device's connected-clients list. The client name is now read at send time from
the runtime's machine name (the configured override, else the detected one), passed in
where the remote workspace handlers are registered, so a rename reaches the next
presence frame without a relaunch.
* fix(runtime): keep the machine-name lookup under the status probe budget
Status publishers now wait for the one-time name lookup, and `orca status`
probes them with a one-second budget. scutil answers in milliseconds, so a
half-second cap keeps a stalled lookup from making a healthy runtime read as
"starting" while still preferring the friendly name.
* refactor(web): drop the unreachable machine-name write path
The Mobile settings section is desktop-only, so the paired web client can
never render the field. Forwarding the name through the web settings sync was
dead code, and against an older host the strict update contract would have
rejected it while the local mirror kept the value. Remove it until a web
surface exists.
* chore(i18n): translate the machine-name strings and document paired-server rows
Add the Machine name field and its Settings search entry to the five non-English
catalogs, explain in the host list spec why paired-server rows report an unknown
platform, and drop a stale timeout figure from a test comment.
* refactor(settings): make the machine name a machine-wide setting with a General home
The name other devices and hosts list this computer under is not a mobile
setting. Rename MobileMachineNameField to MachineNameField, give it a per-mount
id, and put its primary home in Settings > General under "This computer". The
Mobile pane keeps the same field. One shared search entry feeds General, the
Mobile pane, and the copy now says "other devices and hosts" in all six locales.
The web client has no machine of its own to name and its settings mirror cannot
persist one, so the field renders nothing there and General omits the section.
* feat(mobile): name this computer in the Orca Mobile pairing step
The "Pair this computer" step now shows the same machine name field above the
connection choice and code, so a user pairing a phone from the sidebar page can
name the computer right there.
* feat(settings): name this host when sharing it with other devices
Share this host produces the access link other devices use to reach this
machine, so it mounts the machine name field first. The pane's search entry
takes the shared machine-name keywords so a search lands there.
* feat(sidebar): name this desktop when adding a remote host
This desktop introduces itself to a new SSH host or remote server under its
machine name, so the Add Remote Host dialog mounts the field once, between the
header and the host fields, in both modes. Submit logic is unchanged.
* feat(settings): name this computer in the SSH pane add form
The SSH pane's add form mounts the machine name field above the host fields.
Editing a saved host leaves it out; that host already met this computer.
* fix(mobile): drop the empty machine-name grid row on the web client
The pairing step wrapped MachineNameField in its own grid-area div. On the
web client the field renders nothing, so the wrapper left an empty row and
an extra row gap between the copy and the connection options. The field now
takes a className for its root, so the grid slot disappears with it.
* fix(settings): let Enter in the machine name field submit its form like sibling inputs
The field intercepted Enter to blur and commit instead of submitting the enclosing
SSH add form. The draft is already flushed on blur and on unmount, and the name is
read from the store whenever a peer asks, so nothing is lost when the form submits
first. Enter now behaves like the neighbouring inputs; the test proves the submit
fires and the name still commits when the form closes.
* fix(mobile): keep the machine name inside the pairing copy cell
A dedicated grid row stayed in the template on the web client, where the field
renders nothing, adding an empty track and a second row gap between the copy and
the connection options. The field now sits at the end of the copy cell with the
same 18px rhythm, so an absent field leaves nothing behind.
* fix(runtime): retry a failed machine-name lookup instead of latching the hostname
On a loaded Mac the scutil lookup missed its 500 ms cap during app boot, and
because the fallback was memoized for the process, every status read and the
Settings caption showed the bare hostname for the rest of the session.
The lookup now gets a 5 s timeout, a failed attempt (timeout, spawn error,
non-zero exit, empty output) clears the shared memo so a later ready() retries
after a 30 s interval, and status publishers wait only up to a 750 ms publish
budget before answering with what read() has now. A friendly name and the
non-darwin hostname stay final.
* refactor(settings): show the machine name only where other devices join this computer
The Add Remote Host dialog, the SSH pane add form, and General all describe
another machine, so a field about this computer's own name read as a third
kind of label there. The field now mounts only where other devices pair with
or connect to this computer: the Mobile pane, the Orca Mobile pairing step,
and Remote Servers > Share this host.
|
||
|
|
98e5ea3d5f |
refactor(tabs): delete the terminal tab's dead adopted-session field (#22557)
* refactor(tabs): delete the terminal tab's dead adopted-session field and every branch that read it * refactor(tabs): drop the stale adopted-agent comment and pin legacy load on a chat terminal The comment above the terminal chat-eligibility agent fallback described the removed adopted-session agent fallback. The legacy load test now puts the retired key on a chat-mode terminal tab, the only shape that ever carried it. |
||
|
|
89817ad2b4 |
test(mobile): repin the recording corpus to main's tip after #22570 (#22576)
#22570 pinned its own branch commit, which the squash left off main; the corpus now pins main at
|