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.
HugoXK added a commit to HugoXK/FlyBot that referenced this pull request Nov 9, 2026
…rs (ThinkFlowLab#2555)

Fixes ThinkFlowLab#2553

Owner: Dev-5
Written-by: Dev-5
Reviewer: Birfy
Risk tier: T2 — `core/src/host/memory.js`, its test, the subsystem page
it owns, the generated patch and one changelog fragment.
Base: 39bd520
Tip: 2d05041 (tree
47fdb15)

## The defect

`ends()` in `core/src/host/memory.js` gives the user's own line in the
memory review a 3,000-character budget. ThinkFlowLab#2339 moved its *test* to
`fits()` (characters), but both cut points still spend the budget in
UTF-16 units:

```js
const tail = text.slice(codePointStart(text, text.length - Math.floor(max / 3)))
const head = cut(text, Math.min(Math.ceil(max * 2 / 3), max - tail.length))
```

`text.length` is a unit count, and `max - tail.length` subtracts a unit
count from a character budget. With characters outside the BMP the tail
starts too early, the head gives back units it never had, and the view
keeps about half of its budget.

Readings through the review's own render path on the base `39bd5201`
(rendered line = `User: ` + text):

| user line (3,000-character budget) | characters kept before | after |
| --- | --- | --- |
| `'🚀'.repeat(3500)` — 3,506 rendered chars / 7,006 units | **1,503**
(head 1,003 + tail 500) | **3,000** (head 2,000 + tail 1,000) |
| `'a'.repeat(3500)` — same rendered length in characters | 3,000 |
3,000 |

The ASCII row is the control: it spent the budget correctly before the
change too, so this is the mixed-unit arithmetic and not the budget or
`fits()`.

## Red first

On the base with only the test file changed (`core/test/memory.test.js`,
the existing `ThinkFlowLab#1918` two-ended-cut case at `:570`, no new file and no
new test name):

```
not ok 1 - a long user line cut for the review keeps whole characters at both ends (ThinkFlowLab#1918)
  location: 'core/test/memory.test.js:570:1'
  error: |-
    the view keeps the whole 3,000-character budget

    1503 !== 3000

  expected: 3000
  actual: 1503
  operator: 'strictEqual'
```

The two older assertions in that case (head and tail are byte prefixes
of the input) still pass on the base; the new block asserts head 2,000 +
tail 1,000 = 3,000 for the emoji line and for the ASCII control, and
that both ends are the rendered line's real ends.

## The fix

- a local `charIndex(text, chars)` — the UTF-16 index the *n*th
character starts at, stepping whole code points (the same helper and
shape as ThinkFlowLab#2554's `steward.js` fix);
- `tail = flat.slice(charIndex(flat, codePoints(flat) - tailChars))` and
`head = flat.slice(0, charIndex(flat, max - tailChars))`, so both ends
spend the budget in characters and neither cut can land inside a
surrogate pair;
- the text is flattened with `toWellFormed()` first, as `cut` did, so an
unpaired surrogate still becomes U+FFFD instead of half a character.

`fits()`, `REVIEW_USER_LINE_MAX`, the exported `clip` and every other
bound are unchanged; `codePointStart` leaves the import because nothing
in the file uses it now.

## Docs

`docs/subsystems/core-host.md` owns `core/src/host/` and its text-bounds
row stated the old `ends()` rule (`moves the tail's start back with
codePointStart`), so it is updated in this PR rather than left saying
something the code no longer does: the `ThinkFlowLab#1918` sentence now says the
budget is spent in characters through `charIndex`, that the head is the
rendered line's first 2,000 characters and the tail its last 1,000, that
the text is flattened first, and it links ThinkFlowLab#2553. `arch-docs` therefore
reports 5 changed files.

## Files (`git diff --numstat 39bd520...HEAD`)

```
changelog.d/2553-memory-review-line-budget.fixed.md |   1 +   (added)
core/src/host/memory.js                             | +18 / −5
core/test/memory.test.js                            | +24 / −0
docs/subsystems/core-host.md                        |  +1 / −1
patches/ds-bot/0001-core-fixes.patch                | +44 / −7   (generated)
```

`core/src/host/memory.js` is not in `patches/ds-bot/files/`; it lives in
`0001-core-fixes.patch`, which `node scripts/ds-bot-patches.mjs
generate` rewrote once after the rebase onto `39bd5201` and a second run
leaves byte-identical (`check` clean, `canonical` no-op).
`core/client.js`, `patches/ds-bot/0008*` and
`core/src/host/diagnostics.js` are untouched.

## Ran, at the tip `2d050418`

```
TZ=UTC ARCH_DOCS_BASE=39bd5201950a0657399f726f14c0a22e3e1c47df 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 (5 changed file(s))
oxlint                     0 errors; build-client --check, ds-bot-patches check, canonical   pass
node .github/scripts/changelog.mjs check   1 fragment well formed
```

`core/test/memory.test.js` on its own at the tip: 98 tests, 98 pass. 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

<!-- flybot:checkpoint -->
```json
{
  "pr": 2555,
  "claim": "Dev-5",
  "issue": 2553,
  "summary": "The memory review's two-ended cut judged its 3,000-character budget with fits() but split the line in UTF-16 units, so an emoji-dense user line kept 1,503 of 3,000 characters. ends() now spends the budget in characters through a local charIndex() and flattens with toWellFormed(), giving the rendered line's first 2,000 characters and its last 1,000. The case added to the existing ThinkFlowLab#1918 test fails on the base, and the subsystem page's text-bounds row carries the new rule.",
  "files": ["core/src/host/memory.js", "core/test/memory.test.js", "docs/subsystems/core-host.md", "changelog.d/2553-memory-review-line-budget.fixed.md", "patches/ds-bot/0001-core-fixes.patch"],
  "checks": { "ran": "TZ=UTC ARCH_DOCS_BASE=39bd5201950a0657399f726f14c0a22e3e1c47df npm run verify", "result": "green", "at": "2d0504185fdb3415d6f8f73c7129806cc8d2d505" },
  "next": "none — ready for review"
}
```
<!-- /flybot:checkpoint -->

Co-authored-by: HugoXK <xuke@users.noreply.github.com>
HugoXK added a commit to HugoXK/FlyBot that referenced this pull request Nov 9, 2026
…ed surrogate in the prompt (ThinkFlowLab#2557)

Fixes ThinkFlowLab#2556

Owner: Dev-5
Written-by: Dev-5
Reviewer: Birfy
Risk tier: T2 — `core/src/host/memory.js`, its test, the subsystem page
that owns it, the generated patch and one changelog fragment.
Base: 24bb90d (the branch's base;
`main` moved to 49fda03 when ThinkFlowLab#2554
merged, which also rewrote `0001-core-fixes.patch`)
Tip: 9ca39e3 (tree
1dd36a7)

## The defect

The memory review renders every line of a session for the prompt with
`ends()` (the user's own line, 3,000 characters) or `clip()` (every
other line, 800). Both used to hand an unpaired surrogate straight
through:

- `ends()` answered a line that already fitted unchanged (`if
(fits(text, max)) return text`), so no flattening happened on the path
almost every line takes;
- `reviewLine()`'s non-user branch composed `bot.name: text` and called
`clip()`, which flattens only what it cuts — a line under 800 characters
was answered as it arrived;
- `clip()` in `core/src/host/text.js` is imported by a dozen `client/`
modules, so its body is not touched here: the two callers in `memory.js`
flatten instead.

A message that arrives carrying half of a surrogate pair — a stream that
ended mid-character, a hand-written fixture — therefore reached the
review prompt, and the model, as a lone `U+D800`.

## Red first

One new case in the existing `core/test/memory.test.js` (`test/an
unpaired surrogate in a reviewed line reaches the prompt as one whole
character`), run against the base `24bb90d1` with only the test file
changed:

```
not ok 1 - an unpaired surrogate in a reviewed line reaches the prompt as one whole character
  location: 'core/test/memory.test.js:640:1'
  error: |-
    Expected values to be strictly equal:

    false !== true

  expected: true
  actual: false
  operator: 'strictEqual'
# tests 1 / # pass 0 / # fail 1
```

The failing assertion is the user line's `isWellFormed()`. Both carriers
were read on that base before the fix: user line `{wellFormed:false,
replacements:0, cut:false}`, Bot line `{wellFormed:false,
replacements:0, cut:false}`, `promptWellFormed:false`. Both surrogate
pairs sit inside the kept text (a user line of 201 characters with the
half pair at 100, a Bot line of 160 with it at 80), so neither sample
goes green just because a cut dropped it.

After the fix the same case asserts: the user line is well formed, holds
exactly one U+FFFD at `'User: '.length + 100` = 106, and `x`×100 /
`y`×100 are untouched on either side of it; the Bot line is well formed,
holds one U+FFFD at the label's real length + 80 (`Writer: `, index 88),
with `z`×80 / `w`×80 untouched; the whole prompt is well formed. No
assertion says "no U+FFFD" here — flattening is exactly what puts one
there, and the existing cases that assert its absence only use
well-formed input.

## The fix

- `ends()` flattens once, before the `fits()` test, and both the fitting
and the cut path answer the flattened text;
- `reviewLine()`'s non-user branch flattens the composed `bot.name: …`
string before calling `clip()`;
- `fits()`, `REVIEW_USER_LINE_MAX`, `REVIEW_OTHER_LINE_MAX` and the
exported `clip()` are unchanged. U+FFFD is one UTF-16 unit for the half
pair it replaces, so no length, index or budget moves.

## The wrong line in ThinkFlowLab#2555's comment, corrected here

This PR also touches `core/test/memory.test.js`'s `ThinkFlowLab#1918` case, so the
comment there is corrected in the same place instead of in an issue of
its own (no separate issue was filed for it).

| | characters kept of the 3,000 budget | head | tail |
| --- | --- | --- | --- |
| old code, measured on `main` | **1,503** | 1,003 (`User: ` included) |
500 |
| what the comment said | 1,516 | 1,000 | 500 |
| after ThinkFlowLab#2555, on `main` | 3,000 (2,000 + 1,000) | 2,000 | 1,000 |

`'🚀'.repeat(3500)` renders to 3,016 characters (`User: ` + 3,500 emoji);
the old unit arithmetic kept the rendered line's first 2,000 UTF-16
units (the 6-unit label plus 997 emoji = **1,003** characters) and its
last 1,000 units (**500** characters) — 1,503, not 1,516, and not a
1,000-character head. The comment and the assertions were always right
about the fixed code (head 2,000 / tail 1,000); only the "before"
reading was wrong.

## Docs and changelog

- `docs/subsystems/core-host.md` owns `core/src/host/`: its text-bounds
row gains one sentence — both render paths of the memory review flatten
before they bound — linked to ThinkFlowLab#2556;
- `changelog.d/2556-memory-review-flattened-lines.fixed.md`, one
paragraph with the issue link.

## Base and the generated patch

`main` advanced to `49fda03f` (the steward fix ThinkFlowLab#2554) after this branch
was cut; both pull requests rewrite
`patches/ds-bot/0001-core-fixes.patch`, so the merge was tried locally
rather than assumed: `git merge 49fda03` is clean on all five files,
and on the merged tree `node scripts/ds-bot-patches.mjs canonical` still
answers `generate is a no-op` — the two hunks land in different file
sections of the patch. No rebase, because none is needed and a rebase
would move the head that was verified.

## Files (`git diff --numstat 24bb90d...HEAD`)

```
1   0   changelog.d/2556-memory-review-flattened-lines.fixed.md
5   4   core/src/host/memory.js
52  5   core/test/memory.test.js
1   1   docs/subsystems/core-host.md
65  10  patches/ds-bot/0001-core-fixes.patch
```

`core/src/host/memory.js` and `core/test/memory.test.js` live in
`patches/ds-bot/0001-core-fixes.patch`, which `node
scripts/ds-bot-patches.mjs generate` rewrote for this branch.
`core/client.js`, `patches/ds-bot/0008*` and
`core/src/host/diagnostics.js` are untouched, and so is `clip()` in
`core/src/host/text.js`.

## Ran, at the tip `9ca39e3c`

```
TZ=UTC ARCH_DOCS_BASE=24bb90d1867d11a518106311f69d6936ac15dc44 npm run verify          # exit 0
node scripts/ds-bot-patches.mjs canonical                                              # ok, generate is a no-op
node .github/scripts/changelog.mjs check                                               # 2 fragment(s) well formed
```

`verify` reported `arch-docs: OK (5 changed file(s))` and the core suite
**2944/2944** (2943 on the base plus the one case added here);
`memory.test.js` alone is 99/99. Not run: `dev/bench.sh smoke` — no
`core/index.js` or runtime-path change, the two functions are pure and
the unit suite covers them.

## Checkpoint
<!-- flybot:checkpoint -->
```json
{
  "pr": 2557,
  "claim": "Dev-5",
  "issue": 2556,
  "summary": "ends() answered a reviewed line that fitted untouched, and reviewLine() gave its labelled Bot line to clip() unflattened, so a message carrying half a surrogate pair reached the memory review prompt as a lone U+D800. Both paths flatten with toWellFormed() first, so the prompt holds one U+FFFD at the same index and length; clip() in host/text.js is untouched. The new memory.test.js case fails on the base with false !== true. ThinkFlowLab#2555's comment is corrected to the measured 1,503 characters (head 1,003 with the label, tail 500).",
  "files": [
    "core/src/host/memory.js",
    "core/test/memory.test.js",
    "docs/subsystems/core-host.md",
    "patches/ds-bot/0001-core-fixes.patch",
    "changelog.d/2556-memory-review-flattened-lines.fixed.md"
  ],
  "checks": {
    "ran": "TZ=UTC ARCH_DOCS_BASE=24bb90d1867d11a518106311f69d6936ac15dc44 npm run verify",
    "result": "green",
    "at": "9ca39e3c2b3ba7edcdbccfcc301e4e09b41473a3"
  },
  "next": "none \u2014 ready for review; the merged tree with 49fda03 was also verified green"
}
```
<!-- /flybot:checkpoint -->

Co-authored-by: FlyBot Dev-5 <dev5@flybot.local>
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