jeo-code 0.7.7 → 0.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
+ ## [0.7.8] - 2026-06-24
10
+ _Long-session performance & resource hygiene: the detached memory-distill worker now always terminates itself (closing the `jeo memory-distill` orphan pileup that pinned each session's transcript in RSS), the live reasoning/output block re-wraps only its trailing window (O(tail) per frame instead of O(len²) over a long stream), the team loop stops reprinting the todo guide every iteration, and the readline caret stays aligned after an image attach._
11
+
12
+ ### Fixed
13
+ - **The detached memory-distill worker always terminates itself.** `runMemoryDistillCommand` now wraps its work in `try/finally` and calls an injected `exit` (defaulting to `process.exit`) on every path — success, broken payload, and missing payload. `distillSessionMemory` caps the LLM call with a `Promise.race` timeout, but the race only *rejects*; it never aborts the underlying fetch, so a stalled provider socket kept the child's event loop alive and the `jeo memory-distill` worker lingered forever — one orphan per session, each pinning the full transcript in RSS (the observed "jeo bun" CPU/memory creep). An explicit exit closes the socket and reclaims the process.
14
+ - **The live reasoning/output block re-wraps only its trailing window.** The wrap memo in `app.ts` keyed on the full (up-to-hundreds-of-KB) buffer, so every 120 ms tick and every stream delta copied + compared the whole growing string — O(len) per frame, O(len²) over a long reasoning/tool stream (the streaming slowdown). `liveBlockWrapKey` now slices the ≤16 KB tail first, then keys + wraps that: identical visible output, O(tail) per frame regardless of total size.
15
+ - **The team loop no longer reprints the todo guide every iteration.** `runTeamEngine` printed `formatRalphTodoGuide` once per task — O(N²) lines that climbed the embedded-terminal renderer's CPU on a long task list. The guide prints once up front; per-task progress lines carry the rest.
16
+ - **The readline caret stays aligned after an image attach.** Both the paste and Ctrl+V image-attach paths now call readline's internal `_refreshLine()` after swapping the `[image #n]` tag in (mirroring the slash/tab-completion accept paths), so the stale row/cursor model no longer offsets the real caret several columns on the next keystroke.
17
+
18
+ ### Verified
19
+ - `bun run typecheck` clean; full suite green — 1914 pass / 0 fail across 229 files — including the new `runMemoryDistillCommand` always-exits assertions (success + no-payload paths) and the `liveBlockWrapKey` bounded-key / tail-collision tests.
20
+
21
+
9
22
  ## [0.7.7] - 2026-06-23
10
23
  _Long-running-process hygiene & input robustness: backgrounded grandchildren (`next dev &`, daemons) are now reaped at the turn boundary via per-command process groups — closing the climbing `next-server` RSS leak — while the prompt gains recursive fuzzy `@path` search, ANSI-safe and idle-merged bracketed paste, a configurable OSC 52 clipboard cap, per-role ralplan model routing, and leaked-reasoning-tag stripping on salvaged answers._
11
24
 
package/README.ja.md CHANGED
@@ -193,11 +193,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
193
193
  ## 変更履歴 (Changelog)
194
194
 
195
195
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
196
+ - **[0.7.8]** (2026-06-24) — Long-session performance & resource hygiene: the detached memory-distill worker now always terminates itself (closing the `jeo memory-distill` orphan pileup that pinned each session's transcript in RSS), the live reasoning/output block re-wraps only its trailing window (O(tail) per frame instead of O(len²) over a long stream), the team loop stops reprinting the todo guide every iteration, and the readline caret stays aligned after an image attach.
196
197
  - **[0.7.7]** (2026-06-23) — Long-running-process hygiene & input robustness: backgrounded grandchildren (`next dev &`, daemons) are now reaped at the turn boundary via per-command process groups — closing the climbing `next-server` RSS leak — while the prompt gains recursive fuzzy `@path` search, ANSI-safe and idle-merged bracketed paste, a configurable OSC 52 clipboard cap, per-role ralplan model routing, and leaked-reasoning-tag stripping on salvaged answers.
197
198
  - **[0.7.6]** (2026-06-23) — Release & docs plumbing: the running version's release notes now travel inside the `--compile` single binary (so `/whats-new` works offline and from a global install), the in-TUI markdown table renderer stops splitting on escaped `\|` pipes, and the release pipeline gains a cross-compiled standalone-binary matrix — macOS/Linux **plus a first-class Windows x64 `.exe`** — built on one Linux runner and auto-attached to each GitHub release.
198
199
  - **[0.7.5]** (2026-06-23) — Startup & loop latency: redundant re-reads that fired on every subagent spawn and every loop step are now memoized behind cheap `stat`-signature caches, so cold paths stay correct (edits/deletes still picked up immediately) while warm paths skip the disk and the re-parse. Targets the per-turn and per-spawn overhead that dominates perceived slowness in team/ralph/autopilot fan-outs.
199
200
  - **[0.7.4]** (2026-06-22) — Per-session model isolation + REPL slash-handler testability: a running `jeo` session now pins the model it resolved at start, so a concurrent session running `/model` (which persists the global default) can no longer silently switch a different live session's model mid-run. The read-only code-inspection slashes (`/view`, `/diff`, `/find`, `/search`), the `/undo` git slash, and the `/history` view are extracted into pure, PTY-free handlers so their logic is unit-tested directly instead of buried in the REPL loop.
200
- - **[0.7.3]** (2026-06-22) — Provider catalog: the Tencent Cloud MaaS (international) `knownModels` list is broadened from a 4-id DeepSeek/MiniMax set to the full live-verified line-up across five families — DeepSeek, MiniMax, Zhipu GLM, Moonshot Kimi, and Hunyuan. Because the host exposes no `/v1/models` route, this hand-maintained list is the model picker's source of truth for the offline fallback.
201
201
 
202
202
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
203
203
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -193,11 +193,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
193
193
  ## 변경 이력 (Changelog)
194
194
 
195
195
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
196
+ - **[0.7.8]** (2026-06-24) — Long-session performance & resource hygiene: the detached memory-distill worker now always terminates itself (closing the `jeo memory-distill` orphan pileup that pinned each session's transcript in RSS), the live reasoning/output block re-wraps only its trailing window (O(tail) per frame instead of O(len²) over a long stream), the team loop stops reprinting the todo guide every iteration, and the readline caret stays aligned after an image attach.
196
197
  - **[0.7.7]** (2026-06-23) — Long-running-process hygiene & input robustness: backgrounded grandchildren (`next dev &`, daemons) are now reaped at the turn boundary via per-command process groups — closing the climbing `next-server` RSS leak — while the prompt gains recursive fuzzy `@path` search, ANSI-safe and idle-merged bracketed paste, a configurable OSC 52 clipboard cap, per-role ralplan model routing, and leaked-reasoning-tag stripping on salvaged answers.
197
198
  - **[0.7.6]** (2026-06-23) — Release & docs plumbing: the running version's release notes now travel inside the `--compile` single binary (so `/whats-new` works offline and from a global install), the in-TUI markdown table renderer stops splitting on escaped `\|` pipes, and the release pipeline gains a cross-compiled standalone-binary matrix — macOS/Linux **plus a first-class Windows x64 `.exe`** — built on one Linux runner and auto-attached to each GitHub release.
198
199
  - **[0.7.5]** (2026-06-23) — Startup & loop latency: redundant re-reads that fired on every subagent spawn and every loop step are now memoized behind cheap `stat`-signature caches, so cold paths stay correct (edits/deletes still picked up immediately) while warm paths skip the disk and the re-parse. Targets the per-turn and per-spawn overhead that dominates perceived slowness in team/ralph/autopilot fan-outs.
199
200
  - **[0.7.4]** (2026-06-22) — Per-session model isolation + REPL slash-handler testability: a running `jeo` session now pins the model it resolved at start, so a concurrent session running `/model` (which persists the global default) can no longer silently switch a different live session's model mid-run. The read-only code-inspection slashes (`/view`, `/diff`, `/find`, `/search`), the `/undo` git slash, and the `/history` view are extracted into pure, PTY-free handlers so their logic is unit-tested directly instead of buried in the REPL loop.
200
- - **[0.7.3]** (2026-06-22) — Provider catalog: the Tencent Cloud MaaS (international) `knownModels` list is broadened from a 4-id DeepSeek/MiniMax set to the full live-verified line-up across five families — DeepSeek, MiniMax, Zhipu GLM, Moonshot Kimi, and Hunyuan. Because the host exposes no `/v1/models` route, this hand-maintained list is the model picker's source of truth for the offline fallback.
201
201
 
202
202
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
203
203
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -195,11 +195,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
195
195
  ## Changelog
196
196
 
197
197
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
198
+ - **[0.7.8]** (2026-06-24) — Long-session performance & resource hygiene: the detached memory-distill worker now always terminates itself (closing the `jeo memory-distill` orphan pileup that pinned each session's transcript in RSS), the live reasoning/output block re-wraps only its trailing window (O(tail) per frame instead of O(len²) over a long stream), the team loop stops reprinting the todo guide every iteration, and the readline caret stays aligned after an image attach.
198
199
  - **[0.7.7]** (2026-06-23) — Long-running-process hygiene & input robustness: backgrounded grandchildren (`next dev &`, daemons) are now reaped at the turn boundary via per-command process groups — closing the climbing `next-server` RSS leak — while the prompt gains recursive fuzzy `@path` search, ANSI-safe and idle-merged bracketed paste, a configurable OSC 52 clipboard cap, per-role ralplan model routing, and leaked-reasoning-tag stripping on salvaged answers.
199
200
  - **[0.7.6]** (2026-06-23) — Release & docs plumbing: the running version's release notes now travel inside the `--compile` single binary (so `/whats-new` works offline and from a global install), the in-TUI markdown table renderer stops splitting on escaped `\|` pipes, and the release pipeline gains a cross-compiled standalone-binary matrix — macOS/Linux **plus a first-class Windows x64 `.exe`** — built on one Linux runner and auto-attached to each GitHub release.
200
201
  - **[0.7.5]** (2026-06-23) — Startup & loop latency: redundant re-reads that fired on every subagent spawn and every loop step are now memoized behind cheap `stat`-signature caches, so cold paths stay correct (edits/deletes still picked up immediately) while warm paths skip the disk and the re-parse. Targets the per-turn and per-spawn overhead that dominates perceived slowness in team/ralph/autopilot fan-outs.
201
202
  - **[0.7.4]** (2026-06-22) — Per-session model isolation + REPL slash-handler testability: a running `jeo` session now pins the model it resolved at start, so a concurrent session running `/model` (which persists the global default) can no longer silently switch a different live session's model mid-run. The read-only code-inspection slashes (`/view`, `/diff`, `/find`, `/search`), the `/undo` git slash, and the `/history` view are extracted into pure, PTY-free handlers so their logic is unit-tested directly instead of buried in the REPL loop.
202
- - **[0.7.3]** (2026-06-22) — Provider catalog: the Tencent Cloud MaaS (international) `knownModels` list is broadened from a 4-id DeepSeek/MiniMax set to the full live-verified line-up across five families — DeepSeek, MiniMax, Zhipu GLM, Moonshot Kimi, and Hunyuan. Because the host exposes no `/v1/models` route, this hand-maintained list is the model picker's source of truth for the offline fallback.
203
203
 
204
204
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
205
205
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -193,11 +193,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
193
193
  ## 更新日志 (Changelog)
194
194
 
195
195
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
196
+ - **[0.7.8]** (2026-06-24) — Long-session performance & resource hygiene: the detached memory-distill worker now always terminates itself (closing the `jeo memory-distill` orphan pileup that pinned each session's transcript in RSS), the live reasoning/output block re-wraps only its trailing window (O(tail) per frame instead of O(len²) over a long stream), the team loop stops reprinting the todo guide every iteration, and the readline caret stays aligned after an image attach.
196
197
  - **[0.7.7]** (2026-06-23) — Long-running-process hygiene & input robustness: backgrounded grandchildren (`next dev &`, daemons) are now reaped at the turn boundary via per-command process groups — closing the climbing `next-server` RSS leak — while the prompt gains recursive fuzzy `@path` search, ANSI-safe and idle-merged bracketed paste, a configurable OSC 52 clipboard cap, per-role ralplan model routing, and leaked-reasoning-tag stripping on salvaged answers.
197
198
  - **[0.7.6]** (2026-06-23) — Release & docs plumbing: the running version's release notes now travel inside the `--compile` single binary (so `/whats-new` works offline and from a global install), the in-TUI markdown table renderer stops splitting on escaped `\|` pipes, and the release pipeline gains a cross-compiled standalone-binary matrix — macOS/Linux **plus a first-class Windows x64 `.exe`** — built on one Linux runner and auto-attached to each GitHub release.
198
199
  - **[0.7.5]** (2026-06-23) — Startup & loop latency: redundant re-reads that fired on every subagent spawn and every loop step are now memoized behind cheap `stat`-signature caches, so cold paths stay correct (edits/deletes still picked up immediately) while warm paths skip the disk and the re-parse. Targets the per-turn and per-spawn overhead that dominates perceived slowness in team/ralph/autopilot fan-outs.
199
200
  - **[0.7.4]** (2026-06-22) — Per-session model isolation + REPL slash-handler testability: a running `jeo` session now pins the model it resolved at start, so a concurrent session running `/model` (which persists the global default) can no longer silently switch a different live session's model mid-run. The read-only code-inspection slashes (`/view`, `/diff`, `/find`, `/search`), the `/undo` git slash, and the `/history` view are extracted into pure, PTY-free handlers so their logic is unit-tested directly instead of buried in the REPL loop.
200
- - **[0.7.3]** (2026-06-22) — Provider catalog: the Tencent Cloud MaaS (international) `knownModels` list is broadened from a 4-id DeepSeek/MiniMax set to the full live-verified line-up across five families — DeepSeek, MiniMax, Zhipu GLM, Moonshot Kimi, and Hunyuan. Because the host exposes no `/v1/models` route, this hand-maintained list is the model picker's source of truth for the offline fallback.
201
201
 
202
202
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
203
203
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -64,5 +64,6 @@
64
64
  "playwright": "^1.61.0",
65
65
  "typescript": "^5.6.3",
66
66
  "ws": "^8.21.0"
67
- }
67
+ },
68
+ "packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800"
68
69
  }
@@ -876,21 +876,37 @@ export async function spawnDetachedDistill(
876
876
  }
877
877
 
878
878
  /** CLI worker for the detached child: payload → distill → cleanup. Silent by design. */
879
- export async function runMemoryDistillCommand(args: string[]): Promise<void> {
880
- const payloadPath = (args[0] ?? "").trim();
881
- if (!payloadPath) return;
879
+ export async function runMemoryDistillCommand(
880
+ args: string[],
881
+ // Injected for tests; the REAL detached worker must terminate ITSELF. distillSessionMemory
882
+ // caps the LLM call with a Promise.race timeout, but the race only REJECTS — it never aborts
883
+ // the underlying fetch. A stalled provider socket therefore stays open, the worker's event
884
+ // loop never drains, and the `jeo memory-distill` child lingers forever — one orphan per
885
+ // session, each pinning the full transcript in RSS. That is the observed "jeo bun" pileup
886
+ // (CPU/memory creeping up over time). An explicit exit closes the socket and reclaims it.
887
+ exit: (code: number) => void = (code) => process.exit(code),
888
+ ): Promise<void> {
882
889
  try {
883
- const payloadContent = await fs.readFile(payloadPath, "utf-8");
884
- const payload = JSON.parse(payloadContent) as { model?: string; messages?: Message[] };
885
- const bundleDir = path.join(process.cwd(), ".jeo", "memory");
886
- await saveRawPayload(bundleDir, payload);
887
- if (Array.isArray(payload.messages)) {
888
- await distillSessionMemory(payload.messages, process.cwd(), { model: payload.model });
890
+ const payloadPath = (args[0] ?? "").trim();
891
+ if (!payloadPath) return;
892
+ try {
893
+ const payloadContent = await fs.readFile(payloadPath, "utf-8");
894
+ const payload = JSON.parse(payloadContent) as { model?: string; messages?: Message[] };
895
+ const bundleDir = path.join(process.cwd(), ".jeo", "memory");
896
+ await saveRawPayload(bundleDir, payload);
897
+ if (Array.isArray(payload.messages)) {
898
+ await distillSessionMemory(payload.messages, process.cwd(), { model: payload.model });
899
+ }
900
+ } catch {
901
+ // best-effort — a broken payload must not leave error noise in a detached child
902
+ } finally {
903
+ await fs.unlink(payloadPath).catch(() => {});
889
904
  }
890
- } catch {
891
- // best-effort — a broken payload must not leave error noise in a detached child
892
905
  } finally {
893
- await fs.unlink(payloadPath).catch(() => {});
906
+ // Guaranteed on every path (incl. the no-payload early return and a distill timeout):
907
+ // never leave the detached worker alive waiting on a dangling fetch.
908
+ exit(0);
894
909
  }
895
910
  }
896
911
 
912
+
@@ -2650,7 +2650,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2650
2650
  setImmediate(() => {
2651
2651
  void (async () => {
2652
2652
  try {
2653
- const rli = rl as unknown as { line: string; cursor: number };
2653
+ const rli = rl as unknown as { line: string; cursor: number; _refreshLine?: () => void };
2654
2654
  const before = rli.line;
2655
2655
  const dropped = await attachImagePaths(before, pendingImages.length + 1);
2656
2656
  if (dropped.images.length === 0 || dropped.text === before) return;
@@ -2658,6 +2658,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2658
2658
  rli.line = dropped.text;
2659
2659
  rli.cursor = dropped.text.length; // drop lands at the caret (line end in the common case)
2660
2660
  typedLine = dropped.text;
2661
+ // Keep readline's internal screen model in step with the swapped-in tag so
2662
+ // the caret is not offset on the next keystroke (mirrors the Ctrl+V path).
2663
+ rli._refreshLine?.();
2661
2664
  if (previewArmed) drawFooter(previewLines(typedLine, navIdx));
2662
2665
  } finally {
2663
2666
  pasteInFlight = false;
@@ -2719,12 +2722,17 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2719
2722
  if (!img) return;
2720
2723
  pendingImages.push(img);
2721
2724
  const tag = `[image #${pendingImages.length}]`;
2722
- const rli = rl as unknown as { line: string; cursor: number };
2725
+ const rli = rl as unknown as { line: string; cursor: number; _refreshLine?: () => void };
2723
2726
  const at = typeof rli.cursor === "number" ? rli.cursor : rli.line.length;
2724
2727
  const sep = rli.line.length > 0 && at > 0 && rli.line[at - 1] !== " " ? " " : "";
2725
2728
  rli.line = rli.line.slice(0, at) + sep + tag + " " + rli.line.slice(at);
2726
2729
  rli.cursor = at + sep.length + tag.length + 1;
2727
2730
  typedLine = rli.line;
2731
+ // Sync readline's internal screen model to the injected tag (same as the
2732
+ // slash/tab completion accept paths): without it readline's stale row/cursor
2733
+ // model offsets the real caret several columns on the next keystroke — the
2734
+ // "caret pushed past the tag after attaching an image" shift.
2735
+ rli._refreshLine?.();
2728
2736
  if (previewArmed) drawFooter(previewLines(typedLine, navIdx));
2729
2737
  } finally {
2730
2738
  pasteInFlight = false;
@@ -346,7 +346,8 @@ export async function runTeamEngine(opts: TeamEngineOptions = {}): Promise<{ ok:
346
346
  const currentTask = teamState.pending_tasks[0];
347
347
  log(`\n${categoryBadge("progress")} Current task: "${currentTask}"`);
348
348
  const activeIndex = activeStepIndex(tasks.length, teamState.pending_tasks);
349
- for (const line of formatRalphTodoGuide(tasks, activeIndex, teamState.completed_tasks ?? [], renderOpts)) log(line);
349
+ // Guide printed once above; per-task lines convey progress. No per-iteration
350
+ // reprint — it was O(N²) lines, climbing the embedded-terminal renderer's CPU.
350
351
 
351
352
  if (opts.onProgress) {
352
353
  opts.onProgress({ skill: "team", phase: "executing", detail: `Current task: ${currentTask}` });
package/src/tui/app.ts CHANGED
@@ -126,6 +126,15 @@ export function tailForWrap(text: string, maxChars = FRAME_WRAP_TAIL_CHARS): str
126
126
  return text.length > maxChars ? text.slice(text.length - maxChars) : text;
127
127
  }
128
128
 
129
+ /** Memoization key for a live-block wrap (Thinking / tool Output). Derived from the
130
+ * TRAILING window only (see `tailForWrap`): the rendered rows depend solely on the
131
+ * tail, so keying on the full streamed buffer just made every 120ms tick and every
132
+ * delta copy + compare the whole growing string — O(len) per frame, i.e. O(len²) over
133
+ * a long stream. Bounding the key to the ≤16KB tail keeps per-frame cost O(window). */
134
+ export function liveBlockWrapKey(wrapW: number, text: string): string {
135
+ return `${wrapW}\u0000${tailForWrap(text)}`;
136
+ }
137
+
129
138
  /** Max lines of a committed reasoning block kept in scrollback (gjc-style collapse): a
130
139
  * long chain-of-thought is clipped with a "+N more" hint so it never floods the ledger. */
131
140
  export const THINKING_COMMIT_MAX_LINES = 12;
@@ -1300,14 +1309,18 @@ export class LaunchTui {
1300
1309
  const dim = this.theme.color ? chalk.dim : (s: string) => s;
1301
1310
  if (!text.trim()) return [];
1302
1311
  const wrapW = Math.max(8, cols - 2);
1303
- // Memoize the wrap: only the spinner/clock change on most 120ms ticks, so re-wrapping
1304
- // this (up to 16KB) tail every frame just re-segments graphemes for no visible change.
1305
- // Per-label slot (Thinking / Output) keyed by wrap width + text — a real delta misses
1306
- // once and recomputes; an idle tick hits the cache. `rows` only gates the post-slice.
1312
+ // Memoize the wrap on the TRAILING window only. The block displays just its last few
1313
+ // wrapped rows, so wrap cost is already capped by `tailForWrap`. The cache KEY must be
1314
+ // bounded too: keying on the full (up-to-hundreds-of-KB) `text` made every 120ms tick
1315
+ // and every stream delta copy + compare the whole growing buffer — O(len) per frame, so
1316
+ // O(len²) over a long reasoning/tool stream (the streaming slowdown). Slice first, then
1317
+ // key + wrap the ≤16KB tail: identical visible output, O(tail) per frame regardless of
1318
+ // total size. Per-label slot (Thinking / Output) keyed by wrap width + tail.
1307
1319
  let cache = this.liveBlockWrapCaches.get(cacheKey);
1308
1320
  if (!cache) { cache = lastValueCache<string[]>(); this.liveBlockWrapCaches.set(cacheKey, cache); }
1309
- const wrapped = cache(`${wrapW}\u0000${text}`, () =>
1310
- tailForWrap(text)
1321
+ const tail = tailForWrap(text);
1322
+ const wrapped = cache(liveBlockWrapKey(wrapW, text), () =>
1323
+ tail
1311
1324
  .split("\n")
1312
1325
  .flatMap(l => wrapTextWithAnsi(l, wrapW))
1313
1326
  .filter(l => l.length > 0));