Repository navigation
fix(pull): the pull sink applies post-turn delivery settings (#2329) - #3227
Conversation
A queued /task row carries delivery settings in backlog_metadata. The push drain applied them in run_async_task; the pull sink wrote the terminal and stopped. On a pull pilot a Chat-tab /task (save_to_session, trigger manual) succeeded and never reached the chat session, and the collaboration activity stayed started. - run_post_turn_delivery: the three post-turn steps (chat-session save and broadcast, collaboration activity, self-task finalisation), moved out of run_async_task. The push drain and the pull sink both call it. - request_from_metadata: the queued-request rebuild, shared by _spawn_drain and the pull sink. - Pull sink: on the CAS-won branch, spawns delivery when the metadata asks for it, then signals the sync waiter with the chat session id; signals at once when there is nothing to deliver. Delivery failure never blocks the signal. - A token-gated SUCCESS write refuses a row already SUCCESS or SKIPPED. The claim token survives the terminal write, so a worker's retried result POST won the CAS twice and would have saved the chat turn twice. A late SUCCESS still corrects a FAILED row. - close_execution_activity and the bulk close also close the collaboration / self-task activity a queued row names, so a terminal written outside run_async_task (pull sink, lease-reaper park, expire, watchdog) does not leave it started. - Sync /task keeps the chat_session_id the sink signals. Fixes #2329 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pplied terminal (#2329) - The lease reaper closes the dead attempt as CANCELLED on a row it has just re-queued. The queued-activity close now runs only for a row that is terminal, so the collaboration / self-task activity stays open for the re-delivered attempt (a CANCELLED close is never upgraded). - Sink delivery runs only when the row still holds the status its own CAS wrote, so FAILED then a late SUCCESS under one token saves the turn once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
merge-train: not on today's train. This is not a defect. #3222 is on the train and edits the same region of Once #3222 lands, merge |
|
Resolve by running |
|
Resolve by merging |
AndriiPasternak31
left a comment
There was a problem hiding this comment.
Reviewed #3227 (structural /review + /validate-pr). Looks right. Ready to merge once it's rebased.
What I checked:
- Mutations: I reverted each piece of the fix from a scratch copy, and each one turns its test red (7/7). The counts match your Test Plan exactly (sink 6, CAS 2, owner close 1, bulk close 1, applied-status 1, re-queue guard 1, sync session id 1). Every test runs the real code; none of them asserts on source text.
- I traced post-turn delivery across push drain, pull sink, duplicate SUCCESS, FAILED→late SUCCESS, lease-reaper park/re-queue and the
dispatch_and_await_terminalcallers. It runs once on each path, and only on the CAS-won branch.
Before merge:
- Rebase. It conflicts with dev's #2514 (
520c100af) inpull_coordination_service.py, and the conflict is mechanical. Keep_spawn_breaker_verdict(...), then_spawn_post_turn_delivery(execution, row_status), and drop dev's baresignal_sync_waiter(execution_id, None, None). If both signals stay, a same-worker sync/taskwakes before delivery and loses itschat_session_id. With that resolution, the pull/1804/2514/3114/backlog suites pass locally on the merged tree (254 passed). - PR body, behaviour note 2. On push, the close owner runs first only for
_write_terminal_and_gateterminals (task_execution_service.py:1135). The push SUCCESS and failed-envelope branches close byactivity_idthroughcomplete_activity(:3017,:3153), so on a push SUCCESSrun_async_taskstill closes the collaboration activity, with its ownexecution_time_ms. - Learnings fragment wording. A sequential retried POST was already answered
replayedatapply_task_result's authoritative-terminal check (pull_coordination_service.py:607). The CAS clause closes the concurrent case, where both POSTs read RUNNING. Your test models exactly that; the ledger line should say so too.
Follow-ups (not this PR):
execution.mdnow says every terminal writer closes the collaboration / self-task activity.expire_stale_queued,cancel_queued_for_agent,fail_queued_for_agent(db/schedules/queue.py:664/558/584) and the drain corrupt-metadata / spawn-failed writes (backlog_service.py:301/322) don't. That's pre-existing #1804-class residue; I'd narrow the sentence or file it.- Delivery is spawned after the committed terminal, and a retry can no longer re-drive it, so a restart or exception in that window loses the chat save for good. Push has the same property, so this isn't a regression. Logging only
type(e).__name__atpull_coordination_service.py:548makes a lost delivery hard to see, though. - #3145 overlap. No textual overlap beyond the same #2514 hunk, and no double save: pilot
/chatrows carry nosave_to_sessionor collaboration id, so this sink only signals. But the two PRs use different delivery models. Here the sink owns delivery. In #3145 the waiting caller does, so a/chatturn whose caller hits thequeued_timeout504 is never saved even if the turn later succeeds. That's the #2329 class on the/chatleg. Worth choosing one model on purpose.
Not run: /verify-local and the manual Chat-tab check on a live pull pilot (still unchecked in the Test Plan).
…#2329 close-owner docstrings merge-train: expire_stale_queued writes FAILED and calls no closer, so the docstrings and the test docstring overstated coverage. Mechanical, per the merge-train note on the PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
merge-train (2026-10-06): pushed two mechanical commits to
I also changed the PR body: |
Summary
/taskturn with triggeragent,mcpormanualis claimed by the agent's worker and its terminal lands inapply_task_result. That sink wrote the row and stopped. The Chat tab's/task(save_to_session: true, triggermanual) succeeded and never reached the chat session, and the collaboration activity stayedstarted.run_post_turn_delivery, which both the push drain and the pull sink call. The queued request is rebuilt by one function,request_from_metadata.close_execution_activity) and by the bulk close, so every terminal writer closes it: the pull sink, lease-reaper park and the watchdog. Backlog expiry (expire_stale_queued) calls no closer; the 120-minute sweep covers it.Changes
services/chat_execution_service.py:run_post_turn_delivery(moved out ofrun_async_task); sync/taskkeeps thechat_session_idthe sink signals.services/backlog_service.py:request_from_metadata, used by_spawn_drainand the sink.services/pull_coordination_service.py: delivery spawned on the CAS-won branch when the metadata asks for it; waiter signalled after delivery infinally, or at once when there is nothing to deliver.db/schedules/executions.py: SUCCESS CAS under a claim token excludes SUCCESS / SKIPPED rows. A late SUCCESS still corrects FAILED.services/activity_service.py,db/activities.py: queued-row activities closed by exact id (queued_activity_ids), isolated from the dispatch close.architecture/execution.md,feature-flows/persistent-task-backlog.md, the scrub docstring, a learnings fragment, and the/cso --diffreport (0 findings).Behaviour notes
run_async_task's own collaboration close, so its details win:execution_time_mscomes from the row'sduration_ms(measured from drain/claim), and on FAILEDerroris the terminal's error text. The detail keys are unchanged./taskcaller woken by the DB poll (another worker, or a poll tick that beats delivery) gets nochat_session_idin its response. The turn is still saved. No current producer sends sync/taskwithsave_to_session.Test Plan
cd tests && pytest unit/test_2329_pull_delivery_parity.py -v: 21 pass. They run the real sink, drain and close paths against the harness DB.origin/devin isolation too) or depends on test order (test_ent477_definitions_endpoint.pypasses 25/25 alone and alongside the new file).Fixes #2329
🤖 Generated with Claude Code