mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 01:27:35 +00:00
a0a98a4e692bbd658115fdcebade96ad41d5ef2d
60
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c188c12543 |
fix(orchestration): remote worker liveness reports exited when the host proves the agent gone (#29276)
The execution host upgrades a remote worker's fleet/show verdict to exited only on its own proof: the hook store retains a proven agent exit (fenced to the attachment, cleared by a replacement agent), and after a reboot the process table proves the PTY gone. Lost contact stays unverifiable. Fixes #24353 Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
7a78dbd3c7 |
Native chat: one settlement transaction for everything an ended run left (4/5, rebuild of #26867) (#28111)
* feat(native-chat): record which agent run wrote each journal item Each journal item row, subagent roster entry and background-task entry now records ownerFence: the lease fence of the agent run whose output produced it. Optional, persisted, no row version bump and no migration. The journal's itemFence() returns it, so the turn-verdict rules and per-run provider activity read the producing run rather than the writer's fence. * feat(native-chat): one current-work projection for every host reader; record death first The host decides what work is current in one place (structured-agent-session-current-work.ts) from the lease, the exit this host saw, the runtimes it replaced, its live child and each item's provenance. Every host reader (queue, Stop, prompt answers, commands, status feed, idle sweep, teardown snapshot, history pages and frames, orchestration mailbox) reads it, so a dead agent's leftovers stop reading Working or holding the queue even when their cleanup write failed. The journal's activeTurnId()/liveTurnScope() become the raw runningTurn(). On exit the host records the child's end first and signals generationEnded, which republishes frames, the status feed and settlement waits and wakes the queue drain with no journal row. Pages and frames carry optional working and actionablePromptIds fields. * feat(native-chat): clients read the host's working answer; refuse answers to a stopped agent plainly Desktop and mobile prefer the host's working and actionablePromptIds fields when present (merged across batches like latestTurn) and fall back to today's row derivation against an older host. A prompt an ended agent raised stops looking active; answering it is refused before any provider sees it, with "<Agent> has stopped, so your answer was not sent. Send a message to continue." Cancelling it only dismisses it in the journal: it no longer triggers the chat's Stop, pauses the queue or withdraws queued cards. * feat(native-chat): one settlement transaction for everything an ended run left An exit now settles, in one lifecycle batch planned at its own turn in the write queue, everything its run left (turn, calls, prompts, reasoning, unanswered sends) plus leftovers of earlier ended runs, and only the subagent roster entries and background tasks those runs observed, by each entry's own provenance. Main's stale settle moves to stale-state-settlement.ts with fence scoping; dead-generation-settlement.ts becomes a planner; leftover-settlement.ts combines them. Resume eligibility widens in one case: this host saw the root exit at the lease's fence and the release write failed; the acquisition replaces the owner only once a probe proves it gone. * refactor(native-chat): keep structuredAgentSessionHandsOver file-local until a caller needs it Nothing outside the projection calls it in this part; Part 5 exports it for the start-failure row. * test(native-chat): fewer provenance tests: fold reopen and unstated-owner checks into the main case Drops the epoch-replacement case (Part 2 rewind test reads the same values through the same rebuild) and the corrupt-value case (no version writes one). * test(native-chat): drop current-work mirrors: the viewKey unit case and the still-live rewind case The flow cases keep every outcome: dead leftovers read as nothing, Stop has nothing to stop, a waiting card drains, a live owner publishes as before, and a rewind never promotes dead work. * test(native-chat): fewer client tests: one flow per client, one fallback each Keeps desktop and phone Working from the host (with the older-host fallback), the reducer merge contract, the ended question Cancel that only dismisses, the agent-named refusal and the resolution receipt. Drops per-helper prompt tests, the transcript-placement test, the stop-note client reduction, the Steer cards that match main, and per-reason refusal rows. * test(native-chat): fewer settlement tests: keep the exit-delivery flows and the per-entry scoping flow Drops the no-added-delay timing check, the frame-once check, two stamping unit cases and the Codex combined case (the roster flow test covers its outcome). * test(native-chat): drop the resume-eligibility predicate mirror; move the unstamped-entry case to Part 1 * test(native-chat): an unstamped roster entry from an older build reads as its row provenance Moved here from Part 4 so it sits beside stampJournalEntryProvenance. * test(native-chat): build the older-build roster row inline, after main removed its body helper Main #27947 writes one row per subagent and deleted journal-subagent-group-body; a roster row from an older build is still what this case reads. * refactor(native-chat): provenance is per item only; drop per-entry stamping Main's #27947 writes one saved row per subagent, and each background task already has its own row, so an item's own ownerFence names the run of every entry a producer still writes. Per-entry stamps inside a row mattered only for the shared roster rows saved before #27947, and no run writes into those any more, so the row's own provenance settles them correctly. Removes the entry stamping, the optional ownerFence on roster entries and background tasks, and their tests. * fix(native-chat): an item's provenance is set when it is created; revisions keep it A newer run's provider can revise an item an older run created: Pi's start and an ACP session/load both report context usage onto the newest saved turn. Restamping that turn with the newer run let the newer run's exit settle the older run's unverifiable turn as interrupted. Only creation states an owner now, as main's itemFences rule did. * test(native-chat): keep the provenance test below the production lines it covers * fix(native-chat): an ended agent's card Cancel commits with its receipt, through dismissPrompt The ended-generation dismissal wrote the cancelled resolution without the Cancel's operation receipt and without the dismissal's failure handling, so a receipt write failing after it would answer "outcome unknown" over a card already closed. It now goes through dismissPrompt, which carries the uncommitted receipt in the same append and answers dismissalNotSaved on failure; a null `answer` skips the adapter, since no live provider holds an ended generation's card (Pi's adapter refuses a fence with no live session outright). * Native chat: a resumed subagent or Claude background task belongs to the run executing it A subagent row that run 1 created and run 2 resumed kept run 1's creation fence, so settling run 1's death marked run 2's live subagent unverifiable. Claude background tasks reuse their rows the same way (a matching parent tool use, or a parentless row). Rows of reusable child execution now also name their execution owner: the fence of the child process whose report the write copies, captured when that report arrives, never the lease that is current when a queued write flushes. - Subagent rows: the host captures the live child's fence when its child-work report changes the records, and rewrites a running subagent another child now reports even when its text is unchanged. - Claude background-task rows: written through that child's own event sink, which stamps its bound fence. - Creation provenance (`ownerFence`) is unchanged. Writes that do not state an owner (settlement, Pi's start usage, ACP session/load replay, ACP tasks) keep it, and a row without one falls back to its creation fence. `journal.itemFence()` now answers whose execution an item is (`executionOwner ?? ownerFence`), so every settlement and liveness read, inside its write transaction, follows the run that resumed the work. Also drops the event sink's compatibility alias for the identity resolver (the Codex sink names the resolver itself), keeping the sink under its line limit. * Native chat: a subagent row is written through the child whose report it copies Review of |
||
|
|
865cdaa706 |
fix(native-chat): acting on one chat no longer waits on checks of every other chat (#26555)
* fix(native-chat): acting on one chat no longer waits on checks of every other chat Starting, resuming or switching a chat's agent first ran restart reconciliation, which probed every unreconciled session and committed them together, so one slow or failing probe for another chat delayed or failed this chat's action. Acquisition now reconciles only the addressed session; the explicit startup pass still covers all of them, and an owner that isn't proven still can't be granted a writer. Co-Authored-By: OpenAI Codex gpt-6.1-sol <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(native-chat): settle each restored chat on its own; register new sqlite tests - Startup restore no longer lets one chat's failed lease check skip recovery for the others. - Run the two new SQLite-backed tests in the Node runtime project. - Startup reconcile runs the whole-host pass without a placeholder session id. - Test names describe an injected failing/stalling owner check, not an SSH host. * fix(native-chat): restore keeps one whole-host lease check per startup The restore opens with the whole-host pass again and stops per-chat bookkeeping after the first failure, as on main: on a failing store it makes one lease-check write, not one per chat. User actions stay targeted to their own chat. --------- Co-authored-by: OpenAI Codex gpt-6.1-sol <noreply@openai.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
699709c0ba |
fix(ssh): never leave an SSH host unconnectable after a refused move (phone users, #29083, Move anyway) (#29220)
* fix(ssh): never leave an SSH host unconnectable after a refused move to its managed server
* fix(ssh): keep unsaved drafts out of what Move anyway leaves behind
* test: run the stuck-host profile test in the Node runtime project
* fix(ssh): spend a "Move anyway" consent when the conversion throws
A throw left managedServerLeaveBehind set, so a later connect left saved
state behind without asking again. A resumed fence replays its journal,
never the export, so clearing is safe.
Co-Authored-By: Claude <noreply@anthropic.com>
* Revert "fix(ssh): spend a "Move anyway" consent when the conversion throws"
This reverts commit
|
||
|
|
a3373af635 |
ci: skip unused platform runs and reduce runtime test setup (#29144)
* test: reuse automation fixtures and narrow long-poll setup * test: load terminal RPC methods only in consuming scenarios * ci: skip packaged Orcad lanes for unrelated unit test edits * style: format the WSL Orcad workflow |
||
|
|
635d3853f1 |
fix(agent.launch): avoid blank shells when launching new workspaces (#28451)
* fix: avoid blank shells beside new workspace chats * Carry promised agent chat through worktree creation * Use live agent launch inventory for new workspace surfaces * Type live agent launch owner from runtime contract * Assert one recovery shell after failed agent launch * Keep worktree activation adapters out of creation flow * Restore ordinary workspace setup handoff after launch correction * Cover the phone's agent.launch workspace create in the first-surface flow test |
||
|
|
d9de1fad1d |
Native chat: desktop and mobile read the host's working answer; plain refusal for a stopped agent's prompt (3/5, rebuild of #26867) (#28104)
* feat(native-chat): record which agent run wrote each journal item Each journal item row, subagent roster entry and background-task entry now records ownerFence: the lease fence of the agent run whose output produced it. Optional, persisted, no row version bump and no migration. The journal's itemFence() returns it, so the turn-verdict rules and per-run provider activity read the producing run rather than the writer's fence. * feat(native-chat): one current-work projection for every host reader; record death first The host decides what work is current in one place (structured-agent-session-current-work.ts) from the lease, the exit this host saw, the runtimes it replaced, its live child and each item's provenance. Every host reader (queue, Stop, prompt answers, commands, status feed, idle sweep, teardown snapshot, history pages and frames, orchestration mailbox) reads it, so a dead agent's leftovers stop reading Working or holding the queue even when their cleanup write failed. The journal's activeTurnId()/liveTurnScope() become the raw runningTurn(). On exit the host records the child's end first and signals generationEnded, which republishes frames, the status feed and settlement waits and wakes the queue drain with no journal row. Pages and frames carry optional working and actionablePromptIds fields. * feat(native-chat): clients read the host's working answer; refuse answers to a stopped agent plainly Desktop and mobile prefer the host's working and actionablePromptIds fields when present (merged across batches like latestTurn) and fall back to today's row derivation against an older host. A prompt an ended agent raised stops looking active; answering it is refused before any provider sees it, with "<Agent> has stopped, so your answer was not sent. Send a message to continue." Cancelling it only dismisses it in the journal: it no longer triggers the chat's Stop, pauses the queue or withdraws queued cards. * refactor(native-chat): keep structuredAgentSessionHandsOver file-local until a caller needs it Nothing outside the projection calls it in this part; Part 5 exports it for the start-failure row. * test(native-chat): fewer provenance tests: fold reopen and unstated-owner checks into the main case Drops the epoch-replacement case (Part 2 rewind test reads the same values through the same rebuild) and the corrupt-value case (no version writes one). * test(native-chat): drop current-work mirrors: the viewKey unit case and the still-live rewind case The flow cases keep every outcome: dead leftovers read as nothing, Stop has nothing to stop, a waiting card drains, a live owner publishes as before, and a rewind never promotes dead work. * test(native-chat): fewer client tests: one flow per client, one fallback each Keeps desktop and phone Working from the host (with the older-host fallback), the reducer merge contract, the ended question Cancel that only dismisses, the agent-named refusal and the resolution receipt. Drops per-helper prompt tests, the transcript-placement test, the stop-note client reduction, the Steer cards that match main, and per-reason refusal rows. * test(native-chat): an unstamped roster entry from an older build reads as its row provenance Moved here from Part 4 so it sits beside stampJournalEntryProvenance. * test(native-chat): build the older-build roster row inline, after main removed its body helper Main #27947 writes one row per subagent and deleted journal-subagent-group-body; a roster row from an older build is still what this case reads. * refactor(native-chat): provenance is per item only; drop per-entry stamping Main's #27947 writes one saved row per subagent, and each background task already has its own row, so an item's own ownerFence names the run of every entry a producer still writes. Per-entry stamps inside a row mattered only for the shared roster rows saved before #27947, and no run writes into those any more, so the row's own provenance settles them correctly. Removes the entry stamping, the optional ownerFence on roster entries and background tasks, and their tests. * fix(native-chat): an item's provenance is set when it is created; revisions keep it A newer run's provider can revise an item an older run created: Pi's start and an ACP session/load both report context usage onto the newest saved turn. Restamping that turn with the newer run let the newer run's exit settle the older run's unverifiable turn as interrupted. Only creation states an owner now, as main's itemFences rule did. * test(native-chat): keep the provenance test below the production lines it covers * fix(native-chat): an ended agent's card Cancel commits with its receipt, through dismissPrompt The ended-generation dismissal wrote the cancelled resolution without the Cancel's operation receipt and without the dismissal's failure handling, so a receipt write failing after it would answer "outcome unknown" over a card already closed. It now goes through dismissPrompt, which carries the uncommitted receipt in the same append and answers dismissalNotSaved on failure; a null `answer` skips the adapter, since no live provider holds an ended generation's card (Pi's adapter refuses a fence with no live session outright). |
||
|
|
3bfb1149da |
Native chat: one current-work projection for every host reader, death recorded first (2/5, rebuild of #26867) (#28103)
* feat(native-chat): record which agent run wrote each journal item Each journal item row, subagent roster entry and background-task entry now records ownerFence: the lease fence of the agent run whose output produced it. Optional, persisted, no row version bump and no migration. The journal's itemFence() returns it, so the turn-verdict rules and per-run provider activity read the producing run rather than the writer's fence. * feat(native-chat): one current-work projection for every host reader; record death first The host decides what work is current in one place (structured-agent-session-current-work.ts) from the lease, the exit this host saw, the runtimes it replaced, its live child and each item's provenance. Every host reader (queue, Stop, prompt answers, commands, status feed, idle sweep, teardown snapshot, history pages and frames, orchestration mailbox) reads it, so a dead agent's leftovers stop reading Working or holding the queue even when their cleanup write failed. The journal's activeTurnId()/liveTurnScope() become the raw runningTurn(). On exit the host records the child's end first and signals generationEnded, which republishes frames, the status feed and settlement waits and wakes the queue drain with no journal row. Pages and frames carry optional working and actionablePromptIds fields. * refactor(native-chat): keep structuredAgentSessionHandsOver file-local until a caller needs it Nothing outside the projection calls it in this part; Part 5 exports it for the start-failure row. * test(native-chat): fewer provenance tests: fold reopen and unstated-owner checks into the main case Drops the epoch-replacement case (Part 2 rewind test reads the same values through the same rebuild) and the corrupt-value case (no version writes one). * test(native-chat): drop current-work mirrors: the viewKey unit case and the still-live rewind case The flow cases keep every outcome: dead leftovers read as nothing, Stop has nothing to stop, a waiting card drains, a live owner publishes as before, and a rewind never promotes dead work. * test(native-chat): an unstamped roster entry from an older build reads as its row provenance Moved here from Part 4 so it sits beside stampJournalEntryProvenance. * test(native-chat): build the older-build roster row inline, after main removed its body helper Main #27947 writes one row per subagent and deleted journal-subagent-group-body; a roster row from an older build is still what this case reads. * refactor(native-chat): provenance is per item only; drop per-entry stamping Main's #27947 writes one saved row per subagent, and each background task already has its own row, so an item's own ownerFence names the run of every entry a producer still writes. Per-entry stamps inside a row mattered only for the shared roster rows saved before #27947, and no run writes into those any more, so the row's own provenance settles them correctly. Removes the entry stamping, the optional ownerFence on roster entries and background tasks, and their tests. * fix(native-chat): an item's provenance is set when it is created; revisions keep it A newer run's provider can revise an item an older run created: Pi's start and an ACP session/load both report context usage onto the newest saved turn. Restamping that turn with the newer run let the newer run's exit settle the older run's unverifiable turn as interrupted. Only creation states an owner now, as main's itemFences rule did. * test(native-chat): keep the provenance test below the production lines it covers |
||
|
|
6837a4c180 |
fix(ssh): a relay-era desktop can join the server another desktop on the same login runs (#28886)
* fix(ssh): a relay-era desktop can join the server another desktop on the same login runs A desktop that still had relay-era projects on a host whose managed server another Orca version ran could never connect: its migration skipped the running-version adoption, so it deferred on a terminal count only a paired server can give. Once the versions matched it failed instead because the server already held the same repository path, added by the other desktop. The migration now pairs with the running server like a fresh desktop does, before any upload. Projects the server already holds at the same host path are linked to the server's project instead of imported twice: the journal is re-cut without them, under the same migration id, only after the server proves it holds nothing of the earlier manifest. A conflict the server will keep refusing now says what conflicts and how to get past it. * fix(ssh): never link a project a bridged relay terminal still runs in The bridge reattaches a relay pane only into its own tab, which a linked project never sends to the server, so linking would leave that terminal running out of sight. Refuse the link instead, naming the project and how to end the terminals. * test(ssh): run the link test on Node's SQLite runtime and read the host's activation record * test(ssh): establish desktop B's profile before seeding its relay-era state * test(ssh): connect desktop B's host explicitly in the shared-login spec * fix(ssh): say what a linked project left behind, and never blame it for an unowned relay terminal A project this desktop moved that the host's server already held is linked to the server's copy, so its tabs and automations from this desktop stay behind. The connect now carries those linked projects and the SSH host card says so, with the automation count, instead of dropping them silently. A bridged relay terminal of unknown or removed project still blocks a link, but its guidance no longer names the linked project as where it runs. Both terminal messages now offer removing the project from this desktop as the way out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e2fa611916 |
feat(native-chat): chat scratch folders expire after 30 days unused (#28394)
* fix(files): never write through a dangling symlink A path that doesn't exist yet was authorized by walking up to the nearest existing folder. A dangling symlink fails realpath with ENOENT, so the walk stepped over it and approved the link's own path; the write then followed the link and created its target outside the allowed folders. The walk now continues from the link's target, and returns that real destination. * refactor(native-chat): rename the chat visuals folder to the chat scratch folder The per-chat folder agents write into is the chat's scratch space, not only a visuals folder. Its root becomes <state>/chat-scratch/<hash> (the hash is unchanged) and the agent variable becomes ORCA_CHAT_SCRATCH_DIR. The old ORCA_CHAT_VISUALS_DIR is never set and is still stripped from what every agent child inherits, Pi included. The visuals skill names the new variable and says the folder is temporary. * test(native-chat): keep the delivery test to the rename The launch parity snapshot already pins that every agent drops the pre-rename variable. * feat(native-chat): chat scratch folders expire after 30 days unused A chat's scratch folder was removed only when its record was gone or its local workspace was proven deleted, so the folder of a long-idle chat lived forever. The sweep now expires a chat's folder once its newest journal row and the newest entry anywhere in the folder (complete, no-follow scan) are older than 30 days, unless the host holds a running agent, a delivery or an open dispatch for the chat. The final recheck and a rename to <hash>.expired run in the chat's session lane and then a per-folder lock that Orca scratch saves (desktop IPC and host RPC) also take; the delete runs outside both. A save that settles after the age scan marks that expiry stale. Leftover .expired folders and the pre-rename native-chat-visuals root are removed. The workspace-removed rule and its catalog verdict are deleted. * fix(files): refuse any dangling symlink in the missing-path walk Following a dangling link by hand resolved relative targets against the lexical parent, not the link's real folder: through a folder link the walk could loop forever or authorize the wrong file. Refuse instead: realpath can't say where a dangling link leads, but a write would follow it. * test(files): drop the loop case that duplicated the dangling-link refusal * refactor(native-chat): trim the scratch expiry and test the real live-work veto - Remove the other-profile catalog options (running profile id, never-written profiles) that only the deleted workspace verdict used; history cleanup's reader is unchanged. - The sweep keeps its original remove-and-warn block; an unused chat's folder is parked by one function. Drop the unused `now` option and the candidate identity check. - The live-agent case runs against the host's real runWhileExecutionIdle. * style(native-chat): keep the session host within its line limit after the main sync * fix(native-chat): every scratch save and every overlapping sweep share the folder lock Binary (base64) runtime saves now take the scratch folder lock like text saves, and each folder keeps a set of expiry candidates so one sweep finishing no longer hides a save from another. |
||
|
|
850e71e672 |
Resume in New CLI for OpenCode and OMP chats (#28075)
* feat(chat): Resume in New CLI for OpenCode and OMP chats OpenCode's and OMP's CLIs have no copy flag, but their chat protocol copies a conversation (ACP session/fork). For a fork of one of their chats, the chat's host now has a short-lived agent, launched exactly as the chat's own, copy the conversation; the terminal then opens the copy with a plain resume. The chat keeps its own conversation, so the CLI is never a second writer on it. The copy travels on the existing aiVault.prepareSessionResume fork request as an optional forkedSessionId; a host that predates it answers without one, and the client asks for an Orca update instead of resuming the original. * fix(chat): open an OpenCode 2.x chat's CLI copy with opencode, not opencode2 The copy kept the row's agent, so Resume in New CLI ran `opencode2 --standalone --session <copy>`, which OpenCode 2.x doesn't install. The chat's host now answers the copy with the agent whose CLI opens it: the chat's own. * fix(chat): type the copied conversation's agent as a history agent * fix: preserve independent chat copies through CLI launch * test: keep only the tests that pin user-visible behavior * fix(chat): validate OpenCode copies on their execution host * test(chat): expect copied OMP forks after main integration |
||
|
|
cc03142b9c |
feat(wsl-mover): background writers step aside for a project a WSL move is taking (D1b-5c-3) (#28835)
* feat(wsl-mover): background writers back off a project a WSL move is taking (D1b-5c-3) * fix(wsl-mover): re-check before renames, quiet owned automations, stable folder ids (D1b-5c-3) * test(wsl-mover): seed journals one phase at a time and advance existing moves (D1b-5c-3) * fix(wsl-mover): a branch rename that already ran settles even when the move skips the folder (D1b-5c-3) |
||
|
|
8f83f6b076 |
feat(wsl-mover): pause a moving project's terminals through Sleep and prove they exited (D1b-5d) (#28820)
* feat(wsl-mover): pause a moving project's terminals through Sleep and prove they exited (D1b-5d) * fix(wsl-mover): gate again under the fence, swap journal phases atomically, keep sessions frozen through commit (D1b-5d) * test(wsl-mover): the fence e2e walks the journal one phase at a time, as a move writes it (D1b-5d) |
||
|
|
b0308f24ef |
feat(wsl-mover): refuse edits to a project a WSL move is taking (D1b-5c-2) (#28761)
* feat(wsl-mover): refuse edits to a project a WSL move is taking, and drop its background bookkeeping quietly (D1b-5c-2) * fix(wsl-mover): hide only local rows of a moved project; the host's own row keeps its id (D1b-5c-2) * fix(wsl-mover): refuse a user's unread toggle, fence branch deletes, host setups and issue commands; settings unfreeze once the host owns a repo (D1b-5c-2) * test(wsl-mover): the fence e2e refuses edits to moving projects and drops their bookkeeping quietly (D1b-5c-2) * refactor(wsl-mover): name the update type the fence inspects (D1b-5c-2) * test(wsl-mover): run the fence tests that open a real store in the Node runtime (D1b-5c-2) * fix(wsl-mover): a host setup's own repo stays frozen once the host owns it; e2e pins the quiet drop (D1b-5c-2) |
||
|
|
04b4e962a7 |
fix: a Claude model that's gone switches to the list's default when you send (#27826)
* fix: start a new Claude chat on the listed default when its remembered model is gone The remembered new-chat model was launched as-is even after Claude stopped offering it, so every new chat failed its first message. The host create and the desktop new-chat picker now derive the same replacement from the host's saved model list: the listed default, effort moved to the nearest level it offers. Explicit models (--model, held picks) run as given; with no saved list the remembered model is kept. * fix: read the host's model list only for a Claude create that needs it The check read host.deps before ruling out other agents and explicit models, so creates whose host carries no deps (and every non-Claude create) paid for a lookup they never use. * fix: picker derives the fallback only where the host would A held model runs as picked on the host, so the picker no longer derives an effort beside it. A cached list whose default marker was dropped for this workspace keeps the placeholder instead of naming its first row. * fix: the new-chat picker picks the replacement for a gone Claude model The host-side check read the saved list separately from the picker, so the two disagreed and a cold start never fired it. The desktop new-chat picker now picks the replacement it shows, once, through the existing pick path, so the first send runs it and the remembered selection heals. The host is back to main. * fix: decide a gone Claude model's replacement at send, from the list in hand The open-time auto-pick raced the chat's create and the list's arrival, so the first send could still launch the gone model. Now, like the common pattern, the picker shows the replacement for a model the host's loaded list lacks, and the send step applies it through the existing pick call, awaited, right before the message goes out. A failed pick never holds the send back. * fix: one verified list decides a gone model's replacement The picker, the send step and the option setter now all read the list the picker shows, less the row a session adds for its own unlisted model, so what is shown is what applies and an explicit pick from that list stays. A listing that is still running, failed, or absent keeps the model. The send step re-derives from the chat's state when it runs, so a second send behind one that already moved the model applies nothing. * fix: only rows a finished host listing names can replace a gone model A single verified flag outlived the catalog it described: live options built from the built-in fallback list, or a malformed or empty host answer, still authorized a replacement the host would refuse. The state now keeps the ids a finished, well-formed host listing names, and a replacement must be both shown in the picker and named there. * fix(native-chat): the send carries the composer's Claude model and effort Every composer send (desktop and phone) carries the model and effort the picker shows, as optional agentSession.send options, only to a host that advertises agent-session.send-options.v1. The host writes a carried value that differs from the chat's own through the existing pick path before accepting the message, so the turn starts on it and the chat records it. A queued card keeps its options and applies them when it goes out; a launch's create call carries the replacement for its first message. Replaces the separate awaited pick before each send. * chore: drop an unrelated lockfile change * refactor(native-chat): one helper names a send's control fields for client and host * ci: rerun against current main * fix(native-chat): a message runs on the model and effort it was sent with The carried selection is now stored with the message (its submission row, and a queued card's row) and applied through the shared option writer as the delivery loop hands the message over, or before the start it needs. A pick made after the message was accepted no longer changes the model it runs on. Direct sends, queue drains and Send now share that one apply point; the early apply before acceptance and the queue-only apply are gone. The picker and the send read one effective selection: the replacement for a model the host's finished listing lacks shows only for a host that takes a send's selection, the phone validates picks against what it shows, a send carries the default the composer shows, a resumed launch's first message gets the same resolution, and only the listing proves a row (no label heuristic). * docs(native-chat): a submission's options are the host's alone * fix(native-chat): an opening message keeps its model; a pending pick resolves like the picker The opening message a create sends now stores the model and effort the chat was created with, so a pick made before its handover no longer changes what it runs on, and a card kept after a restart keeps them. A pick on its way to the host resolves through the same option view the picker shows before a send carries it, so a gone model it names is replaced too; the send selection reads only the shown snapshot. The option writer builds its own record-and-publish context, so the message path no longer duplicates it, and the client and host share one function for a send's fingerprint fields. * ci: rerun against current main |
||
|
|
a622bb0f4b |
fix(native-chat): phone Stop reaches a message the agent hasn't answered; unreadable task list reports unconfirmed (#28243)
* fix(native-chat): phone Stop reaches unanswered sends; unreadable task list is unconfirmed The phone named the running turn on every Stop, so a Stop pressed before the agent echoed the message said "Stop not sent" and the agent ran on. Against a host advertising agent-session.conversation-stop.v1 the phone now sends the Stop that names no turn, as the desktop already does; the host stops whatever the chat has in flight. A card's Cancel still names its turn, and older hosts keep the turn-named Stop. A background-task Stop whose host could not read the session's task list read the failure as "no tasks" and answered cancelled:false. It now refuses as outcome unknown, the refusal a timed-out background stop already returns. Adds host tests pinning the Stop-before-echo races on the unnamed Stop: Codex turns admitted or started before the echo, a later send just before the Stop, a retried Stop, and a Stop after a failed one. All pass on main unchanged. * fix(mobile): show Stop before the turn opens; a card's Cancel keeps its turn The phone offered Stop only once a turn existed, so the Stop that names no turn was unreachable while a send waited for the agent's echo. Against a host that advertises conversation stop, a working chat now offers Stop. A card's Cancel chose the untargeted form from the prompt after the prompt-cancel capability filtered it out, so against a host with conversation stop but no prompt cancel it sent a Stop naming nothing. It now always names the card's turn. The race test for an admitted turn that cannot take an interrupt accepts success only once the host has let go of the agent, and otherwise requires the unconfirmed refusal. * ci: rerun against current main * ci: rerun against current main |
||
|
|
885d8a0046 |
feat(native-chat): show context usage in Codex chats (#28084)
* feat(native-chat): show context usage in Codex chats Codex turn-row writes become journal revisions so the reading survives a turn end. * test(codex): run the context-usage journal suite under Node --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
4d65146f2d |
fix(native-chat): Stop is saved before it acts, and a late or repeated Stop does nothing (#27076)
* feat(native-chat): add a chat-owned command receipt store
Adds the agent_session_command_receipts table and an insert-if-absent
receipt write that runs inside a command's own journal transaction. A
receipt records the command's identity, canonical hash, status and a
pointer to the row holding its result; it dies with its chat record.
Nothing uses it yet: the existing operation ledger stays authoritative.
* fix(native-chat): key command receipts like today's ledger ids
Rows are keyed by caller and operation id; a send looks across callers and
other methods look up only their caller's row, matching the existing ledger.
Bad rows and newer table shapes read as unreadable instead of throwing.
* fix(native-chat): order receipt lookups by insertion and key by operation id
Global lookups now take rows in insertion order, so a read and an insert
agree on which receipt answers an id regardless of the host clock. The
primary key leads with the operation id, so its own index serves both
lookups.
* fix(native-chat): accept sends through their own receipt
Send (direct, queued, steer) and /compact no longer commit a separate
ledger reservation before the message is saved. Their command receipt is
inserted-if-absent in the same transaction as the submission or queued
draft; a repeated id replays from it, a changed request conflicts.
Operation ids are checked for syntax only, so age, clock skew, pruning
and the reservation's own storage can no longer refuse a send.
The ledger success row they already co-committed stays as a temporary
insert-if-absent write so families not yet switched keep seeing these
ids; it is never read on this path.
* fix(native-chat): answer a send's receipt one way, share /compact's id space with Send, wake delivery once
- After its commit, a send whose publication fails answers through the same receipt replay a retry takes, so one id gets one answer; the in-memory copy and journal reload are gone.
- /compact ids are global like Send's: a retry under a new connection replays instead of needing a second acknowledgment write.
- The receipt route calls its runner directly; the runner wakes delivery when a new message row commits.
- The compatibility ledger row expires from host time; each temporary site is labelled.
- Receipt read errors and post-commit failures are logged.
* test(native-chat): expect receipt answers where tests assumed the send ledger
Restart-continuation tests now model the case Send cannot vouch for (saved but
not loaded into memory) and keep it unconfirmed; a later throw after the message
is in the chat answers continued, as a resend would. Drop /compact's unreachable
legacy replay branch and fix stale comments.
* test: run the queued tests that open the journal database in the Node runtime project
* fix(native-chat): queued-message actions and prompt answers accept through their own receipt
Send Now, Delete, Resume and prompt answers move onto the command-receipt route
Send uses: the receipt is inserted with the command's own write. A no-op answers
only once its receipt commits (otherwise outcome unknown), and a prompt answer's
replay reads the exact revision it was accepted at.
* fix(native-chat): replay a queued action from what its receipt recorded
Send Now's acknowledgement points at the submission it handed off; a question
answer's receipt keeps the accepted resolution; a no-op that cannot save its
receipt answers outcome unknown and holds nothing in memory; classified refusals
pass through.
* docs(native-chat): receipt comments say what a replay answers from
* fix(native-chat): Stop saves first, captures its target, and holds queued messages
Stop accepts through its own receipt: one transaction saves the Stop record
(target turn, agent generation, cutoff), returns unsent messages to the queue as
paused cards, and writes the receipt; only then does it interrupt the captured
target. A failed save leaves the agent running and says so. A late Stop is a
recorded no-op, and a retry is answered from the receipt.
* fix(native-chat): Stop round 1: late Stop is a no-op, saved-then-failed refuses, hold only what the Stop did not stop
- A Stop naming a turn that already ended is an accepted no-op.
- A card's Cancel saves its receipt with the dismissal row.
- A Stop saved but not carried out is refused as stopFailed ("Couldn't stop <agent>. Try again.") and its receipt is rewritten as rejected.
- A Stop during a start is saved first, then aborts that start; the abort-before-admission path is removed.
- The message the Stop stops is withdrawn as before; messages behind it are held as paused cards. Non-text messages are still withdrawn (TEMPORARY).
- Cleanups: one receipt commit path, dead fields and helpers removed, the returned-to-queue text names the agent.
* fix(native-chat): Stop round 2: narrow to receipts and save-first; message fate as on main
- Stop's withdrawal of queued sends is main's own rows (rejected, cancelled), written in the same
transaction as the Stop event and its receipt; a failed save withdraws nothing.
- A Stop arriving during a start aborts that wait on arrival, as on main (admits-now guard), then
captures its target once in the session lane. The out-of-lane admission is removed.
- A committed receipt is never rewritten: a failure after the save answers from the receipt and
writes the existing 'Cancellation was not confirmed.' note. 'Couldn't stop <agent>' only when
the save fails; a card Cancel that can't be saved says the question or approval wasn't cancelled.
- A Stop naming a turn a rewind removed is late (an accepted no-op).
- Removes the held-card settlement and returnedToQueue: what happens to a stopped message stays as
on main.
* test(native-chat): name the Stop row comparison for what it drops
* fix(native-chat): Stop round 3: a committed Stop always acts; notes only for unconfirmed interrupts
- A Stop whose write throws after its receipt committed still interrupts (never 'Couldn't stop').
- An interrupt that worked answers stopped even when only its note fails to write.
- A strict card interrupt that throws after its receipt committed gets the 'Cancellation was not
confirmed.' note, through one shared helper with the turn path.
- A target-less Stop stands while the main agent works, so a turn that opened while its save waited
(a send made before the Stop) is interrupted, as on main.
- A card Cancel that can't be saved says 'Couldn't cancel this question or approval. Try again.'
* fix(native-chat): Stop round 4: name the turn the Stop actually stops
- The Stop event's turn is resolved when its write runs (the named turn, else the live turn), so a
turn that opened while the save waited is the one marked interrupted and shown stopping.
- The unconfirmed note is keyed by that turn; tests cover it and its own write failing.
- Whether a Stop withdrew anything comes from its capture, so a throw after the commit can't flip it.
- A Stop counts as saved anyway only when its own write committed the receipt.
* fix(native-chat): a new chat's first send gets the Stop-aware operation receipt
Main's create path (from #26332) hands performSend the founding receipt; the
Stop changes widened the turn context's receipt to track its commit. Wrap it
with the existing observedReceipt.
* fix(native-chat): a background task the agent won't stop still says not done
A background-task Stop now saves its receipt before it asks the agent. A
refusal after that save (task still running, or a timeout / lost connection)
was being replaced by the receipt's replay, so the person never heard the Stop
failed. That refusal is now the first answer, as on main; the receipt stays
saved and a retry of the same id is answered from it without asking again.
* test(acp): a Grok Stop naming an ended turn leaves the unechoed follow-up alone
The late-Stop rule makes a Stop naming a turn that has ended an accepted no-op
for every agent. The Claude case was rewritten with it; this is Grok's.
* test(native-chat): Stop tests exercise what they name after the late-Stop rule
- The "leaves the child alone" Stop now reaches the provider (handed over, a
turn with no row yet); the at-rest refusal case was a duplicate of the late
no-op test and is removed.
- The adapter comment and two test names say the gap rule covers a named turn
with no journal row yet.
- The Grok late-Stop test checks no status row and that the follow-up completes.
- Name the two refusals refusalAnswers passes through.
* fix(native-chat): a background-task Stop answers its acceptance and notes what happened
A background-task Stop is saved before it asks the agent. The answer is
that saved acceptance: a stopped task answers cancelled:true as before
(the phone keeps the row "stopping" on it), and anything else answers
cancelled:false. A retry answers from the receipt without asking again.
What the agent then says comes back as a note keyed by the Stop's own id,
written through the existing Stop-note path, and never as a refusal:
- still running: "The background task wasn't stopped." (or "The
background tasks weren't stopped." for Stop all), the new status-row
failure kind backgroundTaskNotStopped;
- timed out or lost the agent: "Cancellation was not confirmed."
The note is the thread's, since tasks outlive their turn. A note that
cannot be written is logged and changes no answer.
The background-task body moves out of performCancel into the targeted
cancel, and the refusalAnswers flag and its receipt are deleted.
* test(runtime): the Codex strip's Stop notes a command it could not stop
A background-task Stop now answers its saved acceptance and says what happened in a note: "The background task wasn't stopped." when the command still runs, "Cancellation was not confirmed." when Codex was lost.
* test(native-chat): keep the Stop tests that pin behaviour, cut the rest
Merges the receipt, save-first, failed-after-save and background-task Stop
tests into one flow file; drops unit and per-branch tests already covered
by a flow test.
|
||
|
|
c5d7628686 |
fix(native-chat): chats from before permission modes keep the mode they were running with (#28099)
* fix(native-chat): chats saved before permission modes run the new-chat default A Claude or Codex chat created before chats had their own permission mode has none saved, and it ran Ask, so every existing chat of a Yolo user started asking for approval after the update. A chat with no saved mode now runs Settings -> Chat -> Permissions, read from the host's settings on each launch, read and restart check. * fix(native-chat): an older chat keeps the new-chat mode its first start ran with A chat saved before chats had permission modes ran the new-chat default on every start, so it kept following that setting. Its first start now writes the mode it ran into the chat's saved options, through the same write that already carries the chat's other options over a start; from then on it is a chat with its own mode. * fix(native-chat): save older chats' permission mode once at host startup Replace the save-at-first-start path with one startup pass: before the chat host is exposed, every Claude and Codex record with no saved permissionMode gets the host's new-chat default, in one store transaction. Saved values, including invalid ones, and every other option are left alone; a read-only store is skipped and a failure is logged and retried next start. The runtime fallback in launch, acquisition, provider-started and attach is restored to main's behaviour. A client-supplied attach that creates a new record now gets the same new-chat seed as every other create path. * fix(native-chat): fill each older chat from its own agent's arguments An older Claude or Codex chat ran with that agent's own Arguments before permission modes existed, so the startup pass now saves full access or ask per agent instead of the combined new-chat default. A user with Claude on bypass and Codex on Manual keeps Claude chats on full access. The attach create path always passes its permission seed; the reservation already applies options only when it creates the record. * refactor(native-chat): read each chat's agent arguments directly in the permission backfill * test(native-chat): give stub runtimes the launch seed the attach path reads |
||
|
|
3a011e48d1 |
fix(native-chat): load long chats on the phone by keeping every update within its limit (#28203)
* fix(native-chat): fit chat stream pages in one mobile bridge frame A long native chat failed to open on a phone with "the shell ended this stream (overflow)". The host sized each history page to ~1.94 MiB (half the relay cap), but the mobile app ends any stream whose event frame is over 640 KiB, so the opening snapshot of a long chat never arrived. Pages now fit that frame; older rows load through Load earlier as before. The page also words a stream the app ended in plain language instead of the protocol's reason. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): keep the outline on the relay page share; plainer end words Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): read the bridge frame cap from its shared definition Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(native-chat): bounded frames for the phone's chat stream A phone page now asks the host (when it advertises agent-session.bounded-frames.v1) to keep every agentSession.subscribe frame within the bridge's 640 KiB frame: - each row is cut to what the phone draws (tool output to its detail length unless it draws as a diff, tool inputs to the diff view's length) and then to one row's share of a page, its long strings cut to a common length; the row is marked `truncated` with its full size - history frames carry rows and metadata held to a fixed ceiling; the draft queue and the background-task roster go in frames of their own; the command list is not sent, since the phone reads it from options - the queue's admission bound counts whole cards and is sized to one frame, so a phone always gets every card whole Other subscribers get the frames they always did. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): bounded frames review round 1 - the phone waits for the host's capabilities before opening the stream, so its opening frame is never sent unbounded - rows are bounded once, by the reader that sizes the page; the item form is an options field and always a function - one cut rule (`cutLongStrings`) serves the display trims and the fit - a side list rides an empty history batch rather than a second frame; commands changes send a bounded subscriber nothing - queue admission measures the candidate list once; a legacy queue of more cards than fit lists the ones it sends first Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): bounded frames review round 2 - an over-budget legacy queue cuts its text to one common length and marks the cards it cut, and no longer names a next card it left out - one predicate says whether a subscriber is owed the `/` list - a lighter invariant fixture Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(native-chat): type the shortening guards against named shapes Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): bound every phone's chat stream at the host The host now bounds frames for every phone (clientKind 'mobile') at the RPC boundary, so the subscribe param, the host capability and the phone's wait for the capability probe are gone. Phones of every version already ignore the stream's slash commands, and a phone reached through the desktop relay signs in as a phone too. - A live gap of more than one page reaches a phone as the newest page, so no publish outruns the bridge's unacked window; older rows load on scroll. - Queue admission sizes each card as wide as a failed send can make it. - Rows are bounded only as a page takes them; the unread row marker is gone and a shortened queue card is marked `truncated: true`. - A bounded subscriber no longer reads or tracks the slash commands. - The stream-end words are back to main's: `overflow` also covers a page that stopped acking, which "too large" would misstate. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(native-chat): count every card in line and never shorten one Queue admission now counts every card the queue can publish again without passing admission: waiting and returned cards, and sent cards whose delivery is not yet accepted, which a refusal or a Stop puts back. The fixed per-card reserve for a failed send's words is gone. When a phone's list is over its frame room anyway (an older build's larger queue, or a card that grew after admission), the phone gets the cards it sends first, whole, and the rest once those leave. Cards are never shortened, so no app version can Edit a partial copy over the full draft, and the `truncated` card flag is gone. Bounded frames strip only the roster from an event; no event carries the other side lists. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): bound only phone frames; keep main's queue and page sizes Queue admission is main's again. The phone's list is bounded where it is sent: the cards it sends first, whole, so every frame fits for any queue size and no card is shortened. History pages keep main's size for every reader but phone subscribe frames. A reader is now its item form plus its page budget, so desktop pages, the outline and phone history replies (chunked by the app's bridge) are as on main, and only bounded frames use the bridge frame's share. Drops the queue's `nextQueuedMessageId` rewrite, which no phone reads, and a test that duplicated the prefix check. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): bound phone rows by size alone; final tidy Rows the journal already caps at a 16 KiB head gain little from a second, display-shaped cut, and the diff check behind it passed a character count as a line count. A row is now held to its frame share by shortening alone, and native-chat-diff is back to main's. Unexports file-local bounds, fixes a capability-era test comment, and adds tests for string shortening and a phone's stop-note refresh. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(native-chat): assert a phone's stop-note refresh pages Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(native-chat): run the bounded-frames suite in the Node runtime project It opens a real SQLite journal. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(native-chat): bound legacy dispatch reasons on read Rows written before the write-side bound carry a provider's full error text, which every page and phone frame attaches with its submission. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
7ed23199a5 |
refactor(orcad-migration): migration source abstraction, capability-gated source.kind, path-rewrite stage (WSL-via-orcad D0) (#27790)
* test(orcad-migration): pin the SSH manifest digest before the source abstraction * feat(orcad-migration): capability-gated source.kind discriminator, SSH wire unchanged * refactor(orcad-migration): MigrationSource abstraction with SSH relay as the only source; table-driven path-rewrite stage (identity for SSH) * fix(orcad-migration): name the wire source type * chore(orcad-migration): exhaustive path-kind switch; no test assertion * test(ssh): run the relay migration source's SQLite test in the Node runtime project * perf(orcad-migration): rename record keys in linear time and skip the walk for the identity SSH exports run the path-rewrite stage on every stage and commit check; re-reading Object.keys per entry made a 10k-key record cost seconds on the main thread. |
||
|
|
8be17ddf3a |
feat(chat): host writes one saved row per subagent (#27947)
* feat(chat): read one saved row per subagent Adds the singular `subagent` journal block (one row per child, revised in place, with a plain-text twin for older clients) and teaches every reader to draw it: desktop and phone fold a turn's sibling rows into the existing "Ran N subagents" row, the client and off-page rosters name children from their own rows, the reopen pass settles a row a dead host left working, and the phone and worker-transcript wires bound it. No producer writes the block yet; that switch lands in the follow-up. * feat(chat): host writes one saved row per subagent Claude, Codex and Grok now only report child-work evidence. The host writes each subagent's row from its child record, so the chat row, strip and sidebar read one lifecycle. Deletes the shared subagent tracker, the per-agent rosters and their caps. An accepted Stop settles only the run it was sent to, provisionally, until that run reports its own ending. Work the host store has no room to record keeps the running-tasks strip and its Stop instead of reading as idle. * fix(chat): open subagent background flag, separate nested group ids, page window skips subagent rows - background is an open boolean so a later build's value never fails a chat load - a helper's own helpers fold under a groupId of their own, so the phone opens them apart - each subagent's own row rides along with the page window instead of counting as conversation * fix(chat): subagent row writer reads the live turn once, stop acks survive a failed batch - the row writer reads the journal's live turn at most once per change and only for a subagent with no saved, queued or in-flight row; a revision keeps its first scope - a background stop names each accepted task as its answer lands, so a stop-all batch that times out or fails part way still ends the runs it was answered for (Claude and Grok) - the strip's untargeted Stop rule lives in the shared view (offersStopAll); both strips inline it again; incomplete tracking no longer holds commands or rewind - clients admit a frame whose only change is incomplete tracking - drop the Claude stop's dead publish; the real-CLI stop test asserts the run's own ending - list the writer test with the real SQLite tests * fix(chat): a late Grok report never revives an ended helper; Codex keeps announced helpers - Grok: once a helper's run ended, a later progress or repeated spawn sends no live evidence, so its record and saved row stay ended after the next send retires the record; the test rig now retires finished records on each turn open, as the host does - Codex: past the bound, only a child that ran and ended is evicted, so an announced helper that has not started keeps its record, row, Stop and parent - cut: Codex parentTurnId (written, never read), the Claude identities' turn-end forwarding (call the corrections directly), the duplicate Grok outcome map - update stale comments that described the deleted producers * test(chat): Claude frames to saved row, kept Codex path and usage rules, agent ingestion exemption - a captured background Claude helper keeps its saved row working past its spawn result and the parent turn's end, through the real adapter, host and child records, until its own end - restore focused tests for the Codex root path, label trim and placeholder, usage before announcement and parent usage - the per-session live limit test uses tasks: subagents are exempt from it * fix(chat): incomplete tracking follows the refused children, and the strip's Stop reaches them - the host remembers which children the store refused for room, by handle; each leaves when a later report of it is admitted or it reports its own end, so incomplete clears on a truthful signal; the provider session's end still clears all, and past 512 a flag holds until then - a parent row the store refused counts every live child it reports as refused - with work no record lists, an untargeted strip Stop while no turn runs ends the agent through the idle sweep's stop; under a running turn it leaves the agent alone - the background Stop moves into its own module * fix(chat): the incomplete Stop is offered for every agent and decides inside its step - the strip offers the untargeted Stop for work no row lists whenever no turn runs, whatever untargeted stop the provider has (ACP agents, Codex before it proves terminal stops), since the host reaches it by ending the agent; never under a running turn - the Stop reads whether work is unlisted and whether a turn runs inside its own step, after the listed stops land, so a wake-up turn opened meanwhile is never ended - Codex executions past the bound evict the oldest ended child, else the oldest never started - test the writer's unnamed-subagent label; qualify the writer's retry comment * refactor(chat): the subagent row writer joins the views with their records Drops the shared host-only row projection and its hook-server read: the writer's wiring reads the views every surface reads and the records they come from, and takes residency and the ending's basis from the record. * refactor(chat): Grok subagent evidence reads its ending through the one outcome map * refactor(chat): a subagent's group entry as one object * fix(chat): a background Stop with nothing listed asks the agent for no task stops So an agent with no stop route (an ACP agent the probe found none for) never refuses before the Stop ends the work no row lists. Tests: the unlisted case is untargeted and reads unlisted work inside the step; the repeat-press rig lists the task its Stops reach; reflow the writer header. * test(chat): a nested helper's saved parent; describe how incomplete tracking clears * fix(chat): a closed session claims no Stop; a helper's row follows its own running work - a session close Orca proves is loss of contact, not a Stop the CLI accepted: Claude no longer ends what still ran as stopped, so the host settles it unknown and its row reads unverifiable, as before this stack; stop-acknowledged comes only from a Stop the CLI accepted - a helper that finished while work it started still runs is written working, by the strip's own rule; once that work ends, or the session ends, it reads its own ending again - the CLI starting a completed helper again under the same spawn call is the helper waking when its work ends: the decoder reports it as a restart, so its one row revises in place and ends with its real duration; after any other ending such a start is still read as late - tests from a captured 2.1.280 frame order; item 4 Stops pin their stop-acknowledged basis * refactor(chat): drop the unused Stop basis on a child's own ending Only the acknowledged-Stop edge writes that basis now; reflow the decoder header. * fix(chat): an older history page keeps a turn's subagent ordinals and row A page now reaches back to the first sibling of each subagent group it holds part of, at most one more page of conversation, so loading the older page no longer renumbers same-named helpers or moves (and closes) their folded row. |
||
|
|
564693f176 |
fix(cli): honor Chat UI setting for worktree agent creation (#27796)
* fix(cli): honor Chat UI setting for worktree agent creation * fix(cli): preserve older host launch and worktree lineage warnings |
||
|
|
e3ef95dc04 | feat(cli): launch agents in existing workspaces (#27794) | ||
|
|
5035baac50 |
Edit agent launch settings on the workspace host (#26479)
* Add host-owned agent launch settings mutations * Edit agent launch defaults on the selected execution host * Await durable writes for host agent launch settings * Fence agent detection by execution-host pairing * Acknowledge host settings before hook reconciliation * Preserve Reset after pending agent launch saves * Simplify agent detection re-pair handling to the clear and the transport fence A re-pair already clears every detected-agent list, flag and pending probe for the environment, so the per-record pairing owner keys, the owner-keyed pending probes, the settle-time pairing checks and the hook's ownership gating were never reached. Detection now captures the pairing before the capability wait and fences its RPCs with it; the refresh fallback still refuses after a re-pair. Pins the fence across a capability wait, the needs-update clear, and the agent-launch RPC permission levels. * Open launch settings on the Active Server without the focus-routing helper Main's new owner-routing ratchet counts getActiveRuntimeTarget calls, and this pane's no-workspace default added one. It now reads the Active Server field the way the Agents pane did before host selection, so the default is unchanged. * Re-probe mounted launch surfaces after a re-pair; rely on the transport fence for the refresh fallback A re-pair clears an environment's detected-agent lists, but a mounted tab bar, quick launch or composer that was still waiting for its first list (or whose first probe had failed) never asked again, leaving "Can't reach this workspace's host" until another surface mounted. The hook now re-runs its effect when the target environment's pairing revision changes. The refresh fallback no longer re-checks the pairing in the renderer: it already carries the pairing captured before the wait, and main refuses any mismatch. Pins the Settings host form's remount key on re-pair. |
||
|
|
4526d8bab4 |
native chat: queued-message actions and question answers accept through their own receipt (#26988)
* feat(native-chat): add a chat-owned command receipt store Adds the agent_session_command_receipts table and an insert-if-absent receipt write that runs inside a command's own journal transaction. A receipt records the command's identity, canonical hash, status and a pointer to the row holding its result; it dies with its chat record. Nothing uses it yet: the existing operation ledger stays authoritative. * fix(native-chat): key command receipts like today's ledger ids Rows are keyed by caller and operation id; a send looks across callers and other methods look up only their caller's row, matching the existing ledger. Bad rows and newer table shapes read as unreadable instead of throwing. * fix(native-chat): order receipt lookups by insertion and key by operation id Global lookups now take rows in insertion order, so a read and an insert agree on which receipt answers an id regardless of the host clock. The primary key leads with the operation id, so its own index serves both lookups. * fix(native-chat): accept sends through their own receipt Send (direct, queued, steer) and /compact no longer commit a separate ledger reservation before the message is saved. Their command receipt is inserted-if-absent in the same transaction as the submission or queued draft; a repeated id replays from it, a changed request conflicts. Operation ids are checked for syntax only, so age, clock skew, pruning and the reservation's own storage can no longer refuse a send. The ledger success row they already co-committed stays as a temporary insert-if-absent write so families not yet switched keep seeing these ids; it is never read on this path. * fix(native-chat): answer a send's receipt one way, share /compact's id space with Send, wake delivery once - After its commit, a send whose publication fails answers through the same receipt replay a retry takes, so one id gets one answer; the in-memory copy and journal reload are gone. - /compact ids are global like Send's: a retry under a new connection replays instead of needing a second acknowledgment write. - The receipt route calls its runner directly; the runner wakes delivery when a new message row commits. - The compatibility ledger row expires from host time; each temporary site is labelled. - Receipt read errors and post-commit failures are logged. * test(native-chat): expect receipt answers where tests assumed the send ledger Restart-continuation tests now model the case Send cannot vouch for (saved but not loaded into memory) and keep it unconfirmed; a later throw after the message is in the chat answers continued, as a resend would. Drop /compact's unreachable legacy replay branch and fix stale comments. * test: run the queued tests that open the journal database in the Node runtime project * fix(native-chat): queued-message actions and prompt answers accept through their own receipt Send Now, Delete, Resume and prompt answers move onto the command-receipt route Send uses: the receipt is inserted with the command's own write. A no-op answers only once its receipt commits (otherwise outcome unknown), and a prompt answer's replay reads the exact revision it was accepted at. * fix(native-chat): replay a queued action from what its receipt recorded Send Now's acknowledgement points at the submission it handed off; a question answer's receipt keeps the accepted resolution; a no-op that cannot save its receipt answers outcome unknown and holds nothing in memory; classified refusals pass through. * docs(native-chat): receipt comments say what a replay answers from * fix(native-chat): refuse a prompt answer on a newer Orca's journal with the update words A prompt answer now runs through the same newer-Orca refusal the queued-message actions use, so a read-only journal answers agent_session_journal_unreadable instead of throwing journal_read_only. |
||
|
|
ee34f729be |
Keep SSH chat providers alive after the desktop disconnects (#26473)
* fix: preserve structured chat work during automatic server stops * fix: require stop-request support before automatic cleanup * fix: retain unverified session owners until exit is proven * fix: preserve unreadable work and retry expired prompt stops * fix: give unheld reservations and queued sends retirement exits * fix: clear empty recovery leases and preserve unverified owners in tests * Keep managed servers alive while chat providers are loaded * Recheck returning clients before managed idle shutdown * test: run the managed idle exit chat provider test in the Node runtime It builds on the native chat rest test rig, which opens a real SQLite database. |
||
|
|
7dfa4d6e54 |
fix(native-chat): a new chat's model and effort pickers match their first frame before the agent starts (#27031)
* fix(native-chat): a new chat's effort pill keeps its first-frame value before the agent starts A chat with no model pick runs the catalog's default model at that model's default effort. The at-rest options answer now says so for every agent, not only those whose live child reports the model's default, so the effort pill no longer blanks and then jumps when the agent starts. * fix(native-chat): read a chat's at-rest options for the folder it runs in The at-rest options read asked the model catalog with no workspace, so it always named the account's default model and effort, even in a repo whose own agent config could pick another. The new chat's first frame, read for that repo, names nothing there. The at-rest read now asks for the session's own folder, so both frames agree on the model and the effort. * fix(native-chat): scope the at-rest options read only for clients that take no model A client that needs a model in every options answer (mobile today, older desktops) refuses an answer naming none, and loses that read's commands with it. Only a client advertising the optional-model capability gets the answer for the chat's own folder; any other keeps today's account-default answer. * test: run the at-rest workspace-scope RPC test in the Node SQLite project * test: build catalog listings so the fixtures compile before and after main drops the tier map * fix(native-chat): one at-rest rule — the chat's folder decides its default for every client The folder-scoped catalog read now decides the default for every client. Where it names one, the answer names that model and its effort. Where it names none, a client that takes a model-less answer gets none, and one that needs a model gets the account default model with no effort: exactly what it got before. The reader is now required, and the first frame and the at-rest read share one rule for which locations name a local folder. * fix(native-chat): keep test callers of the host's options read untouched Only in-process callers omit the reader at the host's entry, and they read as the RPC gate treats an in-process caller. The shared read itself still requires one. This leaves main's newer test call sites unconflicted. * refactor: one native-local location check for Claude, Codex and the at-rest folder read |
||
|
|
0024735e82 |
fix(ssh): remove the relay-only remote workspace sync so reconnect never sticks on pulling (#27409)
* fix(ssh): remove the relay-only remote workspace sync so reconnect never sticks on pulling * refactor(ssh): drop snapshot apply tokens and terminal finalization only the sync used * test: drop leftover sync-status mock --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
ff27338299 |
refactor(ssh): connect fails for hosts orcad can't serve; remove the relay client path (#27214)
* wip: connect fails for hosts the managed server can't serve; remove relay session, deploy and relay UI * wip: delete relay-only client modules left unreachable, prune their tests and gates * wip: format * wip: exhaustive unserved message * wip: drop relay attestation test for the deleted ownership proof * fix: prune stale baselines; type the test transport stub * fix(ssh): census proves leftover relay leases; name the remedy for older terminals; translate every connect-failure surface * test(ssh): leftover leases defer to the host census * test(e2e): remove relay-path SSH e2e specs and their CI lanes * test(e2e): drop the auto-convert case that built its relay era through a live relay * test(e2e): mark relay-era browser and catalog journeys fixme until they seed a profile * test(e2e): seed relay-era browser and catalog journeys from a saved profile The three journeys built their old-version state through a connect without an orcad template, which now refuses the host. They now start from a seeded relay-era profile, so the first connect converts the host as an upgrading user's would. * chore(ci): drop shard timings for the deleted relay tests * test(e2e): carry a relay-era login cookie across the browser conversion; own every launch * test(ci): drop the deleted SSH layout oracle from its route * test(e2e): open the retained browser after conversion, retrying a pre-connect card * test(e2e): bind the relay-era browser partition before seeding its cookie A relay-era build always recorded the partition binding before a page stored data in it. Seeding the cookie into an unbound partition made the binding store refuse it (browser_route_partition_binding_store_invalid), so the retained tab showed the generic routing card and Retry could never clear it. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
dab3f6efeb |
fix(native-chat): preserve running subagent rows in long chats (#26798)
* fix(codex): keep running subagent groups in long chats * fix(codex): remember superseded settled child turns * fix(native-chat): retain running Claude subagent groups |
||
|
|
b44a5796c1 |
fix(native-chat): settle a reply cut off by a server restart (#26460)
* fix: preserve streamed turn duration after host restart * test: run restart duration regression in Node * fix: preserve provider receipt time in journal updates * test: verify provider receipt in Claude ownership assertion * fix(native-chat): time interrupted turns from real provider output, not flushes Shared-timeline checkpoints stamped the coalescer flush or retry time as the provider's latest proof of life, so a turn whose provider died before a delayed flush settled with an inflated duration after a host restart. Carry each delta's receipt time through the coalescer and timeline lane. * fix(native-chat): derive interrupted-turn duration from saved output A reply cut off by a server restart could show "Worked for 0s" because recovery ended the turn at the last heartbeat or the reply's first timestamp. End it at the latest time this owner's output was saved to chat history instead, derived in the existing journal fold from the row save time. Drops the per-frame receipt plumbing through every provider and buffer. * fix(chat): exclude host recovery revisions from provider activity Mark journal-open and connection-cleanup revisions with the existing recovery annotation so an interrupted turn does not include host downtime. Preserve buffered provider output and advance restart fixtures to the restart clock. * fix(native-chat): interrupt turns a replaced server left running When the managed server died mid-reply and a new one started on the same host, the cut-off turn settled `unverifiable` with no end whenever the old provider's identity could not be proven gone, and nothing ever revised it: the chat showed no turn status and the sidebar said "unconfirmed". The new server opens the chat records holding the host's instance lock, so every runtime that wrote them before is gone, and every provider it held lost its pipes with it. Record that at load (the fences those runtimes granted, the loaded owner's last renewal, how its runtime ended) and let the stale settle end those runtimes' running or unverifiable turns as interrupted at the owner's last saved output or renewal, with the existing Orca-stop row. Lease adjudication and the live / unverifiable / exited owner verdicts are unchanged. * fix(native-chat): require the record store for any journal open that settles Only an acquisition's open may skip it; a reader open without the store would leave a replaced server's turns unverifiable. * fix(native-chat): say a remote host's Orca restarted, not that it was closed A reply cut off when a remote host's Orca server stopped read "Orca on <host> was closed while this response was in progress." Nobody closed anything there: the server stopped and Orca started it again before the row could show. For a chat on a remote environment, cause quit now reads "Orca on <host> restarted while this response was in progress." The desktop's own quit keeps "was closed". Renderer-only; no wire change. * fix(native-chat): say a remote host's Orca stopped, true for a shared desktop too * test(native-chat): register Grok without compaction in the recovery-output rig Main's #26502 made Grok declare compaction; the rest-test rig adapter has no compact, so the registry refused it before any recovery case ran. * chore(native-chat): say a replaced runtime also revises an unverifiable turn |
||
|
|
280ef3c6f3 |
fix(native-chat): accept sends through their own receipt (#26672)
* feat(native-chat): add a chat-owned command receipt store Adds the agent_session_command_receipts table and an insert-if-absent receipt write that runs inside a command's own journal transaction. A receipt records the command's identity, canonical hash, status and a pointer to the row holding its result; it dies with its chat record. Nothing uses it yet: the existing operation ledger stays authoritative. * fix(native-chat): key command receipts like today's ledger ids Rows are keyed by caller and operation id; a send looks across callers and other methods look up only their caller's row, matching the existing ledger. Bad rows and newer table shapes read as unreadable instead of throwing. * fix(native-chat): order receipt lookups by insertion and key by operation id Global lookups now take rows in insertion order, so a read and an insert agree on which receipt answers an id regardless of the host clock. The primary key leads with the operation id, so its own index serves both lookups. * fix(native-chat): accept sends through their own receipt Send (direct, queued, steer) and /compact no longer commit a separate ledger reservation before the message is saved. Their command receipt is inserted-if-absent in the same transaction as the submission or queued draft; a repeated id replays from it, a changed request conflicts. Operation ids are checked for syntax only, so age, clock skew, pruning and the reservation's own storage can no longer refuse a send. The ledger success row they already co-committed stays as a temporary insert-if-absent write so families not yet switched keep seeing these ids; it is never read on this path. * fix(native-chat): answer a send's receipt one way, share /compact's id space with Send, wake delivery once - After its commit, a send whose publication fails answers through the same receipt replay a retry takes, so one id gets one answer; the in-memory copy and journal reload are gone. - /compact ids are global like Send's: a retry under a new connection replays instead of needing a second acknowledgment write. - The receipt route calls its runner directly; the runner wakes delivery when a new message row commits. - The compatibility ledger row expires from host time; each temporary site is labelled. - Receipt read errors and post-commit failures are logged. * test(native-chat): expect receipt answers where tests assumed the send ledger Restart-continuation tests now model the case Send cannot vouch for (saved but not loaded into memory) and keep it unconfirmed; a later throw after the message is in the chat answers continued, as a resend would. Drop /compact's unreachable legacy replay branch and fix stale comments. * test: run the queued tests that open the journal database in the Node runtime project |
||
|
|
0f7c06aedb |
fix(native-chat): identify a paired device by its device id, never its credential (#26777)
A paired device's socket passes its bearer credential as clientId, and native chat used it as the caller key it stores in ledger rows, receipts, chat records and prompt answers (shown to other clients). Name a paired device by the device id its socket proved instead. |
||
|
|
8defd660c7 |
A new Pi chat names the model it will run on its first frame (#26821)
* refactor(native-chat): one rule for what a no-pick session says of its configured default ACP's rule (only a new session that restored no model pick and was sent none runs its config's default; an effort it picked says nothing) moves beside the catalog entry types so other agents' adapters report through the same step. An empty listing (a signed-out agent) now says nothing instead of "no listed model", so it can never retire a saved default. * feat(native-chat): check Pi's project config before naming its default model Pi reads `.pi/settings.json` in the chat's folder and loads project extensions from `.pi/extensions` (and the `extensions`/`packages` lists). A settings file that sets defaultProvider, defaultModel, defaultThinkingLevel, modelThinkingLevels or enabledModels, loads extension code, or can't be parsed counts as the project picking the model; extension code may register providers or set the model itself. Key names come from Pi 1.0.4's own settings manager and docs, read without running Pi. * feat(native-chat): a new Pi chat names the model and thinking level Pi starts on Pi's model listing marks no model as the one it runs, so a new Pi chat with no saved pick showed "Model" and then changed to the model its session reported. A new Pi session (not one resumed or forked from a saved conversation) that restored no model pick and was sent none now reports the model and, unless it picked one, the thinking level it started on, through the same host step as Grok, OpenCode and OMP. The host saves them as the account's default behind the project-config check, so the next new Pi chat names them on its first frame. A signed-out Pi, which lists no model, saves nothing. * fix(native-chat): a newer, coarser effort menu keeps the effort a chat ran The saved list takes each model's effort menu from the newer listing. Pi's `--list-models` menu stops at high, while a chat's own menu adds xhigh or max for models whose thinking map allows them. After a background re-list, a learned xhigh was no longer in the menu, so the first frame dropped it. When the configured effort is only in the other listing's menu, that menu is kept. * fix(native-chat): Pi teaches only the model and thinking level it started on A Pi chat re-derived its configured default from Pi's current state on every options read, so a model an extension switched to mid-chat (extensions can call setModel) was saved as the account default, and the next new chat named the wrong model. The choice is now computed once, after start-up restores the chat's saved picks, and every later read reports that snapshot. A pick made in the chat no longer matters to it. Tests cover an extension switch, a pick made in the chat, a forked chat, and a learned xhigh surviving a newer re-list. |
||
|
|
573ff0bca3 |
Save a new chat's first message during create (#26332)
* fix(native-chat): the host owns the send queue; the window keeps no saved outbox The desktop kept each structured chat's unsent messages in localStorage and sent them in order, so one message whose fate was unknown froze every later send, Retry dropped it silently, failed sends could not be discarded, and an offline chat could send hours later. The host already records every message and owns the queue; the window now only sends. - One in-memory sender for composer, launch prompts and messages sent from outside the chat. One send in flight per chat; a transport failure resends the same id for up to 30 s; a refusal that proves nothing was recorded puts the text back in the composer with the reason; a send that went out and was never answered shows an in-doubt line with Send again and holds nothing up. - The host's "unknown" rows get the same in-doubt line and Send again, from the journal, in every window. - A message an older build left in localStorage is never sent: the host's conversation outline decides what goes back to the composer, and the copy is deleted once that is saved. * fix(native-chat): send through the structured chat RPC wrapper, with its timeouts The sender called the runtime RPC directly, skipping the per-method timeouts every other structured chat call gets. Only a remote host's request skips the compatibility check the sender already ran. * fix(native-chat): an unconfirmed send goes back to the composer, with no new row line The common pattern draws nothing extra on a message whose delivery is in doubt once its turn is over, and puts a failed send's text back in the composer with the reason. So a send nobody answered in time, or one a host answers in a way that proves nothing, goes back to the composer worded as unconfirmed, and a host "unknown" row shows nothing extra. Removes the in-doubt phase, Send again, and its strings. A host's made-up row for an id its journal lost now reads as unconfirmed, not as recorded, so that message comes back instead of vanishing. * test(native-chat): pin that nothing resends a send given back as unconfirmed * fix(native-chat): sends survive a tab close, never resend after a Stop, and keep remote images - A normal tab close lets sends on their way settle; what the host never took goes back to the conversation's draft. Only a cancelled launch or a worktree purge drops them. - After any Stop, a send already on its way is never sent again under its id: a doubtful answer, or a resend that was due, hands its text back worded as unconfirmed. - A returned image keeps the SSH connection it lives on; the connection never goes to the host. - An older build's saved message the host recorded and then rejected is left to the host's own row, never handed back. * fix(native-chat): a Stop or a tab close never resends a send already out, and a kept card is the card's A Stop that landed while a same-id resend was being readied (its timer fired, its request not out yet) let that resend go out after the Stop. The sender now tracks whether a request is awaiting its answer: a Stop hands back every send between attempts as unconfirmed, lets one whose request is out settle from its answer, and never issues a request after it. A normal tab close withdraws the same way instead of doing nothing, so nothing more goes out and nothing is dropped. A send the host rejected but kept as a card (keptAsQueuedMessageId) is the card's, from its reply or the journal, and never goes back to the composer. Adds the freeze tests: a send whose fate is unknown holds later sends no longer than its deadline, and one the host can neither confirm nor deny releases the next at once. * fix(native-chat): read a resend's turned-away call or reused id as unproven Ports the send-answer proof contract. A call the host turned away before running it (method_not_found, invalid_argument, unauthorized) proves only that this request wrote nothing, so it reads as never sent on a first attempt only; on a resend an earlier attempt may have landed, and it goes again under the same id. A resent id the host says was already used for other content (messageIdReused) proves nothing either, like an expired or conflicting id. Pins the rest of the contract: any row the host returns is its own, a thrown error is no answer whatever its code, and an older build's refused, held or outlived-Stop copy is handed back once and never sent. * refactor(native-chat): type the structured composer's send with its attachment type Keeps NativeChatStructuredSession.tsx within the file length limit. * refactor(native-chat): drop the kept-card guards the sender never needed A kept send's row is a rejection that was never a Stop's, so the sender already reads it as recorded, from its reply or the journal. The test that pins it stays; the two extra checks only covered a kept row that is also a withdrawal, which the host never writes. * test(native-chat): route launch tests' sends through the client wrapper the sender calls The sender sends through callStructuredAgentSession, but these launch tests replaced that module with a factory that answered nothing (and still named a probe that no longer exists), so every launch prompt resent until its 30 s deadline and the tests timed out. Each factory now hands sends to the runtime RPC mock the tests already answer, and expectations of a local send no longer ask for the remote-only compatibility option. * fix(native-chat): hand a message back without importing the composer's attachment hook The worktree purge reaches the launch prompt, which hands text back, and the attachment hook's imports reach the store. A test that builds the real store behind a mocked one then waited on itself and hung. Handing back now writes images to the draft store directly, as the hook's helper did, and a test pins that each returned image keeps its SSH connection. * fix(native-chat): one send per chat, with no line of sends behind it A chat with a send out took further messages into an in-memory line and sent them one by one. A message typed behind one in doubt then hit its own 30 s deadline and came back as not sent without ever going out. Now, as the common pattern does, a chat takes one send at a time: while it is out, Send is disabled and Enter leaves the text in the box. The sender refuses a second send instead of lining it up, so the 30 s deadline always runs from the send itself. A Stop or a tab close stops the one send: settled from its answer if its request is out, handed back as unconfirmed between attempts, or silently if it never went out. Notes sent from outside the chat while its send is out stay with their sender (not ready); a launch prompt that meets the person's own first message waits in the composer instead of being lost. * fix(native-chat): give a Stop-withdrawn note back to its chat once its notes were cleared Notes sent from outside a chat clear once their message is recorded. A message the host recorded as pending and a Stop then withdrew came back only to its sender, which had already let go of it, so the text was lost. The chat's draft now takes it, as an earlier build's outbox did. * test(native-chat): type the composer-actions probe without a cast * chore(native-chat): drop outbox wording left in comments and an empty locale group * fix(native-chat): pace failed checks, keep the host's reason, and hold the chat for its launch prompt - A send whose checks failed before its request went out (an unreachable or incompatible host, an unreadable history) was tried again at once, about a thousand times a second for 30 s. Attempts are now paced by the attempts made, whether or not their request went out. - A host that refuses every resend by throwing (native chat turned off, a journal that won't open) gave back only "couldn't confirm". The host's reason now comes first, still without claiming not sent. - A launch's prompt now holds the chat's one send from the click, drawn as sending, so a message typed while the chat starts can't overtake it; it goes out once the chat exists, and a cancelled launch frees it. - Notes whose send nobody could confirm say so instead of "did not accept", and notes launched into a new chat let go of their text once that chat's composer holds it. - An open chat keeps drawing a recorded send until its row arrives, so a reply that beats the history no longer makes the message flicker out. * fix(native-chat): hold a chat's sends until it has started, and send a failed chat's message with its restart A message sent to a chat still starting went out at once to a host that had no record of the chat yet, read for a fence it could not get, and came back as not sent. A message sent to a chat whose start failed restarted it but no longer went with the restart. While a chat starts, Send stays off and Enter leaves the text in the box, as with a send already out. A text message sent to a chat whose start failed restarts it and goes as the restart's first message, through the same staged-prompt path a launch prompt takes: it holds the chat's one send slot, drawn as sending, until the chat exists, and comes back to the composer if the restart fails again. Notes wait while a chat starts and ride a failed chat's restart, keeping their text until it is sent. * chore(native-chat): test the queue request, rejection words and card hand-offs; drop an unused clear - Pin which sends ask the host to queue, the moved rejection wording, and that a queue send whose card was handed off and then refused or withdrawn, or whose replay names a withdrawn card, is never handed back. - The send-at-most-once gate now says what the desktop promises after a reload: it never sends the id again, and hands the text back. - The legacy read names when it goes, and drops the notice clear nothing called. * fix(native-chat): keep a sent launch prompt's entry, and give back at once what can't go out - Cleaning up a launch prompt released its send slot even after the prompt had gone out, which deleted the entry the sender keeps once the host records it. A launch prompt recorded as pending and then withdrawn by a Stop was lost from both the chat and the box, and an open chat dropped the new chat's first message before its row arrived. The slot now gives back only a reservation that was never sent. - A send stopped before its request went out by something trying again won't clear (this client and the server can't talk, or the host refused the history read) kept Send off for 30 s and then said Orca couldn't reach the agent. It now comes back at once with its own cause: not sent, since nothing went out, or unconfirmed if an earlier attempt did. Transport errors keep the paced retries. * fix(native-chat): notes keep their own text through a new agent's launch Notes sent to a New agent whose start failed came back to the notes and also sat in the new chat's composer, so sending both delivered the text twice. The notes keep their text until it goes out (they hold it from the click), so the launch now says so and no composer gets a copy, on a failed start or a refused prompt alike. The tests that asserted the copy in the composer pinned the old double ownership and now assert the notes are its only owner. Notes that rode a failed chat's restart and ended unconfirmed now say Orca couldn't confirm them, as notes sent directly do, instead of that the agent did not accept them. * chore(native-chat): the send-once gate says what the desktop keeps across a reload The desktop keeps a send's id in memory only: a send still unsettled at a reload or crash is not resent and not handed back. The coverage notes no longer credit the renderer tests with durable identity across a remount. * fix(native-chat): say once why notes sent to a new agent did not go Since notes keep their own text through a new agent's launch, a prompt the host refused, nobody could confirm, or that found the chat's send taken came back to the notes with nothing said anywhere: the chat shows no notice for text its caller keeps. The notes menu now reports it once, with the toast a send to an existing chat already uses: not accepted, couldn't confirm, or not ready. A start that failed still says so in the new chat instead. * fix(native-chat): retry a history refusal the host says clears, and name it at the deadline A send stopped before its request went out came back at once for any refusal of the history read, including ones the host names as clearing (its journal briefly unavailable, a chat detached while the host quits), so the automatic retry was lost. Only a version mismatch and refusals that won't clear come back at once now; the rest go again on the paced schedule, and if the deadline still finds nothing sent, the words are the host's refusal rather than Orca couldn't reach the agent. * feat(native-chat): a send makes one request, and nothing ever sends it again The desktop resent a message under its own id for up to 30 s when the answer was lost. A send now makes exactly one request, as the common pattern's clients do: - An answer that is lost, dropped or proves nothing hands the text back at once with "couldn't confirm… check the chat". - A failure before the request goes out (the environment check, the history read for the fence, a refused connection) means nothing went out: the text comes back at once with its own reason, or as not sent. - The 30 s cap stays on the one request, so a host that never answers can't hold Send. Nothing ever resends, so nothing can go out after a Stop: a Stop only takes back a send still in its pre-send checks. The resend state goes with it (tries, generation, awaiting, stopped, the resend timer, the send-answers-proof probe, and the first-attempt/resend split in the evidence), and the send-once gate says the desktop never resends. * fix(native-chat): notes keep the chat's line, and a send held behind a rewind says it was not sent - Only a send whose text belongs to the chat's composer clears the chat's line. Notes sent from outside the chat no longer wipe a "couldn't confirm... Check the chat" line that explains text already back in the box. - A send the host turns away behind a rewind it could not confirm went back as "not sent" but said "couldn't confirm what happened. Check the chat". It now gives the rewind's reason and says the message was not sent. - Reliability gate names the single-request test and records a fresh evidence run; the sender test drops its leftover resend mocks and a duplicate Stop test. * fix(native-chat): a send ends even when putting its text back fails If writing the returned text into the chat's draft threw, the send never settled: it stayed "sending", the chat refused every later send until a reload. The send now always ends after a hand-back, the failure is logged, and the chat's line adds "Couldn't save your message." * fix(native-chat): a message typed during /clear stays in the box and follows the chat A /clear moves the chat to a new conversation, and its host refuses any send while it runs. A message sent then went out, came back refused into the old conversation's draft with its line, and vanished when the chat moved on: the box and the line now belong to the new conversation. - A /clear holds the chat's one send slot while it runs: Enter does nothing, Send shows busy and the text stays in the box, as for a send that is out. Notes sent from outside get "busy". - Once it moves the chat, the old conversation keeps taking no send until the view leaves it, and what is left of its draft moves into the new conversation's draft, after anything there: when the composer's /clear settles, and again when the view moves. * fix(native-chat): no "Send message?" or queue clear while the chat's send is out While a send is out (or a /clear runs) the chat takes no message, yet Enter over a held queue still opened "Send message?", and Clear queue deleted every card before its message was refused. Enter now does nothing there and the text stays; Clear queue re-checks and deletes nothing if a send went out after the question opened. * refactor(native-chat): take the /clear draft carry out of this change Moving the old conversation's draft into the one a /clear replaces it with fixes a bug main has too (text left in the box during a /clear stays with the old conversation), so it goes in its own change. Kept here: a /clear holds the chat's send slot while it runs, and the conversation it moved away from takes no send until the view leaves it. * fix(native-chat): a /clear's hold on sends always ends - A view that unmounted while a /clear that moves the chat was out left the old conversation holding its sends until a reload: the late reply kept the hold for a view that was gone. The reply now releases it. - The local call for a conversation command has no deadline of its own, so a /clear that never answers kept Send off for good. The hold now also ends at the command's deadline (195 s, the one the remote call already uses), whichever comes first. * test(native-chat): opening a chat an older build left stuck; hand back its copy in send order Pins, through the real chat hook, sends, legacy recovery and draft store, what opening such a chat does: the queued messages behind a message the host recorded in doubt come back to the composer once, in order, with the "not sent" or "couldn't confirm" wording; the chat is free to send under its read's fence; nothing happens while the chat has no fence here. The legacy reader now hands entries back in the order they were sent (queuedAt), as the older build's reader did, instead of array order. * fix(native-chat): a send this window turned away for a re-paired server comes back as not sent A managed server's update rotates its pairing, and this window's main process then answers the next call itself, before forwarding anything, with runtime_environment_changed. The send read that thrown answer as proving nothing, so the person was told Orca couldn't confirm a message that never left. It is now handed back as not sent, with the reason. Every other thrown answer still reads as unconfirmed once the request may have gone out. * Save a new chat's first message during create * Align opening-message types with the user-message wire * Keep chat publication and cleanup outside store initialization * Complete the create refusal fixture ledger * test(native-chat): prove a created first message waits for the agent's start and is handed over once * refactor(native-chat): keep the host and session hook under the line limit after the main sync * test(native-chat): a chat created with its first message and no pick still teaches its agent's default After the main sync, a created chat's agent starts from the delivery loop rather than the create call. Grok (through the real ACP adapter and model catalog) and Claude (through the real adapter) each still report the model their own config resolved to once the chat reads its options. |
||
|
|
39a01f9c0b |
feat(agent-launch): create a folder workspace and start its agent in one host operation (#26083)
* feat(agent-launch): accept a create-folder-workspace target agent.launch can now create a folder workspace and start its agent there, the same operation it already runs for a new git worktree. The host creates the folder workspace (metadata only, no startup terminal), resolves it the way an existing target is resolved, then settles the mode and builds the surface as for any existing folder workspace. Hosts advertise agent.launch.create-folder-workspace.v1 so a client offers the target only to a host that accepts it. * fix(agent-launch): authorize, classify and navigate folder creates like their create method A create-folder-workspace launch could do three things its two-call equivalent never did: - A phone could create folder workspaces. The mobile allowlist admits agent.launch but not folderWorkspace.create, and is checked by method name only. A create target now needs the authorization of the create method it stands for, read from the same allowlist, and a phone is refused with agent_launch_target_forbidden before admission, so no ledger row and no create. create-worktree maps to worktree.create, which phones already may call, so nothing changes for it. - A create refused before anything was stored (group gone, folder missing, SSH host unreachable) replayed as agent_session_operation_unknown, and a retry under the same operation id was refused until the row expired. runtime.createFolderWorkspace now raises those refusals as FolderWorkspaceCreateRefusedError, with the same message text, and the launch records it as failed for any target. A failure after the create still replays unknown. - A paired caller's folder create activated the new chat on the host window and did not select the caller's own tab. Only create-worktree, whose host activation runs setup, keeps the host navigation now; a folder create navigates like an existing workspace. Tests cover the phone refusal, replay of a success, a refused create settling failed and replaying that failure, an after-create failure staying unknown, an SSH connection reaching the agent's view mode, paired navigation, and an older host refusing the target at parse. * fix(agent-launch): replay a refused folder create with its whole path A folder create refused before anything was stored is recorded as the launch's failure, and a retry under the same operation id answers from that record. The record went through the 128-character cap every launch failure code gets, and folder refusals carry the folder's absolute path, so a retry could show the user a path cut short: a different folder. The refusal is now recorded whole when it fits a path as long as Linux's PATH_MAX after its code. Past that, which no folder a stat accepts reaches, only the code before the path is kept, so the replay degrades to the bare code instead of a partial path. Also: the controller test asserts the exact refusal message, the create-folder-workspace capability comment says a phone needs its own gate, and the forbidden-target code says why it is not `forbidden`. * test(folder-workspace): type the controller test's store stub without an assertion The changed-code quality gate refuses the `as never` cast on the stub store. It is now a RuntimeStore whose unreached members throw. * test(agent-launch): run the folder-create launch tests in the Node runtime project The operationId replay tests open the real SQLite record store, so the file belongs with the other ledger-backed agent.launch tests. * test(agent-launch): pin that a paired folder create's terminal stays off the host window The folder-create test now asserts surfaceOwner: false on the terminal a paired caller's launch creates, rather than matching any options. The caller-selection header now says only an in-process worktree create keeps worktree.create's navigation. * test(agent-launch): drop the per-connection view-mode expectation main removed |
||
|
|
6ba86414bf |
Show model and effort choices for every agent before its session starts (#26407)
* feat(native-chat): one saved model catalog for every structured agent Every agent registration now declares how its models are listed without a session (a probe, or explicitly none). Probes share one supervised runner (15 s budget, bounded output, teardown on every path). Grok lists from initialize (or x.ai/models/list), OpenCode from models --verbose, Pi from --list-models; Claude moves onto the shared runner. ACP option reads no longer copy the running model's effort menu or selection onto every model. Desktop and phone pickers read the same catalog for every agent, with a non-pickable provider-default model pill before any list exists. * fix(native-chat): host names a listing's configured model; live listings saved for every agent - The catalog answer carries an optional listingNamesConfiguredModel field, set per registration (Codex, Grok) and per workspace; pickers name the listed default from it instead of a Codex-only client flag (older hosts fall back). - Grok, OpenCode and Pi live option reads save their model list through one host step, keeping the account's known default and per-model efforts. - Phone reads the host catalog like the desktop and seeds every agent. - Saved picks accept any known agent id (OpenCode, Pi). - ACP agents with no session-free listing register as unavailable; Grok's live effort menu falls back to the session's when the advertised one is empty. * fix(native-chat): discovery and live listings kept apart; every agent's live listing saved by the host; phone-created chats name their model * fix(native-chat): phone new-chat mark clears on the chat's first live answer; Claude's startup listing reaches the host catalog * fix(native-chat): live-listing type lives beside the catalog entry so wire types don't pull the store's persistence into the phone * fix(native-chat): the model picker never waits on a listing; a first listing lands in place * feat(native-chat): the host lists every agent's catalog ahead of use and traces each listing * feat(native-chat): a new Claude chat names the model and effort its config resolves to * test(native-chat): catalog service fakes gain prewarm * test(native-chat): prewarm span recorder without a type assertion * fix(native-chat): project agent config overrides the account default only when it sets a model or effort A committed .claude/settings.json for permissions or hooks, or the settings.local.json Claude writes on always-allow, no longer hides the configured Claude model in that project. Unreadable config still counts; Codex config.toml follows the same rule. * test(native-chat): the Codex picker read and the launch it starts resolve one account key, through the real resolvers Runs resolveHostCodexHomePathForLaunchReadOnly and prepareCodexRuntimeHomeForLaunch as the runtime service wires them, for the system account on the real home, on the shared runtime home, and a managed account. Also scopes the prewarm suite to agents in use. * fix(native-chat): Claude's configured model and effort are saved together behind the project check; prewarm lists only agents in use and stops with its host A live Claude chat no longer stamps its effort onto the account catalog; the effort travels with the configured default, which a no-pick chat whose config resolves to no listed model now forgets. Prewarm lists agents with a saved catalog or a chat, and host teardown (quit included) starts no queued listing and stops running ones through an abort signal the listing runners honor. * test(native-chat): new catalog tests use typed hoisted state and type-only namespace imports * test(native-chat): a prewarm probe sets and clears the sign-in verdict like a read's, and re-checks an aged one beside a fresh catalog * fix(native-chat): an aged sign-in verdict re-probes only from probe paths, not a Codex chat's picker The main merge put the held-verdict check into store.shouldRefresh, which also gates the live Codex picker. A chat's own listing never clears the verdict, so a signed-out Codex whose chat works re-listed model/list on every picker read. shouldRefresh is back to main's rule; the catalog read's probe and prewarm ask probeDue instead. * fix(native-chat): Claude project settings with an effort cap, a main-thread agent or an Anthropic cloud provider count as an override maxEffortLevel clamps every effort including the model default, and agent applies that agent's model (both from the bundled SDK settings type). CLAUDE_CODE_USE_ANTHROPIC_AWS and CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD sit in the SDK's provider-switch list beside Bedrock and Vertex. A project that sets only these no longer shows, or saves as the account default, the account's model or effort. * refactor(native-chat): the catalog store's picker waiters live in their own module No behavior change: pendingListing and the waiter notifications move as they were, which keeps the store under its line limit for the fixes that follow. * fix(native-chat): changing an agent's command or environment re-lists its catalogs and re-checks its notice The settings change called prewarm, but prewarm skipped any catalog still fresh (the catalog key is agent + account, not the command), and a held "not installed" verdict kept its 30-second window. The change now marks that agent's catalogs due and ages its held verdicts, so the prewarm it triggers re-lists them and re-checks the notice. Only agents whose command or env actually changed are touched. * test(native-chat): the sign-in notice tests check the model picker is really usable They checked only that a model entry existed, which a disabled or empty pill also passes. They now check it is settable, carries no disabled reason, and offers the catalog's or the seed's model. * fix(native-chat): a probe the host stops keeps the held notice reason, and a stopped prewarm reads nothing A probe aborted because the host is going away says nothing about the account, so it no longer replaces a held sign-in or not-installed reason with a plain failure. A prewarm after stop returns before reading chat records. * test(codex): the catalog account-key test uses the runtime's real Codex launch-home resolvers The resolvers move to codex-launch-preparation as one export the runtime service spreads, so the test runs exactly what the runtime is handed instead of a copy. * feat(native-chat): keep each host's saved model lists in the renderer A chat picker could only learn the host's saved model list by reading it after it mounted, so its first frame was a guess. The renderer now keeps the host's answers per host and agent: this machine's lists are read once its runtime answers, for the agents its chats were used with; a paired host's again when this client regains contact; and an account or launch setting change drops this machine's lists. Every entry is a host answer. * fix(native-chat): render the model pills once, from the host's list A new chat's composer painted its first frame from the built-in model list: a saved pick that list lacks (Claude's `opus[1m]`) showed as a raw id with no effort pill, then jumped to "Opus (1M context) · High" when the host's list landed. Every new chat painted a guess and then corrected it. Now the first frame reads the host's saved list the renderer already holds, so it is the final frame. Without one, the quiet "Model" placeholder stands until the host answers; a host with nothing saved makes the built-in list usable, naming nothing, until its listing lands. A saved pick is named only from a host or session list that names it. The phone follows the same rule and keeps the host's list across a new fence. * feat(native-chat): answer a saved-only model catalog read The app could preload a host's saved model lists only for agents with a saved pick, because the catalog read starts a listing (spawns the agent's CLI) when nothing is saved. Grok, OpenCode and OMP chats with no saved pick were never preloaded, so their pickers showed "Model" before the host's list. `agentSession.modelCatalog` now takes `savedOnly`: the host answers from what it saved (the entry, configured default and held reason) and starts, joins or re-checks no listing. Its params are strict, so the host advertises `agent-session.model-catalog.saved-only.v1` and clients send it only to hosts that do. An answer also says when its named default holds in every workspace (Grok reads no project config for its model), so a new chat in any workspace can use it on its first frame. OMP stays without a session-free listing: `omp models --json` lists every model without the `enabledModels` filter its chats apply. * fix(native-chat): preload every registered agent's saved model list Grok, OpenCode and OMP chats painted the "Model" placeholder, then the host's pills, because the app preloaded only agents with a saved pick. Now the app reads every agent a host registered through the saved-only read: this machine once its runtime and agents are known, a paired host at connect and on each reconnect, and this machine again after an account or agent-setting change. A host that does not advertise the saved-only read keeps the earlier rule (saved picks only; a paired host's answered agents). An agent the host saved nothing for stays unknown. An answer sent before the host's lists were dropped is not recorded. * test(native-chat): drop a type assertion from the snapshot sync test * fix(native-chat): learn an ACP agent's default from a chat started with no pick Grok's session-free listing named the model `initialize` computed (grok-4.7), but chats ran another (grok-4.6), so a new chat painted 4.7 and then jumped. The listing no longer names a default; a new ACP session with no model pick reports what it runs, and the host saves that as the account's configured default behind the workspace check. A catalog read waits for a recording still in flight, so the chat's own follow-up read names it. * feat(native-chat): check OpenCode and OMP project config before naming their default OpenCode reads opencode.json(c) and .opencode/ in every ancestor of the workspace; OMP merges its .omp settings and other agents' project settings. A workspace whose config sets a model, effort or provider key (or can't be parsed) names no default; any other lets a no-pick chat's model become the account's default, so the next new chat names it on its first frame. * fix(native-chat): re-read the host's list once a no-pick chat reports its model The renderer kept the answer it read before the chat started, so the next new chat's first frame still named no model. A new chat that named no model reads the host's list again once it reports what it runs; a default no workspace can replace also serves chats in other worktrees. |
||
|
|
cbad6412be |
feat(grok): show Grok's subagents as subagent rows (#26508)
* feat(grok): show Grok's subagents as subagent rows * fix(grok): retain live subagent roster ownership * fix(grok): retain child outcomes during shutdown * fix(grok): show live subagents in running task surfaces * fix(grok): settle owned child session completion notices * refactor(grok): fold child-session guards under the translator line limit |
||
|
|
ded0144525 |
fix(native-chat): settle a dead agent's leftover work in one write that always fits (#26664)
* fix(native-chat): settle a dead agent's leftover work in one write that always fits A dead generation's cleanup was several separately committed writes, and a single leftover item larger than one batch row was rejected on every try, so the chat stayed stuck. Plan every row first and commit them in one transaction: bounded batch rows, an oversize item as an ordinary row with its full body, and the settlement's own send verdicts. Readers see it only after the commit. Settlement rows are rebuilt only from schema-known fields, address saved items by their stored key, never invent an end time, and new writes require safe revisions, so cleanup never builds a row it cannot save. * fix(native-chat): settle Codex exits in one write and keep turn fields a newer build saved - Codex's own exit cleanup hands the whole settlement to the journal's one-transaction planner instead of committing chunk by chunk. - One shared planner rejects an item named twice, on both the direct and resolved paths. - Rewriting a turn keeps fields this build does not know, as the journal schema promises; the legacy carrier's kind still cannot override it. - Drop the write-only revision admission and the planner skip that worked around it: insert admission is the reader's again, and a saved revision that cannot advance is skipped by the reducer while the rest settles, as before. - Remove two dead-generation journal members settlement no longer calls. * fix(native-chat): advance repeated items across a settlement's rows and keep nested turn fields Repeated Codex question ids in one write fold in order again instead of failing the write, and a saved turn's context usage keeps fields a newer build stored inside it. * test(native-chat): run the new Codex journal SQLite tests in the Node runtime project |
||
|
|
6235f9a641 |
fix(acp): show the agent's own reason when a prompt fails (#26666)
Agents often answer a failed session/prompt with a generic JSON-RPC message ("Internal error") and keep the real reason in error.data. Only OMP and Grok had hooks that read it, so OpenCode and future ACP agents showed the bare message.
The prompt error detail now reads, for every ACP agent: the agent's hook, else string data, else data.details, else the message. Other structured data is metadata and is ignored. OMP's details hook and Grok's string-data branch are now covered by that rule and removed.
|
||
|
|
496a1712f2 | fix(acp): keep OMP replies that follow its reasoning (#26658) | ||
|
|
bb44f74d00 |
Keep native chat /clear in the same conversation (#26579)
* Keep clear in the same chat and reset its provider context * Derive clear pauses only from journal membership and cover older rewind clients * Point queued command rewind test at the structured hook * Group queue consume errors and rewind replay assertions |
||
|
|
ff4a51c872 |
Show sign-in guidance for all native chat agents (#26544)
* Recognize native chat sign-in failures across structured agents * Fix authentication failures and chat sign-in guidance * Preserve sign-in diagnostics and command retry context * fix(native-chat): keep visible sign-in guidance and localize startup rows * fix(native-chat): separate auth diagnostics from retry guidance * test(native-chat): align startup guidance expectations * fix(native-chat): start a new agent for the send after it reports not signed in * fix(native-chat): scope the signed-out check by fence and read without a snapshot * fix(native-chat): keep a signed-out agent while it owes work, and read only its own rows * fix(native-chat): keep a signed-out agent that may owe a wake-up turn, and skip the check during a Stop |
||
|
|
a062abffd2 |
Stop native chat jumping when Codex starts a foreground command (#26503)
* Keep foreground Codex commands out of the background strip * Run the command-strip host regression in Node * Exercise retained commands after their source turns end |
||
|
|
04ac6d6cb2 |
Hold messages in the host until a chat's agent has finished starting (#26416)
* feat(native-chat): host-owned startup attempt and hold-and-deliver barrier The host now mints one startup attempt before any adapter acquires (lease identity, spawn token, pinned launch context, cancellation, a 60 s deadline from the mint that activity never renews, option intent, and the event and child-work sinks), and the shared delivery loop hands nothing to a child still proving its start. A send is still accepted and journaled at once; readiness (the normalized `started` settlement) wakes delivery, which re-checks the child, fence, close and Stop before handover. A start that fails, crashes, or misses its deadline leaves held messages definitely unsent; a dispatch that throws after handover is `unknown`, never inferred unsent from the child's phase. * fix(native-chat): time agent starts from spawn by progress, and release Claude on initialize Review round 1 on the host-owned startup attempt. - The startup clock now starts when the process exists and fails a start only after 60 s with no stdout frame or stderr line, or 10 minutes after spawn however chatty it is. Every agent's child reports output through the one managed-process spawn, so a slow migration or history replay is no longer killed mid-way. Each start's spawn-to-outcome time is recorded as an `agentSession.startup` span for tuning. - Claude reports `started` on its initialize answer; the settings read follows and is applied and persisted as a later `options-reported` update, so messages no longer wait on it. - A started child's reported options are persisted in their own step after the held messages are handed over, and only if no pick changed the record's options since. - An attempt now ends whenever its child ends or an expiry stopped it. - Removed `optionsAsOf`, which was not an option revision, and comments that described the old publish-and-write-at-once behaviour. * fix(native-chat): keep the startup attempt's contract free of host and process code The adapter contract imported the attempt type from the module that also runs its clock and imports the host types, which pulled the model catalog and process-scan code into the mobile test typecheck. The attempt's shape and limits now live in a dependency-free contract module. * fix(native-chat): never let a stale option report undo a later pick A report of the child's options is now persisted only if no pick, no model-not-found heal and no newer report reached the chat since its read began. The host keeps one option revision per chat; the startup attempt hands the adapter a reader for it, and each report carries the revision it was read at. This replaces the record-snapshot check `started` had and covers the settings report, which had none, so a settings read answered just after a pick can no longer write the older model back. Also: the option read a ready child does inside its start now ends with the start (limit, Stop or close); a start that turns ready as the limit fires is recorded as ready; the startup-attempt host tests run under the Node SQLite runtime; and the retired-model test follows the new startup order. * fix(native-chat): require the option revision and order reports by arrival The host can no longer mint a startup attempt without the conversation's option revision, so no host path can stamp a report at a stale zero and drop every report after the first pick. Adapter tests may still omit it. A report is now dropped only when a pick or a value the child showed it cannot run came after its read. Reports are ordered among themselves by when the host takes them, and only the newest admitted one is persisted. The settings read's stamp therefore holds however late the host takes the start's report, instead of relying on that report being handled synchronously before the read begins. Tests: a settings read stamped before the host took the start's report is kept; a report read after a failed pick is persisted; the minted attempt's revision reader answers the host's current revision. |
||
|
|
e3639ef8d6 |
Phase 3.5: fix the top SSH/remote issues at the root cause (#26483)
* fix(remote-runtime): stop arm64 Linux servers dropping clients every heartbeat (empty WebSocket write EFAULT) (#26200) * fix(remote-runtime): never write an empty WebSocket payload on its own (arm64 Electron EFAULT) Patch ws@8.22.0 Sender.sendFrame so a frame with an empty payload is written as its header alone. Under Electron on arm64 kernels with 39-bit virtual addresses, socket.write() of a zero-length buffer fails with EFAULT and kills the connection, so every empty heartbeat ping, auto-pong and close dropped the remote runtime with 1006. Wire bytes are unchanged. * test(remote-runtime): run the ws empty-payload guard on real Node Bun resolves 'ws' to its built-in client, so under the Bun test project the guard exercised Bun's implementation instead of the patched package. --------- Co-authored-by: m4air <m4air@Mac.localdomain> * fix(orcad): ship the session scanner child so agent session history lists on orcad hosts (#26309) orcad built every forked child except session-scanner-service-entry.js, so aiVault.listSessions always failed on an orcad host. The resolver also skipped the adjacent entry whenever isPackaged was true, so shipping the file alone would not have been enough. - build every ORCAD_CHILD_ENTRY_POINTS entry from one loop and add the scanner - add it to ORCAD_ARTIFACTS so a deploy missing it fails integrity checks - share one forked-child path resolver (asar layout vs flat app root) between the watcher and the scanner - smoke-fork the built scanner during build-orcad under both runtimes Fixes #24105 Co-authored-by: m4air <m4air@Mac.localdomain> * fix(remote-runtime): one owner for a remote pane's binding and recovery (#26308) A remote pane no longer latches in connecting on terminal_not_found, holds keystrokes typed during same-terminal recovery instead of dropping them, reports transport-owned retries so the pane is not remounted mid-retry, and no longer infers absence from an inventory that predates the leaf. Co-authored-by: m4air <m4air@Mac.localdomain> * ci(e2e): move apt off the Azure mirror on runner images that use a mirror list (#26335) Ubuntu 24.04 runner images resolve apt sources through /etc/apt/apt-mirrors.txt, so rewriting only the source lists left package downloads on azure.archive.ubuntu.com, which intermittently fails ('Ign:' on every package). Rewrite the mirror list too, and retry the install once with --fix-missing. Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> * fix(terminal): remote snapshot replay keeps scrollback it cannot replace (#26319) Desktop subscribers of a remote terminal received screen-only snapshots, and every pushed snapshot replay cleared the pane's scrollback with ESC[3J. A reconnect, a remount, or a recovery after a dropped frame therefore wiped history the host never resent. - Host: desktop subscribe snapshots use the same scrollback-row ladder as mobile, and SnapshotStart now carries an optional scrollbackRows field. - Client: a pushed snapshot clears scrollback only when the host says it carries history; screen-only images (scrollbackRows 0, or older hosts that omit the field) repaint the screen and keep the pane's history. Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(daemon): clean launch env, validate client frames, fit the socket path (#26306) * fix(daemon): clean launch env, validate client frames, fit the socket path - Drop Chromium's DBUS_SESSION_BUS_ADDRESS=disabled: marker from the daemon, its systemd-run scope launch and every PTY env (#25580, #21119). - Use the durable user-manager scope only when that manager outlives the caller: lingering on, or the caller already runs inside it (#24201). - Shape-check hello and request frames, and catch request rejections, so a malformed frame closes one connection instead of the daemon (#17841). - Move the daemon socket to a short per-uid private dir when the data root would overflow sun_path (#17840). * fix(daemon): validate a relocated socket dir before any connect, not just before bind --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> * fix(ssh): keep crediting a PTY after its recovery delivery is canceled (#26341) A canceled recovery delivery released its source spans while the renderer and model could still settle them; the stale projection then threw on every later ACK, so the next delivery was never credited and the relay paused the PTY for good. Settling or transferring a span of a closed delivery is now a no-op. Co-authored-by: m4air <m4air@Mac.localdomain> * fix(rpc): deny-by-default caller permissions and a scoped SSH bridge credential (#26337) * fix(rpc): deny-by-default caller permissions and a scoped SSH bridge credential Every RPC method now declares a required permission, and RpcDispatcher checks it against the caller's scope on every transport (owner, mobile, paired runtime, SSH bridge). The SSH relay bridge no longer spawns the client CLI with owner authority: each invocation gets a short-lived credential bound to its SSH target, revoked when the child exits. Without the new per-host opt-in, that credential reaches only its own host's terminals; with it, workspace methods but never desktop control, accounts, settings writes, skill install/share, pairing or host admin. Paired runtime clients need a pairing-time grant (orca serve --grant-desktop-control) for computer.* actions. Fixes #23835 Fixes #25259 Refs #18269 * fix(rpc): let a bridged SSH worker report to its own coordinator; allowlist paired-runtime permissions Without the per-host opt-in, orchestration send/ask/reply/check/inbox now pass when the caller is a terminal on the bridged host, binding pane key, payload Dispatch, run and recipient to that caller's own orchestration (recipient off-host only as the creator of its active Dispatch). Paired runtime clients get an explicit standard permission set; pairing-admin and desktop-control are grant-only. * fix(rpc): resolve a bridged SSH caller's own Run and Dispatch mailboxes before admission Mail routing stores a worker's follow-ups under dispatch:<id> and worker questions under run:<id>, so comparing the original recipient to the caller's terminal handle refused an SSH worker replying to its coordinator, an SSH coordinator answering its own Run's question, and explicit sends to the caller's own run:/dispatch: address. The bridge now resolves the exact mailboxes the caller's live pane and process read (as check does) and admits replies and recipients against those. * refactor(rpc): parse SSH bridge selectors with a typed schema instead of Reflect.get --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> * fix(projects): remove a project only on the host the user picked (#26340) * fix(projects): remove a project only on the host the user picked removeProject now requires a hostId and matches that host's row exactly. The bare-id fallbacks (unique candidate, focused host) could resolve to another host's live project and delete it. Orphaned SSH purge and group deletion now pass each row's own host. Fixes #13071 * fix(projects): group delete removes only the group's member rows Same-id rows on other hosts outside the group were removed because membership was rebuilt from bare ids after the cascade cleared it. Capture (id, host) member pairs before deletion and remove only those. * fix(projects): never let a stale catalog widen a project removal to every host The renderer chose id-only `repos:remove` when its own catalog showed no same-id row on another host. A stale catalog (another window, a paired client, or a pending refresh) hid that row, and main then deleted every host's registration and saved workspace metadata. - The local catalog route now always calls `repos:removeForHost` with the owner row's host, for local, direct SSH, git and folder projects. - `Store.removeProject` refuses an id that rows on more than one host share, so no id-only caller can delete another host's row. - Setup rollbacks remove the just-created row by its host. --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> * fix(serve): load SSH targets on a headless serve; a user Quit only closes serve-hosted windows (#26348) * fix(serve): load SSH targets on a headless serve; a user Quit only closes serve-hosted windows - launchServeMode registers the SSH handler layer before RPC binds, so a windowless 'orca serve' lists and connects the profile's SSH targets (#25886, #8489). - Under serve, the app-menu Quit/Exit, tray Quit and renderer-recovery Quit close the desktop windows instead of quitting the process (#15537). Signals, supervisor stops, relaunches and update installs still quit normally; on macOS a signal reaches before-quit the same way Cmd-Q does, so the guard lives on the user commands, not in before-quit. * fix(serve): SSH profiles serve on Electron; native macOS quits and frozen windows no longer trap or stop serve - orca serve: a profile with saved SSH targets stays on the Electron host by default, since orcad has no SSH stack yet (#25886, #8489). ORCA_SERVE_RUNTIME=orcad still forces orcad. - macOS serve: Dock > Quit, app-switcher Cmd-Q and other native terminates only close the desktop windows (#15537). Signals, supervisor stops, relaunches, update installs and logout/shutdown still quit. - The serve signal listeners now reclaim SIGINT/SIGTERM/SIGHUP from Electron's native handler. Any signal that had a Node listener before app ready was left with Electron's handler, so these listeners never ran. - A serve user Quit closing a frozen renderer's window arms the same 10s ack deadline as a desktop quit, and skips minimize-to-tray. * fix(serve): stay on Electron when the profile's SSH targets cannot be read yet A valid older-schema profile is rejected by the read-only probe, and the selector took that as "no SSH", so a default serve before the first migration started orcad with an empty SSH registry. Fail closed instead. * refactor(window): type user-Quit close marks as a named window slice, not object * test(serve): pass the desktop-control grant default in the headless SSH launch fixture * test(serve): run the serve-selection SQLite fixtures in the Node runtime project --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> * fix(terminal): keep the main-process terminal tail linear under TUI redraw floods (#26316) * fix(terminal): keep the main-process terminal tail linear under TUI redraw floods Full-width in-place row rewrites rebuilt the row string per character (O(width^2)), a long chunk past the line cap spliced the row array once per newline (O(lines x cap)), and multi-frame chunks summed every cursor-up so they fell back to the whole-tail path. Rows now hold mutable cells joined once, capped rows are dropped in batches, and the redraw window uses the net cursor reach. Fixes #11315 * fix(terminal): do not rebuild a redraw row the cursor only revisits Rows stayed cell arrays once touched and re-joined on every newline, so a cursor revisiting one wide row thousands of times cost O(width) per newline. Runs now splice into the row string, a row switches to cells only after 32 edits between snapshots, and cells are joined once and dropped. * fix(terminal): keep one cached row per chunk so unchanged redraw revisits cost O(1) A row now keeps its cells, right-trimmed snapshot and content end across newlines and only invalidates them on a real content change; identical writes leave it clean. Adds a work-accounting matrix over restore, the append path and live onPtyData, plus a per-character row oracle fuzz. * fix(terminal): return redraw rows to flat strings with cached snapshots Cells made every changed snapshot a full-width join, which was far slower than the old flat-string splice. Rows are flat strings again, written one run at a time; each keeps a cached right-trimmed snapshot and content end that only a real change invalidates, so identical rewrites and cursor-only revisits stay O(1). A wall-time test now times every review probe against a vendored copy of the old row model on restore, append and onPtyData. * test(terminal): read runtime leaf and PTY tail through typed members --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> * fix(ssh): relay fallback runs Orca's pinned Node by default, never a host npm install (#26303) * fix(ssh): relay fallback runs the pinned Node ladder by default, never a host npm install A host whose connect falls back to the relay route now gets the A/B/C ladder (Orca's uploaded Node and prebuilt addons) unless it opts into Host Node. A refused rung C steps to rung D with an actionable reason instead of compiling native deps on the host. * fix(ssh): mark the opt-in Host Node relay runtime as unsupported on the SSH host card The relay fallback now defaults to Orca's pinned Node ladder; the host-npm path is reachable only by an explicit per-host Runtime or ORCA_SSH_REMOTE_RUNTIME override. When a relay actually launches on it, main publishes hostNodeRuntime on the connection state (optional, admitted field-by-field for paired clients) and the SSH host card shows an unsupported-configuration warning. * fix(ssh): keep the relay runtime ladder honest about what it cannot verify - NixOS's stub loader refusing Orca's generic Linux Node (exit 127, 'Could not start dynamically linked executable') is classified wrong_libc, so the ladder steps on to the host Node instead of failing the connect at rung A. - In plain SSH mode the shell provider answers absent only for shells it saw exit; a PTY id it never minted (one an earlier relay may still run) is unverifiable, never exited. - The rung D message offers Host Node only when the host's own Node refused the prebuilt addons, and calls it an unsupported configuration; a download, unresolved-host or Windows refusal gets no Host Node advice. * fix(ssh): keep the host-Node relay where the ladder can't judge the host A client without Orca's runtime artifacts (a dev, e2e or source build with no orcad template, or an offline client) refused rungs A and C with artifacts_unavailable, and the connect settled on rung D. That reason describes the client, so it now lands on the host-Node relay it had before the ladder, marked unsupported, and persists no host decision. A proved noexec still lands on D. Windows hosts have no rung B or C yet, so any rung A refusal there keeps the host-Node relay too. The rung D message is worded from the host OS: Windows gets no noexec-mount wording and is offered Host Node. * fix(ssh): a host-Node fallback with no Node settles rung D, and mutable refusals expire The host-Node relay the ladder chooses in place of D (a client artifact gap, or a Windows rung A refusal) hard-failed the connect when the host had no Node: the resolver threw a plain Error, which plain SSH mode does not admit. The fallback now proves host Node with a strict probe first; an answered "no Node" settles D with a classified, host-OS worded reason, and an unanswered probe stays a retryable failure. A gap no longer rewrites D to host Node once rung C proved host_node_missing. Mutable pinned-runtime refusals (missing_lib, security_software, and noexec on Windows) are no longer persisted and replay from memory for ten minutes only, so a repaired host gets rung A back. The in-memory refusal cache moves to ssh-relay-pinned-refusal-cache.ts. * fix(ssh): no host does worse than the pre-ladder host-Node default Restructure the relay runtime ladder around one invariant: A, B and C are tried first, and any refusal past them falls back to exactly the pre-ladder default, the host-Node relay marked unsupported. That covers an unidentified libc, a client artifact gap, Windows, mutable refusals and a C N-API floor the npm build would not have hit. Rung D is reached only when that fallback proves the host has no usable Node, or a proved noexec home would defeat it too. Strict host-Node resolution now treats only a command that ran and exited as an answer, so a refused channel (MaxSessions), session limit or timeout stays a retryable failure instead of a 'no Node' verdict. The launch reuses the Node the fallback probe found. Mutable refusals (missing_lib, security_software, Windows noexec) persist again with their proof time and replay for 24 hours, so a host that still blocks Orca's runtime is not re-uploaded to on every connect, while a repaired one gets rung A back. A table-driven test asserts the invariant per host class. * fix(ssh): every answered rung failure reaches the host-Node default, and D only past it Two shortcuts skipped the pre-ladder default. An answered but unclassified install failure (tar ENOSPC, an extract or hash marker, an unverified Windows promote, a self-test that failed outside every refusal class) aborted the connect on every attempt; it now steps down as a non-persisted install_failed. Only unanswered failures (abort, timeout, channel-open, session limit, an unconfirmed termination, SFTP connection loss, ssh exit 255) stay retryable, and nothing steps down once a relay launch was issued. A proved noexec no longer jumps to D: exec denial can be per binary (AppArmor allowing /usr/bin/node), so it skips B and C, which load from the same tree, and reaches the host-Node fallback. D now follows only that fallback answering with a failure, so its outcome is a superset of the default's by construction. The never-worse test is now the cross-product of every A, B, C and host-Node answer on Linux and Windows, plus named review cases. * fix(ssh): system-ssh upload failures and a broken cached runtime step down before launch System-ssh directory upload and system sftp threw plain Errors that dropped the host's exit status, so a remote tar ENOSPC or a Windows sftp permission refusal aborted the connect instead of stepping down. The remote side now throws SystemSshCommandExitError (ssh exit 255, signals and lost-connection diagnostics stay retryable), and a local tar failure is still blamed on the client rather than the remote tar that saw its truncated stream. The warm runtime check only tested that the verified executable existed, so a cached runtime that the host's exec policy or a removed library had since broken was launched and hung on every reconnect. The same command now also runs it and reports a refusal the way promotion does, so it steps down before any launch, with no extra round trip. * fix(ssh): keep the remote answer when local tar only failed writing to it When the host's tar exits early (ENOSPC, a refusal), BSD tar on macOS exits 1 with 'Write error' and no signal, so the upload rethrew the local error and dropped the typed remote exit, aborting the connect. The remote answer now wins whenever local tar finished, was signalled, failed after ssh closed, or reported a write or pipe error; a local tar that failed on its own input first is still the client's fault. * test(ssh): hostile-host cells allow read-only version probes, never installs; probe each Node once The noexec-home cell failed on 'npm --version': under the never-worse ladder a proved noexec reaches the strict host-Node fallback, whose probe asks npm its version before settling D. The cells now judge install and compile calls (npm install/ci, node-gyp, cc, make) and allow '<tool> --version' / '-v'. The probe ran twice because the login shell named the Node the path probe had already rejected. Candidate checks are now memoized per resolution, for the host-Node fallback and rung C alike. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> * chore(i18n): regenerate the runtime-required English catalog after the main merge * fix(serve): serve on the profile the CLI selected, and refuse any other before the instance lock (#26424) Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> * fix(remote-runtime): open terminal tabs reattach after a managed server update re-pairs the host (#26433) * fix(remote-runtime): a remote pane rebinds after a server update re-pairs its host A pane captured the host's pairing revision once, when it mounted. A forced managed-server update re-pairs the host under the same environment id, so every later request from that pane was refused as stale, and the refusal counted as fatal: the pane stopped retrying and Reconnect replayed the same stale revision. Each request now carries the current revision, a pairing refusal re-reads the catalog before the retry, and the pane treats it like lost contact. * fix(remote-runtime): close a stream left on the old pairing and ignore a 0x0 grid hold A stream that reopened before main re-paired stayed open while sendFrame refused its stale revision, so typed keys vanished. A revision change now closes it as recoverable so the pane rebinds on the new pairing. A just-restarted server also published a 0x0 hold that shrank the visible pane to one cell; holds with no grid are ignored. * fix(remote-runtime): a pane follows only its own machine's re-pair A re-pair under the same environment id to a different host key published its revision before the catalog retired the old tabs, and the pane recovered at the new revision, so its next call was authenticated as the replacement peer. The catalog now classifies each re-pair before publishing revisions: a replaced or removed machine's terminal streams end for good first, and a pane only advances its pairing along a proven same-host rotation (same registration and host key). An unresolved identity holds the pane until it is proven. --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> * refactor(rpc-permissions): Phase 3.5 elegance pass (#26471) * refactor(rpc-permissions): drop never-read incarnation and invocation id from bridge credentials * refactor(rpc-permissions): define runtime device grants once and reuse bridge scope fixtures * refactor(rpc-permissions): decide terminal host membership in one predicate and share the opt-in hint * refactor(rpc-permissions): parse bridge selectors once per call * refactor(rpc-permissions): admit and bind unopted bridge methods from one table --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> * refactor(remote-pane-binding): Phase 3.5 elegance pass (#26470) * refactor(remote-pane-binding): keep the pane's pairing revision in one slot * refactor(remote-pane-binding): share one hold decision across the input entry points * refactor(remote-pane-binding): define the persisted-handle adopt once in attachPane * refactor(remote-pane-binding): build the pushed-snapshot meta once * refactor(remote-pane-binding): drop the test-only peerReplacedEnvironmentIds wrapper * test(remote-pane-binding): share the input and subscribe frame readers via stream fixtures --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> * refactor(relay-ladder): Phase 3.5 elegance pass (#26474) * refactor(relay-ladder): put the host-Node fallback in the ladder before D The Auto ladder now lists legacy as the rung before D, so the step function is one rule plus the next rung. Deletes nextRelayRuntimeStep, the hostOs step context and the mutable hostNodeFallback flag (now a constructor-time laddered field). Windows still ends at legacy: B and C refuse there before any host I/O. * refactor(relay-ladder): one rung-D reason and message over reachable states D is reached only from the host-Node fallback, so its refusal is host_node_missing or install_failed. Merges the reason and message functions, drops three message branches that could not run, and folds noexecRemembered/noexecProved into one noexec field. * test(relay-ladder): one never-worse cross-product with a remembered axis Drops the named cases (each matches a cross-product outcome), the transport case that only checked the model against itself, and the third case the rank check already implies. Adds rung B's noexec answer and per-rung remembered flags for A and B, the only rungs that replay a cached refusal. * refactor(ssh): one answered-miss rethrow for the node resolution probes Four catch blocks repeated the same session-limit, abort and strict checks. They now call rethrowUnlessAnsweredMiss and keep their own fallthrough. Also drops a stale 'Caches nothing' comment. * refactor(ssh): share the host-answered system-ssh exit rule; flatten the upload throw cascade isHostAnsweredSystemSshExit owns the 255/null transport rule for both the upload pipeline and isAnsweredHostFailure. settleUploadPipeline drops a branch that could not run: a failed tar is judged once, then ssh, then pipe. --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> * refactor(serve-daemon-misc): Phase 3.5 elegance pass (#26478) * refactor(ssh-pty): build obligation transitions behind one closed-delivery guard * refactor(build-orcad): table-drive the shipped worker smokes * refactor(daemon): pass scope probe seams as one deps object and share cgroup line parsing * refactor(repos): route every project removal through removeProjectForHost * refactor(build-orcad): type the worker smoke table for the type-aware gate --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: m4air <m4air@Mac.localdomain> * refactor(terminal-tail): Phase 3.5 elegance pass (#26465) * refactor(terminal-tail): share mulberry32 and compare whole results in redraw equivalence tests * refactor(terminal-tail): keep one splice ceiling at the onPtyData entry point --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> * fix(terminal): bound the backing a redraw row can pin across chained erases (#26547) Each erase that keeps at least half the row skipped the copy, so a chain of them left nested slices pinning the original wide row. Rows now track an upper bound on the string they slice and copy once the cumulative shrink passes half. Co-authored-by: m4air <m4air@Mac.localdomain> --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
5252884a9c |
Native chat: say when Codex is signed out or the agent isn't installed, without disabling Send (#25666)
* fix(native-chat): disable Send for unavailable agents * test: update sign-in copy expectations * fix: show automatic chat availability only on Send * fix: keep signed-out evidence when a live session lists models * refactor: split Send availability presentation and tests * refactor(native-chat): keep the sign-in verdict on the catalog's failure record The probe's "signed out" / "CLI missing" verdict now lives on the account's existing model-catalog failure record instead of two extra maps, with the failure TTL as its only lifetime: - Only the probe sets or clears the verdict; a chat's own model listing (which works while signed out) never clears or renews it. An aged-out verdict makes the next read re-probe, however fresh the catalog. - A healthy fresh catalog is no longer re-probed every 30 s; it keeps the normal 10 min fresh window. A Claude start refused as signed out ages the catalog so the next read re-probes. - A waiting picker again answers from whichever catalog lands first and never waits on a probe. A read that still owes the probe's verdict says so, and the composer waits for it once with the new optional waitForAvailability param. - The open-time start-failure line and its transcript row are hidden only while Send is disabled for the same reason; otherwise they show as before. - A host verdict lifetime longer than the client's hold is clamped, not dropped. The catalog read watches only the account settings and the paired host's contact instead of every store write. * fix(native-chat): one send-gate rule, and a missing CLI as its own failure - The sign-in/CLI verdict is gated once where the session hands it to the composer: while a turn runs it neither shows nor refuses, so a follow-up queues as before; goal mode is held by the same gate. The start-failure line and row hide only while that same value is up. - A start whose CLI was never found (a direct spawn's ENOENT, or the one a provider supervisor reports) is now a `cliMissing` failure with the disabled Send's sentence, instead of a generic "couldn't start" beside it. An older client shows the host's sentence for the new kind. - Signed-out words for a slash command keep "Run /x again". - The Claude catalog reads its models only from the reply to its own list_models request, never from the initialize reply. - The Codex probe lists models while its account check runs; a signed-out verdict still wins. - The missing-executable check moves to provider-process, out of native chat. * test(native-chat): signed-out command failures name the command to run again * fix(native-chat): probe the Codex home a launch would read - Before the catalog probe's read-only app-server reads Orca's shared Codex home, it runs the same sync a launch and the usage poll run, so a `codex login` made in ~/.codex is seen at once instead of leaving "Run codex login" up until something else re-syncs (forever on a headless host). Per-account homes are left alone. - A "no account" answer counts as signed out only when the home's auth.json is absent or holds no credential; one caught mid-write by a token refresh, or holding a credential, is unknown. * test(codex-accounts): type the node:os mock with a type import * fix(native-chat): keep the sign-in verdict until a newer answer replaces it - No gap while signed out: the host serves an aged verdict while the probe that re-derives it runs, and the composer never clears its last answer before a read; only an answer without a verdict, an error, lost contact or an account change does. Send stays disabled with its reason through every re-check. - After sign-in the stale "isn't signed in" start-failure line stays hidden: a probe that found the account fine now says so (`accountVerified`), and the line shows only when the host has no answer at all. - A Claude start refused as signed out, or any start whose CLI was never found, is recorded as that home's verdict, and the chat reads it once when that start failure arrives. - The re-probe mark is read only where the catalog read starts its probe, so a Codex chat's own picker no longer re-lists for it on every read. - Choosing an account or signing in again in Accounts settings re-derives the verdict at the next read; the account key now compares settings by value. - A re-read that brings an identical catalog keeps the option state, so focus re-reads re-render nothing. * fix(native-chat): Codex's signed-out words name only the step that unblocks this chat A chat is pinned to the Codex home it started in, so choosing another account in Codex Accounts settings never unblocks it. The system-account sentence now says only to run `codex login`, in every locale. * refactor(native-chat): give an account's sign-in status its own owner The sign-in verdict lived inside the model catalog's failure records, so the store needed guards for who wrote a failure, which listing may clear it, and whether a verdict was served while re-probing. Each guard fixed one way a chat's own listing or an older probe overwrote the account's answer. The answer now lives in AgentAccountStatuses: one in-memory, capped status per account home (ready, not signed in, or CLI missing). Only the session-less probe, a refused start and an account change write it. A blocked answer older than 30 s, or one an account change marked, makes the next read start the probe whatever else is listing, and it is served until that probe answers. Older evidence never replaces newer. The catalog's failure code is main's again, and agent-model-catalog-failures.ts is gone. The wire carries one optional `availability` field in place of `unavailable` and `accountVerified`. The renderer holds its answer per host, agent and chat, re-reads only on that agent's account inputs, and re-reads on a timer only while blocked. After an account change, Send stays disabled with the previous words until the re-probe answers, then changes once. Account switches and re-sign-ins mark the status before the settings change a chat's read follows. * fix(native-chat): re-derive the sign-in answer after an account change mid-probe, and back off while blocked Round-4 review findings on the account status owner: - An account switch or re-sign-in made while a probe was running left the old answer up for up to 30 s: the next read joined that probe, whose answer was then dropped as older than the change. A read now chains exactly one fresh probe after a probe that began before the change. - Removing the selected Claude account swapped another sign-in into the shared config dir without marking the answer; it now marks it first. - Evidence was ordered by wall-clock time, so a clock that stepped back froze a blocked answer. It is now ordered by a sequence and aged on a monotonic clock; a negative age reads as due. - A chat left open while signed out started a CLI probe every 30 s. Probes that keep finding the same block now space out (30 s, 60 s, 2 min, 4 min, then every 5 min), carried to the client as recheckInMs. An account change or a different refusal starts the hold over. - The renderer dropped no settled wait from its leave set; it now does. - A failed Claude probe logged its whole stdout, which now includes the initialize reply naming the account. Control replies are dropped from the log and the failure message. - The account services take the status re-check as an injected onSignInChanged hook instead of importing the native-chat store. - Imports moved below file headers; comments no longer say "ready" means re-checked after the failure. * test(claude-accounts): say why the runtime-auth fakes are cast where the hook was added * fix(native-chat): type the held-answer handle as the account status * fix(native-chat): re-check a block when someone returns, and catch an account change during a home's first probe Round-5 review findings: - After a chat sat blocked for a few minutes, the backoff made every read wait out a hold of up to 5 minutes, so someone who ran `/login` or `codex login` and came back still saw Send disabled for minutes. The backoff now applies only to the client's own timer read, which it marks with an optional `scheduledRecheck` param. A read from someone looking (window focus, the pane becoming visible, a new chat, a start failure) re-probes once the block is older than 30 s. The param is sent only on that timer, which only a blocked `availability` answer arms, so a host that predates the param never receives it. - An account change made during a home's first probe marked nothing, because only existing statuses were marked, so the old account's answer was written as current. Each agent's last account change is now one sequence number that every status and running probe is compared against; statuses are no longer mutated in place. - Removing an account that nothing had selected no longer re-checks. * refactor(native-chat): keep the sign-in and missing-CLI words in their own module Main's retry wording pushed the failure-words table past its line budget. * refactor(native-chat): build the structured chat's view session in the view-state module Main's send-reveal work brought NativeChatStructuredSession.tsx and the session controller to their line budgets. * fix(native-chat): keep a stated start line hidden through a running turn, and re-probe an account changed mid first probe - A chat whose open-time start failed signed out had that line hidden while the host said the same. When a new signed-out answer landed during a running turn, the line came back for the whole turn: the hide read Send's gate, which a running turn suspends. The hide now reads the host's answer itself (blocked for the same reason, or ready); Send's gate still steps aside while a turn runs. Both are views of the one answer the session holds. - An account change during a home's first probe dropped that probe's answer but left nothing to re-check the new account, and the picker's reply said no probe was pending, so the chat stayed unknown until the catalog aged out. A read now chains one fresh probe whenever the running probe began before the change, and its reply says that probe is still owed so the client asks once more for its verdict. - The client's timer read no longer carries `scheduledRecheck`. The timer already fires only when the host's backed-off `recheckInMs` says, which is past the 30 s after which any read re-probes, and every answer re-arms it. One fewer strict-schema param. * build(cli): typecheck the missing-executable check the provider process now imports Main's #25753 added managed-provider-process.ts to the CLI project, which imports this PR's provider-executable-missing.ts. * fix(native-chat): disable Send with the fix when Claude or Codex is signed out or missing What you see: in a native Claude or Codex chat whose CLI is signed out, or not installed on the computer running the chat, Send is disabled and its tooltip (hover or keyboard focus) says how to fix it, in words that match the account ("Codex isn't signed in. Run `codex login`.", "This Claude account isn't signed in. Sign in again in Claude Accounts settings.", "Claude isn't installed on the computer running this chat."). A message refused after sending gets the same words. On desktop the open-time "Chat could not be started" line, its Retry and its transcript row no longer repeat these two reasons, unless a message of yours is still waiting on that start. An unknown answer never disables Send, and while a turn runs Send stays Stop and Enter queues. How: the host's existing session-less model-catalog probe also says why no chat can start. Claude's probe child sends initialize before list_models and applies the rule chat start already uses (token source none with no API key); Codex's probe asks account/read beside model/list; a spawn that finds no executable is "CLI missing". The probe's failure record in the catalog store carries that reason, and every catalog reply carries it as an optional `unavailable` field. Only a probe writes or clears it: a live chat's own listing (which a signed-out Codex still answers) never does. It lasts the store's 30 s failure lifetime; after that the next read starts or joins the probe, serving the held reason meanwhile, and only a read that already asks to wait gets the probe's answer. A change to the managed-account settings expires it. Before probing the shared Codex mirror home, the probe runs the same sync a launch does. The renderer keeps the latest answer until the next one replaces it, and only while blocked re-reads on window focus and when the chat gains focus. No status store, timers or new request params. * fix(native-chat): no picker loading while Send re-checks a sign-in, and plain keyboard focus on the disabled Send A chat whose account is signed out and has no model list no longer shows the model picker as loading each time it re-checks the sign-in after 30 s: the re-check is for Send, so a reply that already carries the verdict ends the picker's wait. The disabled Send's wrapper takes keyboard focus (so its tooltip opens) without becoming a second button; the inner button keeps its label. Trims tests that repeated rules other tests already prove. * fix(native-chat): lift the Send gate whenever Stop shows, including while a queued card is about to send The composer shows Stop while the host's queue is about to send its next card, but the sign-in gate stayed on in that window, so Enter was refused with no message. * fix(native-chat): never disable Send for a sign-in or missing-CLI verdict; show a dismissible notice instead The host's pre-send check can be wrong while the chat would work (saved Arguments it never sees, project settings it can't read, gateways that accept a signed-out Codex), and a disabled Send left no way past it. - Delete the Send gate: the composer, Enter and goal mode no longer read the verdict, and the Send tooltip reason and focusable wrapper go back to main. - Show Codex not-signed-in and Claude/Codex not-installed as one entry in the chat's existing notice card. Dismissed per verdict; shows again when the verdict changes or clears and returns; steps aside when the failed start (launch line or latest failed-start row) says the same. - Claude: drop the probe's signed-out verdict (initialize-on-probe stdin, request-id filtering, control-reply log scrubbing). The chat start's refusal, with its account kind, is the only Claude sign-in signal. Missing-CLI from the probe stays. - Restore the failed-start line and transcript row for these causes (no more hiding on desktop) and drop the chat-root focus re-check; a window-focus re-read stays while a notice is showing. * fix(native-chat): re-check the notice's reason after a chat starts, keep a signed-out Codex's model list Review round 1 of the notice rework. - A chat that proves its start (at acquire, or on its later "started" event) marks the catalog's held reason for that account due, so the next read re-probes instead of trusting it until the TTL; the chat re-reads when a turn starts or ends while a notice shows. - The Codex probe keeps a model list it got beside a signed-out reason, so a gateway user with a wrong verdict keeps the picker list. - Failure records move to their own module; an expired failure with no reason is dropped, and expiry drops reason-less failures. - Notice: any verdict change resets the dismissal; signed-out is Codex-only; the say-once rule ignores a failed start a turn has run past. - Missing-CLI copy: "{{agent}} wasn't found on the computer running this chat. Install it, or check its Command in Settings → Agents." (6 locales). * fix(native-chat): only an acquiring start re-checks the catalog reason; drop the unreachable Claude filter Review round 2 of the notice rework. - A re-attach to a live child proved nothing new, so only an attach that acquired a child which proved its start marks the reason due. - Reword: a start proves the program exists; for a Codex sign-in it only prompts an earlier re-check. - Drop the notice's Claude signed-out filter: no host sends that verdict. - Tests: a re-attach does not notify; a turn ending re-reads once more. * fix(native-chat): never newly show a catalog reason the host is still re-checking After signing in to Codex, a new chat flashed the old "isn't signed in" notice for about half a second: past its 30 s lifetime the host answers a plain read with the held reason while its probe re-checks. A chat now adopts such a reason only when it already shows it; otherwise the joined read's answer decides. * test(native-chat): follow main's harness and the clearer Claude sign-in words after the merge * test(native-chat): run the start-proof catalog test in the Node runtime project with the other SQLite host tests |
||
|
|
b1e0092d5d |
feat(opencode): open OpenCode 2.x in the structured chat too (#26395)
* feat(opencode): run OpenCode 2.x in the structured chat too `opencode acp` on stable 2.x starts its own private `opencode serve --stdio` child with the chat's environment and ends it when stdin closes, so the chat's account pin reaches it just as it does on 1.x. Admit stable 2.x from 2.0.14 beside stable 1.x from 1.18.31; pre-releases, older releases, and other major lines keep the terminal chat. The `opencode2` agent is unchanged. Restart recovery already reads a 2.x session's own tables first; tests now cover a database an upgrade left with both generations, and one whose 2.x message table has an unknown shape (nothing found, no error). * fix(opencode): show OpenCode's own permission option names * test(opencode): run the 2.x recovery tests with the real-SQLite Node project |
||
|
|
b41e2730db | Persist multiple workspace references with atomic edits |