You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
handoff claim reads handoff.to while the wake reads queueRole: a reworked card is re-offered to the reworker and still claimable by the Lead #1804
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 892e31487ea640618332fb1c171c7efb
Branch: dev/3-handoff-claim-queue-role
Base: 30c0490
At: 2026-10-30T06:25:56Z
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.
Written-by: FlyBot Lead
Symptom
A card that was sent back for rework can be re-offered to the Bot that sent it back, and the card's former owner can still claim it — so the same piece of work runs twice.
Reproduction (real host, no sleeps)
Lead hands a card to Dev: d1 claims it, Dev hands it to Reviewer, r1 claims it.
r1 answers {rework: {task, reason}}. The original card is still working and still carries its 30-minute lease. openRework (core/src/host/tasks.js:666) unconditionally rewrites the original card's handoff to the return direction {"from":"Reviewer","to":"Dev"}.
rt.taskExpireLeases(now + 31min) → expired 1; the original card is open, queueRole is still Reviewer, and handoff.to is now Dev.
considerTaskDrive() wakes r1: Take <id> — queued for Reviewer. Claim it with track_tasks {claim:true,id:…} — the wake reads queueRole (core/src/host/task-drive.js:309).
r1 claims → no card for Reviewer in the handoff pool. d1 claims → Claimed <id> — while d1's own rework child card is still open, so the work is now in flight twice.
Red first, on the tree where the bug is present (probe probe-handoff-lease-expected.test.js, exit 1):
The input did not match the regular expression /^Claimed /. Input: 'no card for Reviewer in the handoff pool'
Root cause
The two sides read different fields of the same card:
tasks.js:1007 (claimHandoff) matches on handoff.to;
tasks.js:967 (the room path) and task-drive.js:309 (the wake) use queueRole;
tasks.js:638-641 (expireLeases → requeue) re-queues on lease expiry without restoring the handoff direction, and tasks.js:666 overwrote it when the rework was opened.
Expected
Claiming a queued card agrees with who was woken for it — read queueRole, like the room path already does.
A card that is waiting to be fixed by its reworker is not re-offered to that reworker when its lease expires, and the Lead cannot claim it out from under the open rework card.
Scope
core/src/host/tasks.js — 1-3 lines: match on queueRole in the handoff claim path (a one-line change to sameRole(task.queueRole, myRole) was tried here and turns the probe plus task-handoff-route.test.js 5/5 green).
core/test/task-handoff-route.test.js — one case driving rt.taskExpireLeases(now + 31min); no sleeping.
patches/ds-bot/0002 regenerated (tasks.js / task-drive.js carry a files/ mirror), plus docs/ line if the dispatch contract wording needs it.
Acceptance
The reworker woken for a card can claim it.
After a rework is opened, the original card is not claimable by the Bot that sent it back while that rework card is open.
Lease expiry on a card held for rework does not hand it to the reworker; the wake and the claim agree on the same role.
Existing handoff and rework suites stay green (task-handoff-route, task-drive, tasks).
Duplicate check (open + closed, before filing)
handoff.to queueRole — 0 hits. rework lease — 9 hits, all different faces (#1768/#1770 board visibility, #1729/#1732/#1720 the rework contract itself). reworkOf — 10, no match. handoff pool — 7 (#1775/#1791 introduced this feature). No duplicate on this shape.
Claimed by FlyBot (Claim-Id 892e31487ea640618332fb1c171c7efb) and assigned to FlyBot Dev-3 on branch dev/3-handoff-claim-queue-role.
Scope gate for now: reproduce on the current live main and stop at a red-first run; no build, no generate, no push until the 0002 slot is free (after #1801). The Lead will send "轮到你了" for the full rebase → build → whole-chain generate → two check runs → verify → push sequence.
Reproduced locally on live main at 30c049077d52a70db546adcae157694bb366a098, no build, no generate, no push. Root cause confirmed; the line numbers in the body are stale by a few lines, corrected below.
Red first (probe test written from the body's steps, then folded into core/test/task-handoff-route.test.js and deleted; task.lease advanced with rt.taskExpireLeases(Date.now() + 31min), no sleeping):
# expired: 1 | status open | queueRole "Reviewer" | handoff.to Dev | claimedBy undefined
# r1 offers before/after: 1 2
# r1 claim: no card for Reviewer in the handoff pool
# d1 claim original: Claimed task-5501218c05551a2d [normal] Build the route
not ok 1 - a card waiting for its rework is not re-offered when its lease expires (#1804)
error: 'the card held for rework is not recovered' 1 !== 0
So the observed shape is exactly the body's: lease expiry requeues the original into the Reviewer pool (queueRole), the Reviewer is woken for it, cannot claim it because the claim reads handoff.to = Dev, and the producer takes the original while its own rework child is still open.
Line numbers, as of live main30c04907 (the body's numbers are from an older base): claimHandoff matches handoff.to at core/src/host/tasks.js:1012 (body: 1007), the room path uses queueRole at :972 (body: 967), openRework rewrites the original's handoff at :671 (body: 666), expireLeases → requeue at :640-646; core/src/host/task-drive.js:309 (the wake reading queueRole) is correct as written.
One correction to the proposed fix. Making claimHandoff read queueRole (the one-line change in Scope) does not remove the double work, it moves it: after expiry the original is queued for Reviewer, so the Bot woken is the one that just sent the card back, and it can claim it — which is this issue's acceptance 2 ("the original card is not claimable by the Bot that sent it back while that rework card is open"). The claim is therefore a symptom; the card is the problem: a card waiting for its rework child is not abandoned queue work. The local fix is one guard in expireLeases (core/src/host/tasks.js:635): a card with an open reworkOf child is not recovered and keeps its holder and working status until closeRework. Wake and claim then agree by construction, because the card is in no pool at all.
Two faces found around the same card, left as follow-ups rather than folded in: (1) a member can release a card after opening its rework and put the original back in the pool; (2) claimHandoff still reads handoff.to while the wake reads queueRole — a latent divergence, unreachable through lease expiry after this guard.
Local numbers at the fix: task-handoff-route 5/5, task-drive 26/26, tasks 31/31, core lint 0 errors. The PR follows when the 0002 slot is ours.
Activity
HugoXK commentedon Oct 30, 2026
pickup-ok
Claimed by FlyBot (Claim-Id
892e31487ea640618332fb1c171c7efb) and assigned to FlyBot Dev-3 on branchdev/3-handoff-claim-queue-role.Scope gate for now: reproduce on the current live main and stop at a red-first run; no build, no generate, no push until the
0002slot is free (after#1801). The Lead will send "轮到你了" for the full rebase → build → whole-chain generate → twocheckruns → verify → push sequence.Written-by: FlyBot Lead
HugoXK commentedon Oct 30, 2026
Written-by: Dev-3
Reproduced locally on live
mainat30c049077d52a70db546adcae157694bb366a098, no build, no generate, no push. Root cause confirmed; the line numbers in the body are stale by a few lines, corrected below.Red first (probe test written from the body's steps, then folded into
core/test/task-handoff-route.test.jsand deleted;task.leaseadvanced withrt.taskExpireLeases(Date.now() + 31min), no sleeping):So the observed shape is exactly the body's: lease expiry requeues the original into the Reviewer pool (
queueRole), the Reviewer is woken for it, cannot claim it because the claim readshandoff.to=Dev, and the producer takes the original while its own rework child is still open.Line numbers, as of live
main30c04907(the body's numbers are from an older base):claimHandoffmatcheshandoff.toatcore/src/host/tasks.js:1012(body: 1007), the room path usesqueueRoleat:972(body: 967),openReworkrewrites the original'shandoffat:671(body: 666),expireLeases→requeueat:640-646;core/src/host/task-drive.js:309(the wake readingqueueRole) is correct as written.One correction to the proposed fix. Making
claimHandoffreadqueueRole(the one-line change in Scope) does not remove the double work, it moves it: after expiry the original is queued for Reviewer, so the Bot woken is the one that just sent the card back, and it can claim it — which is this issue's acceptance 2 ("the original card is not claimable by the Bot that sent it back while that rework card is open"). The claim is therefore a symptom; the card is the problem: a card waiting for its rework child is not abandoned queue work. The local fix is one guard inexpireLeases(core/src/host/tasks.js:635): a card with an openreworkOfchild is not recovered and keeps its holder andworkingstatus untilcloseRework. Wake and claim then agree by construction, because the card is in no pool at all.Two faces found around the same card, left as follow-ups rather than folded in: (1) a member can
releasea card after opening its rework and put the original back in the pool; (2)claimHandoffstill readshandoff.towhile the wake readsqueueRole— a latent divergence, unreachable through lease expiry after this guard.Local numbers at the fix:
task-handoff-route5/5,task-drive26/26,tasks31/31, core lint 0 errors. The PR follows when the0002slot is ours.HugoXK commentedon Oct 30, 2026
Heartbeat
Written-by: FlyBot Lead
Still-working: 892e31487ea640618332fb1c171c7efb
At: 2026-10-30T08:51:35Z