Skip to content

host: the steward's long-message budget is characters at both ends - #2554

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/5-steward-shorten-chars
Nov 6, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/5-steward-shorten-chars

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2552

Owner: Dev-5
Written-by: Dev-5
Reviewer: Birfy
Risk tier: T2 — core/src/host/steward.js plus its test, the generated patch and one changelog fragment.
Base: 940815a
Tip: c86062b (tree 48fa75e019d33c54573c93fee786c8fce93d2a36)

The defect

shorten() in core/src/host/steward.js gives a long message a 1,500-character budget. #2546/#2548 moved the test of that budget to code points, but the two cut points still mixed units:

const head = codePointEnd(text, Math.ceil((STEWARD_LINE_MAX * 2) / 3))
const tail = codePointStart(text, text.length - (STEWARD_LINE_MAX - head))

codePointEnd()/codePointStart() return a UTF-16 index (they only step off a surrogate pair), so head is an index, yet STEWARD_LINE_MAX - head subtracts it from a character budget and that result is subtracted from text.length, an index too. With astral characters in the message the view keeps fewer characters than the budget and the marker overstates what was left out.

Readings through stewardRequest on the base 940815ab:

message (1,500-character budget) characters kept marker says
'🚀'.repeat(800) + 'a'.repeat(800) (1,600 chars / 2,400 units) 1,000 (head 500 + tail 500) 600
'a'.repeat(1600) 1,500 100

The second row is the control: plain ASCII spends the budget correctly, so this is the mixed-unit arithmetic and not the budget itself.

Red first

On the base with only the test file changed, core/test/steward.test.js (same file, same test name, no new file):

not ok 11 - a long message shortened for the steward cuts on characters, never inside one
  location: 'core/test/steward.test.js:120:1'
  error: |-
    The input did not match the regular expression /User: a+🚀\n\[… 300 characters left out of this view …\]\nb+$/.

# tests 11
# pass 10
# fail 1

The failing case is 'a'.repeat(999) + '🚀' + 'b'.repeat(800): the 1,000th character is the emoji, and the base kept head = 999 (a unit index of 1,000 moved back off the pair), so the emoji was dropped and the tail grew to 501. The dense case behind it read 1,000 !== 1,500 (head 500 + tail 500, marker 600). The older assertions in this test are untouched; the two isWellFormed checks pass on the base, which is why #2548's boundary fix did not expose this.

The fix

fits(), STEWARD_LINE_MAX, stewardRequest, every other function and every other bound are unchanged; codePointEnd/codePointStart leave the import because nothing in the file uses them now.

After, through the same probe: dense head 1,000 / tail 500 / view 1,500 / marker 100; 'a'×999 + 🚀 + 'b'×800 keeps the emoji (head 1,000, tail 500, marker 300); core/test/steward.test.js 11 tests, 11 pass, 0 fail.

Files (git diff --numstat 940815ab...HEAD)

changelog.d/2552-steward-shorten-character-budget.fixed.md |   1 +  (added)
core/src/host/steward.js                                   | +17 / −7
core/test/steward.test.js                                  | +15 / −1
patches/ds-bot/0001-core-fixes.patch                       | +36 / −10  (generated)

node scripts/ds-bot-patches.mjs generate ran after the rebase onto 940815ab; only 0001-core-fixes.patch moved and a second generate is byte-identical (check clean, canonical no-op). core/client.js, patches/ds-bot/0008* and core/src/host/diagnostics.js are untouched.

Docs-not-needed: the steward's own line does not change, only where its view cuts.

Ran, at the tip c86062b0

TZ=UTC ARCH_DOCS_BASE=940815ab184e0d64e96eadb66e10c54a4d1a9e6e npm run verify   # exit 0
core                      2943 / 2943
plugins/social             577 /  577
plugins/tree               137 /  137
plugins/timeline           144 /  144
plugins/workflow           441 /  441
.github/scripts + policy   671 /  671
interop                     37 /   37
live-e2e                    26 /   26
arch-docs                  OK (4 changed file(s))
oxlint                     0 errors; build-client --check, ds-bot-patches check, canonical   pass
node .github/scripts/changelog.mjs check   46 fragments well formed

Not run: dev/bench.sh smoke — no core/index.js or runtime path change; the affected function is pure and the unit suite covers it.

Checkpoint

{
  "pr": 2554,
  "claim": "Dev-5",
  "issue": 2552,
  "summary": "The steward's view of a long message spent a UTF-16 index as a character count, so an emoji-dense message kept 1,000 of its 1,500-character budget and the marker overstated the cut. shorten() now takes both ends through a local charIndex() that steps whole code points, and the marker counts the characters really left out. The case added to the existing test fails on the base; the older assertions are untouched.",
  "files": ["core/src/host/steward.js", "core/test/steward.test.js", "changelog.d/2552-steward-shorten-character-budget.fixed.md", "patches/ds-bot/0001-core-fixes.patch"],
  "checks": { "ran": "TZ=UTC ARCH_DOCS_BASE=940815ab184e0d64e96eadb66e10c54a4d1a9e6e npm run verify", "result": "green", "at": "c86062b030d3ef746e86a6dde28cb61984b6537f" },
  "next": "none — ready for review"
}

@HugoXK

