Skip to content

core-host: the remaining code-unit cuts (claim cue, read_own_chat query/around, page title, diagnostics lines, peer name) #1916

Closed

Description

@HugoXK

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: 243cd3afc769474d8eaa02a692835b71
Branch: dev/2-p275-residual-code-unit-cuts
Base: 4486660
At: 2026-10-31T09:10:11Z

#1912 (PR #1915, merged in 44866600) gave the host one exported cut(text, max) in core/src/host/text.js and moved six writers onto it. Reviewing that change turned up nine more code-unit cuts on the same paths, none of them touched by #1912, each able to store or emit half a surrogate pair. The table below has eight rows because its diagnostics.js row covers two cuts (:211 and :220): nine cuts, eight rows — PR bodies must say nine.

What and where (live main 44866600)

file:line cut why it matters
core/src/host/groups.js claimCueText title.slice(0, CLAIM_CUE_REF_MAX) (60) a title with no #1234 prefix is the claim line's reference; a pair split at 60 puts an ill-formed string into the room notice and the room log
core/src/host/groups.js:964 String(args.query ?? '').trim().slice(0, 80) the read_own_chat query is stored in the room log and used for matching, so the character is also unsearchable
core/src/host/groups.js:965 String(args.around ?? '').trim().slice(0, 40) the same, for the anchor
core/src/host/browser.js:254 page.url.slice(0, 2000), String(page.title ?? '').slice(0, 300) the page title/url a Bot reads back, from outside the team
core/src/host/diagnostics.js:211, :220 clean(value.slice(0, max)), clean(item.text.slice(0, 500)), clean(item.code.slice(0, 40)) browser-log lines in the diagnostics report
core/src/host/groups.js:330 String(rt.botOf(leadId)?.name ?? 'The task lead').replace(/[\r\n]+/g, ' ').slice(0, 80) the room's task-lead name goes into every member's system prompt; a Bot name is owner-set and may carry an emoji
core/src/host/groups.js:722 String(error?.message ?? error).slice(0, 300) a failed turn's error text lands in the room notice and the room log
core/src/shared/remote-label.js:6 cleanPeerName → .slice(0, 60) a friend's Bot or room name, used as a display label

Reproduce

read_own_chat { query: 'a'.repeat(79) + '😀permalink' } on 44866600 writes \ud83d into state.json and an ill-formed lookup line into the room log; the same string is used for includes/FTS matching, so the emoji is neither stored nor findable. claimCueText({ title: 'a'.repeat(59) + '😀tail' }) returns a cue ending in a lone high surrogate. cleanPeerName('b'.repeat(59) + '😀tail') returns 59 units plus a lone surrogate, which then flows into cleanLabel etc.

Expected

Every one of these goes through cut(...) so the prefix never splits a pair, with the limits unchanged (still UTF-16 units, values at or under the old bound). For remote-label.js the helper lives in host/, which shared/ must not import: move the boundary rule to a shared module (e.g. core/src/shared/text-boundary.js) and have host/text.js build cut on it, so both layers use one implementation.

Red first

  • groups: claimCueText with 'a'.repeat(59) + '😀tail' → isWellFormed() === true, ref is the 59-unit prefix; read_own_chat with 'a'.repeat(79) + '😀permalink' → log string well-formed, ≤ 80, and a byte-for-byte prefix of the input after dropping the tail; one case for around at 40.
  • browser: open-page title/url at the boundary stay well-formed.
  • diagnostics: a browser-log line at the boundary stays well-formed.
  • remote-label: cleanPeerName at 60 stays well-formed.

Out of scope

Input that is already ill-formed (a tool argument or a friend's name carrying a lone surrogate) is a separate, existing shape: cut is a prefix operation and cannot repair the middle of a string. read_own_chat's fixed 20/12 caps are also separate (P-274).

Duplicate check

Searched the live tree for slice(0 in core/src/host and core/src/shared, and the open/closed issues for U+FFFD, surrogate, cleanPeerName, read_own_chat query, claimCueText: the only closed matches are #1907/#1908 (clip/excerpt, merged), #1912 (the six writers, merged in 44866600), and unrelated naming changes (#1151, #502, #541). Nothing open covers these six lines.

Record: the #1915 changelog

changelog.d/1912-archive-cut-boundary.fixed.md (merged with #1915, now on main) ends "… and the claim line, diagnostics report, activity-feed label and browser text beside them — so the stored record round-trips through UTF-8 as the whole character instead of U+FFFD". That sentence only becomes true once this issue lands: claimCueText's fallback ref (groups.js) still cuts by code unit on main. Do not edit the merged changelog — the PR body for this issue must carry one line: The #1915 changelog sentence about the claim line only holds once this change lands. so the audit can line the two up.

Corrections from the audit (measured on 44866600)

  • Line numbers above were taken from an older tree; on 44866600 they are groups.js:51 (claim cue ref), :964/:965 (query/around), :330 (task-lead name) and :722 (failed-turn error text), browser.js:254, diagnostics.js:211/:220.
  • Two more sites of the same family belong here: groups.js:330 and groups.js:722 (both added to the table above).
  • Red-first assertions must not require an exact length: cut(text, max) returns max - 1 units when the pair straddles the limit. Assert length <= max, isWellFormed() === true, and that dropping the tail gives a byte-for-byte UTF-8 prefix of the input.

Not in this change (next round)

core/src/host/meetings.js:30 slugOf's trailing .slice(0, 80): reachable only when toLowerCase() expands (İ → i̇) and the slug then ends on an astral \p{L} letter (Deseret and similar); it only affects the archive file name. No runtime reproduction was produced for it, and the scope of this issue is frozen at the eight sites above — it goes into the next round instead of this pull request.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions