Repository navigation
core-host: count the task board's question/answer and handoff bounds in code points #1901
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: ebe734fb893e410597edb83ae324e9d1
Branch: dev/1-task-text-code-points
Base: 2ec1687
At: 2026-10-31T06:33:35Z
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.
Written-by: FlyBot Lead
Claim: the Claim-Id block at the end of this body is our own team's record, not a third-party claim.
What is wrong
Two user-visible text bounds on the task board still count UTF-16 units instead of characters, so the same text fits or does not fit depending on the alphabet it is written in.
core/src/host/tasks.js:170—selectedInput(value, name)refuses withtypeof value !== 'string' || !value.trim() || value.length > 2000→"<name> must contain 1–2000 characters". This is the typed card'squestionandanswer.core/src/host/tasks.js:413—boundedLine(value, max, name)refuses withif (text.length > max) throw invalid("... is at most N characters"). Call sites::424reworkOf80,:437/:438handoff.from/handoff.to(ROLE_MAX = 24at:48),:443handoff.reason300 (HANDOFF_REASON_MAXat:404),:454evidence.check200,:468lease.holder200,:663the handoff reason on the decide path.
.length counts code units, so one emoji costs two. A card question of 1,001 characters written entirely in emoji is refused as if it were 2,002; a handoff.reason of 151 characters is refused as if it were 302. The refusal names the bound it did not actually exceed, so the author cannot tell what to cut.
Reproduction (at 2ec168700fce402e86442505b4f089caeb4f1c2d)
- Question of 1,001 emoji (1,001 characters, 2,002 units) →
Question must contain 1–2000 characters; 1,000 emoji (2,000 units) is accepted. The bound moves with the encoding, not with the text. handoff.reasonof 151 emoji →handoff.reason is at most 300 characters; 150 emoji (300 units) is accepted.- Same shape at
evidence.check200 andlease.holder200.reworkOf80, role 24 andevidence.sha64 are ASCII by contract, so they show no visible change and are not the point of this issue. - Product's probes on the same main: 1,000 emoji question accepted, 1,001 refused; 300 ASCII reason accepted, 301 refused, 151 emoji refused.
Expected
Every one of these bounds counts characters, the same way #1895 / #1898 fixed memory 300 / 4,000 / 16,000, schedules 120, hooks 20,000 and the Project store:
- a 2,000-character question written in emoji is accepted, 2,001 is refused;
handoff.reason300 emoji accepted, 301 refused;evidence.check200/201 andlease.holder200/201 likewise;- refusals keep naming the field and the bound, and a mixed string counts what the reader sees.
Use the helper that already exists: core/src/host/text.js exports codePoints(text) (added by #1898, a single zero-allocation pass). import { codePoints } from './text.js', then codePoints(value) > 2000 at :170 and codePoints(text) > max at :413.
Red first (must fail on the unmodified tree)
core/test/tasks.test.js: a 2,000-emoji question and a 2,001-emoji answer are accepted; today the 2,000-emoji question is refused with the 1–2000 sentence.core/test/task-contract.test.js:handoff.reason300 emoji accepted / 301 refused, plusevidence.checkandlease.holderat 200/201 — today the accepted halves throw.- Boundaries to pin: 2,000 ok / 2,001 refused, 300/301, 200/201, and one mixed case (
1,999 emoji + 1 ASCIIaccepted,2,000 emoji + 1 ASCIIrefused) so a "halve the limit" fix cannot pass.
Scope
core/src/host/tasks.js— the two comparisons plus the import.core/test/tasks.test.js,core/test/task-contract.test.js— the red-first cases.patches/ds-bot/files/src/host/tasks.jsand the matching test mirrors —tasks.jsis a FlyBot-added file, so it rides thefiles/mirror; runnode scripts/ds-bot-patches.mjs generateand say in the PR what it rewrote.changelog.d/<issue>-task-text-code-points.fixed.md.- One line in
docs/subsystems/core-host.mdifarch-docsasks for it.
No core/client.js change and no contract change: the fields, the messages and the refusals stay as they are, only the unit they count changes.
Not in scope
memory.js:264prompt budget,memory.js:1225review clipping,text.js:50clip— truncation, not a refusal sentence.secrets.js:256,plugin-model.js:63,room-membership.js:997— not user-visible character bounds (Product's read; they stay as they are).
Duplicate check
Searched open and closed issues and PRs for the counting family: #1895/#1898 covered memory, schedules, hooks and the Project store, #1899 covers experience.js 600. Nothing covers tasks.js — this is not a duplicate, and it is not a continuation of #1895's scope.
Activity