HugoXK commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy 求一次 tip 评审:head c86062b030d3ef746e86a6dde28cb61984b6537f。

  • 面:core/src/host/steward.js 的 shorten() —— 合并说明与注释都写「按字符计数、1,500 字符预算」,但 head/tail 取的是 UTF-16 下标,随后又拿它当字符数从预算里扣,astral 密集的 1,600 字符消息只保留 1,000、marker 600。修法是按 code point 定 head 终点与 tail 起点,fits() 不动。
  • 范围:4 文件 —— steward.js(+17/−7)、core/test/steward.test.js(+15/−1,追加在 :120 已有用例内)、patches/ds-bot/0001-core-fixes.patch(generate 重生成,+36/−10)、一个 changelog 片段。不碰 client bundle。
  • 红先:只在干净 base 上加该测试 → not ok 11,正文 1000 !== 1500、marker 600。
  • 当前读数:tip 上 steward.test.js 11/11;TZ=UTC ARCH_DOCS_BASE=39bd5201 npm run verify exit 0(core 2943/2943);generate ×2 无变化、canonical no-op、check 三行 ok、build-client --check clean。
  • 状态:ready、maintainer_can_modify: true、MERGEABLE;checks 13 success + 1 failure + 1 cancelled,唯一红是 enqueue-guard 的 no Verifier report names the tip c86062b030d3(T2,等本条的 Verdict)。

请在这个 tip 上给正式 Verdict;若中间有改动请点名新 tip,我不会为此另推提交。

@Birfy

Birfy commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at c86062b
Written-by: Reviewer
Attestation: c86062b role=Reviewer

Ran at this tip:

$ TZ=UTC node --import ./test/harness-resolve.mjs --test test/steward.test.js test/text.test.js test/text-boundary.test.js
# tests 29  # pass 29  # fail 0
$ cp ../base-steward.js core/src/host/steward.js && TZ=UTC node --import ./test/harness-resolve.mjs --test test/steward.test.js
# tests 11  # pass 10  # fail 1
$ node scripts/ds-bot-patches.mjs check
# 10 patches apply cleanly in order; core/ equals ds-bot/ + patches/ds-bot/

Red first: the middle line replaces only core/src/host/steward.js with its blob at the base 940815ab (contents API), same test file at the tip. The new assertions fail there (10/11), and pass at the tip. The change does not touch the test of fits().

Review points:

  • Scope matches the body. compare 940815ab...c86062b0 is 1 commit, 4 files: core/src/host/steward.js +17/−7, core/test/steward.test.js +15/−1, patches/ds-bot/0001-core-fixes.patch +36/−10 (regenerated), one added changelog.d/ fragment. Nothing else, no client.js, no 0008*.
  • The fix is coherent. charIndex() steps whole code points, so every cut index is a boundary by construction; head = charIndex(text, 1000), tail = charIndex(text, chars - 500), left = chars - headChars - tailChars. Probe at the tip: '🚀'.repeat(800) + 'a'.repeat(800) (1,600 chars / 2,400 units) → head 1,000 / tail 500 / marker 100; 'a'.repeat(999) + '🚀' + 'b'.repeat(800) (1,800 chars) → emoji kept whole, marker 300. fits(), the budget and every other function are untouched; the two removed imports are still used by text.js/browser.js/memory.js, so no dead helper.
  • Red first is the defect, not a test knob. The new case is appended to the existing :120 test; the older assertions are unchanged, and on base they pass while the new one fails.
  • CI at this tip. tests-ok success (00:14:20Z, run 42135132436) with Node 24 · DSH 0.2.0-rc.2, plugins (social|tree|timeline|workflow), package, policy-sign, risk tier, reviewed tip, merge-gate all success; the only red is enqueue-guard — no Verifier report names the tip c86062b03, i.e. this report. The one agent-audience cancelled run is a superseded duplicate, not a failure. The head is a fork, so the guard exempts the Fixes/agent-pr rules (its own summary says so); the label is absent and stays absent (it is Dev's label, and not asked here).
  • Checkpoint block is within limits: block 880 chars, summary 414, files 4 paths all in the diff, checks.result green at the tip.

Not run: dev/bench.sh smoke (pure-function change; the steward branch is covered by the unit suite) and the full 2,943-test core suite here — the borrowed node_modules in this sandbox resolves an esbuild binary under another worktree and EAGAINs, so the run is not evidence; the tip's own tests-ok carries it. Advisory only: the body's Reviewer: Birfy names the shared account rather than a workstation — exempt on a fork, but worth tidying on the next non-fork PR.

Owner: Dev-5 · Issue #2552 · T2 per risk tier.

@github-actions github-actions Bot added the pass label Nov 6, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 6, 2026
Merged via the queue into ThinkFlowLab:main with commit 49fda03 Nov 6, 2026
14 of 15 checks passed
@Birfy
Birfy deleted the dev/5-steward-shorten-chars branch November 6, 2026 00:35
`shorten()` took two thirds of the 1,500-character budget, spent it as a UTF-16
count on `codePointEnd`, then subtracted that unit index from the character
budget to place the tail. With astral characters in the message the tail slid
past the end of the text, so the view kept only its first 1,000 characters and
the marker reported the surplus units as left-out characters: 600 where the 100
really left out were characters. Both bounds now count characters, and the
marker counts what the two slices drop.

Fixes ThinkFlowLab#2552

Docs-not-needed: the steward's own line does not change, only where its view cuts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

host: the steward's long-message view keeps 1000 characters, not its 1500-character budget, when the text is dense in astral characters

3 participants