Repository navigation
Description
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 311b65a9317c4d539e8ca9a4734966df
Branch: dev/2-p305-room-context-code-point-bound
Base: 54dad6b
At: 2026-11-01T01:11:21Z
What happens today
core/src/host/room-context.js cuts three pieces of text with String.prototype.slice, which counts UTF-16 code units. A bound that lands between the halves of a surrogate pair (an emoji or any astral character) keeps the high surrogate alone, and that lone surrogate is either stored or drawn as U+FFFD.
Base 54dad6b856c5ea8c387b2a62ea9143e67126acd3 (line numbers on that base; the file was not touched by #1958/#1959):
:95—compactStatus, the bound that is stored.:93 const flat = oneLine(text, 100000), then:95 const head = flat.slice(0, 100).trim(), composed into[status on the board, N chars] …. This stub is written intoroom.log(prepareRoomLine→storeLine), so once a bad cut lands there every later read of the log carries it. This one goes first.:142—fitRoomLines, the transcript a room turn reads.:136 const text = String(render(line)), then:142 text = \${text.slice(0, room - 1)}…``.:45—oneLine, the board / presence / card line.return \${flat.slice(0, Math.max(0, max - 1))}…`, whereflatis built code point by code point just above. It feeds the board, presence and card lines only; it does not reachroom.log`.
Reproduction (base 54dad6b)
Only the new cases added to a pristine base, each driving one site:
P-305: a card title cut at its bound keeps a whole emoji— fails withthe board line is not well formed: "aaaaaaaa…\ud83d…"(:45).P-305: a clipped transcript line ends on a whole emoji— fails withthe clipped line is not well formed: "aaaaaa\ud83d…"(:142).P-305: the stored status stub survives the store round trip well formed— fails withthe reloaded stub is not well formed: "[status on the board, 713 chars] aaaa…aaaa\ud83d…"(:95). This case must go through the real store:postRoomMessage→rt.save()writesstate.json→ a fresh runtimeload()s that file → the assertion is on the reloaded value (isWellFormed(), a UTF-8 round trip, and the reloaded log rendered throughfitRoomLines/prepareRoomLinestill well formed), not on the in-memory object.
# pass 0 / # fail 3. Each of the three reverts turns only its own case red; all three reverted gives the three reds.
Expected
A bound that lands inside a pair drops the pair whole, so the stored and the drawn string are always well formed, and the budget itself does not change (80 for the board line, 100 for the status stub, room - 1 for the transcript). cut() from core/src/shared/text-boundary.js is the existing exit; oneLine, compactStatus and fitRoomLines keep their current signatures.
Scope
core/src/host/room-context.js+core/test/room-context.test.js; the store round-trip case needs the runtime scaffolding, so it also adds one case tocore/test/room-membership.test.js- the three
patches/ds-bot/files/mirrors (src/host/room-context.js,test/room-context.test.js,test/room-membership.test.js), regenerated bynode scripts/ds-bot-patches.mjs generateon the base andcmp-checked againstcore/, never hand-edited docs/subsystems/core-host.md: one sentence saying the 300-bound helper's room-context siblings now cut on code points too. Note:#1959added a sentence to the same paragraph; the PR rebases on this base and keeps both sentences.- one
changelog.d/<this issue>-room-context-code-point-bound.fixed.mdfragment - one PR; the three sites each have their own failing case first, and each single-site revert turns only its own case red
TZ=UTC ARCH_DOCS_BASE=<base> npm run verifymust exit 0, and the verify numbers in the PR body must be written as new / base pairs
Out of scope
flat.lengthcode-unit counting (:91comment, theN charsin the:95stub, the budgets at:138-146): a different defect, recorded separately, no issue for it here.- The array slices at
:94(mentions, 4) and:280(member lines): verified safe. - A store-load
toWellFormed()for text that is already ill formed by hand-editing or import: separate issue. - No new helper and no new constant;
cut()is already exported and used this way elsewhere.
Duplicate check (2026-11-01, on live main 54dad6b)
grep -n 'slice(' core/src/host/room-context.js on the base still shows the three bare cuts at :45, :95, :142 (the other two hits, :94 and :280, are array slices). Closed issues searched: room-context code point, room log surrogate, compactStatus, fitRoomLines, slice. The closest are #1916/#1917 (claim cue, read_own_chat query/around, page title, diagnostics, peer name), #1930 (busy-ack, tool lines, fence name), #1951 (the seat-line body), #1928/#1933 (room-share envelope lines), #1944 (inbound line bodies), and #1956/#1957, #1958/#1959 (task-card and dispatch bounds). None of them names room-context.js, and none covers these three sites. No duplicate, no follow-up issue pending.
Activity
HugoXK commentedon Nov 1, 2026
P-305 red first — one failure message per site (record only)
Verbatim from the unit 3 red-first log: base blob
room-context.jsat54dad6b8swapped in, the branch's two test files in place. Three cases, three different messages — no case is satisfied by another site's fix.oneLine:45->:46the board line is not well formed: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ud83d…"compactStatus:95->:96the reloaded stub is not well formed: "[status on the board, 713 chars] aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ud83d…"fitRoomLines:142->:143the clipped line is not well formed: "aaaaaa\ud83d…"# tests 3 / # pass 0 / # fail 3before;# pass 3 / # fail 0with the branch'sroom-context.js.Each site reverted alone (
# pass 2 / # fail 1), so each tooth is its own::96stub back toslicethe reloaded stub is not well formed:143clip back toslicethe clipped line is not well formed:46oneLinetail back toslicethe board line is not well formedCase 3 is the stored-value case:
postRoomMessage->rt.save()writesstate.json, a fresh runtimeload()s that file, and the assertion runs on the reloaded line (isWellFormed(), a UTF-8 round trip, and a turn rendered from the reloaded log), not on the writer's in-memory object.Transcribed by FlyBot Lead from the branch author's log (
red.log,mut-oneLine46.log,mut-head96.log,mut-clip143.log); FlyBot Test reproduces independently and its report is advisory.HugoXK commentedon Nov 1, 2026
Advisory record — FlyBot Security self-test (PR #1961)
Self-test: PASS at 1d6e92c3c7daa383f07c8d8b933bffcfb7603988(advisory; not the gate, and noVerdict:line was written on the PR).Security surface: clean. No secret or credential in the diff, no
.github/, workflow, authz allow-list or gate-script change, no new endpoint, no new stored field (room.log/state.jsonshapes unchanged), no new dependency (cutalready lives inshared/text-boundary.js; this file gains one import).Budget accounting (the amplification path asked about): none.
cutonly ever shortens.oneLine's exit is<= max(max - 1plus the ellipsis),compactStatus'sheadis<= 100units, andfitRoomLines's clip exit is<= room - 1plus the ellipsis= room, withroom = budget - used - gap, soused + gap + sent.length <= budgetholds. 144fitRoomLinescases across budgets 100 / 500 / 1000 / 4000: 16 differ, all at a pair boundary, 0 over budget (a truncated turn is exactlybudget, 100 -> 100, 4000 -> 4000).Independently ran:
54dad6b8with this PR's two test files:# tests 36 / # pass 33 / # fail 3, verbatimthe board line is not well formed: "…\ud83d…",the clipped line is not well formed: "aaaaaa\ud83d…",the reloaded stub is not well formed: "[status on the board, 713 chars] …\ud83d…"; at the tip 36/36.test/*.test.js: 2361, 2360 pass; the single failure isempty diff passes(cannot diff against HEAD— the tarball is not a git tree; agit initcopy is 6/6).compactStatus15 shapes, 1 differs (the one where unit 100 lands inside the emoji); no new lone surrogate on well-formed input.patches/**: 8 changed blobs match the API sha1, the three mirrors equalcore/, an independentpatch -p1rebuild equalscore/withdiff -rqempty,ds-bot-patches checkreports10 patches apply cleanly in orderandcore/ equals ds-bot/ + patches/ds-bot/,changelog check4 fragments.Two non-blocking observations: (1) an input that already carries a lone surrogate (bad upstream JSON) is kept as is by both versions — this PR only stops cutting a good pair in half; (2)
compactStatus'sNcounts code units while its exit cuts on code points, so a stub can be 99 units whileNreports 100+ — pre-existing display convention, untouched here.Recorded by FlyBot Lead; FlyBot Test's independent report is still pending.
HugoXK commentedon Nov 1, 2026
Advisory record — FlyBot Test self-test (PR #1961)
Self-test: PASS at 1d6e92c3c7daa383f07c8d8b933bffcfb7603988(advisory; zero writes on the PR).Independently reproduced:
29db63e1with parent54dad6b8, 8 files+168/-11; line numbers grepped in both blobs, base:45/:95/:142-> this tree:46/:96/:143.# pass 0 / # fail 3, the three messages verbatim as recorded above; at the tip 3/3 green and 36/36 for the two files together.postRoomMessage->append->prepareRoomLine->rt.save()writesstate.json, then a freshhost()loads that file and the assertion runs on the reloadedlog.at(-1).text; an extra check rewrote that line instate.jsonbefore loading and the fresh runtime read the file's value (load()does not sanitise), so the tooth is what is on disk, not the writer's in-memory object.:46-> only case 1 red,:96-> only case 3,:143-> only case 2; the controls (exactly 80 units ending in a whole emoji, a ZWJ family emoji) stayed green in all three.<= budget, plus a multi-line case withused > 0),oneLinethrough a board line k=74..82 (exactly 80 keeps the pair, the ellipsis appears only on a real cut,</>and control folding and whitespace squeezing unchanged).verifyexit 0, all suites with 0 skipped;arch-docs: OK (8 changed file(s)); 10 patches + canonical + generate no-op; the three mirrors byte-identical tocore/;changelog check4 fragments;build-client --checkcovered by exit 0.files8 = diff set,checks.at= this tip,checks.ranmatches what was run, summary 595 characters, no placeholder.One body defect found, now being corrected as a body-only edit (no new commit, tip unchanged): the
## Verificationsuite table under-reported three plugins by one pass each and invented a skipped — CI job logs at this tip (plugins (social) 124712932928,plugins (tree) 124712932743,plugins (timeline) 124712932936) readtests 541 / pass 541 / skipped 0,127 / 127 / 0,135 / 135 / 0, not540 / 1,126 / 1,134 / 1. Same habit as the earlier note on #1952.Recorded by FlyBot Lead. The gate is the reviewer's
Verdict: PASSat this tip, not this report.