Repository navigation
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
Merged
Conversation
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.
… 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.
…wers 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.
This was referenced Oct 13, 2026
…l until a caller needs it Nothing outside the projection calls it in this part; Part 5 exports it for the start-failure row.
…wner 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).
…nd 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.
…plit-3 # Conflicts: # src/main/native-chat/agent-session-wire/structured-agent-session-current-work.test.ts
…ack 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.
…s as its row provenance Moved here from Part 4 so it sits beside stampJournalEntryProvenance.
…plit-1 # Conflicts: # src/main/acp/acp-timeline-grok-subagent-retention.test.ts # src/main/acp/acp-timeline-grok-subagents.test.ts
…n 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.
…amping 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.
…plit-2 Brings in main 6741f4f through part 1. Main's history page now reads with a HistoryReader, so the published current work moves to agent-session-published-work.ts and lays over each finished page. Main's new Stop path (chat-stop target, acceptance, unconfirmed note, targeted cancel) and the per-subagent row writer read what runs through the host's current-work projection, as the rest of this part does.
…plit-3 Brings in main 6741f4f through parts 1 and 2. Main now names the agent in "didn't accept this message"; the stopped-agent words this part adds keep their own module beside it.
…isions 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.
…plit-2 Carries part 1's fix: an item's provenance is set when it is created and revisions keep it.
…plit-2 Syncs main b83a6f7 through part 1.
…plit-3 Syncs main b83a6f7 and part 1's creation-only provenance through part 2. Main grew both chat-session hooks to their line limit, so this part's prompt reads take the transport state whole on desktop and one memo on the phone.
…pt, 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).
brennanb2025
changed the base branch from
brennanb2025/settle-split-2
to
main
October 15, 2026 03:21
Part 2 landed on main as 3bfb114, so files this part never changed take main's text. Where both sides changed a file, the result is this part's change laid onto main, checked against Part 2's final head 674dbed: - the client coalescer keeps this part's merged `actionablePromptIds` and `working` beside main's `hasNewer` (#28333, the "New" line above unseen messages); - the transcript row keeps this part's actionable-prompt import beside main's new-rows marker imports (#28333).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part 3 of 5 of a stack that rebuilds #26867 as smaller PRs. Stack: Part 1 #28092 (merged) → Part 2 #28103 (merged) → Part 3 (this PR) → Part 4 #28111 → Part 5 #28122. Base:
main(Part 2 is merged as3bfb1149da6).ELI5
Part 2 taught the host to say, in one place, whether a chat's agent is really working and which approvals or questions are still waiting on you. This part makes the desktop app and the phone app use that answer instead of guessing from saved rows, so a dead agent's leftovers stop showing as Working or as an answerable approval. If you do try to answer a question from an agent that has stopped, Orca now says so plainly instead of sending the answer into the void.
What Changed
The problem
Before and after, as you see it
Before. After an agent died with an approval pending, the chat could still show Working and Stop, the approval still looked answerable, and pressing Cancel on it also stopped the chat's next turn.
After.
How it works
workingandactionablePromptIdsacross batches the way they already keeplatestTurn(a batch with rows restates them; one without leaves them alone).structuredAgentSessionHostSaysWorkingandisActionableStructuredAgentSessionPromptprefer the host's answer and fall back to the old derivation when a host sends none. Desktop (transport state, message list, transcript rows, resolution receipt, attention observation) and mobile (session hook, cancel, prompt responses, turn timing, send outcome) read them.performPromptrefuses an answer to a prompt that is not current work with a new refusal reason,promptOwnerEnded, before any provider sees it. Its copy names the agent and says what to do next (providerExitedAnswer, in all six locales).dismissPromptwith no provider call (no live provider holds that card), so the Cancel's receipt commits in the same write and a failed save answers as main's dismissal does. The live turn is interrupted only when the cancel names it, through its own cancel, never the chat's Stop.Why
The host already knows which work is current (Part 2); having every client re-derive it from rows is what kept leftovers looking live. Preferring the host's fields and falling back otherwise keeps mixed versions working in both directions, which matters because desktop, mobile and remote hosts update independently.
Differences from the common pattern and from main
Linked Issue
Part of the rebuild of #26867 (root-cause follow-up to #26664).
Visual Proof
No new UI: the change is which existing state shows (no Working, no answerable approval) after an agent dies. The full stack's before/after screenshots and QA, run on the combined code, are in Part 5.
Testing
I manually tested these changes locally
Automated tests added/updated, or explained why not below
New:
structured-agent-session-ended-prompt-cancel.test.ts(Cancel on an ended agent's question only dismisses it: no Stop, no interrupt, queue untouched) anduse-structured-agent-session-transport-state.working.test.tsx(desktop Working is the host's answer; an older host falls back to rows). Added cases: the reducer keeps the host'sworkingand prompts across a quiet frame, takes a row-less restatement, and drops them for rows from a host that names none; the phone reads Working the same way with the same fallback; the refusal names the agent; the resolution receipt stops breathing for an ended agent's question; failure words cover the new sentence. The current-work test from Part 2 now also checks what a client reduces from the host's frames (working, the running turn, whether the prompt is answerable).One flow per client and one older-host fallback each. Per-helper prompt tests, a transcript-placement test, the Steer cases that match main and per-reason refusal rows were dropped.
Merged main
b83a6f74be9through Part 2. Main now names the agent in "didn't accept this message"; this part's stopped-agent words keep their own module beside it. Main grew both chat-session hooks to their line limit, so the prompt reads take the transport state whole on desktop and one memo on the phone.Merged main
3bfb1149da6(Part 2's squash). Files this part never changed take main's text; where both changed a file, this part's change sits on main's, checked against Part 2's final head674dbedd87e. Main's feat(native-chat): add a "New" line above unseen messages #28333 (a "New" line above unseen messages) crossed two of them: the client coalescer keeps this part'sactionablePromptIdsandworkingbeside main'shasNewer, and the transcript row keeps this part's actionable-prompt import beside main's new-rows marker.Local at
3dce3120fea:tc:node,tc:web,tc:cli, mobiletypecheckand the mobile tests typecheck ratchet pass (on openclaw);src/main/native-chat(405 files), the native-chat renderer (381 files) and the mobile session suites (257 files) pass under Node; CI's lint and verify scripts pass apart from the known local-only mobileno-cycle. The changed-code and type-aware lint passes are left to CI.CI at
3dce3120fea: green, 16 pass and 16 skipped, including the five test shards, relay integration, static analysis and typecheck, both packaging jobs and the test vs non-test line count check against main.package (windows)passed on its one rerun: its first run timed out a Windows installer process probe (nsis-process-check.test.mjs, untouched here) after the probe had printed its result.Size against main: production +474/-119, tests +386/-39.
What the reviewer should check
workingandactionablePromptIdswhen present and falls back otherwise.latestTurnalready merges.AI Disclosure
Review
Parts 1 and 2 have merged, so this targets
main. Parts 4 and 5 build on this branch.Agent skill upstream boundary
Notes
en,es,fr,ja,ko,zh.Author: Brennan Benson (@BrennanKB5).
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)