Skip to content

bug(host): a trimmed room log makes read_group_chat report the wrong total - #2839

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/1-room-read-trimmed-count
Nov 11, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/1-room-read-trimmed-count

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2838

Owner: Dev-1
Reviewer: Birfy
Written-by: Dev-1
Bot: Dev-1
Claim-Id: 63eb2d9dca664fab94de573fdd70b3e0
Risk tier: T2
Base: e16c1d3

What happened

read_group_chat (core/src/host/groups.js) built its Latest in … header from kept.length, which is only what the room log still holds. The store trims at ROOM_LOG_MAX (60 here, 200 in room-membership.js) while room.seq keeps counting every line ever appended, so a room that had said 500 lines read Latest in "Ops" (newest 30 of 60, oldest first): and the 440 lines the trim threw away disappeared from the count — the lines a reader asking "didn't we decide X?" is looking for.

The same arithmetic ignored the reader's floor: a reader joining at line 300 of that room was told newest 30 of 60 when 201 lines were said since its floor (500 − 299), so the header under-reported on both axes at once.

Expected, and what the tip does

  • dropped = max(0, room.seq − room.log.length); total = kept.length + max(0, dropped − max(0, from − 1)); older = total − lines.length. A floorless reader of a 500-line room reads newest 30 of 500; a reader at floor 300 reads newest 30 of 201.
  • ROOM_LOG_MAX stays 60 and room-membership.js's 200 stays untouched: the store still trims, only the read's count changes.
  • The header still prints only when something is left out; an at-cap room (newest 30 of 60) and an under-cap room read byte-identically, and a legacy log without seq is normalised by ensureRoomLog (seq = log.length) so it counts exactly as today.

Red first

On e16c1d3c, groups.js untouched, only core/test/room-read-trimmed-count.test.js added; two runs, identical apart from temp paths:

# tests 5 / # pass 3 / # fail 2
not ok - the total counts what the log still holds, not what the room said:
  Latest in "Ops" (newest 30 of 60, oldest first):
not ok - the total ignores the reader's floor:
  Latest in "Ops" (newest 30 of 60, oldest first):

The three guards (late joiner at floor 480 → 21 lines, no header; room exactly at the cap → newest 30 of 60; room under the cap → byte-identical) were green at the base and stay green. After the fix the same file is 5/5.

What I ran at this tip

  • core/test/room-read-trimmed-count.test.js: 5/5 green.
  • Neighbourhood — 29 room-* / group* / remote-room / link-room-title / face-room-hidden / mock-group-roster / details-group-* suites: 360/360.
  • Full core suite: 3091/3091.
  • TZ=UTC npm run verify at the tip: exit 0 — core 3091/3091, social 619, tree 149, timeline 172, workflow 528, project 147, scripts+policy 673, interop 37, live-e2e 34, core lint 88 warnings / 0 errors, arch-docs: OK (6 changed file(s)), 10 patches apply cleanly in order, canonical no-op.
  • ARCH_DOCS_BASE=e16c1d3c npm run arch-docs on the committed tree: OK (6 changed file(s)).

Not run: build-client (no core/src/client/ file changes, so 0008 and core/client.js are untouched), the smoke test (no index.js / host boot change), and the merge-queue checks (CI runs them).

Generated artifacts

node scripts/ds-bot-patches.mjs generate rewrites exactly two files on this base:

  • patches/ds-bot/0012-room-membership.patch — +10/−3 (181,726 → 182,325 bytes), carrying the groups.js hunk.
  • patches/ds-bot/files/test/room-read-trimmed-count.test.js — new mirror, 97 lines / 5,710 bytes.

The other nine patches are byte-identical to e16c1d3c. check says ok 10 patches apply cleanly in order / ok core/ equals ds-bot/ + patches/ds-bot/, and canonical says ok patches/ds-bot/ is what generate writes (generate is a no-op).

Scope

6 files, +215/−6:

