Repository navigation
Keep a Planner's runs and questions through a refused reply, and capture decisions across edits - #777
Merged
Merged
Conversation
…eply A Chat reply the Planner could not move into inline decisions failed its turn, and a failed turn let go the retained Planner session. A workflow run that session still owned was then shown as "stopped after N min" and its open questions were withdrawn, although nothing had stopped it. N is only how long the run had lasted when the reply failed; no budget or timer was involved. - A turn that fails only because its Chat reply could not be verified or moved into inline decisions (PlannerReplyError) keeps a session that still owns live or paused runs. The failure is still announced and the withheld text is still never published. Any other failure, an interruption, Stop, the owner binding ending or the document closing lets the session go as before, and a run reported stopped stays stopped. - A repair that repeats open_decisions keys an earlier turn already created counts as having created them: the turn now records every decision open_decisions returns, new or not. Retrying with stable keys, as the repair prompt asks, no longer fails the turn again for want of a new card. The summary says such cards are already in the document rather than claiming they were added. - A Default machine Planner that is replying now counts as in use, like one waiting on a question or a run, so a reply that outlasts PLANNER_IDLE_MS with nobody viewing the document is not cut off, and idle time counts from its end. A Planner with no reply, question or run, and nobody viewing the document, is still released after 30 minutes. Assistant-model: Claude Sonnet 5.5 Assistant-workflow: inline Assistant-duration: 43m converged, estimated 45-90m Assistant-verification: red before fix passed: on the old code the new Chat lifecycle tests failed (a refused reply destroyed the session and posted "plan-review was stopped after 31 min."; a repeated capture threw "could not move its questions") and the long-reply planner-dispatch test failed (the Planner was stopped mid-reply after a simulated 31 minutes); the new reply-policy unit tests only failed to load without PlannerReplyError Assistant-verification: bun test unit passed: GIT_CONFIG_GLOBAL=/dev/null bun test apps/server/src/chat apps/server/src/harness/remote apps/server/src/experiments on this commit's tree alone (836 pass, 0 fail) Assistant-verification: full suite, types, ci and e2e unavailable: run once on the final head of this two-commit change, recorded on the next commit, not on this commit alone User-preference: no wall-clock timeout for active work or pending human decisions Co-authored-by: Alex Lavaee <lavaman131@github.com>
open_decisions refused a whole batch whenever the document's revision had moved on since the Planner's read, whatever had changed: a person typing, a workflow run writing, or the Planner's own earlier card. The Planner got "The document changed; read it again before opening a decision" and had to read and write the batch again, which a document with live writers can keep refusing. Every related block already carries the digest the Planner read, so a stale batch now keeps its place instead. A block that still carries its digest is used wherever it now stands. A block that changed, or whose digest is ambiguous, still refuses the whole batch with the same error and creates nothing. Each card is still created once under its stable key, so a retry never duplicates one, and the Planner's access is still confirmed before the batch is read. open_decision and ask keep their strict revision check. Assistant-model: Claude Sonnet 5.5 Assistant-workflow: inline Assistant-duration: 50m converged, estimated 45-90m Assistant-verification: red before fix passed: on the old code the new capture-decisions tests (an unrelated edit, a moved paragraph, a mixed replay) and the open_decisions tool test failed with "The document changed; read it again before opening a decision"; the existing mixed-replay test that pinned the strict revision now expects the rebase, and a changed paragraph is still refused with nothing created Assistant-verification: bun test unit passed: GIT_CONFIG_GLOBAL=/dev/null bun test on the final head (6788 pass, 0 fail); a second full run had one failure in the timing-dependent WriterLease test, outside this change, which passes 5 of 5 times alone Assistant-verification: bun run types passed: whole workspace, on the final head Assistant-verification: bun run ci passed: dprint, oxlint, tokens and design checks, on the final head (72 existing lint warnings, none in changed files) Assistant-verification: bun run e2e passed: 55 tests across harness, planner-decisions, run-card, planner-attachment, machine-choice and open-ended-decisions on the final tree; an earlier run of the same files had two machine-choice sidebar menu failures that passed on rerun and on the unmodified base, and no web code changed Assistant-verification: bun run test:postgres unavailable: not run, storage is unchanged User-preference: no wall-clock timeout for active work or pending human decisions Co-authored-by: Alex Lavaee <lavaman131@github.com>
…ng a captured decision Independent review found four ways the previous two commits let a turn count a decision as captured when nothing live held the Planner's questions, or let a batch land beside the wrong passage. - A repeated open_decisions key returned the original card whatever the new input said, so a different question, response kind, choices or passage was dropped silently and the turn reported "already in the document". A key now repeats only the decision it made: the stored question, response kind and choices (apart from options or criteria people added since) and its passages must match, otherwise the whole batch is refused, naming the key, and nothing is created, replaced or replayed. Identical replays stay idempotent, including after a restart or an option a person added. - A stale batch rebased by digest used the addressed block whenever its digest still matched, so an identical paragraph inserted ahead of the original anchored the card to the copy. A stale batch now needs exactly one block carrying the digest; a repeated digest refuses the whole batch with the same "read it again" error and creates nothing. Other callers of resolveBlock keep their behaviour. - The reply policy counted any record the turn returned, including a card a person discarded or that was cancelled, so a replayed discarded key made a turn with two withheld questions succeed. Only a card that still holds its question (open, reopened or answered) counts, for repeated and new cards alike; a discarded card is never recreated and the turn still fails visibly. - A refused or unverifiable reply still destroyed a Planner session that waited only on a question it asked, such as an ask_user_question with no reported workflow run, aborting its host signal and cancelling the decision. Every atomic or remote Planner session now counts its open host requests; a session with one is kept, with or without a refused reply, and let go when the request ends. An explicit Stop, the owner binding ending and the document closing still let it go. Assistant-model: Claude Sonnet 5.5 Assistant-workflow: inline Assistant-duration: 19m converged, estimated 45-90m Assistant-verification: red before fix passed: with the source of this commit stashed (the PendingInputs class kept so the tests load), 15 of the new or extended tests failed on the old code: six capture-decisions (a repeated key with another question, description, response kind, choices or passage, and a mixed batch, all resolved to the original card), one capture-decisions (an identical paragraph inserted ahead anchored to the copy), three invoke (changed, discarded and discarded-new reported success), three invoke (a Planner with only an open question was destroyed after a refused reply, after a normal turn, and the Stop and close cases found nothing retained) and two session tests (no pending-request count) Assistant-verification: bun test unit passed: GIT_CONFIG_GLOBAL=/dev/null bun test apps/server/src/chat apps/server/src/harness apps/server/src/questions apps/server/src/agent apps/server/src/plan on this tree (1936 pass, 0 fail) Assistant-verification: bun run types passed: whole workspace, on this tree Assistant-verification: bun run ci passed: dprint, oxlint (72 existing warnings, none new), tokens and design checks, on this tree Assistant-verification: full suite and bun run e2e unavailable: run once on the final head of this round, recorded on the next commit, not on this commit alone User-preference: no wall-clock timeout for active work or pending human decisions Co-authored-by: Alex Lavaee <lavaman131@github.com>
A stale open_decisions batch found each paragraph by digest in the document as it stands, so it still anchored a card to an identical copy when the paragraph the Planner read had been rewritten, or when the paragraph was already repeated at the time of the read. A retry of a key whose paragraph had been rewritten was refused with "already holds a different decision, use a new key", which misdescribes it. A stale batch is now matched against the revision the Planner read. The digest must have named exactly one block then, the block that was read must still be the block that carries it, and it must carry it alone. Otherwise the whole batch is refused with the document-changed message, naming the key, and nothing is created. A revision the server no longer retains is refused the same way. An identical replay of a key whose card exists stays idempotent, judged by the card's own anchor. The outline remembered for a revision was overwritten by the result of an edit made from that revision, so what the Planner read was lost; the edit result no longer replaces it. Each remembered outline now also records the live block behind every entry, which is how a rewritten paragraph is told from its twin. Assistant-model: Claude Sonnet 5.5 Assistant-workflow: goal (run 29d6269b-6026-4c1f-8a50-399c757f21c0) Assistant-duration: unmeasured converged, estimated unmeasured Assistant-verification: red before fix passed: with the source stashed, 4 of the new capture-decisions tests failed on the old code (rewritten paragraph beside a copy, paragraph already repeated when read, revision not retained, repeated key after a rewrite reporting a different decision) and all 30 pass with it Assistant-verification: bun test unit passed: GIT_CONFIG_GLOBAL=/dev/null bun test on this head (6807 pass, 0 fail, 22 skipped for TEST_DATABASE_URL) Assistant-verification: bun run types passed: whole workspace and e2e, on this head Assistant-verification: bun run ci passed: dprint, oxlint (72 existing warnings, none new), tokens and design checks, on this head Assistant-verification: bun run e2e passed: 43 tests across harness-decisions-deck, planner-decisions, run-card, planner-attachment, open-ended-decisions, decision-prose and inline-decision-deck on this head Assistant-verification: bun run test:postgres unavailable: not run, storage is unchanged Co-authored-by: Alex Lavaee <lavaman131@github.com>
…lay beside a twin Review of the previous commit found three gaps in the stale decision batch. An edit_plan result hands the Planner a new revision to work from, but that revision's outline was never remembered, so any later edit by another writer made open_decisions at that revision refuse with "no longer retained", undoing the survival of an edit elsewhere. The outline of the revision an edit returns is now remembered, with its block anchors, once the edit is published. A replay of an existing key was judged by the card's anchor alone, and a card whose paragraph was rewritten beside an identical copy could be recovered by digest onto the copy and reported as captured. Where the server still holds the Planner's read, the block that read named must now be the card's own block; where it does not (after a restart), an identical replay is still judged by the card's anchor as before. A stale replay of a known key that points at a different, intact paragraph now gets the "already holds a different decision" refusal again, since the document-changed message is wrong for it. Assistant-model: Claude Sonnet 5.5 Assistant-workflow: goal (run 29d6269b-6026-4c1f-8a50-399c757f21c0) Assistant-duration: unmeasured converged, estimated unmeasured Assistant-verification: red before fix passed: with the source of this commit reverted, three new tests failed on the previous head (open_decisions at an edit_plan-returned revision after another writer's edit, a replay after a rewrite beside an identical copy, a stale replay naming a different intact paragraph) and pass with it Assistant-verification: bun test unit passed: GIT_CONFIG_GLOBAL=/dev/null bun test on this head (6810 pass, 0 fail, 22 skipped for TEST_DATABASE_URL) Assistant-verification: bun run types passed: whole workspace and e2e, on this head Assistant-verification: bun run ci passed: dprint, oxlint (72 existing warnings, none new), tokens and design checks, on this head Assistant-verification: bun run e2e passed: 43 tests across harness-decisions-deck, planner-decisions, run-card, planner-attachment, open-ended-decisions, decision-prose and inline-decision-deck on this head Assistant-verification: bun run test:postgres unavailable: not run, storage is unchanged Co-authored-by: Alex Lavaee <lavaman131@github.com>
… was refused Review of the previous commit found that a replay of an existing key whose read the server no longer holds (after a restart, or once more than eight revisions have been outlined) was still judged by the card's anchor alone. That anchor can have been recovered by digest onto an identical copy of a rewritten paragraph, so the replay was reported as captured. Without the read, a replay now needs the card to stand beside the paragraph the input names, which the recovered anchor does not show; an identical replay of a card still beside its paragraph stays idempotent. The refusal text also listed causes that had not happened. It now names the one that did: the server no longer has that read, the address does not match the block read at that position, the paragraph appeared more than once when it was read, the block read was rewritten, removed or moved since, or an identical paragraph now stands beside it. A stale address whose index disagrees with its digest is refused, as it is at the current revision. Assistant-model: Claude Sonnet 5.5 Assistant-workflow: goal (run 29d6269b-6026-4c1f-8a50-399c757f21c0) Assistant-duration: unmeasured converged, estimated unmeasured Assistant-verification: red before fix passed: with the source of this commit reverted, five new or extended capture-decisions tests failed (the three refusal causes, an evicted-read replay beside a twin, an index that disagrees with its digest) and all 34 pass with it Assistant-verification: bun test unit passed: GIT_CONFIG_GLOBAL=/dev/null bun test on this head (6812 pass, 0 fail, 22 skipped for TEST_DATABASE_URL) Assistant-verification: bun run types passed: whole workspace and e2e, on this head Assistant-verification: bun run ci passed: dprint, oxlint (72 existing warnings, none new), tokens and design checks, on this head Assistant-verification: bun run e2e passed: 43 tests across harness-decisions-deck, planner-decisions, run-card, planner-attachment, open-ended-decisions, decision-prose and inline-decision-deck on this head Assistant-verification: bun run test:postgres unavailable: not run, storage is unchanged Co-authored-by: Alex Lavaee <lavaman131@github.com>
… a twin Review of the previous commit found that, without the Planner's read, a replay of a multi-passage card was checked against where the card stands for its first passage only. A later passage was judged by its anchor, which can have been recovered by digest onto an identical copy of a rewritten paragraph, so the replay was reported as captured while the same input with the read retained was refused. Without the read, a replay is now accepted only for a card standing beside the one passage it was opened on. A card with several passages is refused, with the cause that the server no longer has that read, until the Planner reads the document again; a retry at a fresh revision is judged normally and stays idempotent. The decisions guide states this. Assistant-model: Claude Sonnet 5.5 Assistant-workflow: goal (run 29d6269b-6026-4c1f-8a50-399c757f21c0) Assistant-duration: unmeasured converged, estimated unmeasured Assistant-verification: red before fix passed: with the one-line change reverted, the new capture-decisions test (a card on two passages, the second rewritten beside an identical copy, the read evicted by nine later outlines, then a replay) failed and all 35 pass with it Assistant-verification: bun test unit passed: GIT_CONFIG_GLOBAL=/dev/null bun test on this head (6813 pass, 0 fail, 22 skipped for TEST_DATABASE_URL) Assistant-verification: bun run types passed: whole workspace and e2e, on this head Assistant-verification: bun run ci passed: dprint, oxlint (72 existing warnings, none new), tokens and design checks, on this head Assistant-verification: bun run e2e passed: 43 tests across harness-decisions-deck, planner-decisions, run-card, planner-attachment, open-ended-decisions, decision-prose and inline-decision-deck on this head Assistant-verification: bun run test:postgres unavailable: not run, storage is unchanged Co-authored-by: Alex Lavaee <lavaman131@github.com>
lavaman131
force-pushed
the
fix/planner-decision-lifecycle
branch
from
November 4, 2026 08:38
9ccd43c to
1fb197c
Compare
lavaman131
added a commit
that referenced
this pull request
Nov 4, 2026
…ction until it returns
A Planner machine that lost its connection (asleep, network down) ended its own work: the
server finished the turn, withdrew every question the client had asked, showed its live runs
as stopped and forgot the session, so a network failure read as the Planner stopping
("was stopped after N min") and cancelled Decisions nobody had answered. The reattach
protocol could not undo it, because the client's own close had already settled its questions
and a stopped run card is terminal. The user's decision is that a dropped connection ends
nothing by itself, however long the machine stays away; only an explicit Stop, a sign-out,
unpairing or revoking the machine, a Planner reset, or archiving or deleting the document
does.
A client may now ask for the new `resumable-sessions` feature, and the package client does by
default (`resumable: false` opts out). For such a link that drops unexpectedly (the socket
closes with no detach or refusal, or the server closes it as unresponsive) the server keeps
each session that has a live or paused run, or a question waiting on a member, with no
deadline. The document's Planner is announced as `disconnected` and its live run cards carry
`disconnected` with their last status; Chat says once that the Planner disconnected and once
that it came back; `@chopin` and `invoke_planner` say it is disconnected; Decisions stay open
and answerable and answers are held. The same account's next link sends `resume` first (the
sessions and questions it still holds) and gets `resumed` back: answered questions are
delivered once, withdrawn ones are withdrawn, and runs carry on. `PLANNER_ATTACH_GRACE_MS` now
leases only a detached link's ownership, or a drop with nothing to keep; a quiet machine's
connector connection, the Default machine idle period and the owner's GitHub credential being
renewed no longer end a disconnected Planner either, and the document is not offered to another
of its owner's machines meanwhile.
Explicit acts are never lost on the way. A Stop pressed while the machine is away, or sent and
unanswered when the link dropped, is held and delivered first after `resumed`, and Chat says
only what is true: stopped when the client confirmed it, asked when the connection is known to
be down, not confirmed when it did not answer, and kept once the drop is noticed. A Resume now
undoes only the Stops it covers, and a Resume pressed while away is refused, not held. A
question the client withdraws while the link is down settles at once and is withdrawn in
Chopin on resume. The client acknowledges every `input-result` with `input-received`, so an
answer written into a socket the server had not yet noticed was dead, including to the only
question of a Planner with no run, is kept with its session until acknowledged and sent again
after `resumed`. A same-account client that notices its own drop first and reconnects at once
takes the old link's work over with its first `resume`, which shows that link is dead. A
refusal carries `retry` only when Chopin could not decide, and the package client keeps its
questions pending only for that.
A server restart still forgets every session in memory: a client's `resume` finds nothing and
it settles its questions as cancelled, the Decisions stay open and answerable exactly as after
a crash, the stored run cards come back stopped and stay so until a session is started again,
and a graceful shutdown posts no Chat line about it. The documentation says so rather than
promising more.
The shipped connector does not speak the agent protocol itself; it starts a Planner backend
command. It now keeps a current Planner credential in a private file,
`CHOPIN_PLANNER_TOKEN_FILE`, for as long as a backend runs, so a backend that keeps its
sessions through a drop can attach again hours later with a fresh one, and the agent
connection guide states the revised backend contract. No Planner backend ships in this
repository, and no test composes the connector, a backend and a drop longer than the
credential's 15 minutes end to end; what is tested is each side: the connector's renewed
file, the server accepting a fresh machine credential and resuming, and the package client's
resume, acknowledgement and retries.
The stale-twin capture finding of the previous review round was already settled by the
decision-capture work in main (#777); a test pins its second variant.
Assistant-model: Claude Sonnet 5.5
Assistant-workflow: goal (run 2ec31f8e-bdf5-4bf5-a416-011edba43bee)
Assistant-verification: red before fix passed: with each fix reverted its new tests failed, in three rounds of fixes (no-deadline hold, acknowledgement kept, Stop wording and late answers, shutdown cancelling and posting, hand-over, retry classification, withdrawal after resume, malformed frame, credential file renewal, skewed clock, hung stop, file mode)
Assistant-verification: mutation check passed: re-adding a grace timer for a suspended link failed the four no-deadline link-resume tests, and dropping the acknowledgement or the hand-over failed theirs
Assistant-verification: bun test unit passed: GIT_CONFIG_GLOBAL=/dev/null bun test on this commit's tree
Assistant-verification: bun run types passed: whole workspace and e2e, on this commit's tree
Assistant-verification: bun run ci passed: dprint, oxlint (72 existing warnings, none new), tokens and design checks, on this commit's tree
Assistant-verification: bun run test:postgres unavailable: not run, storage is unchanged
User-preference: A dropped connection never ends runs or open questions; only an explicit Stop, sign-out or unpairing does
Co-authored-by: Alex Lavaee <lavaman131@github.com>
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.
Fix the Planner's decision lifecycle
A Planner turn could report "stopped after 30 min" while its workflow runs and questions were alive, an
open_decisionsretry could be counted as captured when nothing held the question, and a stale batch could land beside the wrong paragraph. This PR fixes those as one standalone change. Link-drop, suspension and reconnect handling is not here; it follows in a separate PR.Root causes
open_decisionswith the stable keys of cards an earlier turn had already created therefore counted 0 captured decisions, the Chat reply was refused ("could not move its questions into inline decisions"), and the refusal released the Planner session, again reporting the run as stopped. A retry with the stable keys the repair prompt asks for could never succeed.open_decisionsrefused the whole batch whenever the revision had moved on since the Planner's read, whatever had changed: a person typing, a workflow run writing, or the Planner's own earlier card. A document with live writers could keep refusing it.Fixes
edit_planreturns is remembered once published so a batch written from it survives another writer's edit. A replay of an existing key is checked against the same read where the server still holds it.Tests
capture-decisionstests fail without the source change for a paragraph rewritten beside an identical copy, a paragraph already repeated when read, a revision no longer retained, a replay whose read was evicted beside an identical copy, a replay after a rewrite, a replay naming a different intact paragraph, an address whose index disagrees with its digest, and anopen_decisionsbatch written from the revisionedit_planreturned after another writer's edit.bun test6813 pass, 0 fail (22 skipped for the missing PostgreSQL URL);bun run types;bun run ci; and 43 Chromium tests across the harness decisions deck, planner decisions, run card, planner attachment, open-ended decisions, decision prose and inline decision deck.bun run test:postgreswas not run; storage is unchanged.Not in this PR
Link-drop, suspension, resumable-session and reconnect behavior is out of scope and follows in a separate PR. This change does not alter how a dropped link is handled.
Assistant-model: Claude Sonnet 5.5
Assistant-workflow: goal (run 29d6269b-6026-4c1f-8a50-399c757f21c0)
Assistant-duration: unmeasured converged, estimated unmeasured
Assistant-verification: bun test unit passed: 6813 pass, 0 fail on the final head
Assistant-verification: bun run types passed: whole workspace and e2e on the final head
Assistant-verification: bun run ci passed: dprint, oxlint, tokens and design checks on the final head
Assistant-verification: bun run e2e passed: 43 tests across seven decision and Planner files on the final head
Assistant-verification: bun run test:postgres unavailable: not run, storage is unchanged
Co-authored-by: Alex Lavaee lavaman131@github.com
Known limitation
One narrow case is left for #776: a replay whose read was evicted can be accepted beside an identical copy of a rewritten paragraph.
Assistant-workflow: goal (runs 5c2dd386-1e50-4873-be57-3bc47480ae71 and 29d6269b-6026-4c1f-8a50-399c757f21c0)
Assistant-duration: 5h15m converged, estimated 45-90m
Assistant-verification: bun run types passed: after rebase onto main 6dbb78e
Assistant-verification: bun test passed: apps/server chat, questions, agent, harness, experiments, tasks, 1936 pass, 0 fail after rebase
Co-authored-by: Alex Lavaee lavaman131@github.com