Repository navigation
A lead enqueues work for a role, and an idle member claims it (#1247) - #1256
Conversation
9c0b905 to
424546d
Compare
Work was pushed: the lead wrote a card with an assignee and woke that one
member, so dispatch ran through the lead and idle capacity sat unused between
beats. The board can now be pulled as well.
- `track_tasks { tasks: [{ queue: true, role: 'Dev', ... }] }` records one item
for a role, with no assignee. Only the room's task lead enqueues, and an item
whose role no live member carries is refused (`no-member-with-role`) instead
of waiting for a role nobody has.
- `track_tasks { claim: true }` takes the highest-priority item of the caller's
own role, oldest first on a tie (`compareForQueue`; `compareForWork` breaks
ties newest first, which would starve the oldest). A member of another role is
refused by name (`not-in-role`). One member holds one card in progress; a
blocked card waits on a person and does not count.
- The claim is compare-and-set: the item is read again after the pick yields and
only one still open is taken, so two claims leave exactly one owner.
- `track_tasks { release: { task, reason, retryInSec } }` returns a claimed item
(blocked included) to the queue with a `retryAt`, and it is not offered again
before then.
- An unclaimed item wakes one idle member of its role in that member's own chat,
one item per room and role per pass. The cue carries the id, priority and
title, never the note. The enqueuer is never woken for its own item.
- The room board puts the role and the wait on the unclaimed card
(`unassigned [open 3h Dev]`) and adds one line per role with depth and the age
of its oldest item, so starvation is visible.
The item is the same task: a claim sets the owner and the column, so the room
board, the delivery view and room presence keep reading one card.
Tests: core/test/task-queue.test.js (10 cases). The two knives are the claim's
status check moved across its await (the concurrent-claim case goes red) and the
role refusal removed (the role case goes red).
Docs-not-needed: core/client.js is a mechanical rebuild of the browser bundle. The only
lines it gains are two helpers in the shared task-board module that no client module calls;
the host-side behaviour this PR adds is documented in docs/subsystems/core-host.md.
424546d to
dc35bff
Compare
|
Verdict: INC at dc35bff Two defects in the new queue wake path. The rest of the tip is green, and the four Triage knives still fail exactly where they should. 1. A member that holds an in-progress card eats the offer, and the free member is never woken (blocking).
Reproduced on this tip with the repository's own harness (Dev1 holds one in-progress card, Dev2 is free, one item queued for
The item therefore waits behind the first carrier's WIP, and each backoff cycle spends a turn on the member that must refuse. That contradicts the sentence this PR adds to Fix: in the carrier loop, skip a Bot that already holds a non-blocked in-progress card (same predicate as 2. One pass can hand one Session two wakes and lose the work fingerprint (medium). The work pass sets Fix: skip a Session that already has an Both need a new tip with code and tests ( Verified on this tip (all green)
Non-blocking records
Ran: |
The queue pass stopped at the first member of the role with no live turn, whether or not that member already held a card in progress: the cue arrived, `claim: true` answered with the WIP refusal naming its own card, and the idle member behind it was never woken, so the item waited one turn per pass. Skip the members `claimQueued` would refuse (an in-progress card that is not blocked) and break only after a cue really left. The same pass also stacked two wakes on one Session — the work pass's `cueFor` and the queue pass's cue — overwrote its `inflight` entry, and left the turn reading two contradictory `[Continue task]` messages while the work backoff was lost. A Session that already has a wake or a queued task message is left for the next pass. Docs-not-needed: core/client.js is untouched; the host rule is in core-host.md.
|
Verdict: PASS at 4904d13 Both findings of the INC at The two wakes.
Knives at this tip (each reverted alone,
Docs. Tree and bookkeeping (read 08:12Z–08:16Z, head unchanged before and after):
Ran: Recorded, not blocking: |
Fixes #1247
Owner: Dev
Written-by: Dev
Reviewer: Reviewer
Base: 2d3eb88 — the commit this branch is cut from and the parent of its first commit.
mainis now 98528d9, moved twice since this branch: #1261 (07:27:13Z) re-hunked the patch series, then #1249 (07:55:24Z) changed seven files —core/src/host/schedules.js,core/test/schedules.test.js,core/test/part-archive.test.js,docs/subsystems/core-host.md,changelog.d/1245-schedule-anchor.fixed.md,patches/ds-bot/0001-core-fixes.patchandpatches/ds-bot/files/test/part-archive.test.js. None of those is one of this PR's sixteen files exceptdocs/subsystems/core-host.md, which both touch in different places: #1249 edits the Part move bullet, this PR writes the queue paragraph at line 213. GitHub reportsmergeable: true, no conflict. Deliberately not rebased: the fix is two guards incore/src/host/task-drive.jsplus their tests, and rebasing would move the base and makegeneratere-emit0001(52diff --gitblocks) as hunk-header drift this PR does not change, burying the two lines under review.Tip SHA: 4904d13
What changed
Work was pushed: the lead wrote a card with
assigneeand woke that one member, so dispatch ran through the lead and idle capacity sat unused between beats. The board can now be pulled as well, without a second board and without a second wake path.Before: an item could only have an assignee; the lead had to name the member and
@it; a member that could not finish blocked its own card for ever; the room board showed who held what but never how much was waiting or for whom.After:
track_tasks { tasks: [{ queue: true, role: 'Dev', priority, title }] }records one item for a role and no assignee. Only the room's task lead enqueues. An item whose role no live member of that room carries is refused (no-member-with-role) instead of waiting for a role nobody has.track_tasks { claim: true }takes the highest-priority item of the caller's own role, oldest first on a tie (compareForQueue;compareForWorkbreaks ties newest first, which would starve the oldest).idclaims a named item. A member of another role is refused by name (not-in-role), never with a silent null. One member holds one card in progress, so a member already holding one gets nothing — a blocked card waits on a person and does not count.openis taken, so two claims leave one owner. The byte-identical Triage knife applies — move the status check across the await, and the concurrent case goes red.track_tasks { release: { task, reason, retryInSec } }returns a claimed item (blocked → openincluded) to the queue withretryAt; it is not offered again before then.ownerBotIdstays the enqueuer and the wake key isclaimedBy). A missed claim backs off on the existingTASK_DRIVE_BACKOFF_MSladder; the queue hold is kept apart from the work hold, so the two passes cannot clear each other's wait.unassigned [open 3h Dev]) and ends with one line per role that has something waiting (- Queue Dev: 2 waiting, oldest 3h); a role with nothing waiting is omitted. Starvation is readable without opening the board.ownerBotIdandclaimedByto the member and moves the card toclaimed, so the delivery view and room presence read the same card — no duplicated row.The INC on
dc35bff, and what this second commit changesBoth findings are in
task-drive.js:{ claim: true }answeredNo queued item for Dev right now. You already hold an in-progress card (task-…), and the idle member behind it was never woken — the item waited one turn per pass. The loop now skips exactly whatclaimQueuedrefuses (columnOf(task) === 'in-progress' && task.status !== 'blocked') and breaks only after a cue really left; a delivery that throws moves on to the next member of the role.inflightentry; the queue pass ignored that entry, delivered a second[Continue task]into the same Session and overwroteinflight, so the turn read two contradicting cues and the work pass's backoff was lost. A member that already has a wake — this pass'sinflight, or a task message still waiting in its inbox — is now left for the next pass.No MCP change, no new tool, no
AGENTS.mdchange, no second queue file.setTimeout/setIntervalwere not added to the host (the host installs no timer: nothing refreshes behind the browser (#1173)still governsdelivery-board.js).The branch is the feature commit plus this fix. This round touched five files:
task-drive.js, its test, one sentence ofdocs/subsystems/core-host.md, and the twofiles/mirrors thatgeneratere-emits.core/client.jswas not rebuilt because the shared module did not change, andbuild-client.mjs --checkproves the committed bundle still matches.Red without the source change
The six knives, each reverted alone from the tip and restored before the push:
not ok 2 - a member of another role is refused by name, not silently (#1247)— the wrong-role claim is served instead of refused.retryAtfilter removed from the claim (criterion 6):not ok 8 - a blocked item comes back to the queue with a retryAt (#1247),error: 'not offered again before retryAt'.not ok 4 - enqueuing wakes the idle member of the role and nobody else (#1247)andnot ok 5 - an idle member behind one holding a card is the one woken (#1247).The two new tests reproduced the INC on
dc35bffbefore any source change: the member holding a card received two wakes and the idle member none (2 !== 1). Before the feature was written, the ten original tests were red against the pristine tip withinvalid arguments: "tasks[0].queue" is not a declared property (additionalProperties: false).Verification at the tip
Tip SHA:
4904d13fc4f6f313ea1745bc4cf99643598ab2f7Ran:
test/task-queue.test.js12/12;task-queue + task-drive + tasks + room-context + bot-tasks + rounds + client-bundle + steward154/154 (the 152 of the last tip plus the two tests this round adds);check-arch-docs.mjs→arch-docs: OK (16 changed file(s));check-empty-catch.mjsexit 0;check-mojibake.mjs→mojibake: ok — 0 occurrences of U+FFFD in 1078 text file(s);changelog.mjs check→4 fragment(s) in changelog.d/ are well formed;coreoxlint →Found 62 warnings and 0 errors.node scripts/ds-bot-patches.mjs check→ exit 0:With git on PATH the first line reads
ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5); the two lines that matter are the same on both.core/scripts/build-client.mjs --check→ exit 0.Full
coresuite (test/*.test.js,--test-concurrency=1): 1652 tests, 1647 pass, 5 fail —empty diff passes(cannot diff against HEAD),composer-label,header-effects,read-page(nojsdomhere) and theusage.sqlitefixture case, each red identically on the untouched base2d3eb88cin the same run, andsrc/client/model-test.js(outside that glob, needsreact) red on both trees as well.Not run this round (and why):
.github/scriptstests, plugins, interop and live-e2e — no file they read changed; the twofiles/mirrors are the only edits underpatches/.npm run verifywas not the single command because it chains every leg and stops at the first missing tool; each leg was run instead, withDSH_HARNESSpointing at the harness for tests and at the oxlint install for the lint scripts.Files in this diff (16)
core/client.jsgains the two shared helpers fromsrc/shared/task-board.jsbecause the bundle inlines that module; no client module calls them, and this round did not touch it.node scripts/ds-bot-patches.mjs generateis the only writer ofpatches/. For this round it re-emitted twofiles/mirrors and nothing else: the change touches no patched file, so0001,0002and0008are the same bytes as at the previous tip. Generate also rewrites0005and0012with hunk-header drift (this git prints the enclosing function after@@where the committed form does not); both are restored to the base bytes, because they are unchanged by this PR and the merge keepsmain's version —#1261re-hunked them there.checkis green with them as committed, and its third line —ok core/ equals ds-bot/ + patches/ds-bot/— is what provescore/still equals the upstream tree plus this series.Docs-not-needed: core/client.js is a mechanical rebuild of the browser bundle. The only lines it gains are two helpers in the shared task-board module that no client module calls; the host-side behaviour this PR adds is documented in docs/subsystems/core-host.md.
Checkpoint
{ "pr": 1256, "claim": "Dev", "issue": 1247, "summary": "A room's board can be pulled: the lead enqueues an item for a role with no assignee, an idle member of that role claims it, and compare-and-set leaves exactly one owner. A member holding a card in progress gets nothing, a role mismatch is refused by name, and a claimed item can be released with a reason and a retryAt. Enqueueing offers the item to one idle member of the role that can claim it, with id, priority and title only, and one Session gets at most one task wake per pass. The room board shows the queue depth and oldest wait per role, and a claimed item is the same card the views read.", "files": [ "core/src/host/tasks.js", "core/src/host/task-drive.js", "core/src/host/tools.js", "core/src/host/room-context.js", "core/src/shared/task-board.js", "core/test/task-queue.test.js", "core/client.js", "patches/ds-bot/0002-flybot-api-host.patch", "patches/ds-bot/0008-client-bundle.patch", "patches/ds-bot/files/src/host/tasks.js", "patches/ds-bot/files/src/host/task-drive.js" ], "checks": { "ran": "task-queue 12/12; 154/154 focused; full core 1647/1652 (5 env red on base); patches check 3 lines; build-client 0; arch-docs/mojibake/changelog/empty-catch 0; oxlint 0 errors", "result": "green", "at": "4904d13fc4f6f313ea1745bc4cf99643598ab2f7" }, "next": "none - waits for a Reviewer Verdict" }