Repository navigation
fix(rpc): stop a turn that is waiting on an ask_user question - #1073
Conversation
Stopping a turn while its ask_user question was on screen did not stop it. QuestionBroker.await_question answered the cancellation with the question's default, so the cancellation ended inside the broker: the tool returned "the user did not answer, proceed with best judgment", the model carried on with its own recommendation or asked the question again, and turn.cancel waited for that whole continued turn before replying. The person had pressed stop and the page said the turn was stopped. A cancellation aimed at the waiting task now propagates after the prompt is closed. A pending future cancelled by anyone else still resolves to the default, as before. The elicitor already turns a cancelled task into a cancelled form, so its behaviour is unchanged; two docstrings that described the old broker are reworded. Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: update the remaining cancellation contract documentation to match the new propagation behavior.
I found one introduced contract-documentation mismatch, marked inline. The implementation itself looks correct: I traced turn.cancel through TurnHandle/Lane to cancellation of the actual running turn task, checked the broker cleanup and clarify.closed path, and verified that future-only cancellation and cancel_all() still resolve to the default.
Coverage included the full diff, relevant callers, scheduler/turn cancellation architecture, file history, the canonical Runtime context, AGENTS.md/CLAUDE.md rules, backward compatibility, and whether tests were weakened (they were not). Branch/commit naming and source/test-file rules conform; dependency and asset rules are not implicated.
Verification:
- Initial targeted run: setup error before test bodies because the fresh environment lacked workspace package
raven_everos. uv sync --all-packages: passed.uv run pytest tests/test_question_broker.py tests/test_ask_user_tool.py tests/test_rpc_turn_cancel.py -q: 134 passed.uv run pytest tests/test_acp_elicitation.py tests/test_acp_ask_user.py -q: 73 passed.git diff --check github/main...HEAD: passed.
|
Windows compatibility review of head No Windows compatibility issue was found in the inspected scope. The change distinguishes cancellation of the calling task from cancellation of its pending future using asyncio task APIs already supported by the project's Python requirement and already used by the ACP elicitor. No platform-specific path, subprocess, signal, dependency, or I/O API was added. Native Windows verification with Python 3.13.12 and the default Proactor event-loop policy: 207 tests passed (question broker, ask_user and RPC turn cancellation: 134; ACP elicitation/questions: 73). These exercise task cancellation propagation, The existing cancellation-contract documentation discussion was read; this is a Windows compatibility review, without an overall merge verdict. Coverage limits: UTF-8 mode was enabled; no real browser Stop interaction, live ACP subprocess/channel session, Python 3.12 run, or full repository suite was exercised. Exact verification commands and resultsEach Python invocation used an isolated uv environment and temporary profile/Raven/OAuth/temp directories. UTF-8 mode was enabled and imports were asserted to belong to the selected checkout. $run = 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\1292e0f475b0450bb12d681f2a4fa415'
$env:UV_PROJECT_ENVIRONMENT = Join-Path $run 'python-env'
$env:UV_PYTHON = '3.13'
$env:PYTHONUTF8 = '1'
# 1073-question: 134 passed, 0 failed, 0 skipped; exit 0
Set-Location -LiteralPath 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\1292e0f475b0450bb12d681f2a4fa415\pr1073\head-archive'
$env:PYTHONPATH = 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\1292e0f475b0450bb12d681f2a4fa415\pr1073\head-archive' + ';' + (Join-Path $run 'probes')
uv 'run' '--frozen' '--no-sync' 'pytest' 'tests/test_question_broker.py' 'tests/test_ask_user_tool.py' 'tests/test_rpc_turn_cancel.py' '-p' 'no:randomly' '-n' '0' '-x' '--tb=short' '--basetemp' 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\1292e0f475b0450bb12d681f2a4fa415\checks\1073-question\pytest' '--junitxml' 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\1292e0f475b0450bb12d681f2a4fa415\checks\1073-question\junit.xml'
# 1073-acp: 73 passed, 0 failed, 0 skipped; exit 0
Set-Location -LiteralPath 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\1292e0f475b0450bb12d681f2a4fa415\pr1073\head-archive'
$env:PYTHONPATH = 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\1292e0f475b0450bb12d681f2a4fa415\pr1073\head-archive' + ';' + (Join-Path $run 'probes')
uv 'run' '--frozen' '--no-sync' 'pytest' 'tests/test_acp_elicitation.py' 'tests/test_acp_ask_user.py' '-p' 'no:randomly' '-n' '0' '-x' '--tb=short' '--basetemp' 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\1292e0f475b0450bb12d681f2a4fa415\checks\1073-acp\pytest' '--junitxml' 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\1292e0f475b0450bb12d681f2a4fa415\checks\1073-acp\junit.xml'
$bare = Join-Path $run 'objects.git'
git --git-dir=$bare diff --check b27b0ae97492f799ac3c551a8ca2bc844cbe9912..295b41da95289885f3b82b1c14171068180d073f # exit 0Event-loop check, from the same checkout/environment: uv run --frozen --no-sync python -c "import asyncio,os;loop=asyncio.new_event_loop();print(os.name, type(loop).__name__);loop.close()"
# exit 0: nt ProactorEventLoop |
…ault AskUserTool.ask_direct still promised an empty answer on cancellation because the broker never raised, and the CONTEXT.md entry listed cancellation among the broker's fail-safe defaults. Both now separate the asking task being cancelled, which raises, from the pending wait being cancelled from outside, which still answers the default. Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
The new revision resolves the sole prior blocker. I reviewed the complete change as previously assessed plus the 105bcf7123cb..4ddee4ef5e58 delta, checked the updated caller and canonical Runtime contract against the broker behavior, and found no new issue. My original thread has been replied to and resolved.
Coverage: repository rules and canonical terminology, the diff and relevant callers, cancellation architecture, backward compatibility, history, and test integrity. The added commit changes documentation/comments only and does not weaken tests.
Verification:
uv run pytest tests/test_question_broker.py tests/test_ask_user_tool.py tests/test_rpc_turn_cancel.py tests/test_acp_elicitation.py tests/test_acp_ask_user.py -q: 207 passed.git diff --check github/main...HEAD: passed.
|
Windows compatibility follow-up at head No Windows compatibility issue found in that scope. The caller-task versus future-only distinction uses asyncio task APIs already supported by the project's Python requirement; no path, subprocess, signal, dependency or platform I/O operation was added. The previous cancellation-documentation discussion is reflected in the updated caller/Runtime descriptions. Native Windows verification, Python 3.13.12 with uv run --frozen --no-sync pytest tests/test_question_broker.py tests/test_ask_user_tool.py tests/test_rpc_turn_cancel.py tests/test_spine_scheduler.py tests/test_acp_elicitation.py tests/test_acp_ask_user.py tests/test_acp_questions.py -p no:randomly -n 0 -x --tb=short --basetemp C:\Users\livxu\AppData\Local\Temp\raven-windows-review\79783fbc38724cf7967237cdeb44f8f8\checks\1073-cancellation\pytest --junitxml C:\Users\livxu\AppData\Local\Temp\raven-windows-review\79783fbc38724cf7967237cdeb44f8f8\checks\1073-cancellation\junit.xml
uv run --frozen --no-sync python -c 'import asyncio,os; loop=asyncio.new_event_loop(); print(os.name, type(loop).__name__); loop.close()'303 passed, no skips/failures, exit 0; the event-loop probe returned Limits: no real browser Stop interaction, live ACP subprocess/channel session, Python 3.12 run, or full repository suite was performed. |
|
Blocking: 1 finding needs correction before this revision can merge; see the inline note. Scope: I reviewed only the What I examined: Findings:
Verification:
Checked and not reported: |
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: close a first-round stint record when its approval wait is cancelled.
The newly reported stint-lifecycle evidence invalidates my earlier clean stance. I traced the existing inline finding's path: StintDriver.start() writes a running record before _submit(); caller-task cancellation now escapes _confirmed() as CancelledError; _submit() converts only budget and explicit non-approval exceptions into receipts; and start() closes the record only after an error receipt. The cancelled path therefore leaves a fresh running record that blocks an immediate retry and can later resume without confirmation. The registry's 120-second wait_for also reaches that path without an explicit Stop.
I am not duplicating the existing inline blocker or resolving another reviewer's thread.
Verification: uv run pytest tests/test_playbook_stint.py tests/test_ask_user_tool.py tests/test_question_broker.py -q passed (273 tests), but the suite does not cover cancellation during first-round stint approval.
With a stopped turn now raising out of its open question, a stint's first round could leave StintDriver.start without a receipt: the turn stopped, or load_playbook's tool ceiling ran out, while the "Run it / Not now" question was on screen. start closed the record only on an Error receipt, so the record stayed running with round one on it. The next request for the same playbook was refused as if something were advancing it, and once the stamp went stale it was taken up through resume, which asks nobody, so a round nobody approved ran. start now closes the record as stopped on any exit from _submit that is not a receipt, while no round has been opened, and re-raises. The ask_user module docstring no longer says the loop never sees an exception. Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
The new revision resolves the stint-lifecycle issue that changed my prior stance. StintDriver.start() now marks a persisted first-round record stopped on any exceptional exit before the round opens, then re-raises the original cancellation or timeout. Already-opened rounds remain under their existing run/heartbeat lifecycle. The regression tests cover both a stopped task through a real question broker and the tool-ceiling path, and verify that a later start does not silently resume the unapproved stint.
I reviewed the 4ddee4ef5e58..9e56c76ebfc0 delta in the context of the complete change, repository rules/canonical terminology, relevant callers and history, backward compatibility, cancellation architecture, and test integrity. I found no new issue or weakened test. The remaining open inline thread belongs to its original reviewer, so I have not resolved or re-graded it.
Verification:
uv run pytest tests/test_question_broker.py tests/test_ask_user_tool.py tests/test_rpc_turn_cancel.py tests/test_acp_elicitation.py tests/test_acp_ask_user.py tests/test_playbook_stint.py -q: 369 passed.git diff --check github/main...HEAD: passed.
|
Windows compatibility re-review of the complete nine-file diff at No Windows compatibility issue found in the inspected scope. Native Windows verification passed 303 related cancellation tests and both new first-round regression tests. Coverage includes calling-task versus future-only cancellation, timeout/EOF defaults, Two additional probes reused the repository's first-round cancellation/ceiling scenarios under a real directory containing spaces and non-ASCII characters, and explicitly asserted ProactorEventLoop: 2 passed. They verify that the record becomes stopped, an unapproved round is not submitted, resume has nothing to take up, and the subsequent permitted start is fresh. This independently checks the fix discussed in the existing stint-lifecycle thread without adding a duplicate finding. Runtime: native Windows, Python 3.13.12, UTF-8 mode; exact-head source imports and temporary uv/profile/Raven/OAuth/test resources were isolated outside the active checkout. All nine changed files were rechecked against the pinned head and Exact verification commands, from the isolated head checkoutuv run --frozen --no-sync pytest tests/test_question_broker.py tests/test_ask_user_tool.py tests/test_rpc_turn_cancel.py tests/test_spine_scheduler.py tests/test_acp_elicitation.py tests/test_acp_ask_user.py tests/test_acp_questions.py -p no:randomly -n 0 -x --tb=short --basetemp C:\Users\livxu\AppData\Local\Temp\raven-windows-review\3d295dd1b92a4b4b84fe8b2eb91f85c0\checks\1073-cancellation\pytest --junitxml C:\Users\livxu\AppData\Local\Temp\raven-windows-review\3d295dd1b92a4b4b84fe8b2eb91f85c0\checks\1073-cancellation\junit.xml
uv run --frozen --no-sync pytest tests/test_playbook_stint.py::TestRunning::test_a_first_round_stopped_while_its_approval_waits_closes_the_stint tests/test_playbook_stint.py::TestRunning::test_a_first_round_whose_approval_outlives_a_tool_ceiling_closes_the_stint -p no:randomly -n 0 -x --tb=short --basetemp C:\Users\livxu\AppData\Local\Temp\raven-windows-review\3d295dd1b92a4b4b84fe8b2eb91f85c0\checks\1073-stint\pytest --junitxml C:\Users\livxu\AppData\Local\Temp\raven-windows-review\3d295dd1b92a4b4b84fe8b2eb91f85c0\checks\1073-stint\junit.xml
uv run --frozen --no-sync pytest C:\Users\livxu\AppData\Local\Temp\raven-windows-review\3d295dd1b92a4b4b84fe8b2eb91f85c0\probes\test_1073_windows.py -v -s -p no:randomly -n 0 -x --tb=short --basetemp C:\Users\livxu\AppData\Local\Temp\raven-windows-review\3d295dd1b92a4b4b84fe8b2eb91f85c0\checks\1073-windows\pytest --junitxml C:\Users\livxu\AppData\Local\Temp\raven-windows-review\3d295dd1b92a4b4b84fe8b2eb91f85c0\checks\1073-windows\junit.xmlThe external probe emitted an unknown imported Limits: the ceiling test uses the repository's shortened 0.2s limit. No live browser Stop, ACP subprocess/channel session, LLM-backed stint, other Python version, real UNC share, full playbook/repository suite or concurrent-reader file-lock stress was exercised. These results do not establish full compatibility or a general merge verdict. |
A first-round approval can stand for the broker's whole budget, and no round is open to beat for. The record went stale after five minutes, so a second request took the stint up through `resume` with `confirm=False` and ran the round nobody approved. Claim the record before asking and start a stamp-only beat; `StintStore.touch` moves only `touched_at_ms` under the write lock, so a stop or an extend is not undone by the beat. Close a first round that never opened on every non-receipt exit, but re-read first: a pause, stop or extend that landed during the question survives, and a record another process claimed is left to it. A failed cleanup write is logged rather than raised, and only a cancellation or a timeout is blamed on the approval. `extend` refuses a first round that never opened, and `resume` asks before taking one up. Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
…aits `Task.cancelling()` is cumulative. A task that swallowed an earlier cancellation kept the count for life, so a pending future cancelled by someone else was mistaken for a new cancellation of the calling task and raised out of `await_question`. Remember the count before the wait and propagate only when it grows. The real ACP route is covered: a stopped `AskViaTool` question now raises instead of answering empty. Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
The broker now raises when the calling task is cancelled, but the `QuestionResponder` and `SupportsDirectAsk` papers, the ACP design note and the proactivity reference still described a broker that never raises. Update those and the ACP test doubles, which answered a cancellation with an empty string as the broker used to. Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
This revision adds three substantive follow-ups beyond the previously reviewed head: it finishes the asking contracts and design references, distinguishes new caller cancellation from a task's cumulative historical cancellation count, and hardens the first-round stint lifecycle with a live pre-approval claim, stamp-only heartbeat updates, safe cleanup, and re-confirmation on legacy/unopened resumes. I traced the broker, ACP, turn, stint start/resume/extend, heartbeat, atomic persistence, and cross-process guard paths and found no plain error.
Coverage included the complete github/main...HEAD diff, the 9e56c76ebfc0..180822b6aed2 delta, repository rules and canonical terminology, callers and history, backward compatibility, architecture boundaries, and whether tests were weakened. The new tests add the missing cancellation, cumulative-count, heartbeat, persistence, resume, extend, and cleanup cases. My own prior thread remains resolved; the remaining open thread was started by another reviewer and is theirs to close.
Verification:
- Initial targeted run: 457 setup errors because the recreated environment lacked workspace package
raven_everos; no test body ran. uv sync --all-packages: passed.uv run pytest tests/test_question_broker.py tests/test_ask_user_tool.py tests/test_rpc_turn_cancel.py tests/test_acp_elicitation.py tests/test_acp_ask_user.py tests/test_acp_questions.py tests/test_playbook_stint.py tests/test_stint_record.py tests/test_atomic_io.py -q: 457 passed, with 2 Python 3.13 multiprocessing deprecation warnings.git diff --check github/main...HEAD: passed.
|
Windows compatibility review — PR #1073 @ Scope: the PR's delta since the previously reviewed head Finding (inherited, present at base — not introduced by this PR): a dead holder is never detected on Windows, so stint recovery waits for the five-minute stale window instead of resuming at once.
No new Windows-specific issue was found in this PR's delta. The new heartbeat path writes through Verification (native Windows 11; detached worktrees
Coverage gaps: the eight symlink-privilege failures are environmental on this host and identical at base; no live gateway restart or cross-process stint recovery was exercised end to end. |
The diff coverage gate asks for 90% of changed lines. The resume refusal and refused dispatch left two close branches unexecuted, and the stamp-only touch left its missing-file and unreadable-file fallbacks unexecuted. Four tests close those seven lines. Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
The only delta from the previously reviewed clean head is a tests-only coverage commit. It adds focused cases for resumed first-round refusal, resumed dispatch failure, and the missing/unreadable StintStore.touch fallbacks. The tests exercise the intended branches, preserve the existing behavioral expectations, and do not weaken coverage.
I reviewed the 180822b6aed2..067eaf2d086b delta in the context of the complete github/main...HEAD change, repository rules, relevant callers and history, backward compatibility, architecture, and test integrity. No new finding. My own prior thread remains resolved; I did not alter another reviewer's thread.
Verification:
uv run pytest tests/test_playbook_stint.py tests/test_stint_record.py -q: 180 passed.git diff --check github/main...HEAD: passed.
|
Windows compatibility re-review — PR #1073 @ Scope: the delta since the previously reviewed head Windows angle: nothing platform-specific was added; the new cases use plain files under Verification (native Windows 11; detached worktree
Previously reported inherited issue — still open at this head (source unchanged): Coverage gaps: same as the previous review — the eight |
A pid is unique per host, not across hosts: `holder_gone` already compared the pair, but the beat and the first-round close compared only the pid. A record claimed by the same pid on another machine looked like ours, so our beat kept it alive and our close could stop it under the other host. `StintStore.touch` and `_close_first_round` now compare `holder_host` when the record has one, and the beat passes the record's host. A record with no host recorded is still accepted, so an older stint file stays usable. Co-Authored-By: Claude Code <noreply@anthropic.com>
The close re-read the record to preserve a concurrent pause, stop, extend or claim, then wrote it back in a second lock acquisition. An update that landed between the two was overwritten by the stop. Read, checks and write now share one `write_transaction`, so an update lands either before the read (and is preserved) or after the write (and wins). The whole cleanup is also guarded: a record that cannot be read or written is logged, and the cancellation or timeout that brought us here is what the turn sees -- a cleanup error must not become the tool result. Co-Authored-By: Claude Code <noreply@anthropic.com>
`extend` refuses a first round that never opened and tells the caller to start the playbook again. The sentence always promised that restarting "asks for approval", which is false for a `confirm: false` playbook -- and the new first-round close makes that state reachable for one. Co-Authored-By: Claude Code <noreply@anthropic.com>
A first round turned down by the hourly dispatch budget writes the record PAUSED and returns without opening a round. The pre-approval beat stayed alive until its next tick, so `holding()` kept the CLI at "Holding this terminal" for up to HEARTBEAT_EVERY_SEC after the pause was already final. Co-Authored-By: Claude Code <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
Reviewed the four-commit delta from 067eaf2d086b and rechecked it in the complete github/main...HEAD diff. The cross-host holder comparison matches holder_gone, the first-round close is serialized under the existing re-entrant per-path write lock, the restart message follows spec.confirm, and the budget-pause path now releases its pre-approval beat immediately.
Coverage this round: repository rules (AGENTS.md, CLAUDE.md, and CONTEXT-MAP.md), the diff, affected callers and commit history, backward compatibility for hostless records, test-strengthening versus weakening, and the runtime/RPC architecture boundary. I found no concrete regression after trying to refute the concurrency and ownership changes.
Verification: git diff --check 067eaf2d086b..HEAD passed; uv run pytest tests/test_playbook_stint.py tests/test_stint_record.py -q passed (185 tests).
|
Windows compatibility re-review — PR #1073 @ Scope: the delta since the previously reviewed head Windows angle: no new platform surface. The new Previously reported inherited issue — still open (source unchanged): Verification (native Windows 11; detached worktrees
Coverage gaps: the full suite was not rerun; the remaining eight failures are environmental on this host (symlink privileges) and match base; no live gateway restart or cross-process stint recovery was driven. |
A close that lost the race to a round another caller had opened stopped the beat table's current entry before checking whether it had a round to close. That entry belonged to the running round, so a second host could take the same checkout up once its stamp went stale. Stop only the beat this caller started, and only when no round opened. Co-authored-by: Claude (deepseek-flash) <noreply@anthropic.com>
`_submit` opens the round on the record object before it writes. A write that failed between the two left the file without a run id while the graph was running, and the first-round close then recorded the stint as stopped -- which is not unfinished, so a second start could open another stint on the same project. Ask the caller's own object whether round one opened, and leave the record running when it did. Co-authored-by: Claude (deepseek-flash) <noreply@anthropic.com>
`resume` closed a first round that never opened on every Error receipt, so a delegation pause, a validation refusal, or a question that could not be delivered was recorded as "the first round was refused" and the stint became terminal. Only the person's "not approved" is an answer to that question; every other receipt leaves the record interrupted, as a resume that never asked already did. Co-authored-by: Claude (deepseek-flash) <noreply@anthropic.com>
The dispatch budget is charged after the first round's confirmation, so a first round that the budget turned away was approved. It has no run id for `_first_round_opened` to read, and a later resume asked the same question again -- and could close "not approved" a round the person had already approved when they could not answer a second time. Record that approval on the stint when the budget pause follows it, and let resume take the round up without asking again. Co-authored-by: Claude (deepseek-flash) <noreply@anthropic.com>
Only the approval wait needs a claim before the round opens: that is the wait a second process could otherwise take up. A resume that asks nobody still claimed the stint, so a stop during its preflight left the record looking held by a live process with no beat, and the next resume was refused until the stamp went stale. Claim only when this resume is going to ask; `_submit` claims again once the round opens. Co-authored-by: Claude (deepseek-flash) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
Reviewed the five-commit delta from 27ef8c23c793 and rechecked it against the complete github/main...HEAD diff. The beat ownership, dispatched-round write failure, transient resume refusal, and budget-paused approval changes are internally consistent. One retry-delay edge remains in the no-confirm resume path; it has a working escape and is marked as a nonblocking follow-up inline.
Coverage this round: repository rules (AGENTS.md, CLAUDE.md, and CONTEXT-MAP.md), the diff, affected callers and commit history, backward compatibility of the new persisted field, test-strengthening versus weakening, and the runtime/RPC architecture boundary.
Verification: git diff --check 27ef8c23c793..HEAD passed; uv run pytest tests/test_playbook_stint.py tests/test_stint_record.py -q passed (190 tests). A direct state check confirmed that the no-confirm write preserves the remote holder but refreshes the timestamp enough for _held() to return true.
|
Windows compatibility re-review — PR #1073 @ Scope: the delta since the previously reviewed head Windows angle: no new platform surface. The close still runs under Previously reported inherited issue — still open (source unchanged): Verification (native Windows 11; detached worktrees
Coverage gaps: the full suite was not rerun; the remaining eight failures are environmental on this host and match base; no live gateway restart or cross-process stint recovery was driven. No Windows-specific issue introduced by this fix pass. |
A resume that asks nobody writes `running` before `_submit` so a second caller cannot take the same round up while it preflights. Stopped there, nothing put the record back: it kept a fresh stamp, and the next resume was refused for the whole stale window although nothing advanced the stint -- up to five minutes when the holder is another host that `holder_gone()` cannot disprove. Restore what the resume found, under the same write transaction the first-round close uses: a pause stays a pause, a stale `running` becomes `interrupted`, and a record whose round opened or that another process has moved or claimed is left alone. The transient-refusal branch writes through the same helper instead of a whole stale snapshot. Co-authored-by: Claude (deepseek-flash) <noreply@anthropic.com>
The comment listed "a question that could not be delivered" as a transient refusal that leaves the record interrupted. It does not: the broker fails an undeliverable question safe to the default, `_confirmed` reads that as no answer, and the driver closes the record as not approved. Name the refusals that do reach the branch, and say where an unanswered question lands, so the comment matches the code. Co-authored-by: Claude (deepseek-flash) <noreply@anthropic.com>
The first round's approval can stand for the broker's whole budget, and a `stop` or `pause` from another window can land while the question is up. `_submit` then wrote its pre-question snapshot back, reviving a record the person had just stopped: `advance` ends a stint stopped out of band after its running round, and parks a paused one, but only if the status survives the write. Re-read the record and write the opened round under one `write_transaction`. The status the person set is kept; a record that cannot be read is replaced rather than losing a dispatched round's run id. Co-authored-by: Claude (deepseek-flash) <noreply@anthropic.com>
The broker and the tool are covered separately; nothing drove a lane stop through the real scheduler, lane, broker and tool to prove the cancellation reaches the task the broker is holding. The fake `run_turn` is the seam where the loop runs a tool, so everything below it is the real path. Co-authored-by: Claude (deepseek-flash) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
Reviewed the four-commit delta from a03bef38e341 and rechecked it in the complete github/main...HEAD diff. The previous retry-delay follow-up is fixed: a no-confirm resume that stops before opening now restores paused/interrupted state under the write lock, while a concurrently opened or reclaimed round is left alone. The opened-round transaction also preserves an out-of-band stop or pause, and the new RPC-spine test reaches the real scheduler cancellation path while the question is pending.
Coverage this round: repository rules (AGENTS.md, CLAUDE.md, and CONTEXT-MAP.md), the diff, affected callers and commit history, persistence/backward compatibility, test-strengthening versus weakening, and the runtime/RPC architecture boundary. I found no concrete regression after refuting the state-restoration and concurrent-write candidates.
Verification: git diff --check a03bef38e341..HEAD passed; uv run pytest tests/test_playbook_stint.py tests/test_stint_record.py tests/test_rpc_spine.py -q passed (281 tests).
|
Second review round, fixed in four commits on
Verification on the pushed tree (native Windows, Python 3.12.13, isolated The 18 failures are the same environment-only IDs as the merge base: 8 |
|
Windows compatibility re-review — PR #1073 @ Scope: the delta since the previously reviewed head Windows angle: no new platform surface. Both new write paths run under Previously reported inherited issue — still open (source unchanged): Verification (native Windows 11; detached worktrees
Coverage gaps: the full suite was not rerun; the remaining eight failures are environmental on this host and match base; no live gateway restart or cross-process stint recovery was driven. No Windows-specific issue introduced by this delta. |
CONTEXT.md keeps "task" for a unit of delegated work, not for the asyncio object a write or a question waits on. Two docstrings added with the review fixes used it for that object; say caller in both places. Co-authored-by: Claude (deepseek-flash) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
Reviewed the one-commit delta from 80a5ae12d53a and checked it in the complete github/main...HEAD diff. It changes only two explanatory uses of “task” to “caller”, matching the broker contract without altering code, assertions, or test coverage.
Coverage this round: repository rules (AGENTS.md, CLAUDE.md, and CONTEXT-MAP.md), the diff and commit history, the already-reviewed callers and architecture boundary, backward compatibility, and whether tests were weakened. With no executable delta, those behavioral areas remain unchanged and no concrete failure was introduced.
Verification: git diff --check 80a5ae12d53a..HEAD passed; uv run pytest tests/test_playbook_stint.py tests/test_rpc_spine.py -q passed (273 tests).
LivXue
left a comment
There was a problem hiding this comment.
Approved at ecf162023. The two docstrings from the re-review now name the awaited coroutine a caller, the word the project keeps for it, and no code or assertions changed. The review round's behavioral fixes keep their six regression tests, each red when its fix is withdrawn. CI is green on this head (22 passed, 2 skipped), and every thread is resolved.
Summary
Pressing stop while an
ask_userquestion is on screen did not stop theturn.
QuestionBroker.await_questioncaughtCancelledErrortogether withTimeoutErrorand returned the question's default, so the cancellation endedinside the broker: the tool reported "user did not answer; proceed with best
judgment", the model went on with its own recommendation or asked again, and
turn.cancelwaited for that whole continued turn while the page alreadyshowed the turn as stopped. Reproduced on the web page with a headless
browser: after stop the same turn still ran a 45 s command and finished with
an answer, and a prompt that asks again when unanswered put a second question
on screen.
The turn now ends where it was stopped:
await_questionre-raisesCancelledErrorwhen a cancellation of thecalling task arrives while the wait is pending. The count is snapshotted
before the wait because
Task.cancelling()is cumulative: a task thatswallowed an earlier cancellation must not turn someone else's cancelled
future into its own. The prompt is still closed with
clarify.closed.The ACP elicitor already turns a cancelled task into a cancelled form, so
its behavior is unchanged; its docstrings and the contract papers now
describe the new exit.
StintDriver.startwithout a receiptwhile its approval was on screen (a Stop, or
load_playbook's 120 sceiling). The record was left
running, and a later request took it upthrough
resumewithout asking.startnow claims the record and starts astamp-only beat before the approval, and closes the record as stopped on
any such exit before a round is opened.
write_transaction, so apause,stop,extendor claim that landedduring the question survives, and a record another process or host claimed
is left to it. A cleanup that cannot read or write is logged and the
cancellation still propagates; only a cancellation or a timeout is blamed
on the approval.
StintStore.touchmoves onlytouched_at_msunder the write lock andcompares the holder's pid and host, so the beat cannot undo a concurrent
stop or pause and cannot keep another host's claim alive.
extendrefusesa first round that never opened, and says whether starting it again asks
for approval;
resumeasks before taking one up and closes it on refusalor cancellation. A first round paused for the hourly dispatch budget stops
the pre-approval beat, so
holding()is false as soon as the record ispaused.
Review follow-ups on the final head, one commit each:
longer stops the beat table's current entry. It stops only the beat this
caller started, and only when no round opened, so a live round keeps the
heartbeat that keeps a second host off its checkout.
_submitopens the round on the object before writing it, so a failedwrite no longer records
stoppedfor a round that is running (which is notunfinishedand would let a second stint open on the same project).resumecloses a never-opened first round on "not approved" only. Adelegation pause or a validation refusal leaves the record
interruptedand retryable instead of terminal; an unanswered question (a timeout, or
one the broker could not deliver) fails safe to
_NOT_APPROVEDand closeslike any other unanswered ask.
budget pause on round one follows an approval. The record now carries
first_round_approved, andresumetakes that round up without asking asecond time. Records written before the field read as not approved and are
asked again.
resumeclaims the stint only when it is going to ask. A resume that asksnobody has nothing to keep alive while it preflights, and a stop during
that window puts the record back where the resume found it: a pause stays
a pause, and a stale
runningbecomesinterrupted, so the next resumeis not refused for the five-minute stale window.
Follow-ups after the second review round:
transaction as the first-round close, and leaves alone a record whose
round opened or that another process has moved or claimed.
that branch, and says where an unanswered question lands.
write_transaction. Astoporpausethat landed while the approvalquestion was on screen therefore survives the round being recorded:
advanceends or parks the stint after that round instead of the recordbeing revived to
running. A record that cannot be read is replacedrather than turning a dispatched round into an error the turn reads.
question. It drives the real scheduler, lane,
QuestionBrokerandAskUserToolunder a fakerun_turn-- the seam where the loop runs atool -- and fails if the broker answers the cancellation with the default.
Type
Verification
Native Windows 11, Python 3.12.13,
PYTHONUTF8=1, isolatedUSERPROFILE/TEMP, exact-head worktree,uv run --frozen --no-sync:All 18 failures are environment-only and have the same IDs at the merge
base: 8 Windows symlink privilege (
WinError 1314), 1os.killon a deadpid, 2 path/fixture failures in
test_rpc_playbooks, and 7 POSIX referencepaths in
test_subagent_dag_core. The broader run was made on the finaltree.
The six new regression tests fail on the head before their fixes and pass
here: dropping the broker's re-raise fails the end-to-end stop test,
withdrawing the open-round record write fails the two out-of-band
stop/pause tests, withdrawing the stopped-preflight restore fails both
stopped-resume tests, and withdrawing the open-round read fallback fails the
unreadable-record test. Reverting the earlier
_close_first_roundchangestill fails the two tests that cover the beat and the failed-write close.
ruff checkandruff format --checkpass on the touched files, and therepository's commit-message lint passes over the branch. Internal contract
docs (
CONTEXT.md,docs/Proactivity-Implementation.md, and the ACPelicitation design spec) are updated; no user-facing screenshots are needed.
Risk
surface; the change only lets a stopped turn's cancellation reach the
turn.
first_round_approvedis anadditive record field; older builds drop unknown keys and read it as
false, so they ask once more rather than run unapproved. The broker's new
raise only reaches callers whose task was cancelled, and those callers
were audited. The new record writes preserve every field they re-read, so
older builds read the same file.
records carrying the new field remain readable.
Related Issues
#1048 (independent; no dependency)