file +/−
core/src/host/groups.js +9/−2
core/test/room-read-trimmed-count.test.js +97/−0 (new)
patches/ds-bot/0012-room-membership.patch +10/−3 (generated)
patches/ds-bot/files/test/room-read-trimmed-count.test.js +97/−0 (generated)
docs/subsystems/core-host.md +1/−1 (the #2780 sentence now names the trimmed case and the new test)
changelog.d/2838-room-read-trimmed-count.fixed.md +1/−0

Tier basis: risk-tier: T2; basis: core/src/host/groups.js=T2, changelog.d/2838-room-read-trimmed-count.fixed.md=T1, core/test/room-read-trimmed-count.test.js=T1, patches/ds-bot/0012-room-membership.patch=T1, patches/ds-bot/files/test/room-read-trimmed-count.test.js=T1, docs/subsystems/core-host.md=T0 (111 changed lines in the 4 non-generated files; 2 generated files not counted). T2, so it needs a Verdict PASS at this tip.

Known boundary

The fix is the read's count only: the store keeps trimming at 60 / 200 (raising them would change what is stored and shipped), and the lines themselves are still gone from the transcript. A legacy room with no seq is normalised on read, so its count cannot recover lines trimmed before this change. No retry, caching or backfill is added.

Verified commit: 11a84e958b89d2adeb2adce8aef9d7cdcecca5e0 (tree 65c2478f63c47655f82b5ca989d56dd26a342ea7); the pushed head below is that tree rebuilt on live main.

Checkpoint

{
  "pr": 2839,
  "claim": "Dev-1",
  "issue": 2838,
  "summary": "The group read's header counted only the lines the log still holds, so a room that had said 500 lines read `newest 30 of 60`; it now counts the dropped lines from the reader's floor, and a reader at line 300 reads `newest 30 of 201`. `ROOM_LOG_MAX` (60) and the membership store's 200 are unchanged, and a room that never trimmed reads byte-identically. The new test fails 2 of 5 on the old code and passes at this tip, the room/group neighbourhood is 360/360 and `verify` is green.",
  "files": [
    "core/src/host/groups.js",
    "core/test/room-read-trimmed-count.test.js",
    "patches/ds-bot/0012-room-membership.patch",
    "patches/ds-bot/files/test/room-read-trimmed-count.test.js",
    "docs/subsystems/core-host.md",
    "changelog.d/2838-room-read-trimmed-count.fixed.md"
  ],
  "checks": {
    "ran": "TZ=UTC npm run verify",
    "result": "green",
    "at": "f67e2b07f67d26079b61e2be7aaafbd70ccf4a8c"
  },
  "next": "none — ready for a Verdict PASS at this tip"
}

…total (ThinkFlowLab#2838)

The header was built from `kept.length`, which is only what the room log
still holds: the store is capped at ROOM_LOG_MAX (60 here, 200 in
room-membership.js) while `room.seq` counts every line ever appended. A
room that had said 500 lines read as "newest 30 of 60", hiding the 440
lines the trim threw away — and the same arithmetic ignored the reader's
floor, so a reader at line 300 read "newest 30 of 60" when 201 lines were
said since it joined.

The read now counts `dropped = max(0, room.seq - room.log.length)` and
adds only the dropped lines at or above the reader's floor:
`total = kept.length + max(0, dropped - max(0, from - 1))`. The caps stay
where they are, the store still trims, and a room that never trimmed (or
sits at or under the cap) reads byte-identically.

Fixes ThinkFlowLab#2838

Owner: Dev-1
Reviewer: Birfy
Written-by: Dev-1
Bot: Dev-1
Claim-Id: 63eb2d9dca664fab94de573fdd70b3e0
Risk tier: T2
Base: e16c1d3
@HugoXK

HugoXK commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy review request for the current tip f67e2b07f67d26079b61e2be7aaafbd70ccf4a8c (tree 65c2478f63c47655f82b5ca989d56dd26a342ea7). This is the only request for this tip.

What this PR is: read_group_chat built its Latest in … header from the lines the room log still holds, so a room that had said 500 lines read newest 30 of 60; the same arithmetic ignored a reader's floor (a reader joining at line 300 of that room was told newest 30 of 60, not newest 30 of 201). The tip counts the dropped lines from room.seq and subtracts the reader's floor; ROOM_LOG_MAX stays 60 and room-membership.js's 200 is untouched.

Verification at this tip: red first on e16c1d3c with only the new test was # tests 5 / # pass 3 / # fail 2 (both runs identical); the tip is 5/5, the 30-file room/group neighbourhood is 360/360, full core is 3091/3091, and generate rewrites only patches/ds-bot/0012-room-membership.patch plus the new test mirror (check/canonical no-op). CI at this tip: 15 success + 2 skipped + 1 failure, and that one failure is enqueue-guard saying no Verifier report names this tip.

Asking for: Verdict: PASS at f67e2b07… (or the exact reason it is refused). This is risk:T2, so the named Verdict at the tip is the one gate left.

@Birfy

Birfy commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator

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

Ran at this tip:

$ cd core && node --test test/room-read-trimmed-count.test.js
# tests 5  # pass 5  # fail 0
$ cp src/host/groups.js /tmp/groups.tip.safe.js
$ cp /tmp/groups.base.verify.js src/host/groups.js
$ node --test test/room-read-trimmed-count.test.js
# tests 5  # pass 3  # fail 2
$ cp /tmp/groups.tip.safe.js src/host/groups.js
$ node --test test/*room*.test.js test/*group*.test.js
# tests 360  # pass 358  # fail 2
$ node --test test/room-external.test.js
# tests 19  # pass 19  # fail 0

Red first, 2 of 5: with the tip tarball's core/src/host/groups.js replaced by the base blob fetched from e16c1d3c (sha256 b69d8735…, same as the base file) and the new test kept, room-read-trimmed-count.test.js gives # pass 3 # fail 2 — not ok 1 (Latest in "Ops" (newest 30 of 60…) instead of naming the 500 lines the room said) and not ok 2 (the floor case reads of 60, not of 201). Restoring the tip blob (sha256 cd4b5b2e…) returns 5/5. The other 3 tests are guards that pass on both, as they should.

The 360-test room/group neighbourhood had 2 failures, both spawn /usr/local/bin/node EAGAIN in room-external.test.js (848/250); that file alone is 19/19 green, so it is this sandbox's process-spawn limit, not the change. Diff is exactly one commit and the 6 listed files (+215/−6); compare shows nothing else. The arithmetic is right: dropped = room.seq − room.log.length, total = kept.length + max(0, dropped − max(0, from−1)), so a floor above the trim point only counts lines since the floor, and the under-cap guards keep the header byte-identical. Caps untouched (ROOM_READ_MAX 30, ROOM_LOG_MAX 60, membership 200), docs line updated with the #2838 reference, patch mirror is byte-identical to the new test, changelog fragment present. CI at this tip: 15 success, 2 skipped (live, window check), and the only failure is enqueue-guard saying no Verifier report names the tip f67e2b07f67d — that is this report.

@github-actions github-actions Bot added the pass label Nov 11, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 11, 2026
Merged via the queue into ThinkFlowLab:main with commit 40f59a1 Nov 11, 2026
19 checks passed
@Birfy
Birfy deleted the dev/1-room-read-trimmed-count branch November 11, 2026 07:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(host): a trimmed room log makes read_group_chat report the wrong total

2 participants