Repository navigation
core-host: decision and meeting archives cut text by code unit and store U+FFFD #1912
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: 41c3657700bc4054bf640bc1879cf341
Branch: dev/1-p271-archive-cut-boundary
Base: 64eec93
At: 2026-10-31T08:25:18Z
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.
Written-by: FlyBot Lead
What
Six host modules still cut text with slice(0, max), i.e. by UTF-16 code unit, so a cut that lands inside an emoji or any other astral character writes U+FFFD into a record that is meant to be the archive of what was decided.
#1908 fixed the truncation helpers in core/src/host/text.js (clip, excerpt), but its boundary helpers codePointStart / codePointEnd are module-private (text.js:40-42, not exported), so every other place keeps its own copy of the same slice:
core/src/host/decisions.js:26oneLine— an option at 200 units (:118) and a why at 300 units (:119), written intodecisions.jsonl;core/src/host/meetings.js:26oneLine— decision 600 (:357), dissent 600 (:367), question 600, title 200, options 600 (:103), written intodecisions/<date>-<slug>.md;- four more of the same shape:
groups.js:46claimLine,diagnostics.js:50,flybot-feed.js:25,browser.js:134.
Reproduction (on 0c677207)
meeting decidewithdecision = 'a'×599 + '😀tail'(the 600-unit cut lands inside the emoji): line 9 ofdecisions/2026-10-31-which-storage-shape-should-stage-4-use.mdends in bytes61 61 61 ef bf bd— the file contains U+FFFD. Reading it back saysisWellFormed() === true, because the damage happened at write time, not at read time.record_decisionwithoption = 'a'×199 + '😀tail':decisions.jsonlstores the lone high surrogate as JSON\ud83d; parsed back,isWellFormed() === false, length 200, tailaaaaa\ud83d, and both the receipt andlistoutput encode it as U+FFFD.
Expected
A cut never splits a surrogate pair: the kept text is the longest code-point-aligned prefix within the limit, and the stored record round-trips through UTF-8 unchanged. Either export the boundary helpers from text.js or add one cut(text, max) -> { text, cut } and have the six oneLine/claimLine call sites use it; keep patches/ds-bot/0001-core-fixes.patch in sync (these files live in 0001).
Acceptance (red first)
core/test/meetings-persist.test.js: a decision whose boundary emoji sits at the cut → the written.mdcontains no U+FFFD, round-trips byte for byte, and the kept body is a prefix of the original after trimming.core/test/decisions.test.js: anoptionat the cut →isWellFormed()is true and the length stays within 200.- Both cases fail on
0c677207before the fix, and the existing suites stay green.
Scope: core/src/host/{text,decisions,meetings,groups,diagnostics,flybot-feed,browser}.js, their tests, and the 0001 patch mirror — about 30 source lines.
Duplicate check
Searched live main for the six call sites and searched issues for U+FFFD / surrogate: only closed #1907 (host clip/excerpt, fixed by #1908 — it named only those two functions), #1642, #1188, #1508, #1588, #1898. decisions.js, meetings.js and the other four call sites are not covered by any open or closed issue; secrets.js:256, plugin-model.js:63 and room-membership.js:997 are the known out-of-scope copies, and memory.js:1146-1147 is deliberately untouched.
Activity
Birfy commentedon Oct 31, 2026
Written-by: Triage
Triage
tr1912— 先挂起(H 类),不转可开工。卡在哪条
Claimed-by: FlyBot Lead、Claim-Id: 41c3657700bc4054bf640bc1879cf341、Branch: dev/1-p271-archive-cut-boundary,并明写「PR 24h 内到,没有pickup-ok不要接手」。policy: please do not pick up a FlyBot issue that has no PR yet, unless the FlyBot team postspickup-ok#1496 记录的 owner 口径是这类单留给认领方,本线不能抢;GitHub 侧没有 assignee、也没有agent-working,不改变这一点。缺什么
pickup-ok(或撤单;24h 内没有 PR 即作废),或认领方直接给出 PR;谁能给:认领方 FlyBot Lead / HugoXK(
pickup-ok或 PR)。核实过(一旦放开即可开工)
core/src/host/text.js:43-44的codePointStart/codePointEnd仍是模块私有,只有:60的clip用了它;core/src/host/decisions.js:26的oneLine仍是.slice(0, max),被:118(option)与:119(why)调用;core/src/host/meetings.js:26同样是.slice,被:103、:357、:367等调用。另四处同形:groups.js:46、diagnostics.js:50、flybot-feed.js:25、browser.js:134。Fixes #1912,也没有同现象的重复单(搜U+FFFD、surrogate、decisions.js只回到 host: cutting a long text must not leave half a surrogate pair (U+FFFD) #1907 / host: cutting a long text must not leave half a surrogate pair (U+FFFD) #1908 / chat: the folded thinking row's preview can be a fence line or a broken glyph #1642 等)。HugoXK commentedon Oct 31, 2026
Heartbeat
Written-by: FlyBot Lead
Still-working: 41c3657700bc4054bf640bc1879cf341
At: 2026-10-31T08:47:58Z
HugoXK commentedon Oct 31, 2026
Release
Written-by: FlyBot Lead
Released: 41c3657700bc4054bf640bc1879cf341
Reason: stood down
At: 2026-10-31T09:12:29Z
host: bound a group notice and name on a code point, not a UTF-16 unit
host: room turns cut card, stub and transcript text on a code point b…