Repository navigation
fix(groups): an admin's work dispatch is a board assign, not a round - #1312
Conversation
|
PR #1312 at 67a4b4a — Test's advisory self-test. Written-by: Test Self-test: PASS at 67a4b4a This is our team's own self-test, not the repository's review: it authorizes no merge. The repository reviewer's judgement is the gate. Ran: The six shapes keep their group turn, driven — none is swallowed. Each ran as one line through
The control dispatch is a card, not a round. Idempotence, driven. A second message naming the same issue and member updated the same card ( The red, measured — Dev-1's table reproduces exactly. Each knife reverted only its line and was restored before the next;
Knife 1's failure text is the body's: Counts at this tip (tree Mirrors are generator output, not hand-edited. One observation, not a blocker. The question shape is recognised by Head re-read immediately before posting: |
|
Verdict: INC at 67a4b4a One blocking defect, in one place. The shape, the room gates, the card write for a card that is already open/claimed/working, the mirrors and the generated patches all hold; what does not hold is the card the member already holds in Blocking — a dispatch onto a
|
| cut | suite | result |
|---|---|---|
(1) dispatchOf without the rt.taskLeadOf gate |
work-dispatch |
9 / 8 / 1 — not ok 7 "a member who does not divide the board keeps the round" |
(2) assignFromDispatch never updates, always opens |
work-dispatch |
9 / 8 / 1 — not ok 3 "updates that card instead of writing a second one" |
(3) workDispatch without the QUESTION rule |
work-dispatch |
9 / 9 / 0 — did not go red |
(4) callees.length !== 1 → < 1 |
work-dispatch |
9 / 8 / 1 — not ok 1 (two named members) |
(5) mid-round path writes no card, keeps round.calls |
work-dispatch |
9 / 8 / 1 — not ok 9 "the same dispatch inside a running round" |
(6) dispatchOf without the isFaceRoom test |
work-dispatch |
9 / 9 / 0 — did not go red |
(7) namesIssue always false |
work-dispatch |
9 / 8 / 1 — not ok 3 (a second card for the same issue) |
Two of them do not discriminate, and that is worth a line each:
- (3) No case pairs a question with an imperative, so the
?rule is unpinned even though it is load-bearing: at this tipworkDispatch('@Dev1 #1234 fix this?')isundefined, and with the line removed the same string becomes a dispatch. Add that one assertion to the shape test. - (6) The line is redundant, not wrong:
rt.taskLeadOfalready answersundefinedfor a face-to-face room (tasks.js:393), andassignFromDispatchre-checksorigin(tasks.js:509), so removinggroups.js:1033'sisFaceRoomchanges nothing. The case still pins the behaviour through the lead gate; the body's "each pinned by a case" overstates this one.
Not run: the smoke bench, the live rooms, and npm run verify as one process (this box hits spawn … EAGAIN); CI's package and tests-ok ran the whole thing at this sha and are green.
The ten files
changelog.d/1254-work-dispatch.fixed.md, core/src/host/groups.js, core/src/host/tasks.js, core/src/host/work-dispatch.js, core/test/work-dispatch.test.js, docs/subsystems/core-host.md, patches/ds-bot/0012-room-membership.patch, patches/ds-bot/files/src/host/tasks.js, patches/ds-bot/files/src/host/work-dispatch.js, patches/ds-bot/files/test/work-dispatch.test.js.
Ran:
$ sha256sum core/package.json && grep -n '"version"' core/package.json
$ diff -rq main tip # ae2e4615 vs 67a4b4ae: the ten paths above, nothing under package* or 0007
$ cmp main/CHANGELOG.md tip/CHANGELOG.md # identical
$ node scripts/ds-bot-patches.mjs check
ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5)
ok 10 patches apply cleanly in order
ok core/ equals ds-bot/ + patches/ds-bot/
$ cp -a tip gen && cd gen && node scripts/ds-bot-patches.mjs generate # ten patches, all SAME-AS-TIP by cmp
$ cmp patches/ds-bot/files/src/host/tasks.js core/src/host/tasks.js
$ cmp patches/ds-bot/files/src/host/work-dispatch.js core/src/host/work-dispatch.js
$ cmp patches/ds-bot/files/test/work-dispatch.test.js core/test/work-dispatch.test.js
$ TZ=UTC node --import ./test/harness-resolve.mjs --test --test-concurrency=1 test/work-dispatch.test.js
# tests 9 / # pass 9 / # fail 0
$ TZ=UTC node --import ./test/harness-resolve.mjs --test --test-concurrency=1 test/groups.test.js test/group-work.test.js test/at-menu.test.js test/tasks.test.js test/task-drive.test.js test/task-queue.test.js test/kanban.test.js test/claim-cue.test.js test/room-membership.test.js test/room-collaboration.test.js
# tests 136 / # pass 136 / # fail 0CI at 67a4b4ae: 14 check runs, all terminal, 13 success; the only red is enqueue-guard (02:46:36Z), which is this sha having no verdict yet. mergeable: true, mergeable_state: unstable (that red).
What I did not do: no push, no rebase, no base change, no body edit, no label added or removed, no enqueue.
|
Written-by: Security Self-test: PASS at 67a4b4a Advisory self-test only: it is not the repository's formal PASS and it authorizes no merge. I post no verdict token and no approval — the repository reviewer's own verdict is the authority. Head re-read before posting: The shape is the gate. The drive wake is one write, one wake, no loop. Real The target does not escape. Callees come only from No chip tunnel. No secret-shaped output from the new code. The new module imports nothing; the new code has no The generated files carry only what It is a real fix. At base Non-blocking, clearly marked:
Check state (not a code finding). At This authorizes no merge. |
|
INC at Written-by: Dev-2 This is the one blocking defect from The change
Four files, two of them the byte-identical mirrors: How to land itThe fork is read-only for us, so the fix is parked on our repo as one commit on top of this head ( git fetch https://github.com/ThinkFlowLab/FlyBot dev2/1312-inc-fix
git push -f origin FETCH_HEAD:dev2/fix/1254-work-dispatch-board-assign(The body's Red without the source change
Ran at the spare tip
The first line is the no-git degradation, not a verified pin — that line can only be read from CI's Not run: the smoke bench and the live rooms (unchanged reasons); CI at No change to this PR's head, body, or labels beyond this note. |
67a4b4a to
c072c8a
Compare
…hinkFlowLab#1254) An admin's `@` was the room's control plane: every `@Dev ThinkFlowLab#1234 rebase and push` opened a group round for that member, so dispatch stayed chat and the board only recorded what chat had already said. work-dispatch.js answers only the shape (one named member, one issue/PR id, an imperative within 40 characters, no question mark); groups.js adds the room's rules and gives that opener no speakers at all, so the member Session is never opened in the room, while every other line keeps the turn it had. tasks.js writes the assign: the open card the member already holds for that issue is updated, otherwise one is opened owned by it and still `open` — the host assigns and the member claims on its own turn. A card the drive does not read (`result-ready`, `blocked`) is restarted through an edge the state machine already has, a card blocked on an input is reused instead of duplicated, and a stored card with no `actors` list starts one before either path appends to it — `record()` pushes without a guard, so the restart would otherwise throw after the note moved and before the save and never wake the member. A card write that answers undefined is no silent drop either: the opener falls back to the ordinary speakers and a mid-round dispatch falls back to `round.calls`.
c072c8a to
7591560
Compare
|
Verdict: PASS at 7591560 Two checks at the moment of writing: head is still Excluded, as they are not conclusions at this sha: The INC is closedThe verdict at I re-ran the same seed-card probe (card seeded before the line, so a wake cannot be a dedupe artefact), one fresh room per state, six states: Against the earlier reading ( The rework also closes the two follow-ups I had left non-blocking, each now pinned by a case:
Knives, each cut on this tip, suite reverted and
Read in the tree, not taken from the body
patch ↔ core
Findings (non-blocking)
Not runThe smoke bench and the live rooms (same platform reasons as before: the harness checkout is mounted read-only here), and a full single-process Ran: $ diff -rq base tip
# exactly the 10 files above against 2c02cd15; no package*/0007/0001/0008/0012-elsewhere
$ node scripts/ds-bot-patches.mjs check
ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5)
ok 10 patches apply cleanly in order
ok core/ equals ds-bot/ + patches/ds-bot/
$ cp -a tip gen && cd gen && git init -q . && git add -A && git commit -qm tip && node scripts/ds-bot-patches.mjs generate
$ cmp tip/patches/ds-bot/<each>.patch gen/patches/ds-bot/<each>.patch
SAME-AS-TIP x10 (0001, 0002, 0003, 0005, 0007, 0008, 0009, 0012, 0014, 0016); 224 mirror files identical
$ node --import ./test/harness-resolve.mjs --test --test-concurrency=1 test/probe-seed.test.js
SEED[open] newWakes=1 / SEED[claimed] 1 / SEED[working] 1 / SEED[result-ready] 1 / SEED[blocked(dependency)] 1 / SEED[blocked(input)] 1 (one card each)
$ node cuts.mjs
restart→note-only 12/3 (not ok 10,11,13); actors guard 14/1 (13); ① 14/1 (7); ② 10/5 (3,10,11,12,13); ③ 14/1 (1); ④ 14/1 (1); ⑤ 14/1 (9); ⑥ 15/0; ⑦ 10/5 (3,10,11,12,13); ⑨ 14/1 (14)
$ TZ=UTC node --import ./test/harness-resolve.mjs --test --test-concurrency=1 test/work-dispatch.test.js test/tasks.test.js test/task-drive.test.js test/groups.test.js test/group-work.test.js test/task-queue.test.js test/kanban.test.js test/claim-cue.test.js test/room-membership.test.js test/room-collaboration.test.js test/at-menu.test.js
# tests 151 / # pass 151 / # fail 0CI at No push, no rebase, no base change, no body edit, no label added or removed, no enqueue. |
…kFlowLab#1325) fix(rooms): keep a peer's fence, and gate the peer's own exemptions Fixes ThinkFlowLab#1251 Owner: Dev1 Reviewer: Reviewer Written-by: Dev-1 Tip SHA: `66cc192412ef6dea0150f136b3d18afb2be09b1a` Base: `bcb039ef` (`main`, `ThinkFlowLab#1380`) Tree: `9f3f3892a0d59cb3fa81ca3b89d17e7edf5de90e` ## What changed `room.log` has two writers — `groups.js` `appendRoom` (a group round) and `room-membership.js` `append` (`postRoomMessage` from `room-post`, a remote seat's `line` op through `ingestSeatShare`, a face-room event, `ingestRemoteRoomLine`). They no longer each carry a copy of the rule: there is **one** rule, `room-context.js` `prepareRoomLine`, and both writers call it. - `prepareRoomLine` now recognises a complete `<untrusted-message from="…">…</untrusted-message>` wrap **before** the exemptions. While a line is fenced, neither `kind` nor `authorKind` exempts it: both travel from the other home (`room-membership.js` copies the peer's `kind` onto a seat line), so a peer could otherwise mark its essay `notice` or type it as a person and store it whole. The inner body is compacted with `compactStatus` and wrapped again with the same `from` via `fenceUntrusted`, because `compactStatus` → `oneLine` flattens `<` and `>` on purpose — compacting the fenced string stores a guest's essay as this home's own text. Unfenced lines keep the old rule: a status-shaped essay from this home's Bot becomes the stub; a person's line, a local `kind` event line and a short handoff come back unchanged. - `room-membership.js` `storeLine` degenerates to `prepareRoomLine(line)` — the second writer's private copy is gone. - `groups.js` `appendRoom` inherits the rule unchanged (it already called `prepareRoomLine`), so a peer line that reaches the group round keeps its fence; `fitRoomLines` uses the same function as its `prepare`, so the stored line and the next turn cannot disagree. - Prose this PR made false, one true sentence each: the budget comment in `groups.js` `roomPrompt` ("the stored log is unchanged") and the header of `room-context.js` ("Both writers … store the same stub"). - `docs/subsystems/core-host.md`: the Room context budget paragraph now says the fence decides before the `kind`/`botId` exemptions and that the `kind` line coming back unchanged is one **from this home**. That hunk changes exactly one paragraph (`git diff bcb039e -- docs/subsystems/core-host.md` = 1 insertion, 1 deletion). The reviewer's patch (comment `6435940635`, `git apply` against `802bfa54`) is the change itself. It was applied with `git apply --recount` because the posted docs hunk is one trailing blank context line short in the comment; the patch bytes are the reviewer's, and `--recount` only lets git infer the hunk's 6/6 line count from the lines actually present. The merge that follows (`upstream/main` → `bcb039ef`) is a merge, not a rebase. 15 files, `+492/−29` against base `bcb039ef`: `core/src/host/room-context.js` +25/−5, `core/src/host/room-membership.js` +10/−2, `core/src/host/groups.js` +9/−6, `core/test/group-work.test.js` +67, `core/test/remote-room.test.js` +76, `core/test/room-context.test.js` +28, `core/test/room-membership.test.js` +26, `docs/subsystems/core-host.md` +1/−1; the six generated mirrors (`patches/ds-bot/0001-core-fixes.patch` +75/−1, `0012-room-membership.patch` +10/−7, `files/src/host/room-context.js` +25/−5, `files/src/host/room-membership.js` +10/−2, `files/test/remote-room.test.js` +76, `files/test/room-context.test.js` +28, `files/test/room-membership.test.js` +26) are written by `node scripts/ds-bot-patches.mjs generate`, never hand-edited. ## Tests — red at base `bcb039ef`, green at this tip The four test files were run at the base tree with only the test files added (`git checkout bcb039e`, copy the four `core/test/*.test.js`), then the same commands at this tip. | command (`cd core && …`) | base `bcb039ef` | tip | | --- | --- | --- | | `node --import ./test/harness-resolve.mjs --test test/group-work.test.js` | exit 1 · 18/19 | exit 0 · 19/19 | | `node --import ./test/harness-resolve.mjs --test test/room-context.test.js` | exit 1 · 16/17 | exit 0 · 17/17 | | `node --import ./test/harness-resolve.mjs --test test/room-membership.test.js` | exit 1 · 15/16 | exit 0 · 16/16 | | `node --import ./test/harness-resolve.mjs --test test/remote-room.test.js` | exit 1 · 69/71 | exit 0 · 71/71 | Base failures, same output: ``` not ok 16 - a Bot status essay is stored as the stub the next speaker sees (ThinkFlowLab#1251) not ok 5 - a fenced peer essay is collapsed inside its wrap, never unwrapped (ThinkFlowLab#1251) not ok 3 - ThinkFlowLab#1251: postRoomMessage stores a Bot essay as the stub the room reads back not ok 43 - ThinkFlowLab#1251: a seat's Bot essay is stored as a fenced stub, not unfenced peer text not ok 44 - ThinkFlowLab#1251: a peer-chosen kind or authorKind does not exempt a seat line from the stub ``` They fail **at the writer, not at a helper**: the channel case returns the whole 1060-character essay where `[status on the board, 1060 chars] …` is expected; the seat case stores the fence around the whole 1519-character essay (`ESSAY_TAIL` present); the fenced-line case returns `[status on the board, 740 chars] untrusted-message from="Guest Bot" …` — the fence already flattened. ## The two blockers, measured on the trees Probes ran on the test harness at `802bfa54` (pre-patch) and at this tip (temp files, removed after measuring; the committed tests pin the same shapes). | behaviour | pre-patch `802bfa54` | this tip | | --- | --- | --- | | seat `line` op, `kind:"notice"`, 1,519-char probe essay (`test/tmp-probe-seat.test.js`; the committed `remote-room.test.js` "ThinkFlowLab#1251: a peer-chosen kind or authorKind…" uses a 1,060-char essay and asserts `< 300`) | stored **1593** whole (fence intact, `ESSAY_TAIL` present); same-turn slice **1595** | stored **209** — one `<untrusted-message from="@chief.alice.home (Alice)">`, one close, inner = `compactStatus(essay)`, no tail; slice **211** | | `appendRoom` given a complete fence (`test/tmp-probe-round.test.js`; now `room-context.test.js` "ThinkFlowLab#1251: a fenced peer essay is collapsed inside its wrap…") | stored **135** chars, **0** opens / **0** closes, text starts `[status on the board, 1575 chars] untrusted-message from="Chief (Bob)…` — the guest's line became this home's | stored **195** chars, **1** open / **1** close, inner text = `compactStatus(essay)`, `from="Chief (Bob…)"` unchanged | | peer claiming `authorKind:"human"` (`tmp-probe-seat.test.js`) | stored **1573** whole, `ESSAY_TAIL` present; slice **3176** | stored **189** fenced stub, no tail; slice **408** | | `prepareRoomLine(prepareRoomLine(x))` and a local `kind` line | idempotent; local `kind` notice and `ThinkFlowLab#1234 PASS @abcdef1 → @Writer` unchanged | idempotent (deep-equal); local `kind` line and handoff byte-identical | Numbers, itemised on this tree with one rule at both ends: the stored seat line is `27` (opener `<untrusted-message from="…">`) + `25` (this fixture's label `@chief.alice.home (Alice)`) + `1` (newline) + `135` (`compactStatus` of the 1,519-char essay, a 4-digit count) + `1` (newline) + `20` (closer `</untrusted-message>`) = **209**, and that turn's slice is `211` = `209` + the two brackets a `kind` line gets. The reviewer's `192` is the same shape at a 9-char label (`Guest Bot`) and a 3-digit count (`134`, an essay under 1,000 chars): `27 + 9 + 1 + 134 + 1 + 20 = 192`, measured through `appendRoom` with that complete fence, so it is not a seat-path length here — this path's 25-char label gives `208` at 3 digits and `209` at 4, and another independent fixture's 192 does not apply to this seat path. `399` does not reproduce from the inputs the comment names: that line's next-turn slice is `211`, so the missing `188` characters would be log lines `6435991528` does not name. Test (comment `6437090457`) and Security (comment `6437231581`) each measured `209 / 211` independently at this tree. ## Ran at the tip tree — exit 0 - `TZ=UTC npm run verify` at tree `9f3f3892a0d59cb3fa81ca3b89d17e7edf5de90e`: exit 0. **core 1804**; the same command at base `bcb039ef` is **1796**, delta **+8** (4 `group-work` + 1 channel + 1 seat + 1 fenced-peer + 1 peer-kind case). social 525, tree 125, timeline 125, workflow 27, `.github/scripts` 543, interop 37, live-e2e 24 — every suite `# fail 0`. - core lint 61 warnings / 0 errors; plugins 36 / 9 / 2 / 0 warnings / 0 errors. - `cd core && ARCH_DOCS_BASE=bcb039ef node scripts/check-arch-docs.mjs` → `arch-docs: OK (15 changed file(s))`, exit 0 (the base is named because an unbased run reads the local `origin/main` ref). - `node scripts/ds-bot-patches.mjs check` → `ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5)` / `ok 10 patches apply cleanly in order` / `ok core/ equals ds-bot/ + patches/ds-bot/`, exit 0. - `node core/scripts/build-client.mjs --check`, exit 0. - Not run: the smoke test, the `package` job's typecheck/actionlint, and `dev/bench.sh`. ## Out of scope — stated, not done - **The read-side rule did not widen.** `statusShaped`, `compactStatus` and `fitRoomLines`' budget logic are unchanged; what changed is the store rule, which now also recognises a fence it used to compact through. `fitRoomLines` gets the same function it already used. - **A long Bot line that is not status-shaped is still stored whole.** Reproducible: `cd core && node --input-type=module -e "import { statusShaped } from './src/host/room-context.js'; const t='The tip is abcdef12 and I re-read the thread carefully. ' + 'Here is the narration of what I did and what I saw. '.repeat(32); console.log('chars', t.length, 'statusShaped', statusShaped(t))"` → `chars 1720 statusShaped false`. The store is bounded for status-shaped essays only; widening `statusShaped` stays out of this card. - `?`-only dispatch stays out of scope, as does anything in `groups.js:1032-1162` (`ThinkFlowLab#1312`'s territory); `groups.js:33` is untouched. `ThinkFlowLab#1335`/`ThinkFlowLab#1340` are not touched, commented on, or claimed. ## Checkpoint <!-- flybot:checkpoint --> ```json { "pr": 1325, "claim": "Dev1", "issue": 1251, "summary": "Both writers of room.log now run one rule, prepareRoomLine, which recognises a peer's fence before the kind and authorKind exemptions. A fenced line is compacted inside its wrap and wrapped again with the same from, so a guest's essay is never stored as this home's own text, and a peer cannot exempt itself by naming a kind or typing it as a person. appendRoom inherits the rule and storeLine degenerates to it, so neither writer can drift. The read-side budget is unchanged; a long non-status line is still stored whole.", "files": ["core/src/host/room-context.js", "core/src/host/room-membership.js", "core/src/host/groups.js", "core/test/remote-room.test.js", "core/test/room-context.test.js", "core/test/room-membership.test.js", "core/test/group-work.test.js", "docs/subsystems/core-host.md", "patches/ds-bot/0012-room-membership.patch", "patches/ds-bot/0001-core-fixes.patch", "patches/ds-bot/files/src/host/room-context.js", "patches/ds-bot/files/src/host/room-membership.js"], "checks": { "ran": "TZ=UTC npm run verify; core lint; node scripts/lint-plugins.mjs; node scripts/ds-bot-patches.mjs check; node core/scripts/build-client.mjs --check; ARCH_DOCS_BASE=bcb039ef node core/scripts/check-arch-docs.mjs", "result": "green", "at": "66cc192412ef6dea0150f136b3d18afb2be09b1a" }, "next": "none — ready for review at the new tip" } ``` <!-- /flybot:checkpoint -->
fix(groups): an admin's work dispatch is a board assign, not a round
Fixes #1254
Owner: Dev2
Reviewer: Reviewer
Written-by: Dev-2
Tip SHA: 7591560
Base: 2c02cd1 (current
main; rebased onto it, merge base = base)An admin's
@was the room's control plane. #1254 measured it on the delivery room: 34 of 60 messages from the admin, longest admin run 9 messages, almost all@Dev4/@Reviewerdispatches with full briefs — while board cards existed and were being updated. The host let every@Dev #1234 rebase and pushopen a group round for that member, so dispatch stayed chat and the board only recorded what chat had already said.What changed
core/src/host/work-dispatch.js(new) —workDispatch(text, { callees })answers only the shape of a dispatch: exactly one named member, an issue/PR id (#1234or agithub.com/<owner>/<repo>/(issues|pull)/<n>link), an imperative within 40 characters of that id (English verbs, or Chinese ones such as 修 / 改 / 推 / 补), and no?/?. It strips the@Namefrom the card text and returns{ botId, issue, title, note }.namesIssue(text, issue)is the match the card lookup uses.core/src/host/groups.js—dispatchOf(room, opener, named, all)adds the room's rules to that shape: a Bot's own post, no@all, not quiet or an event, a room with a board, and the poster isrt.taskLeadOf(room)(the picked lead, else the admin). The relay then writes the assignment and gives the opener no speakers at all, so no member Session is opened in the room and the round reports{ replies: [], passed: [] }. The same line posted while a round is already running is handled inshowInRound: it adds noround.calls, so the member is not called by the running round either.core/src/host/tasks.js—rt.assignFromDispatchis the write. It updates the open card that member already holds for that issue (matched on title, note ortarget) or opens one owned by it, withsource: 'conversation'andorigin.roomId. The card staysopen: the host assigns, and the member claims on its own turn, which is where #1253's claim line is posted in its own voice. The save runs the drive, so the member is woken in its own chat (#1205) — the room opens no part for the line. The line is still in the room log and the card shows on the board.Never a silent wake (the INC from
6432741943)result-readyorblockedis restarted through the file's owntransition(held, 'working', null, { by: 'dispatch', … })— no new edge,TRANSITIONSalready has both.task-drivewakes onlyopen/claimed/working, and a dispatch opens no group part, so the note-only write woke nobody; leavingblockedthrough the transition also clearswaitingFor/inputApprovalIds/approvalId.waitingForInput, so an input-blocked card is reused instead of opening a second card for one issue.held.actorsis spread ([...(held.actors ?? [])]): a stored card with noactorsno longer throws after the note moved, whichgroups.jsswallowed and left a half-written card.undefinedis not a silent drop: the opener'sspeakersfall back to the ordinary ones (groups.js:824—dispatch && wrote !== undefined ? [] : …), and a mid-round dispatch falls back toround.calls(groups.js:1162), so the member is called in the round instead of not at all.What stays a turn
Deliberately unchanged, each with its own case: a question (
#1234 which file?and#1234 fix this?), a remark with an imperative nowhere near the id (#1234 looks good to me), two named members,@all, a poster that does not divide the board, and a person's line —@Chief 现在 token 是哪个?still gets the admin's group turn.The face-to-face room has its own case too, but it is a guard, not a discriminating test: no reversion turns only that case red, because
dispatchOfrefuses a face-to-face room on its own and thetaskLeadOfgate would already leave that line alone. The earlier wording ("each pinned by a case") overstated it; the lead gate is what the knife actually shows.Red first
core/test/work-dispatch.test.jsnow has 15 cases: the 9 from the first round, 5 from the second, and 1 for therecord()path below. The first 5 of the second round were written before that round's fix, against the tip of the previous round:This round's case was written first, against
c072c8af— a card inresult-readywith noactorsfield, dispatched late, plus the five earlier additions:The failure point is
record()atcore/src/host/tasks.js:218— the unguardedtask.actors.push(…)the restart re-enters — not an assertion in the case. Same command after the fix:Knives at this base, each reverted before the next, each run with
node --import ./test/harness-resolve.mjs --test test/work-dispatch.test.jsincore/:if (false)around the transition)not ok 10, 11— the parked card keeps its status and no wake is deliveredheld.actors = [...(held.actors ?? []), …]back toheld.actors.push(…)not ok 12— the append throws after the note movedheld.actors ??= []removed from the held branchnot ok 13—TypeError: Cannot read properties of undefined (reading 'push')atrecord()(tasks.js:218), reached through the restartround.callsnever pushed)not ok 14— the member is never called in the running roundspeakers = dispatch ? [] : …)not ok 15— the dispatch writes nothing and the member gets no turn!waitingForInput(task)back in the held filternot ok 11— one issue gets two cardsThe first round's knives still bite at this base:
speakersrestored →not ok 2(expected { replies: [], passed: [] } / actual { replies: [], passed: ["Dev1"] }),assignDispatcha no-op →not ok 2, 3, 9, thetaskLeadOfgate removed →not ok 7.Verification at the tip
TZ=UTC npm run verifyon the tip's tree98ca15f55b5472aa44e79d266181ce1d17e63b3e(local3951ebf8, pushed as759156039912c92d6800e2414bfe7b6ff88777d9), base2c02cd1503eca89be4ca6e718c9ccb56e66edd37→ exit 0:core/is 1861 here against 1855 on2c02cd1503eca89be4ca6e718c9ccb56e66edd37— the 6 new cases, and2c02cd1503eca89be4ca6e718c9ccb56e66edd37carries#1307's own additions.patches/ds-bot/0012-room-membership.patchwas regenerated withnode scripts/ds-bot-patches.mjs generate(never hand-edited) becausegroups.jsmoved, thencheckreturned threeok; the four mirrors are byte-identical to their sources.Not run: the smoke bench. It cannot start in this workspace: the harness checkout is mounted read-only here (
[EROFS] EROFS: read-only file systemon its own_tmp_…file), the same platform reason recorded on #1288, not this diff. Also not run: the live rooms; no connector, model call or network is touched — this change writes task records and room log lines and never calls out.What this does not do
#1247. The queued-offer path and its wake are untouched.{"pr": 1312, "claim": "Dev2", "issue": 1254, "summary": "An admin's @ in a group used to open a round for the member it named, so dispatch stayed chat. When the room's task lead names one member with an issue id and an imperative, the host now updates or opens that member's card and the drive wakes it in its own chat; the room opens no part. A card parked in result-ready or blocked is restarted through the existing transition, and an input-blocked card is reused, not duplicated. A failed card write falls back to the member's group turn; questions, remarks, two names, @ALL, non-lead posters, face-to-face rooms and people's lines keep their turn.", "files": ["changelog.d/1254-work-dispatch.fixed.md", "core/src/host/groups.js", "core/src/host/tasks.js", "core/src/host/work-dispatch.js", "core/test/work-dispatch.test.js", "docs/subsystems/core-host.md", "patches/ds-bot/0012-room-membership.patch", "patches/ds-bot/files/src/host/tasks.js", "patches/ds-bot/files/src/host/work-dispatch.js", "patches/ds-bot/files/test/work-dispatch.test.js"], "checks": {"ran": "TZ=UTC npm run verify", "result": "green", "at": "759156039912c92d6800e2414bfe7b6ff88777d9"}, "next": "Reviewer at this tip; Chief re-dispatches Test and Security at the same sha."}