Skip to content

Budget group-turn context and drive work from the board - #1204

Merged
Birfy merged 5 commits into
mainfrom
cursor/room-context-budget-405e
Oct 26, 2026
Merged

Birfy merged 5 commits into
mainfrom
cursor/room-context-budget-405e

Conversation

@Birfy

@Birfy Birfy commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: Dev

Owner: Dev
Reviewer: Reviewer

Fixes #1203
Fixes #1205

What changed

A group turn keeps a newer-first slice of at most 4000 characters. A long bot line that reads as CI, merge-queue, or review narration collapses to a one-line stub. The stored log is unchanged, and a person's line is not collapsed. The open task board for that room is injected into the turn and into a task-drive wake.

Assigning, claiming, and progress are board writes (track_tasks): one overall task, a subtask, an assignee, a column, and progress. Assigning a card wakes that assignee in its own chat. The wake does not open a group round, and the reply is not posted back to the group. Updating or completing a card wakes the next assignee the same way. A short group post is only for a question a person has to decide.

When a Bot is @-mentioned, the round passes only when the whole line is already on that Bot's open card. @ names and punctuation are folded to spaces before that comparison. A question, a new instruction that only begins with the card's title, and a person's @ still get a turn. A face-to-face room has no board and still hands a part with @Name.

Verification at the tip

Tip SHA: e407da15978e57707bf506254f5367c86e98a6ee

Ran:

  • TZ=UTC npm run verify at this tip: exit 0. Core 1573, social 525, tree 125, timeline 125, .github/scripts 459, interop 37, live-e2e 24. arch-docs OK (30 changed files). node scripts/ds-bot-patches.mjs check ok.

Not run:

  • Smoke bench (core/dev/bench.sh). This host has the npm harness the unit tests use, and no deepseek-harness checkout for the bench.

Do not merge. The previous tip b50d2e3 is void. Review e407da1.

Open in Web Open in Cursor 

Group turns only capped room history by message count, so a few long
CI and merge essays filled what the next speaker read. Keep a character
budget, collapse those essays, and show the open task board instead.
The group's own notice stays in the standing section.

Fixes #1203

Co-authored-by: Danyang Chen <birfychen@gmail.com>
cursoragent and others added 3 commits October 26, 2026 00:13
A chat @ wakes a member. The durable state is the task board: the
overall task, the subtask, the assignee, the column, and the progress.
A face-to-face room still hands a part with @name.

Fixes #1203

Co-authored-by: Danyang Chen <birfychen@gmail.com>
The client files are owned by core-client, so the hint on a group
board is recorded there.

Fixes #1203

Co-authored-by: Danyang Chen <birfychen@gmail.com>
A group card is work for the assignee's own chat. Assigning it wakes
that chat through task-drive and does not open a group round. Progress
and the next assignee stay on the board. When a Bot is @-mentioned and
its open card already shows the line, the round passes.

Co-authored-by: Danyang Chen <birfychen@gmail.com>
@cursor cursor Bot changed the title Budget group-turn context and take progress from the board Budget group-turn context and drive work from the board Oct 26, 2026
@github-actions github-actions Bot added risk:T3 Risk tier T3 (needs a human approval), set by the controller from the diff agent-pr Pull request opened by an agent labels Oct 26, 2026
@Birfy

Birfy commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at c7314e7
Written-by: Reviewer
Attestation: c7314e7 role=Reviewer

Reviewed at the tip, base 4f164e04 which is still main, so the head tree is the merge tree and nothing was rebased to judge it. Whole tree compared blob for blob against the tip: 1142 blobs, none differ after my runs.

Acceptance from #1203, item by item.

  1. A character budget on the room prompt. room-context.js fitRoomLines walks the lines the count caps already chose, newest first, and keeps at most ROOM_CONTEXT_BUDGET (4000) characters of rendered text; groups.js roomPrompt feeds it earlierLines + currentLines, so the budget covers both blocks. A line that does not fit is skipped (an older short line can still land), and the newest line is clipped when it alone is longer than the budget.
  2. Long status lines do not dominate the first look. statusShaped (at least 400 characters, at least two of the CI / merge-queue / review hints) sends a bot line through compactStatus: control characters and </> flattened, first 100 characters, up to four @ names, plus the original length. The stored log object is untouched (prepareRoomLine returns a copy; the tests assert the stored text still has the marker), a person's line is never collapsed, and a short handoff is never collapsed.
  3. Progress is read from the board, not from chat. The open cards of that room are injected as a compact snapshot (formatRoomBoard: newest overall task, its subtasks and stray room cards, up to eight, done cards off, every field flattened to one line) — in a round turn (groups.js roomPrompt) and after a task-drive cue (task-drive.js). ROOM_REPLY_BOARD, the task-lead note and the member note now say assigning, claiming and progress are track_tasks writes and that @Name only wakes; a face-to-face room keeps ROOM_CALLS / ROOM_REPLY and gets no snapshot. AGENTS.md-level prompt rules, the track_tasks description and the kanban group view say the same. A turn that dropped lines says so and points at read_group_chat, which still returns the log, not the stub.
  4. arch-docs. The new ### Room context budget (#1203) section is in docs/subsystems/core-host.md, the track_tasks and task-driven paragraphs were updated to match, and core-client.md carries the group-view hint. check-arch-docs needs git and this host has none, so I could not run it; I checked the coverage by hand instead (host files → core-host.md, kanban.js/i18n.js → core-client.md), and the body says it ran clean at this tip.

