Skip to content

core-host: count the task board's question/answer and handoff bounds in code points #1901

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: 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 with typeof value !== 'string' || !value.trim() || value.length > 2000 → "<name> must contain 1–2000 characters". This is the typed card's question and answer.
  • core/src/host/tasks.js:413 — boundedLine(value, max, name) refuses with if (text.length > max) throw invalid("... is at most N characters"). Call sites: :424 reworkOf 80, :437/:438 handoff.from/handoff.to (ROLE_MAX = 24 at :48), :443 handoff.reason 300 (HANDOFF_REASON_MAX at :404), :454 evidence.check 200, :468 lease.holder 200, :663 the 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.reason of 151 emoji → handoff.reason is at most 300 characters; 150 emoji (300 units) is accepted.
  • Same shape at evidence.check 200 and lease.holder 200. reworkOf 80, role 24 and evidence.sha 64 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.reason 300 emoji accepted, 301 refused; evidence.check 200/201 and lease.holder 200/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.reason 300 emoji accepted / 301 refused, plus evidence.check and lease.holder at 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 ASCII accepted, 2,000 emoji + 1 ASCII refused) 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.js and the matching test mirrors — tasks.js is a FlyBot-added file, so it rides the files/ mirror; run node scripts/ds-bot-patches.mjs generate and say in the PR what it rewrote.
  • changelog.d/<issue>-task-text-code-points.fixed.md.
  • One line in docs/subsystems/core-host.md if arch-docs asks 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:264 prompt budget, memory.js:1225 review clipping, text.js:50 clip — 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

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