Skip to content

host: five bounds in turns.js cut text on UTF-16 units, not code points #1984

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: ac124b1661744f348c1a09cabe4a35ca
Branch: dev/3-p2-c2-turns-code-points
Base: 5b72400
At: 2026-11-01T05:42:57Z

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.

Written-by: FlyBot Lead

The defect

Five places in core/src/host/turns.js bound text with a bare String.prototype.slice, which cuts on UTF-16 units. When the bound lands between the halves of a surrogate pair, the value that reaches state, the owner-visible line, or the model prompt is not well formed (U+D83D at the tail, U+FFFD once encoded).

Live main 5b72400c6fef51c9a2145b78de9530c338853b74:

  1. :36 turnFailure — error.message.trim().slice(0, 500)
  2. :45 failureLine — failure.message.slice(0, 200)
  3. :303 stepOf — line.length > 80 ? \${line.slice(0, 79)}…` : line`
  4. :363 callDetail — text.trim().slice(0, 300) and code.trim().slice(0, 400)
  5. :405 noteEnded — (failure.message || failure.code).slice(0, 160)

:630 is a second cut of the :45 field and is out of scope.

Reproduction (red first, base 5b72400c, source untouched)

Inputs put the cut exactly on the high surrogate ('a'.repeat(N-1) + '😀' + 'tail'), driven through the real functions/routes:

diagnostics  # tests 18 # pass 16 # fail 2
not ok 12 - a failure message cut at its 500-unit bound keeps the pair whole
    the message is not well formed: "aaa\ud83d"
not ok 13 - a failure line cut at its 200-unit message bound keeps the pair whole
    the line is not well formed: "aaa\ud83d"

group-status  # tests 25 # pass 23 # fail 2
not ok 2  - an approval card cuts its description and command at their bounds without splitting a pair
    the description is not well formed: "aaa\ud83d"
not ok 20 - a step line cut at its bound keeps the pair whole, and the ellipsis only shows when it cut
    the step is not well formed: "aa\ud83d…"

dead-turn  # tests 6 # pass 5 # fail 1
not ok 3 - a failed turn records its reason cut at 160 units without splitting a pair
    the stored reason is not well formed: "aaa\ud83d"

Reverting one fix at a time reddens exactly one case (1→:12, 2→:13, 3→:20, 4→:2, 5→:3), so no case rides on another fix.

Expected

Every value goes through cut(text, max) from ../shared/text-boundary.js (core/src/host/text.js re-exports it); no bound changes. :303 appends … only when cut().cut says something was dropped, instead of guessing from length > 80. Each value stays a code-point prefix of its input, is well formed, and keeps its budget.

Scope

core/src/host/turns.js (five cut sites + one import), one case per site in core/test/{diagnostics,group-status,dead-turn}.test.js, one line in docs/subsystems/core-host.md, a changelog fragment, and the regenerated patches/ds-bot/{0001,0007} + files/ mirror (generated by generate, never hand-edited).

Dedupe (2026-11-01, live main): open issues searched for 702 no; turns.js code point 0 hits; failureLine/callDetail/stepOf no open issue. #1979 covered connectors, face-lead, project-team, capabilities, remote-chat only.

Activity

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