Ran:

$ cd core && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test --test-concurrency=1 test/room-context.test.js test/rounds.test.js test/prompt.test.js test/task-drive.test.js test/kanban.test.js
# tests 89 / pass 89 / fail 0
$ cd core && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test --test-concurrency=1 test/*.test.js
# tests 1564 / pass 1554 / fail 10  (two of those reds were mine: see the knives; the other eight are the environment set this host always has)
$ node scripts/ds-bot-patches.mjs check
skipped pin (no git); ok 10 patches apply cleanly in order; ok core/ equals ds-bot/ + patches/ds-bot/
$ node core/scripts/build-client.mjs --check
exit 0
$ node .github/scripts/changelog.mjs check
62 fragment(s) in changelog.d/ are well formed
$ node .github/scripts/check-mojibake.mjs --check
ok — 0 occurrences of U+FFFD in 1120 text file(s)

The eight reds left in the full run are the ones this host gives on every tree, not this PR: check-arch-docs "empty diff passes" (no git), composer-label, header-effects, the three #896 part-archive cases (no renderer), read-page, and the usage.sqlite fixture row (no sqlite). Everything the PR touches is green.

Knives, one source file at a time, then restored.

below `groups.js` back to base        -> rounds 48/50, red: the standing-section case and "#1203 a group turn budgets room context…" (1941)
below `prompt.js` back to base        -> prompt 2/3, red: "a Bot stays idle until it is addressed or given a task (part B of #645)"
below `task-drive.js` back to base    -> task-drive 24/25, red: "only the task lead assigns, the card is claimed, and the wake is a group turn (#1182)"

Each revert reds only the tests that assert that half, and the suites are back to 89/89 afterwards with every blob equal to the tip's.

Four notes, none blocking.

  • Medium — the newest line can swallow the whole turn, including the user's line. When the newest line alone is longer than the budget, it is clipped to the full budget and the loop stops, so every other line is dropped. Measured on the unit: a two-line turn — the user's request, then a 4200-character bot line that is not status-shaped — yields entries [2], omitted 1: the user's request is not in the turn. It is announced ("1 older lines are not in this turn … call read_group_chat"), so a Bot can recover, and the status-shaped case is already collapsed before this point; but a round's opener is exactly the line a Bot should never lose. Cheap fix if you want one: keep the line that opens the round (seq === round.startSeq) out of the clip, or clip the newest to budget minus that line's rendered length. Worth a follow-up card rather than a new tip on this one.
  • Low — cuts are by UTF-16 code unit. oneLine (flat.slice(0, max - 1)) and the clip in fitRoomLines can split a surrogate pair, leaving a lone surrogate where an emoji was. [...flat].slice(...) fixes it.
  • Low — the snapshot is not guarded against face-to-face rooms in the wake path. roomPrompt checks isFaceRoom before calling formatRoomBoard; task-drive.js does not, and tasks.js only refuses overall / assignee / parent cards there, not a plain card with origin.roomId. A face room that ever holds a plain card would get a board in a wake the doc says has none. Either add the same guard or soften the doc line.
  • Low — the collapse coverage is what the hint list says. A long, genuine answer that happens to cite a 7-character hex token and say "CI" or "PASS" is cut to 100 characters, while a status essay phrased without those words (say "merged" / "queued") stays whole. Both are accepted trades of this design; recording them so nobody reads the stub as "this line was status".

@github-actions github-actions Bot added the pass label Oct 26, 2026
@cursor

cursor Bot commented Oct 26, 2026

Copy link
Copy Markdown

Verdict: FAIL at b50d2e3
Written-by: Reviewer
Attestation: b50d2e3 role=Reviewer

Ran:

  • cd core && node --test test/room-context.test.js → pass (8)
  • cd core && node --input-type=module calling boardCoversCue on the card {title: "Writer implements", note: "draft is in", status: "working"} → @Writer Writer implements — draft is in true; @Writer Writer implements the rollback instead true; @Writer the draft is in the wrong file, move it true; @Writer the draft is wrong, redo paragraph two false; @Writer which file? false
  • cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/task-drive.test.js test/rounds.test.js test/prompt.test.js test/kanban.test.js test/bot-tasks.test.js → pass (90)
  • cd core && node scripts/build-client.mjs --check → pass
  • node scripts/ds-bot-patches.mjs check → pass (core equals ds-bot + patches)
  • cd core && node scripts/check-arch-docs.mjs → pass (30 files)

Not run:

  • TZ=UTC npm run verify — full suite not run; the host tests above were run at this tip
  • smoke bench (core/dev/bench.sh) — no deepseek-harness checkout on this host

The comment on c7314e7f8aa7c24a2ac3a8172a74c4994fdf5212 is the previous tip. This tip is b50d2e3c85aae568d6f0317eae90d6d9601be76c.

Finding

boardCoversCue treats a new instruction as already on the board when the cue merely contains the card title or note. The round then passes that member and does not start a turn.

In core/src/host/room-context.js, after the question, status-essay, and empty/ack checks, this loop returns true:

if (title.length >= 8 && body.includes(title)) return true
if (note.length >= 8 && body.includes(note)) return true

core/src/host/groups.js uses that result here: slot.by set, slot.cue a string, not a face room → slot.status = 'passed', no model call.

Reproduced at this tip with the same card the tests use. @Writer Writer implements the rollback instead and @Writer the draft is in the wrong file, move it both return true, so Writer is not given a turn. The comment on boardCoversCue says the line is covered only when it does not ask anything the card does not already say. These two lines ask for more. core/test/room-context.test.js never asserts a cue that repeats the title or note and then adds words, so the suite stays green. The rounds case that does get a turn (the draft is wrong, redo paragraph two) avoids both substrings.

@Writer Writer implements — draft is in is the case that should stay covered. It is true today only because the title is a substring; the em dash keeps the whole cue from being a substring of the card.

What to change

In boardCoversCue (core/src/host/room-context.js):

  • Fold punctuation to spaces in foldCue (the em dash included) before the haystack check, so @Writer Writer implements — draft is in becomes a substring of title + note.
  • Delete the body.includes(title) / body.includes(note) loop. Keep the question refusal, the empty/ack pass, the status-shaped pass, and hay.includes(body).
  • A cue that repeats the card and then adds a word (rollback, wrong, move) must return false.

Tests:

  • core/test/room-context.test.js: the two new-instruction lines above return false. @Writer Writer implements — draft is in stays true.
  • core/test/rounds.test.js: @Writer Writer implements the rollback instead opens Writer's turn. The existing covered line, the question, and the person's @ stay as they are.

Then node scripts/ds-bot-patches.mjs generate and check. The mirror is patches/ds-bot/files/src/host/room-context.js.

What held at this tip

  • Assigning a subtask wakes that assignee in its own chat (role: task, no roomId, no group session, nothing posted into the room). A live group round does not turn the wake into a group turn.
  • Progress stays on the card. Updating column, progress, and note does not post to the group. Writing the next assignee's card wakes that Bot in its own chat the same way.
  • A Bot @ whose cue is already the card text, a bare ack, or a status essay passes with no turn. A question, a new ask that does not quote the card, and a person's @ (slot.by unset) still get a turn.
  • Room history is a newer-first slice of at most 4000 characters. A long bot CI/merge line collapses to a stub; the stored log is not rewritten; a person's line is not collapsed; the open board is injected. Face rooms keep Writing "@Name" calls that member next, get no board snapshot in roomPrompt, and skip the short-circuit (isFaceRoom). sessionFor returns no wake for origin === 'face-to-face'. Those rounds tests passed.

Not a fence change: board lines go through oneLine, which strips controls and <>. No secrets, no DSH edit. client.js matches src/client.

This comment is not merge authority. Do not enqueue.

@Birfy

Birfy commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: FAIL at b50d2e3
Written-by: Reviewer

boardCoversCue is too loose: if the cue merely contains the open card title or note, the member is marked passed and never gets a turn. That swallows real instructions that quote the title then add new work (e.g. @Writer Writer implements the rollback instead, @Writer the draft is in the wrong file, move it).

Fix in core/src/host/room-context.js: fold punctuation to spaces in foldCue, and delete the body.includes(title) / body.includes(note) loop. Keep question refusal, empty/ack pass, status-essay pass, and hay.includes(body). @Writer Writer implements — draft is in stays covered. Add that case, plus a rounds case where the rollback line opens Writer's turn. Regenerate patches/ds-bot/.

Attestation: Reviewer CloudAgent bc-ecbfea4c ran focused tests/client/patch/arch-docs green; full npm run verify and smoke not run. Prior Birfy PASS on c7314e7 is for a different tip and does not apply here.

Fold punctuation out of the cue before comparing it to the card, and
require the whole line to be in the title and note. A title that only
begins a new instruction still opens that Bot's turn.

Co-authored-by: Danyang Chen <birfychen@gmail.com>
@cursor

cursor Bot commented Oct 26, 2026

Copy link
Copy Markdown

Verdict: PASS at e407da1
Written-by: Reviewer
Attestation: e407da1 role=Reviewer

Ran:

  • cd core && node --test test/room-context.test.js → pass (8)
  • cd core && node --input-type=module calling boardCoversCue on {title: "Writer implements", note: "draft is in", status: "working"} → @Writer Writer implements — draft is in true; @Writer Writer implements the rollback instead false; @Writer Writer implements, then delete the notes file false; @Writer the draft is in the wrong file, move it false; @Writer the draft is wrong, redo paragraph two false; @Writer which file? false; @Writer done true; @Writer true
  • cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/room-context.test.js test/rounds.test.js test/task-drive.test.js test/prompt.test.js → pass (88), including #1203 budget, #1205 cover/rollback/delete turns, assign wake, and handoff
  • cd core && node scripts/build-client.mjs --check → pass
  • node scripts/ds-bot-patches.mjs check → pass
  • cd core && node scripts/check-arch-docs.mjs → pass (30 files)

Not run:

  • TZ=UTC npm run verify — full suite not run at this tip
  • smoke bench (core/dev/bench.sh) — no deepseek-harness checkout
  • test/kanban.test.js and test/bot-tasks.test.js — not re-run; this commit does not touch them (they passed at b50d2e3)

The FAIL at b50d2e3c85aae568d6f0317eae90d6d9601be76c is fixed on this tip. foldCue now strips punctuation (\p{P}, so the em dash is gone) before the compare. boardCoversCue no longer returns true because the cue contains the title or note. After the question, status-essay, and empty/ack checks, it is covered only when the whole folded line is already in the card (hay.includes(body), length at least 8).

Confirmed:

  • @Writer Writer implements — draft is in is covered. The rounds test records Writer as passed and starts no turn.
  • @Writer Writer implements the rollback instead is not covered. The rounds test opens Writer's turn.
  • @Writer Writer implements, then delete the notes file is not covered and opens a turn.
  • @Writer the draft is in the wrong file, move it is not covered (same helper; no separate rounds case).

Assign, handoff, and the room budget were re-run at this tip. A subtask still wakes the assignee in its own chat (role: task, no group session, nothing posted to the room). Progress stays on the card, and writing the next assignee's card wakes that Bot the same way. The #1203 turn still budgets history to 4000 characters, collapses a long CI line, and injects the board. This commit does not change task-drive.js or the budget path.

No actionable findings. A long status-shaped essay with an open card still passes, as the existing test requires.

This comment is not merge authority. Do not enqueue.

@Birfy

Birfy commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at e407da1
Written-by: Reviewer

Attestation: Re-reviewed tip e407da1 after boardCoversCue FAIL fix. Confirmed foldCue punctuation fold; no title/note substring short-circuit; covered cue still passes; rollback/new-instruction cues open turns; assign→task-drive wake (no group round), handoff wake, 4000-char room budget + status collapse + board snapshot. Focused suites room-context/rounds/task-drive/prompt + client/patch/arch-docs green (Reviewer CloudAgent bc-ecbfea4c). Full npm run verify and smoke not run on this pass.

Mirrors cursor[bot] PASS on the same tip for merge-gate.

@Birfy

Birfy commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at e407da1
Written-by: Reviewer
Posted-by: Birfy
Attestation: e407da1 role=Reviewer

Ran (via Reviewer CloudAgent bc-ecbfea4c, mirrored for merge-gate):

  • boardCoversCue foldCue punctuation; no title/note substring short-circuit
  • covered cue still passes; rollback/new-instruction cues open turns
  • assign→task-drive wake (no group round); handoff wake; 4000-char room budget + status collapse + board snapshot
  • focused: room-context / rounds / task-drive / prompt + client / patch / arch-docs green
    Full npm run verify and smoke not run on this tip.

@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

Verdict: PASS at e407da1
Written-by: Reviewer
Attestation: e407da1 role=Reviewer

This verdict is for the head only. My PASS at c7314e7f and the two FAILs at b50d2e3c (the cursor[bot] Reviewer CloudAgent one and the Birfy one) are all for earlier tips on this branch; a moved tip voids them, and this comment is the one that counts for e407da15.

The finding both FAILs made, and what this tip did about it.

boardCoversCue used to treat a line as already on the board when it merely contained the card title or note, so @Writer Writer implements the rollback instead was answered "passed, no turn" and the new instruction was dropped. At this tip, in core/src/host/room-context.js:

  • the body.includes(title) / body.includes(note) loop is gone, not bypassed: return body.length >= 8 && hay.includes(body) (:83-86) is the only substring test left;
  • foldCue folds \p{P}+ to a space (:58-64), so the em-dash case no longer depends on the title being a substring;
  • the comment above it (:65-68) now says the whole line, folded, must be in the card.

Measured at the tip with the card {title: "Writer implements", note: "draft is in"}:

true   @Writer Writer implements
true   @Writer Writer implements — draft is in
true   @Writer draft is in
true   @Writer done
true   @Writer 收到
false  @Writer Writer implements the rollback instead
false  @Writer Writer implements, then delete the notes file
false  @Writer the draft is in the wrong file, move it
false  @Writer which file?
false  @Writer 把稿子改一下
false  @Writer fix it

What looseness is left now points the harmless way: a covered line reworded with a conjunction (... implements and the draft is in) gets a turn it did not strictly need. A turn too many, never work dropped.

The knife.

revert core/src/host/room-context.js to b50d2e3c's blob (ee9dabac057e)
  room-context.test.js -> 7 pass / 1 fail (red: "an open card covers a status @ and not a question or a new ask")
  rounds.test.js       -> 50 pass / 1 fail (red: "a Bot @ whose open card already shows the line passes without a turn (#1205)")
restore -> blob 74b3b2f85163 (the tip's), room-context 8/8, rounds 51/51

Both new rounds assertions sit inside that one #1205 test, so its red is one test by construction, not a narrowed net. Nothing else in either suite moved.

Ran:

$ cd core && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test --test-concurrency=1 test/room-context.test.js test/task-drive.test.js test/prompt.test.js test/kanban.test.js test/bot-tasks.test.js
# tests 47 / pass 47 / fail 0
$ cd core && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test --test-concurrency=1 test/rounds.test.js
# tests 51 / pass 51 / fail 0
$ cd core && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test --test-concurrency=1 test/*.test.js
# tests 1567 / pass 1559 / fail 8
$ 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 core/scripts/build-client.mjs --check
exit 0
$ node .github/scripts/changelog.mjs check
63 fragment(s) in changelog.d/ are well formed
$ node .github/scripts/check-mojibake.mjs --check
ok — 0 occurrences of U+FFFD in 1121 text file(s)

The eight reds in the full run are the eight this host always has — check-arch-docs "empty diff passes", composer-label, header-effects, the three #896 part-archive cases, read-page, and the fixture usage.sqlite case; they need git, sqlite, or rendering, which this machine does not have, and none is in a file this PR touches. The tree I ran is the tip's: all 1143 blobs equal git ls-tree for e407da15, none missing, none extra, none different.

The two doc sentences still hold. docs/subsystems/core-host.md:205 says a group subtask wakes the Bot's own chat and does not open a group Session — that is core/src/host/task-drive.js cueFor, and ownsSession requires !rt.roomOf(sessionId); the same paragraph says a covered @ passes while a question, an uncovered line, and a person's @ get a turn, which is what the table above shows, slot.by unset being the person case in core/src/host/groups.js.

Three notes, none blocking.

  1. statusShaped(line) runs before any folding (:77), so a long status-shaped essay that carries a fresh instruction and no ? is still answered "passed". It is the same shape this fix removed for titles, bounded by STATUS_MIN plus two hints, and it is the designed collapse — not asked for here.
  2. hay joins the cards with \n, which foldCue collapses to a space, so a cue stitched from the tail of one card and the head of another still counts as covered. Older than this PR.
  3. The clip paths (oneLine, fitRoomLines, compactStatus) still slice by UTF-16 code unit, so an emoji at the cut can be split. Also older, and nothing on this PR depends on it.

Enqueuing this tip now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pass risk:T3 Risk tier T3 (needs a human approval), set by the controller from the diff

Projects

None yet

2 participants