mirror of
https://github.com/stablyai/orca.git
synced 2026-09-27 16:02:35 +00:00
7d2c399329ffc60dd67c30ca2021ec4fdc8c307f
11739
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7d2c399329 |
fix(web): keep Remote Web loading over plain HTTP without crypto.randomUUID (#22516)
* fix(web): keep Remote Web loading over plain HTTP without crypto.randomUUID
Browsers hide crypto.randomUUID outside secure contexts, so Remote Web over
http://<lan-or-tailnet-ip> threw while importing the store and never painted.
createAgentStatusAuthorityId now takes its UUID source (renderer passes
createBrowserUuid, main passes node:crypto randomUUID), and the other
unguarded renderer calls go through createBrowserUuid.
* refactor(renderer): route remaining randomUUID fallbacks through createBrowserUuid
Replaces five hand-rolled crypto?.randomUUID?.() fallbacks (including a copy
of the browser-uuid fallback in mint-stable-pane-id) with createBrowserUuid,
and adds an oxlint no-restricted-properties rule so renderer code cannot call
randomUUID directly again.
* refactor(shared): move the non-secure-context UUID generator into src/shared
The white screen came from src/shared, so the fix belongs there. src/shared had
three hand-rolled copies of the same randomUUID-then-getRandomValues-then-Math.random
ladder (nested-repo-telemetry, project-groups, setup-agent-sequencing) because there
was nothing in that layer to import; createBrowserUuid lived one directory over in
the renderer.
createNonSecureContextUuid() now holds the single implementation, @/lib/browser-uuid
re-exports it under the renderer's existing name so no renderer import site changes,
and the three duplicates call it.
That also lets createAgentStatusAuthorityId go back to one argument. The injected
randomUuid source was justified as keeping browser APIs out of shared code, but this
generator is runtime-agnostic — it works unchanged in Node. Injecting it bought no
layering and made the safe choice a parameter every future caller had to get right,
unguarded: a caller could pass () => globalThis.crypto.randomUUID() and restore the
white screen with lint and tests green.
* fix(lint): ban crypto.randomUUID in src/shared and scope the escape hatch
vite.web.config.ts compiles src/shared straight into the web bundle, but the new
randomUUID ban only covered src/renderer/src — so the exact module that white-screened
the app sat outside the guard it shipped with, and the regression could come back with
a green lint. The override now covers src/shared/**/*.ts too; it costs zero diagnostics
because the duplicates it would have flagged are gone. `import { randomUUID } from
'node:crypto'` is untouched, so main-only shared modules keep working.
Both blanket "off" overrides are gone. no-restricted-properties is keyed by property
name, so the moment a second property joins the renderer block those overrides would
have silently exempted it — in the one file that is the escape hatch, and in every test
in the repo. Tests are where people copy patterns from, so they stay covered; the four
real uses carry line-scoped disables with a reason.
* fix(terminal): keep render-desync capture ids inside main's 120-char cap
createCaptureId builds `${Date.now()}-${panePart}-${nonce}`. A real paneKey is
`${tabId}:${leafId}` — two UUIDs, 73 chars after sanitizing — so with a 36-char UUID
nonce the id is 124 chars and main rejects it with 'Invalid render-desync capture id'.
persistHealedReference swallows that into console.error, so it shows up as diagnostics
that silently never appear.
This was already broken on the desktop app, where randomUUID is available; routing the
non-secure path through the same generator would have made it unconditional, including
on the plain-HTTP web client this branch exists to repair.
Bound the pane part rather than the nonce: keep the trailing 40 chars, which is the
whole leaf id (the identifying half, unique on its own) and drop the tab-id prefix, so
ids stay unique and traceable at 91 chars. The 120-char contract now lives in
src/shared next to the IPC args, imported by both sides, so the renderer cannot mint an
id main will reject without the test noticing.
* test(web): cover the whole store graph and the Vault token without randomUUID
The reported stack was the store chunk, not two named modules, so the repro test now
evaluates the store root under the stubbed non-secure crypto. Any new import-time
secure-context call anywhere in that graph fails here, not just the one this branch
removed.
Also ports the request-token regression from #20465, the one piece of coverage the
competing branches for this bug contributed that this one lacked. Both cases fail with
"randomUUID is not a function" when their production change is reverted.
* test(web): restore the real crypto.randomUUID after the non-secure Vault case
randomUUID lives on Crypto.prototype, so stubbing it as an own property of
globalThis.crypto left the restore branch with an undefined descriptor and a
leaked own `randomUUID: undefined`. Swap the whole crypto own property instead,
through one shared stub the repro suite already needed.
---------
Co-authored-by: Neil <neil@stably.ai>
|
||
|
|
2796a3ac15 |
fix(claude): prove a stopped chat's child processes gone when they exit with it (#22918)
* fix(claude): prove a stopped chat's child processes gone when they exit with it Stopping a Claude chat snapshots its child processes, closes Claude, and then verifies each child is gone before the stop counts as proven. The verifier only accepted a child as gone after that child had appeared in one of its own process-table reads. When Claude exits gracefully it takes its short-lived children with it before the first read, so none of them was ever seen again. Every read confirmed them absent, and the verdict was still "unverifiable" after the full 3.5 s window. Measured live: 37 complete reads, target absent from all, verdict unverifiable, on every idle stop. The snapshot is itself a table read that saw each child alive, so it now counts as the first sighting. An absence counts only from a read that started after the child was last seen, which keeps what the old rule protected against: a shared or in-flight read begun before the snapshot cannot list a child forked since. Two such absences prove a child gone. Live, the same stop now proves the tree gone in about 150 ms. The daemon's terminal shutdown uses the same verifier and gets the same rule. Test reads that reused one capture stamped with the snapshot's own time now stamp each read when it starts, as real scans do. * fix(claude): keep the latest sighting and count the final read as an absence A matching read that started earlier but resolved later could move a target's last sighting back and let an older absence count; the sighting now only moves forward. The read after the deadline now records its absences the same way the polling loop does, so a second qualifying absence there proves the target gone. |
||
|
|
841503152c |
fix(runtime-environments): don't crash when a server removed via the CLI still responds (#22517)
* fix(runtime-environments): don't crash when a server removed via the CLI still responds orca environment rm edits the environment store behind the running app, so the next ok response on a live socket called markEnvironmentUsed, which threw 'Unknown environment' out of an unguarded socket callback. Main-process callers now use markEnvironmentUsedIfPresent, which skips a missing environment and keeps every other store error; the status owner pauses shared control instead of re-establishing it for a removed server. * fix(runtime-environments): guard usage bookkeeping at the main-process boundary Keep one strict store contract and move the leniency to the caller that cannot report a failure to anyone. - Revert markEnvironmentUsedIfPresent: drawing the line around one error string left corrupt, unreadable and oversized store files still fatal on the same unguarded socket callback. - Add recordRuntimeEnvironmentUsage, a named main-process boundary that says lastUsedAt is advisory and swallows every store failure. Route only the three sites with no observer through it (subscription onResponse in transport- and support-routing, and the status owner's verified hook, where a throw skips settleWaiters and hangs refresh callers). Awaited request paths stay strict. - Guard onResponse/onBinary in the subscription frame router the way the sibling request router already guards validateStatus, so no consumer throw can reach the ws 'message' emitter and become main_uncaught_exception. - Drop the status-owner `capable && present` gate: pauseStandingRetry no-ops while subscriptions exist, and removal teardown belongs to #21048's watcher. Co-authored-by: mmarabel <mmarabel@users.noreply.github.com> * test(runtime-environments): cover the real socket path a consumer throw escapes The existing tests invoke the captured onResponse directly, which never touches the surface that actually kills the app. Drive a real WebSocket server through subscribeRemoteRuntimeRequest so the throw travels ws 'message' -> handleFrame -> consumer; without the frame-router guard vitest reports it as an unhandled error, which is main_uncaught_exception in production. --------- Co-authored-by: Neil <neil@stably.ai> Co-authored-by: mmarabel <mmarabel@users.noreply.github.com> |
||
|
|
c220d92c03 |
fix(codex): Codex 0.157+ starts in Orca-managed homes instead of failing with SUN_LEN (#22878)
* fix(codex): turn off Codex daemon auto-start in homes whose socket path exceeds sun_path
Codex >= 0.157 auto-starts a background app-server daemon and connects to
<CODEX_HOME>/app-server-control/app-server-control.sock. Orca's managed homes
under userData make that path longer than sun_path (104 bytes on macOS, 108 on
Linux/Windows), so every interactive codex in an Orca terminal failed with
'path must be shorter than SUN_LEN'. The config mirror now writes a marked
[features] daemon_auto_start = false into only those homes, removes it when the
home fits, and never promotes it into ~/.codex.
* fix(codex): address review of the daemon socket guard
- A runtime config.toml holding only Orca's daemon override no longer reads as a
config-sync stall, so users without ~/.codex/config.toml get no false
"missing" warning in the accounts pane.
- The legacy shared-home refresh re-applies the guard, so retained pre-rollout
panes keep daemon auto-start off after a system-default launch.
- Warn once when an inline `features = {...}` or `[[features]]` blocks the
override instead of failing silently.
- Rename the upsert's TUI-specific internals now that it serves any table.
* fix(codex): apply the daemon socket guard even when the settings mirror stalls
When the settings write-back or mirror refused (unreadable baseline, failed
write to ~/.codex, unreadable source), the whole pass returned before the
daemon guard was applied. A home whose config.toml predates the guard then
kept failing with SUN_LEN on every launch for as long as the stall lasted.
The guard now lands on those paths too; the mirror itself is unchanged.
* fix(codex): guard managed account homes when ~/.codex/config.toml is missing
* test(codex): keep reset-credit ownership checks scoped to the retry, not service construction
* test(codex): build the account mirror test without a type cast
* fix(codex): keep blocking WSL ownership checks off the no-config guard pass
Guarding account homes with no ~/.codex/config.toml ran the WSL ownership
check, a synchronous wsl.exe call per account, at startup before the window
opens and on every account switch. WSL homes are guarded by WSL launch prep,
so that pass now covers host homes only.
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
|
||
|
|
64704daac5 |
feat(feature-tips): one-time tip for agent session search (#22923)
* feat(feature-tips): one-time tip for agent session search Session search is only discoverable from Settings. Add a feature tip that existing users see once, which turns search on, shows the first index build's progress in place, and opens the sidebar search once it is ready. A toast says when a build left running in the background finishes. Also share the two-column tip layout across the voice, Cmd+J and session search dialogs, and move the CLI tip dialog into its own file. * refactor(feature-tips): simplify the session search tip after review - Watch for a background finish only when this tip closes mid-index; closing any other tip no longer arms a stray "ready" toast. - The hook detects the close itself, so dialogClosed/reset and the onStatus callback on useSessionSearchStatus are gone. - Table-driven dialog copy, reuse FeatureTipActions, and share the eyebrow badge and settings link across the voice, Cmd+J and session search tips. - One getPendingFeatureTips for the startup gate and the modal. - Demo: a phase timing table and hoisted header props. - e2e helpers mark the new tip seen too. * fix(feature-tips): retire the session search tip once the user has switched search Turning session search on or off in Settings, or enabling it from the sidebar, now marks the tip seen, the way the Voice switch does, so a user who turned search on and later off is not pitched it again. * test(ai-vault): give the legacy-filter store mock markFeatureTipsSeen Also mark the tip seen only after the sidebar's enable actually saves. |
||
|
|
acf8e679ea |
feat(native-chat): Claude sessions write their subagents into the host status store (#22536)
* refactor(native-chat): the host hands out client delivery's status subscriptions as they are subscribeStatus and subscribeTurnCompletions wrapped client delivery's bound methods in forwarding lambdas; they are now the same members, the way waitForSendSettlement already is. The host is at its size limit, and the next channel it hands out needs the line. * feat(native-chat): Claude sessions write their subagents into the host status store The Claude background-task tracker queues child-work evidence at each decision it already makes (start, update, progress, terminal frame, roster replacement, turn end, session end), plus the two facts its legacy row ignores: a foreground child's progress and a foreground spawn call's result. The adapter drains that evidence after the journal handled the frame and the parent row was republished, and the host folds it into one record per child in its canonical store. Nothing reads the records yet; the strip and sidebar keep their current sources. * test(native-chat): pin the Claude child-work evidence and the host reduction of it * test(native-chat): prove every hop from a Claude frame to the host's child record The adapter delivers evidence after the frame's journal rows and the parent's republished row; the frame script keeps the parent state today reads while the records add outcome and activity; the runtime hands the evidence to the status sink under the session's own address; both entry points wire the sink to the ingest. * test(native-chat): read an optional task list as optional in the producer script * test(native-chat): an address whose publish threw carries no child work * test(agent-status): a foreign record differs from ours by producer alone * feat(native-chat): a foreground Claude child's own tool call is what its record says it is doing A child's tool traffic reaches the parent stream only for a foreground child. Read after the journal handled the frame, the child's newest call still awaiting a result becomes its open operation, previewed the way a hook-reported row previews its own tool; the result closes it. The open call is derived from the journal's own bookkeeping, not held a second time. * fix(native-chat): a Claude child restarted under a new spawn call keeps reporting to its record A task that ended and starts again stays hidden from the legacy row until a roster lists it, so the tracker held no run for it: the new run's progress reached nothing and a foreground re-run's own spawn result settled nothing. The run is now held beside the live map, where the legacy row never reads it, until a roster hands it back or it ends. A parity test pins the record's run count to the journal roster's attempt on a new spawn call, the one event both count. * refactor(native-chat): the Claude child-tool queries and translator contract get their own homes The translator's child-tool queries move into claude-child-tool-queries.ts and its contract type into claude-journal-translator-contract.ts. Brings the translator back under the size limit. * refactor(native-chat): Claude child evidence carries only its own edge's facts Admission now keeps what a child's record already knows: labels, model, owner, residency, the last message within an invocation, and a token count that never shrinks. The evidence side copied all of those forward itself, a second owner of the same rule. It now sends only what this edge observed, and a task's token count comes from the frame that reported it. * refactor(native-chat): Claude child evidence hands admission its raw labels Admission now folds provider text to one line and drops a malformed fact instead of refusing the record, so the evidence side no longer folds labels itself. The description keeps admission's longer bound. * fix(agent-status): admission alone decides a settled child's second ending The reconciliation returned before admission whenever a record had already settled with a definite outcome. That dropped the evidence an `unknown` ending carries (its last message and tokens), which admission's refine-only rule keeps, so that rule never ran for the structured producers. The latch goes. Admission keeps the definite outcome, lands the late evidence, and refuses a conflicting definite ending as `stale-invocation`, which the host ingest already counts as the fence doing its job, not a fault. Pinned through the real Claude producer and the host's own ingest. * perf(agent-status): keep child records off the status hot paths Child records made every store write and every status notification scale with the whole store. Each Claude child progress frame cost about 2 ms with 5 chats holding ~200 child records (about 14 ms at ~1,400), and every status change on any lane re-parsed every child record just to list parent rows. - The store derives each frozen record's key once instead of re-parsing it on every mutation's validation and every alias lookup. - Settled history is trimmed only when a batch settles something. - Parent listing and the structured row's revision stamp read the parents and the revision directly instead of building a full snapshot. A progress frame now costs about 0.3 ms at the same size, and listing parent rows no longer depends on how many child records the store holds. * fix(native-chat): an errored Claude spawn result no longer decides how the child ended Interrupting a foreground Claude agent while it runs a tool delivers the spawn call's errored result before the child's own killed/stopped frames. The spawn result settled the record `failed` first, and admission then refused the later `cancelled` as a conflicting ending, so an interrupted child read as a failure. An errored spawn result now settles the child `unknown`; the child's own terminal frame refines it to `cancelled` or `failed`. A successful spawn result still settles `succeeded`. The test replays both frame orders the real CLI produced when interrupted. * test(native-chat): pin a Claude foreground child's real finishing order The real CLI ends a foreground agent with its own completed update, then a notification carrying the final summary and usage, and only then the spawn call's result. Existing tests modeled the spawn result arriving first, so nothing checked that the notification's summary and tokens still land on a record the update already settled. * perf(agent-status): a store write costs what it touches, not the whole store With child records on the host, every mutation copied all five store maps and re-validated every record, and reads scanned every child and alias. A parent status publish cost about 10 ms with 4,000 child records in the store, and a child update about 13 ms. - A mutation writes into drafts over the committed maps and lands in place; a refused one is dropped with nothing to undo. The drafts keep the exact map order a copy would have. - Only what a mutation touched is re-validated: touched parents, children, aliases, facts and tombstones, plus every alias of a touched child and whatever a removed parent owned. The full validation stays for snapshot restore. - The snapshot byte budget is a running total instead of a re-measure. - Children by parent, facts by parent, aliases by child, aliases by identity and retired aliases are indexed, so reads return stored records without scanning or re-parsing. - The memoized alias identity and tombstone-key checks are gone: indexes derive them once. A parent publish now costs about 0.015 ms and a child update about 0.06 ms at 40, 1,000 and 4,000 children alike. A seeded fuzz holds the store to the copy-and-validate-everything path decision for decision, snapshot for snapshot and read for read, and a replica fed the envelopes ends identical. * fix(native-chat): a Claude child ends only on its own terminal frame The child records were fed from the legacy background-task tracker's display decisions, so they inherited rules that are not truth: a turn ending swept foreground children, a roster omitting a background child settled it, a foreground spawn call's result ended the child, and a new background start after any roster produced no record. Captured from the real CLI, an agent moved to the background keeps its own shell running for 40 s after the parent's turn ends, and that shell was settled `unknown` at the parent's `result`. Replayed with the spawn result ahead of the roster, the same agent settled as a false success and was then revived as a spurious second run. A new decoder reads the task frames directly. `task_started` opens a child (a start for an ended task id is a restart, the way messaging a finished agent resumes it), progress and a live `task_updated` update it, and a terminal `task_updated` or `task_notification` ends it. Rosters, turn ends and spawn results say nothing about a child. Every child in every capture gets its own terminal frame, so no evidenced ending is lost. The notification's `tool_use_id` names the run that ended (captured on a resumed agent's second run), so an ending from a run that is already over no longer ends the current one; a run id the record never saw still ends it, so nothing strands. The tracker, its settled-task retention and the frame readers are back to exactly what main has: the aggregate-roster split and the restart holding map are deleted, and the legacy row is unchanged by construction. * fix(agent-status): a session's end settles its live children instead of erasing them When a structured session ended, the reducer removed every child record it held, finished or not, so a reader could no longer tell how the session's work had ended. Now a child still live when its session ends settles `unknown` (nothing reported how it ended), and a child that had already ended keeps its outcome. The records still die with their parent: closing or releasing the session drops the parent row, and the store drops its children with it. A child's own outcome arriving after the session ended still refines the `unknown`. The `inventory` and `turn-ended` edges, and the rules that settled children on a roster omission or at a turn boundary, are deleted: no producer sends them any more. A restart is now its own flag on a live edge, which is what a producer reports when a finished child starts again under the same run handle. * test(native-chat): replay the real Claude CLI's frame orders into a real host Scrubbed cuts of five Claude CLI 2.1.280 stream-json captures (ids, paths and prompts replaced, frame order and relative clock kept), replayed through the adapter into a hook server: - an agent moved to the background keeps its own shell live past the parent's turn, and the shell settles at its own notification's time; - the same capture with the spawn result ahead of the move ends the agent once, from its own notification, with no second run; - a roster that omits a background child without its own ending leaves it live; - a session that ends settles what still runs `unknown` and keeps every record; - messaging a finished background agent opens its second run, which ends from its own frame; - interrupts in both captured orders end `cancelled`, and a finished foreground agent keeps its summary and usage. * test(agent-status): hold the store's running indexes and byte total to a rebuild The copying-store fuzz never reaches the snapshot byte budget, so a drift in the running byte total (or any index the public reads do not surface) passed it. After every fuzzed step, including refusals, compare every index with one rebuilt from the committed maps. |
||
|
|
f59ee6c859 |
Simplify .map().flat() to .flatMap() in onboarding tests (#22917)
Replace the two-method chain with the equivalent idiomatic flatMap method for clearer, more concise code. |
||
|
|
8fb13edd9f | fix(mobile): keep terminal input working when reopening worktrees (#22505) | ||
|
|
01ed4b92f7 | fix(desktop): release a native chat when you leave it, so its idle clock can start (#22801) | ||
|
|
c1edd1d97d |
chore(i18n): translate 85 new keys to es/fr/ja/ko/zh (#22746)
* Add translations for chat resume, Git toolchain, and notebook support * Fix translation terminology in French and Korean locales - Standardize Korean terminology from "restart" to "resume" for chat resume functionality - Clarify French error message for conflicting Orca windows/terminals - Fix Korean context translation (문맥 → 컨텍스트) * fix jupyter notebook translation |
||
|
|
9643e16fdb | fix(native-chat): keep an idle chat alive while its subagents or background commands run (#22794) | ||
|
|
56dfddc297 |
Show close button for single terminal panes (#22770)
* Show close button for single terminal panes Single-pane terminals previously had no close affordance; now display "Close tab" button while multi-pane terminals show "Close Pane". Pinned tabs omit the close button. Refactored terminal-unified-tab-lookup to include tab pinned state alongside chat view fields. * Show close button for titled single terminal panes For split panes, the X button remains remove-title only. For single panes with titles (including agent terminals that acquire runtime titles), a close tab button is needed to close the pane. |
||
|
|
8009939381 |
fix(claude): let a Claude chat start again after its root exited with unverifiable descendants (#22802)
* fix(claude): let a chat stopped with unverifiable descendants start again When the idle release clock stopped a Claude chat whose root process exited but whose descendants could not be verified, the host released the lease and forgot the session, as designed. The adapter, though, kept its session indexed as not yet closed. Every later start of that chat first tried to close the stale session again, got the same verdict, and was refused with "provider close unproven". The chat then read as "Could not load conversation" until the app restarted. A proven root exit or processless close now finalizes the adapter session like a proven close: it persists the resume point, emits the end, and drops the entry, and it still reports the verdict to the caller. Only a genuinely unknown exit stays indexed for a retry. * fix(claude): let a chat whose crash left unverifiable descendants start again The same refusal had a second emitter. A Claude root that crashed with unverifiable descendants stays recorded as an exit, and the next start of the chat re-checked that exit and refused on the root-exit verdict, although that verdict is what let the host release the lease. The start now proceeds and settles the recorded exit; only an exit this host cannot vouch for still blocks it. * fix(claude): let a start over a live session whose close saw the root exit go ahead Closing the previous session finalizes it before reporting the root-exit verdict, so the start that closed it must not fail on that verdict, the same as a retained root exit. * fix(native-chat): read a root-exit stop the same way at every host stop site Idle eviction already treated a stop whose provider root was seen to exit as a stop, but handing a chat to the terminal and recovering from a journal write failure treated the same verdict as a failure. Now that the Claude adapter finalizes the session before reporting that verdict, those two paths left the host holding a child the adapter had already ended: the first hand-off failed with "provider close unproven" and sends failed until the chat was evicted. One function now reads a stop's result for eviction, hand-off, sink-failure recovery and the adapter's own start-over-a-live-session path. * fix(claude): publish a crash whose root exited even when its descendants went unverified A Claude crash was published to the host only once its whole process tree was proven gone, or when it failed during startup. A crash whose root was seen to exit but whose descendants could not be verified was held back, so the chat stayed marked live: sends failed with the crash message and nothing restarted Claude until the idle clock stopped the chat. The owner already releases the lease on a first-hand root exit, so the crash now publishes like a proven one and the host reconciles and recovers at once. * test(claude): prove an open chat restarts after a crash whose descendants went unverified The crash-publication test only asserted the lease was released, so nothing showed the restart the fix exists for. It now holds the chat as an open surface does and asserts Claude comes back on the saved session with a live lease. * test(claude): settle the in-flight send and deliver the next one across a root-exit crash restart The restart test now also covers the sends around the crash: a message Claude took but never echoed settles as exited before acknowledgement instead of waiting forever, and a send after the restart is accepted by the new process. |
||
|
|
d443320af2 |
refactor(native-chat): remove the unused terminal handoff (#22783)
* refactor(native-chat): remove the unused terminal handoff No client ever called agentSession.requestHandoff or mounted the handoff chrome. Delete the handoff coordinator, the terminal-owner runtime, the proof write path and the unmounted UI. Keep agentSession.handoffStatus, which released desktop clients read for worktree activation, and let records an older build left mid handoff reconcile through the ordinary restart and recovery paths. * fix(native-chat): never let the pre-stop snapshot hold a chat's stop Eviction now drains delivered events before quit's resume-offer snapshot. An unbounded wait there sits ahead of the provider stop, so a sink whose journal write stalls kept the child running until the step deadline aborted the eviction. The offer is advisory: bound the drain and stop the child regardless. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(native-chat): drop helpers only the terminal handoff called `claudeAuthEnvCarriedForward`, `isPathWithinDirectory` and `queryWindowsProcessRowsFresh` lost their last caller with the handoff. The fresh-scan tests now go through `queryWindowsProcessDescendants({ fresh: true })`, the teardown path that still depends on that contract. Co-Authored-By: Claude <noreply@anthropic.com> * docs(native-chat): stop citing the removed handoff in lifecycle comments Six comments still named the handoff coordinator, a handoff suspend, or a terminal-owned session as live participants in the flows they describe. Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): type the stalled snapshot drain without a cast Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): pin that a start dead before proving owes no settlement The removed restart handoff test pinned this branch; nothing else did. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): keep the owner-status read behind an in-flight attach The handoff removal dropped the per-session queue from `handoffStatus`, so a read landing mid-start reported the reservation (no owner) instead of the settled chat owner, and shipped desktop clients blocked worktree activation on it. The read is queued again, as it was before the removal. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(terminal): remove the agent-session PTY write gate The gate only refused a write when a PTY had been bound to a chat session, and the only code that ever bound one was the terminal handoff this branch removes. With it gone, every admit/readmit returned "admitted" unconditionally, so the checks on the renderer write path, the runtime controller backstop, terminal.send, agent prompts, preview input and orchestration pointers, the refusal fields on terminal.send and worker-start receipts, the plugin and CLI refusal copy, and the adopted-pane orchestration routing could no longer run. Ordinary writes take the same path in the same order as before. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(native-chat): drop the transcript helpers only the handoff called appendLegacyTranscriptMessages fed the terminal transcript catch-up and proveClaudeTranscriptBranch backed the terminal owner's exit proof. Both lost their last caller with the handoff. Their tests now go through the live entry points instead: the roster bounds through the legacy import, the pinned-read and growth tests through the ancestry replay the history window uses, and the marker rules through the string proof in their own file rather than the session-file resolver's. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): stop calling a starting chat "mid-handoff" A send refused because the chat's owner is not settled showed "The session is mid-handoff (<stage>)." in the composer. With the handoff gone, the stages that reach it are a chat that is still starting, or one whose previous agent process has not yet been confirmed stopped. The message now says which of the two it is. The refusal code is unchanged. Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): type the stand-in roster decoder without a cast Co-Authored-By: Claude <noreply@anthropic.com> * refactor(codex): name the pinned rollout lookup for what it does With the terminal handoff gone, the module named codex-tui-rollout-proof holds only the pinned rollout lookup that structured Codex launches use to resume a thread, so the name described code that no longer exists. Rename the module and its options type. Also drop a mobile allowlist assertion that pinned the removed agentSession.requestHandoff method, which no longer exists to allow. * refactor(native-chat): type the owner-status reply as the host sends it The handoffStatus reply type still listed the terminal handoff's fields and states (terminal placement, host label, proof retry, queued and waiting phases, the to-terminal direction). No host writes them any more and the only client reader parses the reply as unknown, so they described nothing. The reply on the wire is unchanged. * refactor(native-chat): normalize terminal-handoff lease values once at decode Nothing in this build writes a terminal owner (`runtimeKind: 'tui'`) or the handoff's `preparing` / `old-owner-stopped` stages, but the in-memory types still admitted them, so readers across the host kept branches for values no path produces and the compiler could not point at them. The store now validates the on-disk shape, which still accepts those values so an older record is not quarantined, and maps them once while parsing: - `preparing` and `old-owner-stopped` become `recovering` - a `tui` lease becomes `native`; when it records a process it also becomes `conflicted`, the claim every build probes but never stops. A plain native owner would be stopped by restart recovery, here and in older builds. Revisions are taken over the normalized state on both sides of every compare, and the mapped record reaches disk with the store's first transaction, the same way the tab-id backfill does. The in-memory types narrow to what this build writes, and the branches that existed only for the removed values go. Structured-worker identity keeps its verdict for a former terminal owner by refusing a conflicted claim rather than a non-native kind. * refactor(native-chat): stop threading the owner kind through a reservation A reservation only ever names a native owner now, so the request no longer carries a kind and the reserved lease records `native` directly. The attach params keep `runtimeKind`: agentSession.ensure and create accept it, and the operation fingerprint stored in the ledger covers it. * test(native-chat): pin the legacy-lease rewrite with a transaction that changes nothing else Hiding a tab also committed the visibility index, so the no-op transaction wrote the file even when its open-time revision was wrong. Committing the index first leaves the pending rewrite as the only reason to write. * test(native-chat): give the legacy-lease store test a tab id so the backfill cannot supply its rewrite The seeded record had no surface tab id, so the next open backfilled one and that rewrite alone made the no-op transaction write. The test passed with the legacy-lease rewrite signal removed. * test(worktree-activation): restore the OMP surfaced-agent resume test The handoff removal deleted it alongside the terminal-owner tests, but it covers the surfaced-PTY block that still guards resume, including an agent whose ownership is unknown. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
646e9a5b02 | Update README downloads badge | ||
|
|
c9d421f3c1 |
fix(mobile): the page's terminal shows its last rows above the command dock (OTA phase C follow-up) (#22806)
* fix(mobile): mount the page's terminal frame with its onLayout On the page every branch of the session content is a bare View in one slot, so the terminal frame reused the loading View. react-native-web observes onLayout only on a View that mounts with it, so the frame never reported its height: every measure fell back to the page's window.innerHeight and the PTY got rows the frame cannot show (63 vs 47 on a 1280x2856 screen), with the last rows under the command dock. Keying the frame makes it mount fresh. The parity pin moves for the key string. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): size the terminal document to its host, not the window Inside the WebView the window is the terminal frame; on the page it is the whole page, header and dock included. Every fit, pan, scroll and overlay bound in the document read window.innerWidth/innerHeight, so a measure without a container height sized the PTY to the page. The document now reads one seam, viewportSize: the window by default (native unchanged), the host element's box on the page. A census keeps raw window size reads out of document/. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): put the terminal document's coordinates in its host Round one moved the page document's size to its host but left its origin at the window, so a tap below the session header mapped rows low (row 19 for row 11), and the selection overlay and scroll indicator, position: fixed, drew over the page. - viewportRect replaces viewportSize: the host's box on the page, the window at 0,0 in the WebView. viewportPoint is the one place a client point meets that origin; cell mapping, mouse reports, the pinch anchor and edge scroll go through it, and a census holds the rest to deltas. - observeViewport: the fit refits on a host ResizeObserver on the page, a window resize in the WebView. - A hidden host measures 0x0; the fit keeps scale 1 rather than 0. - On the page the host is the overlays' containing block. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): state what native measured in the parity note Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): read the page terminal's overlay and refit off its host The overlay is absolute inside the host now, and the page's refit follows the host's box, so the owed-frame case pulses the host rather than the window. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): hold the terminal fit while its host has no width computeFitScale answered 1 for a 0-wide host, but applyFitScale still committed that fit as soon as the cell width was known, so a display:none screen got a fit for no box. The attempt now leaves the fit pending until the host reports a positive width, and observeViewport starts it again once it does: one commit, never at scale 0. A regression test pins edge scroll to the host's edges: with the host at top 100 and height 600, the down band starts at client Y 660, not 560. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): skip the terminal refit while its host has no box react-native-screens hides the session with display:none when another screen covers it, so the host's observer reports 0x0. The refit still clamped pan and repainted, so coming back to the session lost the pan the user left; native never refits on navigation. The refit now does nothing until the host has a positive width, and the next real box refits once. Also untangles the seam-count sentence in document-host-seams. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): refit the page terminal only when its box changes Coming back to a session takes the host from 0x0 to its old box. The observer fired, the refit committed, and the user's pan and zoom were reset, where native keeps them because navigation never resizes its WebView. The fit now remembers the box it was committed for, and the refit acts only on a positive box different from that one. The frame tests wait on the document's own frames instead of a timer, and the hidden-host case asserts the zero-width read happened. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): refit a shown terminal when a fit was asked for while hidden The zero-width gate drops a fit requested while the host is hidden, and the same-box skip then kept the stale fit when the host came back at its old size: 80 columns at 390 px stayed at the 55-column scale, 0.945 instead of 0.65. Any fit request now forgets the last fitted box, and so does a text-scale change that could not resize a hidden grid. A plain hide and show asks for no fit, so it still keeps pan and zoom. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
46079aa20b |
fix(lint): include .mts and .cts in line-limit checks (#22413)
Co-authored-by: DeryFerd <DeryFerd@users.noreply.github.com> |
||
|
|
745cde69cd |
fix(feedback): send text-only report when screenshots exceed the upload limit (#22508)
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
69839c253e |
feat(zcode): explain a ZCode build that has no terminal UI (#22730)
* feat(zcode): explain a ZCode build that has no terminal UI ZCode ships one agent runtime behind two front ends. The desktop app bundles it without `@zcode/tui`, because it draws its own window in Electron. Put that bundle on PATH as `zcode` and it answers `--version`, runs `-p` headlessly, and passes `zcode doctor` — so Orca detects it, launches it, and installs hooks against it, all successfully. Only the interactive session fails, leaving a bare Node stack trace in the pane that reads as a broken Orca integration. Watch a freshly launched ZCode pane's first output and replace that with an explanation: Orca's hooks are fine, this `zcode` just cannot open a session, install one that ships the TUI. The rule keys on Node's own module-resolution error rather than on the healthy build's "TUI requires an interactive terminal." message, because ZCode localizes the latter (`TUI 需要交互式终端。` in zh-CN) and matching it would miss every non-English user. Node's error is not translated and names the package. Scoped so it costs a healthy pane nothing: it runs only for a pane Orca launched as `zcode`, and only over the first 8 KiB, because a module-resolution failure happens before the runtime renders anything. Evidence: `src/main/runtime/__fixtures__/zcode-missing-tui.txt`, a recorded PTY capture of the desktop bundle refusing to start, per docs/reference/agent-pty-transcript-capture.md. Reported-by: JWu527 * refactor(zcode): ask the CLI if it can open a session instead of watching for the failure The stream watcher this replaces never fired. Before/after screenshots were identical and instrumentation showed the hook never ran, so the sidecar was both misplaced and racing a failure that lands ~440ms after spawn. Replace it with a direct question, answered once per run and cached. Reading zai-org/ZCode shows why running it is the only way to ask, and why the answer is unambiguous. `--version` and `doctor` are byte-identical in shape between a build that has the terminal UI and one that does not, because the TUI is only ever touched on the `tui` command path. There, `runTuiCommand` calls `loadTuiRuntime()` before anything else, and `runTui` checks for a TTY only after that module is already loaded. So with stdin at EOF: - no TUI -> fails in the loader -> Node's module-resolution error - has TUI -> loads, then declines -> "TUI requires an interactive terminal." The module error is therefore present exactly when the terminal UI is absent. All three shipping shapes land correctly: an npm/node-bundle install resolves `@zcode/tui` as a real package (esbuild marks it external, so it is never inlined), a SEA build always carries it as embedded assets, and the desktop app's bundled runtime carries neither. Verified against both real builds on this machine rather than a mock: the desktop bundle answers `missing-tui`, a CLI built from source answers `interactive`, and a command that does not exist answers `unknown` — the probe fails open so an unrelated spawn failure never accuses a working CLI. * feat(zcode): warn at launch when the installed zcode cannot open a session Wires the capability probe to the one place a ZCode launch is first known: terminal tab creation, which runs before the pane connects, so the explanation reaches the screen alongside the failure rather than after it. - main exposes the cached probe over `preflight:zcodeInteractiveCapability`, beside the other "what can the installed CLIs do" answers - the web preload stub answers `unknown`, because a paired client has no business deciding anything about the host's CLI install - the renderer notice is advisory: a probe that cannot run never blocks a launch Verified in the running app against the real desktop bundle: creating a ZCode workspace now shows "This ZCode build has no terminal UI" next to the stack trace, where before the trace stood alone. |
||
|
|
90801e2deb |
feat(agents): add first-class ZCode harness (#22464)
* feat(agents): add first-class ZCode harness Add ZCode (Z.ai's `zcode` CLI) as a supervised Orca agent: managed lifecycle hooks on local, SSH and Windows hosts; status, question and approval reporting; synthetic status titles; session resume; orchestration worker launch options; and desktop + mobile agent-picker registration. Written against the newly open-sourced `zai-org/ZCode` (agent CLI 0.16.9), not against a remembered screen: - ZCode's hook runner writes a Claude-compatible stdin alias set, so it routes through the existing Claude-compatible vendor path while keeping its own identity in the sidebar. - `PermissionRequest` fires only once the approval card is on screen and racing the user's answer, so it is proof the pane is blocked, not an auto-approval. - ZCode's clarification tool is literally `AskUserQuestion` with Claude's questions/options shape, so Orca's question card renders it unchanged. - ZCode's `hooks.enabled` defaults to false, which is why configured hooks were reported as never firing; the installer sets it. - ZCode renames its own process to `zcode-cli`, so the expected foreground process cannot be the launch command or dispatch refuses the pane. - ZCode emits no OSC title in any state and repaints its ASCII banner forever, so readiness comes from Orca's synthetic hook title and launch drafts wait on the composer box rather than on a quiet render window. Three files crossed their max-lines limit, so each is split along a real seam: command-line entrypoint parsing out of agent process recognition, skill classification out of skill root discovery, and registry coverage out of the remote hook installer tests. Refs #10564 * fix(zcode): drop the session-option catalog and pin the orchestration contract ZCode's CLI exposes no `--model` flag at all, and the session-option launch path refuses to apply any option until a model id is chosen. A catalog therefore could not deliver `--mode` per worker, and would have accepted `--model` only to drop it silently. Take opencode's position instead: no catalog, so `worker-start --model` is refused with a clear message and ZCode launches with the model from its own config. `--mode` stays reachable through agent args, which is also how the yolo default is applied. Add a contract test covering the parts that make ZCode a usable worker: dispatchable foreground process, stdin prompt delivery, the prompt staying out of the launch command, and the composer-gated draft paste. * refactor(zcode): reuse shared helpers and cut the harness down No behaviour change; every ZCode test still passes. - Use installer-utils' own `hookDefinitionHasManagedCommand` instead of re-walking a hook definition by hand, which also drops a local string reader. - Share one `readZCodeEventMap` instead of keeping the same narrowing in both hook-settings and hook-config-json. - Collapse five identical error returns into one `zcodeHookError` builder, and return early from the status branches instead of assigning through `let`. - Split the event-to-status decision out of `normalizeZCodeEvent` into a pure `readZCodeTurn`, so the normalizer reads as decide-then-build and stops computing the tool name for events that never look at it. - Take a script file name in `readManagedZCodeHookEvents` like its siblings, which removes a `Parameters<typeof …>` indirection at the call site. - Drop the unused `ZCodeHookEvent` export and inline a single-use path helper. - Correct a stale comment: ZCode's loader is a strict `JSON.parse`, so the in-place edit preserves key order and indentation, not comments. * fix(zcode): address review — keep unmanaged event keys, correct comment, de-dupe README - `removeZCodeManagedHooks` deleted any event key whose list ended up empty, so an unrelated `"Notification": []` the user wrote was removed as collateral whenever a managed hook elsewhere made the write happen. Only touch an event Orca actually owned something in; covered by a new regression test. - The `isNewTurnEvent` comment claimed UserPromptSubmit was ZCode's only turn boundary while the expression below it also returned true for SessionStart. Say what the code does: SessionStart lands the idle boundary, UserPromptSubmit is the turn boundary (the Codex/Claude shape). - ZCode appeared twice in the README's single agent-badge block; keep the local-icon entry the link checker validates and drop the favicon duplicate. * docs(zcode): call out that the desktop bundle's CLI cannot open a session From live testing on #22464: pointing `zcode` at the desktop app's bundled `glm/zcode.cjs` installs Orca's hooks fine but then fails with `Cannot find package '@zcode/tui'`, so the pane never opens a session. The symptom reads as a broken harness when the CLI simply has no TUI. Say which build to use and how to check before reporting a problem. Reported-by: JWu527 |
||
|
|
f5d2ce5de7 |
fix(native-chat): keep the background-task strip above a pending prompt card (#22779)
A pending approval or question card replaces the composer, but it rendered above the status block, so the running agents/shells strip dropped below the card. Render the cards in the composer's slot instead. Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
67fc894c8e |
refactor(orchestration): give structured sessions an orchestration actor column (#22522)
* refactor(orchestration): give structured sessions an orchestration actor column Adds nullable session:<id> actor columns to runs (coordinator) and dispatch_contexts (assignee, creator) at schema v42, a shared codec, a fill for rows that provably belong to a structured worker, and a coordinator mail-address cache that remembers a handle-less coordinator by its actor address. * test(orchestration): pin the actor columns, their fill, cache and v40/v41 upgrade paths * refactor(orchestration): fill structured-worker actors from one open-time call site * fix(orchestration): refuse terminal handles as session actors and clear the assignee actor on reassignment * test(orchestration): read the current schema version from its constant in the delivery downgrade contract The contract asserted user_version 41 after old code reopens a database current code wrote, so the v42 bump failed it. Assert SCHEMA_VERSION so the next bump cannot strand it; the pre-v41 pin and its v40 stamp stay. * fix(orchestration): count a Run's coordinator actor only at the generation it was written at A binary without the actor column rebinds and unbinds a Run by rewriting its handle and pane, which it cannot clear the actor beside. A rebind followed by an unbind leaves a row identical to a live chat binding. Both writes bump consumer_generation, which every binary already maintains, so the actor now carries the generation it was written at (coordinator_actor_generation, set in the same statement) and counts only while the two are equal. The coordinator cache, its triggers and the open-time fill read the actor through one rule in run-coordinator-actor; the fill also replaces an actor an older generation left behind. Still schema v42 (unreleased): the column joins migrate-v42 and the v42 skew-probe entries, so a database stamped v42 without it replays the chain. * refactor(orchestration): drop the unused coordinator-actor index and bare-id normalizer Nothing in this stack looks a Run up by coordinator_actor: callers load the Run and compare its current actor, so idx_runs_coordinator_actor would ship in every database with no reader. v42 is unreleased, so it leaves the migration rather than needing a later drop. normalizeOrchestrationActor had no caller outside its tests; bare session ids enter through sessionOrchestrationActor, and the handle-refusal cases stay covered there and in parseOrchestrationActor. * fix(orchestration): keep the coordinator-actor index the caller lookup needs The next step finds a caller's Runs with one statement that ORs a pane-leaf match with `coordinator_actor = ?`. SQLite splits that OR across two indexes only when both sides have one; without idx_runs_coordinator_actor the plan falls back to scanning every Run on each lookup. v42 is unreleased, so the index returns to migrate-v42 rather than needing a later schema step. * refactor(orchestration): store the Orca session id instead of an "actor" "Actor" read as a new concept when the columns only ever named a structured session. Rename them to what they hold: coordinator_orca_session_id (with its _generation), assignee_orca_session_id and creator_orca_session_id, plus the matching indexes, still added by migrate-v42 since v42 has not shipped. The columns now store the bare Orca session id rather than session:<id>. The session:<id> mail address is derived from ORCA_SESSION_ADDRESS_PREFIX where mail needs it: the coordinator address triggers and the cache refill share one SQL builder. isOrcaSessionId keeps refusing terminal-handle-shaped ids, and the generation rule and backfill evidence rules are unchanged. A dev database stamped v42 with the earlier *_actor columns replays the chain and gains the new ones. * fix(orchestration): remember every address a Run coordinator has, not the handle first The v42 coordinator triggers and the on-open refill stored one address, COALESCE(handle, session address), so a structured worker coordinator was remembered by its handle only. Remember each address the coordinator has, its handle and its current session address, each where present, so this cache follows the same rule as bindRun and no precedence is persisted. * docs(orchestration): define the Orca session id without a variable this change does not add The shared codec's comment named ORCA_AGENT_SESSION_ID, which nothing in this change defines, and ran one line past the wrap. It now says the stored id is the one the agent is addressed by (a /clear'd chat's lineage root), as the column comments do, and that PTY agents have none today rather than never. migrate-v42's note stated the lineage rule twice; it is folded into one sentence. |
||
|
|
5c45337a6f |
fix(terminal): make Codex restart replace the pane's process instead of reattaching it (#22737)
* fix(terminal): make Codex restart replace the pane's process instead of reattaching it A spawn for a pane that still has a live process is treated as a reattach. Both Codex restart paths raced that: the open-pane restart killed the old PTY without waiting and then spawned, and the unmounted-tab restart spawned before killing. Either way the "fresh" spawn could re-adopt the old Codex (dialog comes back) or hit the half-killed session and leave a plain shell. pty:spawn now accepts replacesPtyId. Main stops that PTY and waits for the exit before the spawn resolves the pane owner, so the existing dead-owner path launches fresh and records the current Codex home. Both restart paths send it and no longer kill the old PTY themselves. Fixes #18174 * fix(terminal): send the replaced PTY once and keep the hidden-pane replacement Two follow-ups to the restart handoff: - The replaced PTY id rode the transport options, which outlive the first spawn. A later fresh spawn from the same pane (for example a hibernation wake) re-sent it, so main stopped that id again; SSH relay ids restart at pty-1, so it could name another pane's PTY. It is now a connection input consumed by the first fresh spawn only. - The hidden-tab restart still treated a changed binding after the spawn as a reason to stand down and reap the replacement. Main has already stopped the old PTY by then, so its exit can clear the tab binding (a background-launch exit observer does) or a pane can mount, and standing down left the pane with no process. It now keeps the replacement unless the tab or leaf was actually taken over. * fix(terminal): hold the pane while a restart stops its old process A restart spawn stopped the pane's old process before reserving the pane, so a hidden tab revealed during the stop could reattach the dying process (and the restart could then join that reattach and return the old id). The replacing spawn now reserves the pane before the stop, so any spawn for the pane in that window joins the replacement; a failed stop still settles the reservation. The hidden-tab restart also tombstones the old PTY's buffered exit before spawning, so a reveal mid-restart reconnects by pane identity instead of replaying that exit into the pane. * fix(terminal): label a restart's replaced-PTY exit and keep its stop owed until sent Main now marks the PTY it stops for a replacing spawn and stamps `replacedByRestart` on that PTY's exit (provider-observed, synthetic, and SSH-unregistered stops all reach the renderer through the same finalize step). A failed stop clears the mark with nothing sent; an undelivered mark expires. The renderer classifies the labeled exit before any consumer runs: parked-tab watchers end their subscription instead of collapsing the leaf or closing the tab, the pre-attach buffer discards instead of queuing a death for a later mount, and a mounted pane treats it as an intentional restart. With that, the hidden-tab restart no longer tears down its parked watchers and buffered output before spawning; it releases them only after the swap, so a stop that fails leaves the still-running Codex fully observed. The visible restart's pane session now holds the replaced PTY until a spawn request actually carries it (the IPC transport claims it as it sends). If the pane is closed or parked first, disposal stops that PTY with an ordinary kill instead of orphaning it. * test(terminal): type restart test fakes and merge a duplicate import Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
fec8fe4822 |
fix(native-chat): offer a resume for every chat that was working, and say what it was doing (#22560)
* fix(native-chat): offer a resume for every chat that was working, and say what it was doing * fix(native-chat): keep a child-work resume offer after the chat is reopened The offer's cut-off work was read from the items' current state, so once the chat was opened (reattaching the provider, which rewrites the rows it lost), a chat offered for its stopped subagents or background tasks dropped out of the dialog and its retry. Judge the revision rows written after the marker's cursor instead: they only accumulate, so the reading is the same before and after reattach, and the per-run pre-reattach copy is no longer needed. Also pass the lease fence into the shared "shows work" check, as the status feed does, so a send stranded under an older fence cannot hold an unheld session's provider alive while the sidebar shows it idle. * refactor(native-chat): offer is the working check taken right before each child stops Every review loop found the same bug class in the "is this offer still owed?" re-derivation that ran when a child was stopped and again after restart. It re-read a journal the provider had already rewritten on reattach (a notice turn of its own, restated subagent rows) and kept misreading it: a send made after an earlier completed turn was dropped at shutdown, and Claude's notice turn refused the resume after restart. - Teardown now snapshots each session with the sidebar's working check in a new eviction step right before its provider child is stopped (after draining events Orca already accepted), and keeps it once the stop is proven. The capture-first/confirm-on-stop re-judgment is gone. - After restart an offer is withdrawn only by a newer user message, dismissal or expiry, beside the structural checks (record, support, lease, no fork). Listing, retryable and the pre-send check no longer re-derive work state. - The pre-send barrier drains again while a provider keeps streaming instead of refusing; both providers queue a message sent mid-turn. - Row activity stays a display-only read of the rows after the marker's journal cursor. * test(native-chat): pin the re-drained admission barrier and the proven-stop gate on offers * test(native-chat): cover a send-shaped offer across a closed provider notice turn * refactor(native-chat): drop the pre-stop drain nothing depended on * docs(native-chat): align marker and retry comments with the simplified offer rule * fix(native-chat): an unfinished admission drain no longer refuses the restart continuation The drain before the continuation's pre-send check refused the send when provider events were still arriving at its 2 s bound or the barrier failed. That refusal journaled the continuation as rejected, so its own message then read as the user moving on and the offer could never be retried. The drain is now best effort and the pre-send check judges what the journal holds. Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): pin a stalled or failed admission barrier dispatching once Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): name a cut-off reply from the settlement's row, not the turn as restated A reattached provider may restate the offer's turn, so the row's mid-reply label read off the turn's current state could vanish and count the cut-off reply's own tool calls as background commands. Also corrects the ineligible-offer comment to match the user-moved-on rule. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(native-chat): the offer is a stop-time snapshot on the marker The marker records the main agent's own state, the pending prompts and the live child roster at the moment before its child stops; the dialog row, status bar and candidate read only that snapshot. Deletes the journal cut-off reader, journalCursor, the offer TTL and the pre-send admission drain. Superseded offers and chat closes now delete their records. * test(native-chat): every offer ending deletes the durable record * fix(native-chat): preserve restart offers on unreadable journals * fix(native-chat): release idle sessions with pending sends * fix(native-chat): keep a message held while the CLI starts from being released Idle release had been switched to the sidebar's working check minus pending sends, which dropped the rule that a send held while the provider CLI is still starting keeps the session. An unheld chat left during startup was then evicted and its message refused. Restore the release rule this branch never needed to change: an open turn, or a pending send while the child is starting. Also keeps the host file within its line budget. * fix(native-chat): delete a restart offer whose conversation forked A forked conversation can never become the marked one again, but its offer was only skipped, so with no expiry it sat unseen in the recovery file forever. Report it for deletion on the same path as a newer user message. Also corrects comments that still called listing read-only. * fix(i18n): keep Agent untranslated in the Japanese subagent activity rows The catalog keeps Agent in English for Japanese, and the localization gate rejects the translated form. * fix(native-chat): don't call a mid-reply command monitoring in the resume dialog The live task roster also lists the foreground command a reply is running, so a chat stopped mid-command read "Was mid-reply · Monitoring: <command>". Speak monitoring only for an idle lead, as the sidebar does; the row's tooltip still names every task. * chore(native-chat): state the mid-reply label rule exactly --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
e0144a9eb6 |
fix(native-chat): show the Codex and Claude model picker the moment a chat opens (#22756)
* fix(native-chat): show the Codex and Claude model picker the moment a chat opens A new structured chat showed no model picker until its session had been created, spawned, initialized and had answered a model listing — and the picker then listed the models a second time. Codex's listing often goes to the network, so the picker took 0.6-2 s to appear. - Keep a host-owned model catalog per agent and account home, persisted on success only and refreshed in the background once it ages out. A new read-only agentSession.modelCatalog RPC answers from it without a live session; sessions reuse it instead of listing again. - Render the picker while the launch is still provisional, showing the saved default. A pick made before the session exists is held and applied once it publishes; only the host's acceptance saves it as the default. - Mark the model and effort set in the user's Codex config as the listing's default (config/read), so the first frame names what the chat will run. - Resolve the account a record-less read would use without running launch preparation, which writes and syncs account state. * fix(codex): disable plugins in the model catalog probe app-server * fix(native-chat): read the host model catalog only for panes on this machine * fix(native-chat): name a pre-report model only for a chat this view launched * test(native-chat): pin the launch latch across publish * test(native-chat): pin a held pick reaching the host before the first send * fix(claude): pin the catalog probe's config dir by the session spawn's rule * fix(native-chat): read the host model catalog only for a visible chat * fix(native-chat): rewrite the model catalog file only when a listing changes * test(native-chat): type-check the first-send order fixture * refactor(native-chat): keep the structured options hook under the line cap * fix(native-chat): send the first turn only after every pick held during launch settles * fix(claude): name no default effort from the catalog probe listing * fix(native-chat): name no listed default model for a chat resumed from history * refactor(native-chat): let the launch own picks made before it publishes A pick made while a chat launches had no fence to go to, so the pane held it and flushed it after publish; every other sender (the outbox, the launch prompt) then needed its own gate to wait for that flush. The launch now keeps those picks in its own state, applies them against the create receipt's fence before it counts as published, and every sender follows publish by construction. The pane flush, the outbox gate and the module-wide held-pick registry are gone. The launch also snapshots the saved selection its create seeds when the intent is built, so a pick in another chat no longer relabels one still launching, and a pick the host refuses is reported the way a refused mid-session pick is. * fix(native-chat): name no default model a workspace's own config can replace The catalog's default is the account's, read without a working directory, but a chat runs in its worktree, where a project config (Codex's .codex/config.toml between the project root and the worktree, or a Claude .claude settings file that sets a model) picks the model instead. The picker named the account default there while the chat ran the project's model. A new chat's catalog read now names its worktree. The host checks that workspace for such config (existence only for Codex, the model key for Claude) and, when any is present or the workspace is not a local directory, serves the listing with no default, so the picker names nothing until the chat reports its model. * fix(native-chat): name the listed default model before the report only for Codex * fix(claude): let an option pick made while Claude starts wait for it instead of being refused * fix(codex): name no listed default when the configured model is not in the listing * fix(native-chat): show the picker as unavailable until a published chat attaches * fix(codex): keep the catalog probe's listing when config/read stalls * fix(native-chat): write the pending model catalog save before quit * chore: drop an unrelated lockfile rewrite * fix(native-chat): rename the catalog store's listing parameter off the global fetch name * chore: drop an unrelated lockfile rewrite * fix(native-chat): name the model Claude will run before its first turn * fix(native-chat): keep Claude's pre-turn applied effort out of the saved session options |
||
|
|
f9356d491a |
fix(release): run the tag's own skill freshness inventory tests in the release gate (#22775)
The skill-sharing release gate checks out the release tag, then restores main's copy of skill-freshness-inventory.test.ts. That file holds behaviour tests, so a behaviour test added on main runs against a tag that predates the behaviour: #22606 added one and failed the v1.4.211 gate on macOS and Linux. Keep restoring skill-provider-runtime-roots.test.ts from the workflow ref. |
||
|
|
8f7cbad07b |
feat(mobile): name the machine after pairing (#22104)
* feat(mobile): confirm host identity after pairing * refactor(mobile): unify host descriptor state * chore(i18n): translate the last-known host descriptor label The remote-host row's "Last known" label shipped in English only. Every other locale now carries it, worded as each catalog already words "last known". * fix(mobile): make the pairing naming step safe to abandon and show the machine live Pairing: - An unreadable status.get reply no longer strands the pairing race: the descriptor read ran inside the race's success handler and threw, so the candidate was never counted and a direct-only pairing sat on "Connecting..." until the timeout. The race now reads the status through a reader that returns null instead of throwing. - A pending pairing is now a small state machine: a save in flight owns the outcome (Cancel, back, or unmount no longer clear the journal under it), a failed save stays pending and the naming screen shows the error with Save still available, and Cancel never rejects. - When the desktop refuses relay provisioning, the journal is cleared before the naming step instead of at save, so an app kill on that screen no longer blocks every later scan with "recovery pending". - A pairing that resolves after the screen went away is cancelled rather than left with its journal. - The screens keep their root ref callbacks stable (the latest pending pairing is read from a ref) instead of re-creating them per pairing. - The label field's placeholder shows the name that an empty label saves. - "Is this an existing host" is derived from the id identity resolution hands back, so host-store and its tests go back to main's shape. Machine descriptor: - Mobile keeps the host-reported machine name and OS in memory only, filled by the status reads that already happen, and labels it "Last known" from the row's own connection state. This drops the per-host AsyncStorage copy, its web sibling and web-overrides entry, and the removal cleanup. It also fixes a latch: freshness used to stay true for the whole process once a host had answered. - Desktop reads the descriptor through lastVerifiedRuntimeStatus and marks it "Last known" using the same reachability verdict as the row's dot. - Drops the unused hostname/previousPlatform resolver inputs and moves the "OS · machine" formatting into the shared resolver. Also restores main's page-only Reconnect gate in the host header (#22326), undoes the no-op toStoredHostProfile reformat, and re-measures the web session route at 4222 modules (one fewer: the dropped web persistence file). * test(mobile): re-record RPC goldens for the deferred pairing save Repins baseline to the pairing fix commit and re-records every golden. Against main, 781 goldens move only in the header (baseline everywhere, adapterSha256 for the pairing adapter family). Six pairing goldens move in the body, and only in effect order: the pairing now resolves (closing its candidate sockets) before the naming step saves the host, and a refused relay provision clears its journal before the save rather than after. The set of effects and every outcome match main. This also removes the unhandled-rejection effects and the failed result-absent/result-null cells that the previous recording captured from the pairing race's throwing status read. * fix(mobile): keep the machine name out of the host header title while the label loads The host screen starts with an empty saved label and loads it asynchronously. With a descriptor already in memory, the resolver fell back to the machine name as the title for that window, so opening "Windows-Low Spec" briefly titled the header with the Mac's name. Read the descriptor only once the label is known. * test(mobile): build the pending-pairing status through its schema so the test typechecks The mobile tests typecheck ratchet rejected a partial status literal: the reply type keeps every optional field as a required key. Parsing the literal through the status schema yields that shape without a type assertion. * test(mobile-web): re-pin the session route closure after merging main #22301 added two src/shared modules this route reaches; its own CI never ran this suite, so the merged branch read 4224 against the 4222 pin. Measured on the merge. * feat(mobile): name each host by what its desktop reports Pairing saves the host immediately again and names it after the machine name the desktop publishes; every connection refreshes that name and OS, so a rename on the desktop reaches the phone. A name typed on the phone's Edit screen is kept as a phone-local override that wins; clearing it returns to the desktop's name. - Stored host profiles gain optional personalName, lastKnownMachineName and lastKnownHostPlatform; `name` stays the resolved value older builds read. Legacy records classify a generated "Host N" as desktop-named and any other name as a phone override. - The connection layer runs one retrying status probe per connected host and records the descriptor; the capability probe becomes a projection of it. - Rows and the header always show the OS, add the machine name under a phone override that hides it, and mark it "Last known" while the host is offline. - Removes the deferred-save naming screen and the one-shot home status fetch. - Name rules move to host-name-identity.ts and the host-list mutation queue to host-list-mutation-queue.ts; an unchanged mutation no longer rewrites storage. * test(mobile): restore the RPC recordings to main's Pairing saves the host back to back again and the recording adapter is main's, so every recording matches main byte for byte; the earlier deferred-save re-record and its baseline repin no longer apply. * fix(mobile): keep stored host name identity across snapshot saves and on the web page A connection re-saves its host profile snapshot on relay credential rotation or relay re-resolution. The re-pair merge let that snapshot's name identity win, so a phone rename cleared or changed since connect came back, and a newer desktop machine name rolled back. The stored record now keeps the name identity on every save; the save supplies the rest. The web page receives only the app's resolved host name, with no identity fields, so the docked host header treated a phone rename as "no override" and titled the host with the live machine name. The display hook now reads such a source the way storage reads a legacy record: a non-generated name is the user's label. * fix(mobile): hand the web page the host's stored name identity The page received only the app's resolved host name, so it had to guess whether that name was the phone's override or the desktop's name. It guessed "override" for any non-generated name, which froze a desktop-adopted name as the title after the desktop was renamed, and left an offline page header without the last-known OS and machine name. The shell now puts the stored personalName and last-known descriptor on the init host as optional fields. The page reads them exactly as the app does; a page handed its host by an older shell still falls back to classifying the name, and an older page ignores the fields. * fix(mobile): drop an unreadable host name field, not the whole host The stored host record and the page's init host checked the platform against a closed list and required non-empty names. A value this build does not know, such as a platform added in a later build, failed the whole record: the host list dropped the paired host and the next write persisted the list without it, and the page refused its init message. Those three optional fields are now salvaged, so an unreadable value drops only that field. Also states the one exception to the stored name rule (an OS reported without a machine name keeps the adopted name), and brings four mobile test files in line with the branch: the edit screen now saves `personalName`, and host opens now start a descriptor status probe. * refactor(mobile): name the shared host name fields for their role * fix(mobile): drop the "Last known" prefix from the host machine line The OS and machine name line under a host's name reads the same whether or not the host is connected; the connection status already says when it is offline, and a prefix that users could read as applying to the name added nothing. Removes the resolver's liveness input and the desktop row's translation key. |
||
|
|
8fa5883217 |
fix(terminal): stop dropping visible alt-screen output tagged as hidden-resize repaint (#22587)
* fix(terminal): stop dropping visible alt-screen output tagged as hidden-resize repaint After a resize that main received while a PTY was hidden, main tagged the next chunk it accepted as background. The hidden delivery gate drops chunks before they reach that tagging, so the tag outlived the hidden period and landed on the first chunk after reveal. On the alternate screen the renderer dropped that chunk without a model restore and "repaired" it with a cols-1/cols resize pulse. OpenTUI debounces SIGWINCH and ignores an unchanged size, so OpenCode's full post-reveal repaint was lost for good. Remove the hidden-resize tag lifecycle in main and the renderer's background alt-screen skip and pulse. Reveal already restores from the host model and queues live chunks until the restore completes, so later chunks are in order with the model and are written like any other chunk. The snapshot-restore path that omits a too-wide alt frame no longer pulses either: when the fit lands back on the capture grid (no real SIGWINCH), it requests a fresh model restore instead of relying on the app to repaint. * fix(terminal): restore a dropped reattach alt frame from the model when the fit lands on its capture grid The reattach replay omits a too-wide alt frame and relied on the grid push's resize plus SIGWINCH to make the app repaint it. When the fit lands back on the capture grid that is a same-size signal, which OpenTUI ignores, so the alt screen stayed blank. Record the omitted frame's capture width and request a model restore after the grid push in that case. The reattach fit moves to reattach-grid-fit.ts to keep apply-reattach-payload.ts within its line budget. * test(terminal): drop type assertions from the reattach alt-frame test * test(terminal): dispose the reattach alt-frame test's pane connection |
||
|
|
2c7609bf6a |
fix(terminal): serialize only the visible width after a column shrink (#22586)
* fix(terminal): serialize only the visible width after a column shrink xterm does not reflow the alternate buffer (or a normal buffer under pre-21376 ConPTY), so after a shrink each line keeps its old length. SerializeAddon walked every non-final row to line.length, so any snapshot taken after a shrink carried stale right-hand cells that wrapped into extra rows on replay; restores repainted that garbage and a differential TUI such as OpenCode never cleared it. Clamp the row walk and the wrap-boundary lookups to the terminal's columns in Orca's addon-serialize source patch, and regenerate the bundles, maps and lockfile hash per docs/reference/xterm-patch-regeneration.md. * test(terminal): read shrink-snapshot fixtures through public APIs Drops the private-terminal casts the casting gate flags; the normal-buffer case now drives a plain pre-21376 ConPTY terminal and its SerializeAddon directly. * fix(terminal): blank a wide glyph clipped by a column shrink when serializing After a non-reflowing shrink a width-2 glyph can have its lead half in the last column and its trailing half past the grid. Serializing the lead half makes the replay wrap it to the next row and shift every row below, so serialize that cell as a blank and keep the row exactly the grid's width. A glyph ending exactly at the edge is unchanged. * test(mobile): move the session closure pin past the main agent status modules #22452 added src/shared/main-agent-status.ts and src/shared/agent-turn-outcome.ts, which agent-status-types.ts imports, so the session route's closure grew by two local modules (4218 -> 4220). That change was src/shared-only, so its own CI never ran this suite; main has been at 4220 since, and any PR that fires the mobile web app job fails on the stale pin. Measured on |
||
|
|
a05649de91 |
fix(terminal): prove an idle Git Bash prompt through its bin launcher (#22752)
* fix(terminal): prove an idle Git Bash prompt through its bin launcher Git for Windows' bin\bash.exe is a launcher that runs usr\bin\bash.exe as a child and waits, so an idle Git Bash pane's job always holds two pids and the Windows shell proof never confirmed it. Accept exactly the launcher plus its direct bash.exe child, checked against the identity process table. * test(terminal): wait for the Git Bash prompt before asserting the hand-off job * fix(terminal): prove a Git Bash prompt as one unbranched MSYS bash chain Orca launches Git Bash as bin\bash.exe -c "chcp.com ...; exec \"$BASH\" ... -i", and each MSYS exec leaves its pre-exec process alive as a stub, so an idle pane's job is launcher -> stub -> interactive bash. Accept any job that is one parent-to-child chain rooted at the launcher whose every later member is bash.exe, instead of a fixed two-process shape. * ci: register the Git Bash shell-proof win32 test in the package-test list * fix(terminal): read the spawned shell as a path, and keep one shell map A spawned shell path with a space (/Users/John Doe/bin/zsh) was split as a command line, so the POSIX proof compared against "john" and never confirmed. Local panes now keep only the spawned shell path and derive the name from it; the Git Bash chain walk drops guards the member check already covers. |
||
|
|
f559c0588a |
fix(terminal): ground a program that dies with input modes armed on the normal screen (#22739)
* fix(daemon): rebase durable checkpoints on the live terminal A durable checkpoint was folded from the previous checkpoint plus recorded output, so it inherited that checkpoint's modes forever. After a daemon restart killed a full-screen TUI and a new process started inline, the chain kept the dead TUI's alt screen and mouse tracking (?1049h ?1003h ?1006h) while the live emulator was clean. Every reattach and getBufferSnapshot served the stale chain, the renderer re-armed mouse tracking, and wheel scrolling went to a program that never asked for it: scrolling froze. Each full checkpoint is now the live snapshot verbatim (screen, layout, alt frame, modes, owner) with only the normal-buffer rows live has evicted taken from the durable replay. A checkpoint can no longer carry a dead process's modes, and checkpoints already poisoned on disk heal on the next compaction. - The first fold after a cold restore replays the same seed segments live was given, so rows line up even over a dead TUI's alt screen. - Idle zero-record folds keep the disk copy when it already agrees with live, so quit and relaunch bursts don't replay every session. - Held teardown bytes are already in the drained records and the live snapshot, so they are no longer replayed twice or appended as a tail. - The bounded getBufferSnapshot path honors the requested depth even when the live window is deeper, without phantom link rows. - The fold's ownership scanner and frame merge are removed; owner and frame come from live. * fix(terminal): one process-boundary ground for every known or proven boundary Three copies of the "the process that armed these modes is gone" reset had drifted: the cold-restore seed cleared only pen and mouse, the recovery barrier used the renderer's dead-TUI profile, and the cold-restore payload had none. A cold restore therefore left the dead process's focus reporting, bracketed paste, application cursor and keypad modes armed in the live emulator, the first checkpoint, and main's mirror. And the seed wrote the dead process's torn escape after the reset, so the new shell's first bytes could complete it (for example retitling the pane). PROCESS_BOUNDARY_GROUND replaces them: CAN, leave the alt screen without moving the normal-buffer cursor, every mouse protocol and encoding off, focus/paste/app-cursor/keypad off, cursor shown and style reset, kitty popped, SGR reset, grounded DECSC. It stays inert for the lifecycle scanner. The seed, the recovery barrier, and the cold-restore payload all use it, and the seed no longer carries the torn tail. The first fold after a cold restore now always rebases on live, because focus and keypad are not in TerminalModes and the zero-record shortcut could not see them differ. * fix(terminal): ground a program that dies with input modes armed on the normal screen The daemon's in-stream crash detector only fired when a program died with the alternate screen up. A normal-buffer program that armed mouse tracking, focus reporting, keypad or kitty keyboard flags and exited without disabling them was cleaned up only in the renderer, so the daemon kept the modes and re-armed them on the next reattach, mobile included (#13077's garbage-at-the-prompt family). The lifecycle scanner now tracks armed input modes (mouse protocols and encodings, ?1004, ?66, and kitty flags as per-screen stacks that mirror xterm's main/alt swap and its 16-entry cap). ?2004 and ?1 are excluded: shells arm them at their own prompts. Modes armed when a command starts (OSC 133;C) count as the shell's, so a prompt that leaves modes on never triggers. At OSC 133;D the trigger is now "alt screen or a program-armed input mode", still one-shot and still gated by the shell proof, and the existing PROCESS_BOUNDARY_GROUND is recorded through the stream so live and durable history change together. WSL panes spawn wsl.exe, which the shell proof does not recognise, so the detector never grounds them; a test pins that and the renderer keeps covering them. The mouse-leak e2e now keeps its arming process alive until the live pane is checked, because the daemon grounds a proven exit. * fix(terminal): keep shell- and host-armed input modes through the process-boundary ground ConPTY arms focus reporting (?1004h) before the first prompt, and the live recovery ground cleared it for the rest of the pane. The barrier now re-arms the modes that were on at OSC 133;C right after the ground, so only the dead program's modes are reset. * fix(terminal): re-assert only modes the shell or host armed outside a command A mode a program leaked past a refuted proof was still on at the next OSC 133;C, so the baseline snapshot re-armed it after a later ground. Record who armed each mode instead: only enables outside a command (before any marker, or between 133;A/D and C) form the baseline. * fix(daemon): keep OSC links and kitty flags through durable checkpoint folds and trims Stop seeding persisted OSC link ranges into the fold replay: they index the base buffer, so rows evicted by pending output left a link on the wrong text. The serializer already writes OSC 8 into the ANSI the fold replays. Re-apply kitty keyboard flags when replaying a snapshot for trimming, since rehydrateSequences omits them. Bound a smaller restore request by trimming the committed checkpoint instead of re-reading disk and rebasing the live window at a smaller depth. * test(daemon): follow the isFirstTake rename in the process-boundary ground suite * refactor(daemon): drop the unreachable deep-live branch from the durable fold The live window's override cap now derives from the restore depth, so live can never be deeper than the fold. pendingRecords and isFirstTake are required. * test(daemon): pass pendingRecords to the process-boundary ground fold * fix(terminal): reset alt-screen kitty flags in the process boundary ground Kitty keyboard stacks are per screen, so resetting only after ?1049l left a dead TUI's alt-screen flags for the next alt-screen app. Also drop the inert CAN from the ground (every site grounds after complete bytes) and correct two stale comments. * fix(terminal): track input-mode ownership in one map Each armed mode now has one owner: host (before any marker, or a prompt a 133;C proved), prompt (unproven until C), command, or stale (left past a D). Host arming is sticky, 133;D demotes command modes (the one-shot), and the ground re-asserts only host modes. Fixes a D without C triggering on host modes, an ESC c mid-command turning later enables into host modes, and a program's repeated host enable dropping host ownership. The reattach e2e now keeps the arming program alive so only the reattach reset can disarm it. * refactor(terminal): stop treating kitty flags as host state fish, the one shell that pushes kitty flags at its prompt, pops them before running a command and re-pushes at the next prompt, so the ground never needs to restore them. Only host private modes are re-asserted now. * fix(terminal): keep host input-mode ownership across RIS ConPTY answers a mid-command ESC c by re-sending ?1004h, which reset() had recorded as the command's, so the ground turned host focus reporting off for the rest of the pane. RIS now drops only non-host ownership. * fix(terminal): let only the host own focus reporting and leave it in the ground Host ownership covered every mode armed before the first marker, so a tmux that died with mouse on had it re-armed by the ground. And the re-assert's ?1004h enable made the runtime's ownership mirror revoke, so remote owners never settled on Windows. Only ?1004 can be host-owned now, and the ground skips its ?1004l instead of turning it off and back on, so injected bytes carry no enables. |
||
|
|
58ba75b5a5 |
feat(agent-status): child work records say what the child is doing, how it ended, and when (#22521)
* test(agent-status): pin the legacy child-work projection of published background tasks * feat(agent-status): child work records say what the child is doing, how it ended, and when A child-work record gains the facts every surface needs from one host-owned record: the child that owns it (parentChildWorkId), whether the provider said it may outlive its launch turn (residency, host-only), what it is doing now (operation, with an open/reported basis), the newest thing it said (lastMessage), and when its current invocation settled (settledAt, stamped by admission, never by a producer). The codec enforces one membership x state legality matrix: live work is never done and carries no outcome or settle time; only a shell or monitor stores monitoring; settled work is done with an outcome and a settle time inside its own evidence window; an operation exists only while live and working, waiting or blocked. A settled record written without an outcome reads as unknown, never success. Malformed descriptive fields drop and keep the record. A new read-only view (AgentChildWorkView) is the one projection surfaces read; the legacy subagent and background-task shapes are derived from it with today's output unchanged for today's inputs. deriveAgentChildDisplayState folds a child's own state and the liveness of the work it owns through the same fold a parent row uses, so a child whose own work is idle or done reads monitoring while a shell it launched runs. Codex children get a thread_id alias kind. * fix(agent-status): an unknown child ending can gain its real outcome; operation clock clamped A settled child whose ending was first recorded as unknown (a roster omission can land a tick before the frame naming the outcome) now accepts the definite outcome for the same invocation and keeps its original settle time. A definite ending still never changes, and a later unknown ending is ignored rather than downgrading it. Admission clamps operation.observedAt into the child's evidence window, so an operation stamped in provider time is kept instead of silently dropped. The record codec is pinned as host-internal: it rejects a whole record over one unknown key, so a ratchet test fails if anything outside the host store and admission path imports it. * test(agent-status): pass the fold-parity input as a value; name the hook lane's alias kinds * fix(agent-status): a sparse child observation never erases what the record already knows Admission merged a later observation by replacing the whole record, so an ending that knew only that the child was gone dropped its name, model and token count, and an outcome refinement dropped the recorded last message. Labels now fill or replace but never clear, tokens never shrink, and a settled ending keeps its last message unless new evidence carries one. The provider-id preference is keyed by alias kind so a new kind cannot compile without a rank. * fix(agent-status): a child's owner, residency and last message outlive a sparse observation A settle that knows only that the child is gone dropped who owned it and whether it ran in the background, and the last thing the child said while live. They now survive like the labels do: the last message for its invocation, owner and residency for the child. * fix(agent-status): an unknown ending keeps a definite outcome and still lands its evidence A settled child's later `unknown` (or omitted) ending was acknowledged without a write, so a late last message, token count, alias or reclassification it carried was dropped while the caller was told it was accepted. The outcome now merges like every other sparse fact: an `unknown` claims nothing and keeps the stored definite outcome, and only a different definite ending conflicts. * fix(agent-status): group child aliases and owned work in one pass Appending by spread copied each bucket on every insert, quadratic in a bucket's size on the projection and per-row liveness paths. * fix(agent-status): group child aliases and owned work without Map.groupBy The relay runs this core on Node 18, which lacks Map.groupBy; a plain loop into a Map is equally linear and portable. * fix(agent-status): a child's activity and last message survive the codec Admission folded raw provider text with the status-row normalizer, which can leave a tab or other control character and can end a truncation on a space. The record codec drops such a field, so a long command cut at a space, a tab in a command, or an escape in a message silently erased the child's current operation or last message. Admission now folds control characters to spaces and trims the cut, with the codec's own control-character predicate. * refactor(agent-status): parse child facts, merge, then check the record Admission merged provider values before anything knew they were valid, and the codec then either rejected the whole record or silently dropped the field depending on how old the field was. A malformed owner erased the stored one, a label cut on a space rejected the announce, and a bad token count blocked a settle. Admission now parses every descriptive fact into a value the codec accepts or "not said", merges it over the stored record with one rule per fact (a typed map, so a new request field without a rule fails to compile), and the codec checks the result. Text goes through one normalizer and the codec accepts exactly its image; any value outside it is a writer bug and rejects. The owner is now a fact of the invocation, like the last message. * refactor(agent-status): name each erasure row by what makes its value malformed * refactor(agent-status): pin the token parse where the max-merge cannot hide it * fix(agent-status): provider timing lasts only for its own run providerTiming records the provider's start and end of one run. Keeping it across a resume left a live restarted child claiming the previous run's completion time. It now follows the owner and last message: kept within an invocation, reset by a new one. |
||
|
|
eb746a6d32 |
fix(codex): a Codex native chat that never sent a message reopens after restart (#22639)
* fix(codex): start a new thread when a chat's thread was never saved A structured Codex chat records its thread at create time, but Codex writes no rollout until the first input. After a restart, launch resumed that thread, Codex answered "no rollout found for thread id", and the chat could never run again. When the head of the handle chain is the session's own creation and Codex answers that exact error for that exact thread, start a new thread instead. The new link supersedes the unsaved creation in place and names it, so the chain keeps one live identity and does not grow across restarts. A thread a resume, fork or adoption proved is never superseded, and no other resume error starts fresh. * test(codex): build launch-resolution chains without a type assertion * fix(codex): match only Codex's own no-rollout text, pinned through the real connection The fallback matched Orca's own error-wrapper prefix too, and every test built that string itself, so rewording the wrapper would have disabled the fallback with the suite green. Match the method, code -32600 and Codex's exact detail as the message suffix, and drive Codex's raw error frame through the real connection in a test. The link builder now refuses, at the type level, a supersession on an adopted or resumed link, which the chain would reject downstream anyway. * docs(codex): note why the no-rollout text is safe on the resume path |
||
|
|
a0e24905f6 |
fix(agent-status): a cancel never hides live work (#22476)
* fix(agent-status): a cancel never hides live work After the user cancels a turn, a background shell, scheduled check or subagent that is still running keeps reading as it truly is in both lanes. The fold no longer takes a verdict input; the cancellation survives only as lead.outcome, restated as the row's interrupted flag on a settled row for readers that predate lead. * fix(agent-status): keep a cancel's verdict and clock on every settle path A Grok turn cancelled while a task ran now reads monitoring, and the idle_prompt backstop that later settles it restated done without the row's `interrupted` flag, so notification readers announced the cancelled turn as a clean finish. Derive `interrupted` from the main agent's outcome, as the Claude builder already does. The inferred Claude cancel now folds through the host's local main agent record, which a relayed pane never refreshes, so a second cancel on an SSH pane inherited the first cancel's clock. The caller admits only a working main agent, so the cancel always starts a new done clock. * fix(agent-status): keep the shell fact on an inferred cancel so restart can seed it An inferred Ctrl+C cancel beside a working subagent publishes a row held open by child work, but the synthesized event dropped the row's paired claudeRunningNonAgentTask fact because mainAgent changed. Hydration seeds a settled main agent only when that fact says no shell ran, so after a restart the child's drain left the row working with no mainAgent. Carry the fact forward: a cancel does not change what the shell inventory said. * fix(agent-status): a Ctrl+C at an idle main agent's prompt cancels nothing Every row that publishes the main agent fact now admits an inferred cancel only while that main agent is working. Grok's Ctrl+C at the idle prompt leaves its background task running, so settling the monitoring row to done hid live work. Rows without the fact keep the evidence guard, and Codex keeps it too because its synthesized row is a plain done. * fix(agent-status): fold a relayed pane's cancel from its row, not the desktop's records The inferred Claude cancel read and wrote the desktop's own listener records for every pane. For an SSH pane those records are not the relay's: hydration seeds them from the saved row and nothing reaps them, so a subagent that finished on the remote after a desktop restart kept a cancelled row spinning with nothing running. A local pane still records the verdict on its listener and folds its own roster; a relayed pane folds only the child work its row carries. The relayed-pane parameter and forced clock the shared record path grew for this are gone. * fix(agent-status): hold a cancel verdict in the store until a new turn or the provider's own A relay never learns of the cancel the desktop infers from Ctrl+C, so its next child hook or reconnect replay restated the main agent as working and flipped the row back. The late-hook suppression that guarded this keyed on a done row flagged interrupted, which a cancel held open by a shell or subagent no longer is; it also dropped Grok's own stop_cancelled when the inference won the settle race, hiding the task that hook reported. The suppression is replaced by a latch derived from the row: its main agent reads cancelled (or, from an older host, a done row flagged interrupted). A settled incoming main agent, another prompt, an explicit prompt or a session start releases it. Child and replayed events keep the latched main agent and are re-folded with their own child evidence; late main agent work is held as before, and Codex keeps its record re-mark. * test(agent-status): pin Codex's evidence guard beside the main agent fact * fix(agent-status): a prompt submission ends the cancel verdict latch The task notification Claude starts when background work ends is a real turn, but it keeps the cached prompt and carries no explicit prompt, so within 15 s of a cancel the latch held its prompt submission and every tool event after it: the turn read as monitoring under a cancelled main agent until its Stop. The captured shell cancel has exactly this: the notification lands 0.17 s after the cancel key. * fix(agent-status): derive a Codex row's interrupted flag from its main agent The cancel verdict latch lets any settled mainAgent through, so a late root Stop after an inferred Codex cancel now applies where the old same-prompt window held it. It restates the cancellation on mainAgent but, unlike Claude and Grok rows, carried no interrupted flag, so mobile, the dashboard and notification text read the cancelled turn as finished. Codex rows (local and relayed) now derive the flag from the main agent record, like the other providers that publish one. * docs(agent-status): describe cancel admission for every provider and the store's cancel-verdict hold * docs(agent-status): correct the idle-prompt Ctrl+C claim to the measured CLI behavior * fix(agent-status): preserve waiting relay children on cancel * fix(agent-status): resolve the cancel hold before a child's permission card adopts a relayed main agent The permission-card hold took the incoming event's mainAgent before the cancel hold ran, so on an SSH pane a child's next tool under a sticky card restated the relay's stale working main agent and dropped the cancellation the desktop had inferred. * test(agent-status): pin that a cancelled turn's drained subagent settles as stopped, not completed * fix(agent-status): keep a cancel through a restarted relay's child hook and a teammate's idle A relay that restarts after a desktop-inferred cancel has lost its prompt cache, so the child's next hook arrived with an empty prompt, read as a new turn, and replaced the cancelled main agent with none; the row then stayed working after every child stopped. A child's empty prompt is now unknown, not another turn; a non-empty different one still releases, since it is the listener's newer prompt. TeammateIdle names its child by teammate_name and carries no agent id, so the latch treated it as the main agent's and let the late-hook window apply it after 15 s, reviving the cancelled turn. It is now re-folded as child work. |
||
|
|
b419b3183e | test: remove redundant mobile and GitLab checks (#22748) | ||
|
|
c279760fcc |
fix(docs): update translated readme asset and WeChat links (#22763)
Update feature wall asset paths to new documentation site structure and bump WeChat community group reference from 8/9 to 10 across all translated README files. |
||
|
|
fe46138716 |
fix(terminal): one process-boundary ground for every known or proven boundary (#22735)
* fix(daemon): rebase durable checkpoints on the live terminal A durable checkpoint was folded from the previous checkpoint plus recorded output, so it inherited that checkpoint's modes forever. After a daemon restart killed a full-screen TUI and a new process started inline, the chain kept the dead TUI's alt screen and mouse tracking (?1049h ?1003h ?1006h) while the live emulator was clean. Every reattach and getBufferSnapshot served the stale chain, the renderer re-armed mouse tracking, and wheel scrolling went to a program that never asked for it: scrolling froze. Each full checkpoint is now the live snapshot verbatim (screen, layout, alt frame, modes, owner) with only the normal-buffer rows live has evicted taken from the durable replay. A checkpoint can no longer carry a dead process's modes, and checkpoints already poisoned on disk heal on the next compaction. - The first fold after a cold restore replays the same seed segments live was given, so rows line up even over a dead TUI's alt screen. - Idle zero-record folds keep the disk copy when it already agrees with live, so quit and relaunch bursts don't replay every session. - Held teardown bytes are already in the drained records and the live snapshot, so they are no longer replayed twice or appended as a tail. - The bounded getBufferSnapshot path honors the requested depth even when the live window is deeper, without phantom link rows. - The fold's ownership scanner and frame merge are removed; owner and frame come from live. * fix(terminal): one process-boundary ground for every known or proven boundary Three copies of the "the process that armed these modes is gone" reset had drifted: the cold-restore seed cleared only pen and mouse, the recovery barrier used the renderer's dead-TUI profile, and the cold-restore payload had none. A cold restore therefore left the dead process's focus reporting, bracketed paste, application cursor and keypad modes armed in the live emulator, the first checkpoint, and main's mirror. And the seed wrote the dead process's torn escape after the reset, so the new shell's first bytes could complete it (for example retitling the pane). PROCESS_BOUNDARY_GROUND replaces them: CAN, leave the alt screen without moving the normal-buffer cursor, every mouse protocol and encoding off, focus/paste/app-cursor/keypad off, cursor shown and style reset, kitty popped, SGR reset, grounded DECSC. It stays inert for the lifecycle scanner. The seed, the recovery barrier, and the cold-restore payload all use it, and the seed no longer carries the torn tail. The first fold after a cold restore now always rebases on live, because focus and keypad are not in TerminalModes and the zero-record shortcut could not see them differ. * fix(daemon): keep OSC links and kitty flags through durable checkpoint folds and trims Stop seeding persisted OSC link ranges into the fold replay: they index the base buffer, so rows evicted by pending output left a link on the wrong text. The serializer already writes OSC 8 into the ANSI the fold replays. Re-apply kitty keyboard flags when replaying a snapshot for trimming, since rehydrateSequences omits them. Bound a smaller restore request by trimming the committed checkpoint instead of re-reading disk and rebasing the live window at a smaller depth. * test(daemon): follow the isFirstTake rename in the process-boundary ground suite * refactor(daemon): drop the unreachable deep-live branch from the durable fold The live window's override cap now derives from the restore depth, so live can never be deeper than the fold. pendingRecords and isFirstTake are required. * test(daemon): pass pendingRecords to the process-boundary ground fold * fix(terminal): reset alt-screen kitty flags in the process boundary ground Kitty keyboard stacks are per screen, so resetting only after ?1049l left a dead TUI's alt-screen flags for the next alt-screen app. Also drop the inert CAN from the ground (every site grounds after complete bytes) and correct two stale comments. |
||
|
|
1c1b7829ec |
fix(daemon): rebase durable checkpoints on the live terminal (#22732)
* fix(daemon): rebase durable checkpoints on the live terminal A durable checkpoint was folded from the previous checkpoint plus recorded output, so it inherited that checkpoint's modes forever. After a daemon restart killed a full-screen TUI and a new process started inline, the chain kept the dead TUI's alt screen and mouse tracking (?1049h ?1003h ?1006h) while the live emulator was clean. Every reattach and getBufferSnapshot served the stale chain, the renderer re-armed mouse tracking, and wheel scrolling went to a program that never asked for it: scrolling froze. Each full checkpoint is now the live snapshot verbatim (screen, layout, alt frame, modes, owner) with only the normal-buffer rows live has evicted taken from the durable replay. A checkpoint can no longer carry a dead process's modes, and checkpoints already poisoned on disk heal on the next compaction. - The first fold after a cold restore replays the same seed segments live was given, so rows line up even over a dead TUI's alt screen. - Idle zero-record folds keep the disk copy when it already agrees with live, so quit and relaunch bursts don't replay every session. - Held teardown bytes are already in the drained records and the live snapshot, so they are no longer replayed twice or appended as a tail. - The bounded getBufferSnapshot path honors the requested depth even when the live window is deeper, without phantom link rows. - The fold's ownership scanner and frame merge are removed; owner and frame come from live. * fix(daemon): keep OSC links and kitty flags through durable checkpoint folds and trims Stop seeding persisted OSC link ranges into the fold replay: they index the base buffer, so rows evicted by pending output left a link on the wrong text. The serializer already writes OSC 8 into the ANSI the fold replays. Re-apply kitty keyboard flags when replaying a snapshot for trimming, since rehydrateSequences omits them. Bound a smaller restore request by trimming the committed checkpoint instead of re-reading disk and rebasing the live window at a smaller depth. * refactor(daemon): drop the unreachable deep-live branch from the durable fold The live window's override cap now derives from the restore depth, so live can never be deeper than the fold. pendingRecords and isFirstTake are required. |
||
|
|
bf40d35b0b |
docs: restore translated README assets reverted by stale APK bump (#22755)
#22740 bumped the translated Android APK links from a stale base, which also reverted #20416's feature-wall paths, the WeChat group 10 QR, and the Muse agent badge. Restore those from the parent commit and keep only the 0.0.50 APK bump. |
||
|
|
7a71e20860 |
fix(pi): isolate status ownership in new terminals (#22717)
* fix(pi): isolate status ownership in new terminals * docs(pi): explain terminal ownership boundaries * docs(pty): clarify environment rescrubbing |
||
|
|
517ef56c66 |
fix(agent-status): end a Claude helper's turn when an API error stops it (#22745)
* fix(agent-status): end a Claude helper's turn when an API error stops it When a Claude helper agent's request fails (for example a 429 rate limit), Claude skips the helper's SubagentStop and TeammateIdle hooks and sends only a StopFailure carrying the helper's agent_id. Orca treated that StopFailure as ordinary helper activity, so the helper row stayed "working" and pinned the pane "working" indefinitely, even after the lead agent finished. Route a helper's StopFailure through the same child turn-end path as SubagentStop, in both the hook listener (roster update) and the server's sticky-permission rule (a failed helper no longer holds its permission prompt). * test(agent-status): cover a failed background child that held a permission prompt * test(agent-status): pin the captured order of a rate-limited teammate before the lead stops |
||
|
|
60b0d01edb |
fix(agent-history): list older Pi sessions in Workspace and Project views (#22482)
* fix(agent-history): surface older Pi sessions in Workspace and Project scopes The session list is capped to the newest sessions across every workspace. Scoped views already bypassed that cap for Claude, whose transcripts live in one directory per cwd, but a Pi session older than the cap vanished from its own workspace's list. Pi buckets transcripts the same way (`--<cwd with separators as ->--`), so the scoped pass now takes a per-agent directory layout and runs for both. Codex keeps date-based folders with the cwd only inside each file, so it still relies on the capped scan. * fix(agent-history): match Pi buckets under a root scope and drop the unused Claude wrapper * fix(pi): verify each scoped transcript when cwd buckets collide * docs(pi): clarify inherited Claude bucket cache behavior --------- Co-authored-by: Neil <neil@stably.ai> |
||
|
|
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 |