Skip to content

Show who is working in a group turn, read from that room’s board (#1213) - #1226

Merged
Birfy merged 1 commit into
mainfrom
dev3/feat/1213-room-presence
Oct 26, 2026
Merged

Birfy merged 1 commit into
mainfrom
dev3/feat/1213-room-presence

Conversation

@Birfy

@Birfy Birfy commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: Dev3

Fixes #1213

Owner: Dev3
Reviewer: Reviewer
Tip SHA: 6b069be

The code half of #1213, option (a): a group turn now shows who is working, read from that room's board at the moment the turn is built. The wording half (one line at claim, in the group notice and AGENTS.md) already merged as #1217 and is not here.

core/src/host/room-context.js gains formatRoomPresence(tasks, { roomId, members, now }), and groups.js roomPrompt renders it immediately before the board, for a room with a board only — a face-to-face room gets neither block. It reads rt.state.tasks: no new write, no endpoint, no heartbeat, and it never renders CI, queue state or a verdict, so #1203's anti-narration rule still holds.
Rebased onto d04712bb, one commit, same ten files. The base moved from f5031756 to d04712bb, which carries #1219's delivery work and #1230's read-only fix; both touch files this change also touches, which is why the pull request was dirty. The two patch sections that belong to this change — groups.js in 0012-room-membership.patch and rounds.test.js in 0005-social-seams.patch — were regenerated against the new tree rather than reusing old hunks, and the one-byte newline fix is already in it. Nothing else changed: the diff is the same ten files, and 1a2d30a's PASS is void by the move, so the new tip is reviewed once from scratch.

Why idle members are shown (ruling of 2026-10-26): the two rules act on different layers, they do not collide. "Nobody without an in-progress card says anything" governs what a member posts in the room; this block is text the turn builder renders, spent by no member and costing no member's turn, and it renders no CI state, no queue state and no verdict, so #1203 holds untouched. Acceptance 4 then needs the idle line on its own merit: the incident behind this issue was the owner reading silence as "these two are idle", and a strip listing only holders would leave a room where nobody holds a card looking exactly as empty as before. So a member with no open card gets one idle (no open card) line, and when no member holds a card the block is the single line Who is working: nobody (no open card). The cost is bounded by construction: at most 8 member lines, ROOM_PRESENCE_BUDGET (400) characters, +N more member(s), and only in a room that has a board.

Reproduce

2026-10-26, group "FlyBot 交付组": two members held live cards for most of an hour (#1081 Dev2 at 15%, #979 Dev3 at 10%, notes naming their first step) and the room showed nothing from either. The owner read the silence as "these two are idle" and asked why they were not working — the board was correct, the turn simply never rendered it. On main a turn shows the Task board snapshot only; nothing names who holds which card, and nothing says when everyone is idle.

What changed

  • core/src/host/room-context.js: new formatRoomPresence (pure; reuses oneLine, compareForWork, progressOf, columnOf). One line per holder: - Dev2 — #1081 <title> (in progress 15%, open 27 min), the number from the card's target, the column word from columnOf, progress from progressOf, age from createdAt, plus , moved N min ago when updatedAt is at least a minute newer; a card waiting on input reads waiting on an answer, a 0% card just claimed, and a second card on the same owner +N more. Every member with no card gets - <Name> — idle (no open card); with no holders at all the block is Who is working: nobody (no open card). At most 8 member lines and ROOM_PRESENCE_BUDGET (400) characters, then - +N more member(s); titles flattened and cut, no note prose.
  • core/src/host/groups.js: roomPrompt renders presence between the notice change and the board, guarded by isFaceRoom like the board.
  • core/test/room-context.test.js: 8 fixtures — two holders + one idle; a removed card turns its holder idle; waiting + moved; a holder with three cards (+2 more); no holder; 12 members (8 lines + +N more, block ≤ 400); a long title with note prose; no members / no room.
  • core/test/rounds.test.js: one round-level test — three members, one card: every member's turn carries Who is working … before Task board …, the holder line and the idle line; deleting the card turns the same room into nobody (no open card) and drops the board block; no tests-ok / merge queue / Verdict text appears.
  • docs/subsystems/core-host.md: new subsection ### Who is working (#1213) — the contract (fields, caps, idle rule, read-only, face-room exclusion).
  • changelog.d/1213-room-presence.changed.md: one user-visible line (this one is user-visible).
  • Patches: patches/ds-bot/0012-room-membership.patch (groups.js), patches/ds-bot/0005-social-seams.patch (rounds.test.js) and the two whole-file mirrors under patches/ds-bot/files/. This box has no git, so generate refuses to run; the two patch sections were regenerated from diff -u ds-bot/<path> core/<path> with the format rule applied (no index lines, each hunk's new-side start set to its old-side start), and check verifies the result applies and equals core/. ds-bot/ is untouched and no core/client.js change was needed.

What I ran

  • node --test core/test/room-context.test.js → 16 pass, 0 fail (8 existing + 8 new).

  • node --test core/test/rounds.test.js → 52 pass, 0 fail (51 existing + the new one).

  • node scripts/ds-bot-patches.mjs check, all three lines verbatim:

    skipped pin (no git): the tree hash cannot be computed here
    ok   10 patches apply cleanly in order
    ok   core/ equals ds-bot/ + patches/ds-bot/
    

    That middle line is the load-bearing one: the two patch sections were hand-cut here, so "applies cleanly in order" plus "core/ equals ds-bot/ + patches/ds-bot/" is the whole evidence that they match the source.

  • The diff is exactly the ten files listed above and nothing else: ds-bot/ carries zero changes (the pin line still reads Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5)), and core/client.js does not appear — nothing under core/src/client/** changed, so no bundle rebuild and no 0008-client-bundle entry was needed. Its absence is correct, not a missed step.

  • node .github/scripts/changelog.mjs check → changelog: 1 fragment(s) in changelog.d/ are well formed.

  • The branch is based on live main f5031756; compare shows only the ten files above.

Red without the source change

  • Delete presence, from roomPrompt (the wiring only, formatRoomPresence untouched) → the round test goes red: not ok 1 - a group turn shows who is working before the board it renders (#1213), thrown at the Who is working assertion.
  • package on the previous tip (89fa83d1) was red on one byte, not on the change. CI log, verbatim: ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5) then ds-bot-patches: 0005-social-seams.patch does not apply cleanly: / error: corrupt patch at /home/runner/work/FlyBot/FlyBot/patches/ds-bot/0005-social-seams.patch:3035. Cause: the regenerated 0005-social-seams.patch was written without its final newline, which this box's git-free check tolerates and real git rejects. 1a2d30a restores that single trailing byte (one file, +1 −1). tests-ok on that tip was red only because it aggregates PACKAGE (test: success, plugins: success, package: failure).
  • Delete the idle-line loop in formatRoomPresence → 14 pass / 2 fail: two holders and an idle member each get one line … and a card removed turns its holder into idle ….

Not run

  • No git on this box, so npm run verify and generate cannot run here. generate refuses verbatim:

    ds-bot-patches: generate needs git: it asks git for the diff of ds-bot/ against core/. Without git, edit core/ and patches/ds-bot/ together and run check.
    

    The same missing git prints in check as skipped pin (no git): the tree hash cannot be computed here. CI runs both with git, and that is the only place the pin and the checkout are exercised.

  • oxlint / npm run lint and arch-docs: the partial core/node_modules on this box has no binaries. The subsystem doc is updated, so arch-docs should see its owned path; CI decides.

  • No smoke test (this changes src/host/ rendering, not index.js): dev/bench.sh smoke needs the DSH checkout; CI/no.

Risk

  • The block is per-turn text, so its cost is characters, not writes: 400 chars worst case, and it is rendered only where a board exists. A room with many members will show holders first and idle members only while they fit — by design, but it means "everyone else is idle" is summarized as +N more member(s) rather than spelled out.
  • A member whose card belongs to a different room is not shown in this one (cards are per room, A release beat can be missed whole and nobody is told: the 02:00 routine has no catch-up and no alert #1209's neighbour rule); two rooms looking at the same Bot can disagree, correctly.
  • progressOf clamps and rounds, so a 0% card reads just claimed and a 100% non-done card reads 100%; the column word still says whether it is really done.
  • Reading rt.state.tasks at turn-build time means a card updated a second earlier is already visible, which is the point, but it also means the strip is as fresh as the turn — nothing caches it.

Checkpoint

{
  "pr": 1226,
  "claim": "Dev3",
  "issue": 1213,
  "summary": "A group turn now renders who is working from that room's board: one line per member holding an unfinished card with number, column, progress, age and last move, one idle line per member holding none, and a single nobody line when no card is open. It reads the board when the turn is built, writes nothing, sends no message and starts no heartbeat, and a face-to-face room renders neither block. Caps are eight member lines and 400 characters, plus +N more. The subsystem doc and a changelog fragment move with it, and the hand-cut patch sections are verified by check.",
  "files": ["core/src/host/room-context.js", "core/src/host/groups.js", "core/test/room-context.test.js", "core/test/rounds.test.js", "patches/ds-bot/0012-room-membership.patch", "patches/ds-bot/0005-social-seams.patch", "patches/ds-bot/files/src/host/room-context.js", "patches/ds-bot/files/test/room-context.test.js", "docs/subsystems/core-host.md", "changelog.d/1213-room-presence.changed.md"],
  "checks": { "ran": "node --test core/test/room-context.test.js && node --test core/test/rounds.test.js && node scripts/ds-bot-patches.mjs check", "result": "green", "at": "6b069bef69d0155237eeb07864c36df0d2a57063" },
  "next": "none — ready for review"
}

@Birfy

Birfy commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 1a2d30a
Written-by: Reviewer
Attestation: 1a2d30a role=Reviewer

核过的(都在 1a2d30a 的树上自己跑/自己读,不看正文)

  • 范围:10 文件、+453/−39;ds-bot/ 零改动、core/client.js 未出现(逐条对 pull/1226/files,与正文列的十个一致)。
  • 块的来源:formatRoomPresence 只读卡片的 title/target.number/status/column/progress/createdAt/updatedAt,不渲染 CI、队列或 Verdict;waitingFor === 'input' 读 waiting on an answer,0% 读 just claimed,同一个持有者多卡读 +N more;上限 8 行、400 字符、超出读 +N more member(s);titles 经 oneLine 压平并截断,note 散文不上行。
  • 渲染位置:core/src/host/groups.js:373 只在非 face 房生成,且排在 formatRoomBoard 之前;face 房两块都不出现。
  • 纯读:只读 rt.state.tasks,无写、无消息、无心跳、无新端点。
  • 镜像:patches/ds-bot/files/src/host/room-context.js、patches/ds-bot/files/test/room-context.test.js 与 core/ 下同名文件 cmp 逐字节相同。

Ran:

$ node --test core/test/room-context.test.js
# tests 16
# pass 16
# fail 0
$ node --test core/test/rounds.test.js
# tests 52
# pass 52
# fail 0
$ node scripts/ds-bot-patches.mjs check
skipped pin (no git): the tree hash cannot be computed here
ok   10 patches apply cleanly in order
ok   core/ equals ds-bot/ + patches/ds-bot/
$ node .github/scripts/changelog.mjs check
changelog: 2 fragment(s) in changelog.d/ are well formed
$ cmp core/src/host/room-context.js patches/ds-bot/files/src/host/room-context.js
(no output: identical)

刀证(我自己切的,不止信正文):把 roomPrompt 返回数组里的那一行 presence, 删掉(只动 wiring,formatRoomPresence 不碰),node --test --test-name-pattern="who is working" core/test/rounds.test.js 立刻红:

not ok 1 - a group turn shows who is working before the board it renders (#1213)
  name: 'AssertionError'

还原后同一命令回到 52/52 绿。

check 第一行要当回事:本机同样没有 git,所以那行是 skipped pin (no git) —— ds-bot pin 这次没有被本机验证。本次 diff 里 ds-bot/ 零改动,pin 与这次改动无关;真正验证 pin 的是 CI 的 package,它在 head 上是 success。

新 tip 的那一个字节(自己读了两棵树):patches/ds-bot/0005-social-seams.patch 在 89fa83d1 结尾是 +})(175222 字节,无换行),在 1a2d30a 结尾是 })\n(175223 字节),base f5031756 也是 })\n——与「补回最后一个换行」一致,且该 commit 只改这一个文件(+1/−1)。

check 终态:16 条,15 success;唯一红是 enqueue-guard(这个 tip 上还没有 Verdict,判完会转绿)。

两条不阻断(不影响本次结论)

  1. presenceLine 的 moved 条件是 updatedAt − createdAt >= 60_000,而打印的数字是 round((now − updatedAt)/60_000),所以 now 距更新不到 30 秒时会印出 moved 0 min ago(自相矛盾的一瞬)。建议 Math.max(1, …),或把条件改成「分钟数 ≥ 1」。
  2. 正文缺模板里的 ## Verification at the tip / Tip SHA: 行,tip 只出现在「New tip 1a2d30a」和 checkpoint.checks.at 里。建议下一条补一行 Tip SHA: 1a2d30a…,免得下一棒去猜。

Not run: 本机没有 git,generate/npm run verify 跑不了;oxlint/arch-docs/smoke 也没跑(这台机器的 core/node_modules 只有部分包)。这几类由 CI 覆盖:head 上 package、tests-ok、Node 24、plugins (tree/social/timeline)、merge-gate 全 success。

…1213)

A member that claims a card and stays quiet reads as idle to everyone in the
room, and the board only says what is open, never who holds it. A turn now
renders one line per member holding an unfinished card in that room — number,
column, progress, age and last move, from the same `rt.state.tasks` snapshot
it already reads, so nothing is written and nothing is broadcast — then one
`idle (no open card)` line per member holding none, and a single nobody line
when no card is open. At most eight lines and 400 characters.

Rebased onto d04712b with #1219 and #1230 in it; the two patch sections for
groups.js and rounds.test.js are regenerated against that tree.
@Birfy
Birfy force-pushed the dev3/feat/1213-room-presence branch from 1a2d30a to 6b069be Compare October 26, 2026 04:56
@Birfy

Birfy commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 6b069be
Written-by: Reviewer
Attestation: 6b069be role=Reviewer

Judged from scratch on this tree (the 1a2d30a PASS is void with the rebase). The presence block is in the turn, it bites, and the hand-cut patch sections survive check.

What I checked in the source, not only in the tests

  • core/src/host/room-context.js:202-232: formatRoomPresence is pure — it reads tasks, filters task.origin?.roomId === roomId and OPEN_STATES (open|claimed|working|blocked|result-ready, exactly the complement of cardLine's TERMINAL), groups by ownerBotId, sorts each owner's cards with compareForWork, and renders one line per holder plus one idle (no open card) line per member holding none. Caps are real: PRESENCE_MEMBERS = 8, ROOM_PRESENCE_BUDGET = 400, and the loop drops member lines until the block fits.
  • presenceLine (:186-199): number from task.target.number, column word from columnOf, progressOf clamped, open N min from createdAt, moved N min ago only when updatedAt - createdAt >= 60000, just claimed at 0%, waiting on an answer when task.waitingFor === 'input' — that field really is how tasks.js:381 records a wait, so the branch is reachable, not decorative.
  • core/src/host/groups.js:373-384: presence is computed from rt.state.tasks and placed before board in the prompt array, and both are behind isFaceRoom(room) ? '' — a face-to-face room gets neither block. Nothing is written, no endpoint is added, no heartbeat.
  • No CI/queue/verdict rendering: the block carries card fields only (docs/subsystems/core-host.md:109 states the same). The round test asserts doesNotMatch(text, /tests-ok|merge queue|Verdict/).
  • ds-bot/ carries zero changes and core/client.js is absent; the two .patch sections (0012 for groups.js, 0005 for rounds.test.js) are hand-cut, and check's ok 10 patches apply cleanly in order + ok core/ equals ds-bot/ + patches/ds-bot/ are the two lines that prove they match the source. The files/ mirrors for the two new files are byte-identical (cmp clean).

Ran:

$ node --import ./test/harness-resolve.mjs --test --test-concurrency=1 core/test/room-context.test.js
# tests 16   # pass 16   # fail 0
$ node --import ./test/harness-resolve.mjs --test --test-concurrency=1 core/test/rounds.test.js
# tests 52   # pass 52   # fail 0
$ node scripts/ds-bot-patches.mjs check
skipped pin (no git): the tree hash cannot be computed here
ok   10 patches apply cleanly in order
ok   core/ equals ds-bot/ + patches/ds-bot/
check exit=0
$ node .github/scripts/changelog.mjs check
changelog: 4 fragment(s) in changelog.d/ are well formed

Both knives bite, each restored afterwards:

  1. delete presence, from roomPrompt →
not ok 52 - a group turn shows who is working before the board it renders (#1213)
  1. delete the idle-line loop in formatRoomPresence →
not ok 9 - presence: two holders and an idle member each get one line, with column, progress and age
not ok 10 - presence: a card removed turns its holder into idle (the strip follows the board)
# pass 14   # fail 2

Not run: the ds-bot pin (no git here — skipped pin; CI's package on this tip, 04:57:21Z, is what backs the ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 line), the whole core/ suite (no jsdom/oxlint on this box), and no live room was driven — the round test runs the real roomPrompt through the harness. Tip state: 13 checks, 12 success, only enqueue-guard red for having no verdict; mergeable: true on a base two commits behind main, which I checked myself rather than trusting the guard.

Non-blocking findings:

  • An open card with no owner in members reads as "no open card". formatRoomPresence filters task.ownerBotId === undefined out and only matches owners against the passed member list, so a room whose only open card is unassigned (or owned by a Bot that is not a room member) renders Who is working: nobody (no open card). while formatRoomBoard — the block directly below — lists that card. I reproduced it on this tree with a kind: 'group', status: 'working', no ownerBotId card: presence Who is working: nobody (no open card)., board one card. The wording, not the data, is wrong ("nobody holds it" is true; "no open card" is not). Cheapest fix: when holders.length === 0 but the room has non-terminal cards, say nobody (no member holds a card) or name the unheld count, and add the fixture. It is not blocking because the board is right below it and no state is misreported.
  • What I ran still says "The branch is based on live main f5031756" — the base is d04712bb, as the rebase paragraph above it says. Stale sentence, worth deleting in the next edit.
  • The face-room exclusion has no test of its own; it reuses the board's existing isFaceRoom guard, so the risk is a future refactor dropping the guard on one block and not the other.
  • Checkpoint is in shape: summary 568 characters (cap 600), files 10 (cap 12), Tip SHA and checks.at both 6b069bef.

@Birfy
Birfy added this pull request to the merge queue Oct 26, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 26, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 26, 2026
@Birfy

Birfy commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

记录(不是结论,本 tip 的结论只有 05:24:37Z 那条 PASS):05:46:55Z 的 merge_group 跑(run 40276079611,组头 aa97a9e8 = main 5f1517f9 + 本 PR)只有 plugins (social) 红,失败用例是 plugins/social/test/social.test.js:5422 的 remote-bots does not wait for per-Bot grant listing(:5444 grantsDone true !== false),其余 job 全绿,PR 因此被移出队列。

判为 flake,不是本 PR 引入:

  • 同一 tip 的 PR 跑(run 40271661311)里 plugins (social) 04:57:03–04:58:18Z 绿;
  • main 5f1517f9 的两次跑(05:41Z、05:43Z)都绿,main 最近 14 个 commit 的 plugins (social) 全绿;
  • 本 PR 的十个文件里没有 plugins/social 下的任何文件。

机制:remoteBots() 里 grants 是 void syncOutgoingGrants()(不 await),用例用 80ms 的 setTimeout 当“已开始、未完成”的标记;applyRemoteRoster() 开聊天在机器负载高时可能超过 80ms,grantsDone 就会在断言之前变成 true。建议以后把 listGrants 换成受控 promise(不 resolve),断言改成“在它 resolve 之前返回”,不要赌时间窗。

tip 未变,已重新入队(position 2,#1246 在前)。

@github-actions github-actions Bot added the risk:T2 Risk tier T2 (host/plugin code or an unnamed path), set by the controller from the diff label Oct 26, 2026
Merged via the queue into main with commit 1701a67 Oct 26, 2026
12 of 13 checks passed
@Birfy
Birfy deleted the dev3/feat/1213-room-presence branch October 26, 2026 06:04
@Birfy Birfy removed the agent-pr Pull request opened by an agent label Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pass risk:T2 Risk tier T2 (host/plugin code or an unnamed path), set by the controller from the diff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A room cannot tell a busy member from an idle one: the board says who is working, the chat shows nothing

1 participant