scream-code 0.16.4 → 0.16.6

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.
@@ -6,7 +6,7 @@ const __dirname = __cjsShimDirname(__filename);
6
6
  import { i as __require, o as __toESM, r as __exportAll, t as __commonJSMin } from "./chunk-D90kvbyJ.mjs";
7
7
  import { C as join$1, D as resolve$2, E as relative$1, S as isAbsolute$1, T as parse$7, a as isSupportedFile, b as basename$1, i as ingestFile, r as ingestDirectory, t as multiSearch, w as normalize$1, x as dirname$2, y as KnowledgeStore } from "./src-tDEINaMV.mjs";
8
8
  import { t as require_base64_js } from "./base64-js-DzVmk6Nb.mjs";
9
- import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog--J3ngWFv.mjs";
9
+ import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-DBlx8pMR.mjs";
10
10
  import { createRequire } from "node:module";
11
11
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
12
12
  import * as fs$1 from "node:fs/promises";
@@ -33,6 +33,7 @@ import * as nodeOs from "node:os";
33
33
  import { homedir, release, tmpdir } from "node:os";
34
34
  import * as vs from "zlib";
35
35
  import Qr from "zlib";
36
+ import { diffLines, diffWords } from "diff";
36
37
  import { EventEmitter as EventEmitter$1 } from "node:events";
37
38
  import { StringDecoder } from "node:string_decoder";
38
39
  import Pi from "assert";
@@ -52,7 +53,6 @@ import { AsyncLocalStorage } from "node:async_hooks";
52
53
  import chalk from "chalk";
53
54
  import { Box, Container, Image, Input, Key, Markdown, Spacer, Text, decodeKittyPrintable, fuzzyFilter, getCapabilities, getImageDimensions, matchesKey, sliceByColumn, truncateToWidth, visibleWidth } from "@liutod-scream/pi-tui";
54
55
  import { highlight, supportsLanguage } from "cli-highlight";
55
- import { diffWords } from "diff";
56
56
  import { gt, valid } from "semver";
57
57
  //#region ../../packages/agent-core/src/errors/codes.ts
58
58
  /**
@@ -48278,10 +48278,25 @@ var BackgroundProcessManager = class {
48278
48278
  const outputSessionDir = this.outputSessionDirFor(taskId);
48279
48279
  if (outputSessionDir !== void 0) {
48280
48280
  await entry?.outputWriteQueue;
48281
- const persisted = await readTaskOutput(outputSessionDir, taskId);
48282
- if (persisted.length > 0) {
48283
- if (tail !== void 0 && tail < persisted.length) return persisted.slice(-tail);
48284
- return persisted;
48281
+ if (tail !== void 0 && tail > 0) {
48282
+ const size = await taskOutputSizeBytes(outputSessionDir, taskId);
48283
+ if (size > 0) {
48284
+ const windowBytes = Math.min(size, tail * 4 + 7);
48285
+ const start = size - windowBytes;
48286
+ let persisted = await readTaskOutputBytes(outputSessionDir, taskId, start, windowBytes);
48287
+ if (persisted.length > 0) {
48288
+ if (start > 0) {
48289
+ let lead = 0;
48290
+ while (lead < persisted.length && persisted.codePointAt(lead) === 65533 && lead < 3) lead += 1;
48291
+ if (lead > 0) persisted = persisted.slice(lead);
48292
+ }
48293
+ return tail < persisted.length ? persisted.slice(-tail) : persisted;
48294
+ }
48295
+ return this.getOutput(taskId, tail);
48296
+ }
48297
+ } else {
48298
+ const persisted = await readTaskOutput(outputSessionDir, taskId);
48299
+ if (persisted.length > 0) return tail !== void 0 && tail < persisted.length ? persisted.slice(-tail) : persisted;
48285
48300
  }
48286
48301
  }
48287
48302
  return this.getOutput(taskId, tail);
@@ -53612,16 +53627,68 @@ async function loadEmbedder(cacheDir) {
53612
53627
  }
53613
53628
  }
53614
53629
  /**
53615
- * Remove any previously downloaded model files for the fixed BGESmallZH model.
53616
- * Called before a manual re-download so that a corrupted/partial cache does not
53617
- * cause fastembed to fail repeatedly.
53630
+ * Cache entry names fastembed uses for the fixed BGESmallZH model: it extracts
53631
+ * into `<cacheDir>/<name>/` from the staged archive `<cacheDir>/<name>.tar.gz`.
53632
+ */
53633
+ const BGESMALLZH_CACHE_NAME = "fast-bge-small-zh-v1.5";
53634
+ const PLATFORM_FAILURE = /@anush008\/tokenizers|Failed to load native binding|Unsupported (?:OS|platform|architecture)|ERR_DLOPEN_FAILED|invalid ELF header|Exec format error|is not a valid Win32 application|GLIBC_\d+[^"\n]*not found|Error loading shared library|cannot open shared object file/iu;
53635
+ const ARCHIVE_FAILURE = /\bTAR_[A-Z_]+|incorrect header check|unexpected end of file|invalid tar|zlib/iu;
53636
+ const NETWORK_FAILURE = /\b(?:ENOTFOUND|EAI_AGAIN|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EHOSTUNREACH|socket hang up|fetch failed)\b|network error|HTTP \d{3}/iu;
53637
+ /**
53638
+ * Classify a load failure so callers can say something true about it: the four
53639
+ * causes need four different answers (unsupported platform, corrupt cache,
53640
+ * network, unknown), and they used to be reported as one network problem.
53641
+ */
53642
+ function classifyEmbeddingFailure(message) {
53643
+ if (message === void 0) return "other";
53644
+ if (PLATFORM_FAILURE.test(message)) return "platform";
53645
+ if (ARCHIVE_FAILURE.test(message)) return "archive";
53646
+ if (NETWORK_FAILURE.test(message)) return "network";
53647
+ return "other";
53648
+ }
53649
+ /**
53650
+ * Loads only the module entry point — no model download — so callers can tell
53651
+ * "this machine cannot run the local model at all" apart from "the download
53652
+ * failed". fastembed imports its native tokenizer binding at module scope, which
53653
+ * is exactly the step a platform without a published binary cannot pass.
53654
+ */
53655
+ async function probeLocalEmbeddingSupport() {
53656
+ try {
53657
+ await import("fastembed");
53658
+ return { supported: true };
53659
+ } catch (error) {
53660
+ return {
53661
+ supported: false,
53662
+ error: error instanceof Error ? error.message : String(error)
53663
+ };
53664
+ }
53665
+ }
53666
+ /**
53667
+ * Remove every trace of a previous BGESmallZH download: the extracted model
53668
+ * directory *and* the staged archive it came from.
53669
+ *
53670
+ * The archive has to go with it. fastembed skips the download whenever
53671
+ * `<cacheDir>/<name>.tar.gz` exists, and it only deletes that file after a
53672
+ * *successful* extract — its download error paths (response error, file stream
53673
+ * error) reject and leave whatever was written behind. So an archive sitting
53674
+ * next to a failed attempt is usually a partial leftover, and keeping it makes
53675
+ * every retry extract the same junk locally: no network traffic, a few
53676
+ * milliseconds, the same failure forever.
53677
+ *
53678
+ * Never throws: a file locked by another process (on Windows a mapped ONNX file
53679
+ * stays locked) must not replace the download error the caller is reporting.
53618
53680
  */
53619
53681
  function clearEmbeddingModelCache(cacheDir) {
53620
- const modelDir = join(cacheDir, "fast-bge-small-zh-v1.5");
53621
- if (existsSync(modelDir)) rmSync(modelDir, {
53622
- recursive: true,
53623
- force: true
53624
- });
53682
+ for (const name of [BGESMALLZH_CACHE_NAME, `${BGESMALLZH_CACHE_NAME}.tar.gz`]) {
53683
+ const target = join(cacheDir, name);
53684
+ if (!existsSync(target)) continue;
53685
+ try {
53686
+ rmSync(target, {
53687
+ recursive: true,
53688
+ force: true
53689
+ });
53690
+ } catch {}
53691
+ }
53625
53692
  }
53626
53693
  /**
53627
53694
  * Small config/tokenizer files that fastembed expects alongside model.onnx.
@@ -69519,6 +69586,77 @@ function withTimeout$1(promise, timeoutMs, parentSignal) {
69519
69586
  });
69520
69587
  }
69521
69588
  //#endregion
69589
+ //#region ../../packages/agent-core/src/tools/support/file-diff.ts
69590
+ /**
69591
+ * Line-diff statistics for the file-mutating tools.
69592
+ *
69593
+ * Edit and Write both hold the pre-write and post-write text of the file they
69594
+ * touch, so they can report exact `+added / -removed` line counts to the UI.
69595
+ * Deriving those counts from the tool arguments instead is systematically
69596
+ * wrong (a Write overwrite hides every removed line; an Edit with
69597
+ * `replace_all` sees only one occurrence), so the tools report them here.
69598
+ *
69599
+ * Only counts leave this module: file contents must never be embedded in the
69600
+ * display payload, which is persisted with the session transcript.
69601
+ */
69602
+ /**
69603
+ * Upper bound applied to either side of the diff. Beyond it the line diff is
69604
+ * skipped (returns `undefined`) so an oversized file cannot stall the calling
69605
+ * tool; the caller then omits the display and the UI falls back to its
69606
+ * argument-derived counts.
69607
+ */
69608
+ const MAX_DIFF_CHARS = 1e6;
69609
+ /**
69610
+ * Wall-clock budget for a single line diff. jsdiff's Myers implementation has
69611
+ * adversarial cases that stay quadratic-ish well inside {@link MAX_DIFF_CHARS}:
69612
+ * two ~400 KB blobs with no common lines measured minutes before aborting. Past
69613
+ * the budget jsdiff gives up and the result is treated exactly like the size
69614
+ * guard (no display, argument-derived fallback) — stalling the tool, and with it
69615
+ * the event loop, is never acceptable for a UI statistic.
69616
+ */
69617
+ const DIFF_BUDGET_MS = 250;
69618
+ /**
69619
+ * `diffLines` with the runtime `timeout` option. jsdiff honours `timeout`
69620
+ * (returning `undefined` when it gives up) but its published typings omit it.
69621
+ */
69622
+ const diffLinesWithBudget = diffLines;
69623
+ /**
69624
+ * Count the lines in a `diffLines` part value the way `git diff --numstat`
69625
+ * counts them: a trailing newline terminates the preceding line instead of
69626
+ * opening an empty one, and the empty string has no lines.
69627
+ */
69628
+ function countLines$1(value) {
69629
+ if (value.length === 0) return 0;
69630
+ const lineBreaks = value.split("\n").length - 1;
69631
+ return value.endsWith("\n") ? lineBreaks : lineBreaks + 1;
69632
+ }
69633
+ /**
69634
+ * Exact line counts between `before` and `after`: every added / removed part
69635
+ * `diffLines` reports contributes its own line count, so a single replaced
69636
+ * line is one addition plus one removal. Whitespace is significant (jsdiff's
69637
+ * `ignoreWhitespace` is off by default) and `\r\n` endings are compared
69638
+ * literally.
69639
+ *
69640
+ * Returns `undefined` when either side exceeds {@link MAX_DIFF_CHARS} or the
69641
+ * diff does not finish within {@link DIFF_BUDGET_MS}.
69642
+ */
69643
+ function fileDiffSummary(before, after) {
69644
+ if (before.length > MAX_DIFF_CHARS || after.length > MAX_DIFF_CHARS) return void 0;
69645
+ const parts = diffLinesWithBudget(before, after, { timeout: DIFF_BUDGET_MS });
69646
+ if (parts === void 0) return void 0;
69647
+ let added = 0;
69648
+ let removed = 0;
69649
+ for (const part of parts) {
69650
+ const lines = countLines$1(part.value);
69651
+ if (part.added) added += lines;
69652
+ else if (part.removed) removed += lines;
69653
+ }
69654
+ return {
69655
+ added,
69656
+ removed
69657
+ };
69658
+ }
69659
+ //#endregion
69522
69660
  //#region ../../packages/agent-core/src/tools/support/scan-cache.ts
69523
69661
  var FsScanCache = class {
69524
69662
  cache = /* @__PURE__ */ new Map();
@@ -69842,6 +69980,29 @@ function replaceOnceLiteral(content, oldString, newString) {
69842
69980
  if (index === -1) return content;
69843
69981
  return content.slice(0, index) + newString + content.slice(index + oldString.length);
69844
69982
  }
69983
+ /**
69984
+ * Attach the exact `+added / -removed` line counts to a successful result so
69985
+ * the UI can total them per activity group.
69986
+ *
69987
+ * The counts come from the real pre-edit and post-edit text, so a
69988
+ * `replace_all` edit reports the whole-file delta instead of one occurrence's
69989
+ * worth. Error results carry no display: nothing was applied, or the failure
69990
+ * path never established the post-edit text.
69991
+ */
69992
+ function withFileDiffDisplay(result, before, after) {
69993
+ if (result.isError === true) return result;
69994
+ const summary = fileDiffSummary(before, after);
69995
+ if (summary === void 0) return result;
69996
+ const display = {
69997
+ kind: "file_diff",
69998
+ added: summary.added,
69999
+ removed: summary.removed
70000
+ };
70001
+ return {
70002
+ ...result,
70003
+ display
70004
+ };
70005
+ }
69845
70006
  var EditTool = class {
69846
70007
  jian;
69847
70008
  workspace;
@@ -69915,7 +70076,8 @@ var EditTool = class {
69915
70076
  output: "new_string contains merge conflict markers (<<<<<<< / ======= / >>>>>>>). Remove them before writing. These markers indicate an unresolved merge and should not be introduced into files."
69916
70077
  };
69917
70078
  try {
69918
- const modelView = toModelTextView(await this.jian.readText(safePath));
70079
+ const raw = await this.jian.readText(safePath);
70080
+ const modelView = toModelTextView(raw);
69919
70081
  const content = modelView.text;
69920
70082
  const replaceAll = args.replace_all ?? false;
69921
70083
  const existingBlocks = scanConflictLines(content.split("\n"));
@@ -69975,20 +70137,20 @@ var EditTool = class {
69975
70137
  output: `old_string is not unique in ${args.path} (found ${String(count)} occurrences at lines: ${matchLineNumbers.join(", ")}${truncatedNote}). To replace every occurrence, set replace_all=true. To target a specific one, include more surrounding context lines from one of those line ranges in old_string.`
69976
70138
  };
69977
70139
  }
69978
- const newContent = replaceOnceLiteral(content, args.old_string, args.new_string);
69979
- await this.jian.writeText(safePath, materializeModelText(newContent, modelView.lineEndingStyle));
70140
+ const writtenText = materializeModelText(replaceOnceLiteral(content, args.old_string, args.new_string), modelView.lineEndingStyle);
70141
+ await this.jian.writeText(safePath, writtenText);
69980
70142
  scanCache.clear();
69981
70143
  const { notice, hasErrors } = await this.appendDiagnostics(safePath);
69982
70144
  const output = `Replaced 1 occurrence in ${args.path}`;
69983
70145
  const message = notice.length > 0 ? notice : void 0;
69984
- return hasErrors ? {
70146
+ return withFileDiffDisplay(hasErrors ? {
69985
70147
  isError: true,
69986
70148
  output,
69987
70149
  message
69988
70150
  } : {
69989
70151
  output,
69990
70152
  message
69991
- };
70153
+ }, raw, writtenText);
69992
70154
  }
69993
70155
  const parts = content.split(args.old_string);
69994
70156
  const replacementCount = parts.length - 1;
@@ -69999,20 +70161,20 @@ var EditTool = class {
69999
70161
  output: `old_string not found in ${args.path} (file has ${String(lineCount)} lines). The file contents may be out of date — re-read with the Read tool. If you already re-read, verify old_string matches exactly: indentation, trailing whitespace, and line endings (LF vs CRLF) must all match the Read output view.`
70000
70162
  };
70001
70163
  }
70002
- const newContent = parts.join(args.new_string);
70003
- await this.jian.writeText(safePath, materializeModelText(newContent, modelView.lineEndingStyle));
70164
+ const writtenText = materializeModelText(parts.join(args.new_string), modelView.lineEndingStyle);
70165
+ await this.jian.writeText(safePath, writtenText);
70004
70166
  scanCache.clear();
70005
70167
  const { notice, hasErrors } = await this.appendDiagnostics(safePath);
70006
70168
  const output = `Replaced ${String(replacementCount)} occurrences in ${args.path}`;
70007
70169
  const message = notice.length > 0 ? notice : void 0;
70008
- return hasErrors ? {
70170
+ return withFileDiffDisplay(hasErrors ? {
70009
70171
  isError: true,
70010
70172
  output,
70011
70173
  message
70012
70174
  } : {
70013
70175
  output,
70014
70176
  message
70015
- };
70177
+ }, raw, writtenText);
70016
70178
  } catch (error) {
70017
70179
  if (error?.code === "EISDIR") return {
70018
70180
  isError: true,
@@ -75681,6 +75843,12 @@ var write_default = "Write does not preserve or infer the previous line-ending s
75681
75843
  const S_IFMT$2 = 61440;
75682
75844
  /** File-type bits of a directory. */
75683
75845
  const S_IFDIR$1 = 16384;
75846
+ /**
75847
+ * Size ceiling (bytes, from `stat`) for reading the pre-write text that feeds
75848
+ * the `file_diff` display. Mirrors the guard inside `fileDiffSummary`: past it
75849
+ * the diff is skipped anyway, so the file is never read only to be discarded.
75850
+ */
75851
+ const MAX_DIFF_SOURCE_BYTES = 1e6;
75684
75852
  const WriteInputSchema = z.object({
75685
75853
  path: z.string().describe("Path to the file to create, append to, or completely overwrite. Relative paths resolve against the working directory; a path outside the working directory must be absolute. The parent directory must already exist."),
75686
75854
  content: z.string().describe("Raw full file content to write exactly as provided. This does not use the Read/Edit text view."),
@@ -75739,6 +75907,8 @@ var WriteTool = class {
75739
75907
  };
75740
75908
  try {
75741
75909
  const mode = args.mode ?? "overwrite";
75910
+ const existing = await this.readExistingText(safePath);
75911
+ const display = mode === "append" ? this.appendFileDiffDisplay(args.content, existing) : this.overwriteFileDiffDisplay(args.content, existing);
75742
75912
  if (mode === "append") await this.jian.writeText(safePath, args.content, { mode: "a" });
75743
75913
  else await this.jian.writeText(safePath, args.content);
75744
75914
  scanCache.clear();
@@ -75746,13 +75916,18 @@ var WriteTool = class {
75746
75916
  const { notice, hasErrors } = await this.appendDiagnostics(safePath);
75747
75917
  const output = `${mode === "append" ? "Appended" : "Wrote"} ${String(bytesWritten)} bytes to ${args.path}`;
75748
75918
  const message = notice.length > 0 ? notice : void 0;
75749
- return hasErrors ? {
75919
+ if (hasErrors) return {
75750
75920
  isError: true,
75751
75921
  output,
75752
75922
  message
75753
- } : {
75923
+ };
75924
+ return display === void 0 ? {
75754
75925
  output,
75755
75926
  message
75927
+ } : {
75928
+ output,
75929
+ message,
75930
+ display
75756
75931
  };
75757
75932
  } catch (error) {
75758
75933
  if (error?.code === "ENOENT") return {
@@ -75766,6 +75941,74 @@ var WriteTool = class {
75766
75941
  }
75767
75942
  }
75768
75943
  /**
75944
+ * Compute the `file_diff` line counts for an append.
75945
+ *
75946
+ * Appending concatenates bytes, so a file that does not end with a newline has
75947
+ * its last line rewritten by the first appended line — counting only the new
75948
+ * content (`+N -0`) would hide that rewritten line. Diffing the real pre-write
75949
+ * text against that text plus the new content is exact; a target that does not
75950
+ * exist yet can only gain lines.
75951
+ */
75952
+ appendFileDiffDisplay(content, existing) {
75953
+ if (existing.kind === "unavailable") return void 0;
75954
+ const before = existing.kind === "missing" ? "" : existing.text;
75955
+ const summary = fileDiffSummary(before, before + content);
75956
+ if (summary === void 0) return void 0;
75957
+ return {
75958
+ kind: "file_diff",
75959
+ added: summary.added,
75960
+ removed: summary.removed
75961
+ };
75962
+ }
75963
+ /**
75964
+ * Compute the `file_diff` line counts for an overwrite.
75965
+ *
75966
+ * A file that does not exist yet can only gain lines, so the argument-derived
75967
+ * count is exact there. Otherwise the previous text is diffed against the
75968
+ * new text, which is the only way to see the removed lines an overwrite
75969
+ * destroys. Returns `undefined` (no display) when the previous text is
75970
+ * unknown or too large to diff.
75971
+ */
75972
+ overwriteFileDiffDisplay(content, existing) {
75973
+ if (existing.kind === "missing") return {
75974
+ kind: "file_diff",
75975
+ added: countLines$1(content),
75976
+ removed: 0
75977
+ };
75978
+ if (existing.kind === "unavailable") return void 0;
75979
+ const summary = fileDiffSummary(existing.text, content);
75980
+ if (summary === void 0) return void 0;
75981
+ return {
75982
+ kind: "file_diff",
75983
+ added: summary.added,
75984
+ removed: summary.removed
75985
+ };
75986
+ }
75987
+ /**
75988
+ * Best-effort read of the text this call is about to overwrite, for the
75989
+ * `file_diff` display only.
75990
+ *
75991
+ * The `stat` size check runs first so an oversized file is never pulled into
75992
+ * memory just to be diffed. A failing `stat` for any reason other than
75993
+ * `ENOENT` is inconclusive, so the read is still attempted.
75994
+ */
75995
+ async readExistingText(safePath) {
75996
+ try {
75997
+ if ((await this.jian.stat(safePath)).stSize > MAX_DIFF_SOURCE_BYTES) return { kind: "unavailable" };
75998
+ } catch (error) {
75999
+ if (error?.code === "ENOENT") return { kind: "missing" };
76000
+ }
76001
+ try {
76002
+ return {
76003
+ kind: "text",
76004
+ text: await this.jian.readText(safePath)
76005
+ };
76006
+ } catch (error) {
76007
+ if (error?.code === "ENOENT") return { kind: "missing" };
76008
+ return { kind: "unavailable" };
76009
+ }
76010
+ }
76011
+ /**
75769
76012
  * Best-effort check that the parent directory exists and is a directory.
75770
76013
  *
75771
76014
  * The path schema documents this precondition; probing it up front turns a
@@ -86069,7 +86312,8 @@ const SNAPSHOT_FOLDED_CONTEXT_TYPES = new Set([
86069
86312
  "context.append_loop_event",
86070
86313
  "context.apply_compaction",
86071
86314
  "micro_compaction.apply",
86072
- "full_compaction.complete"
86315
+ "full_compaction.complete",
86316
+ "request.header"
86073
86317
  ]);
86074
86318
  /**
86075
86319
  * Serialized-line prefix of a context.snapshot record. Persisted records
@@ -86088,11 +86332,42 @@ const SNAPSHOT_FOLDED_LINE_PREFIXES = [...SNAPSHOT_FOLDED_CONTEXT_TYPES].map((ty
86088
86332
  const SNAPSHOT_RECORD_LINE_PREFIX_BYTES = Buffer.from(SNAPSHOT_RECORD_LINE_PREFIX, "utf8");
86089
86333
  /** UTF-8 bytes of each entry of {@link SNAPSHOT_FOLDED_LINE_PREFIXES}. */
86090
86334
  const SNAPSHOT_FOLDED_LINE_PREFIX_BYTES = SNAPSHOT_FOLDED_LINE_PREFIXES.map((prefix) => Buffer.from(prefix, "utf8"));
86091
- function startsWithPrefix(data, prefix) {
86092
- return data.length >= prefix.length && data.subarray(0, prefix.length).equals(prefix);
86335
+ /**
86336
+ * Longest folded-type line prefix: the number of leading bytes a line needs
86337
+ * before the parse filter can decide whether to drop it. Until that many bytes
86338
+ * are seen the head is the only part of the line held in memory.
86339
+ */
86340
+ const SNAPSHOT_FOLDED_MAX_PREFIX_BYTES = SNAPSHOT_FOLDED_LINE_PREFIX_BYTES.reduce((longest, prefix) => Math.max(longest, prefix.length), 0);
86341
+ /** Bytes-level prefix test for a line head that may be shorter than the prefix. */
86342
+ function headStartsWithPrefix(head, headLength, prefix) {
86343
+ return headLength >= prefix.length && head.subarray(0, prefix.length).equals(prefix);
86344
+ }
86345
+ function headStartsWithAnyPrefix(head, headLength, prefixes) {
86346
+ return prefixes.some((prefix) => headStartsWithPrefix(head, headLength, prefix));
86347
+ }
86348
+ /**
86349
+ * Whether folded context records that predate the last snapshot can be skipped
86350
+ * without decoding. Safe only when no wire migration is needed: a version
86351
+ * mismatch triggers migrations/rewrite that must see EVERY record, so
86352
+ * old/new-version files fall back to full parsing. The version lives in the
86353
+ * first (metadata) line. A header that cannot be parsed returns `false` here —
86354
+ * the same line is parsed again below, where the error is thrown.
86355
+ */
86356
+ function readSkipFoldedFlag(headerText) {
86357
+ try {
86358
+ return JSON.parse(headerText).protocol_version === "1.4";
86359
+ } catch {
86360
+ return false;
86361
+ }
86093
86362
  }
86094
- function startsWithAnyPrefix(data, prefixes) {
86095
- return prefixes.some((prefix) => startsWithPrefix(data, prefix));
86363
+ /** File size in bytes, or `undefined` when the file does not exist yet. */
86364
+ async function fileSize(path) {
86365
+ try {
86366
+ return (await stat(path)).size;
86367
+ } catch (error) {
86368
+ if (error.code === "ENOENT") return void 0;
86369
+ throw error;
86370
+ }
86096
86371
  }
86097
86372
  var FileSystemAgentRecordPersistence = class {
86098
86373
  filePath;
@@ -86102,6 +86377,10 @@ var FileSystemAgentRecordPersistence = class {
86102
86377
  directorySynced = false;
86103
86378
  rewriteSeq = 0;
86104
86379
  flushPromise;
86380
+ /** Set while `compact()` runs; writes queue up instead of draining. */
86381
+ compacting = false;
86382
+ /** Folded-history bytes skipped by the most recent full read(). */
86383
+ droppedBytes = 0;
86105
86384
  error;
86106
86385
  constructor(filePath, options = {}) {
86107
86386
  this.filePath = filePath;
@@ -86109,54 +86388,189 @@ var FileSystemAgentRecordPersistence = class {
86109
86388
  }
86110
86389
  async *read() {
86111
86390
  await this.flush();
86112
- const lines = [];
86113
- let lastSnapshotLineNumber = -1;
86391
+ const size = await fileSize(this.filePath);
86392
+ if (size === void 0 || size === 0) return;
86393
+ const lastSnapshotLineNumber = await this.scanLastSnapshotLine(size);
86394
+ if (lastSnapshotLineNumber === void 0) return;
86395
+ yield* this.streamRecords(size, lastSnapshotLineNumber);
86396
+ }
86397
+ /**
86398
+ * Byte-level pass that returns the line number of the last
86399
+ * `context.snapshot` line, or `-1` when the file has none. `undefined` means
86400
+ * the file does not exist (a brand-new session has no wire yet — not an
86401
+ * error). Nothing is decoded and no line is retained: memory stays at one
86402
+ * stream chunk.
86403
+ */
86404
+ async scanLastSnapshotLine(size) {
86405
+ const prefix = SNAPSHOT_RECORD_LINE_PREFIX_BYTES;
86406
+ const head = Buffer.allocUnsafe(prefix.length);
86407
+ let headLength = 0;
86114
86408
  let lineNumber = 0;
86115
- let openChunks = [];
86116
- const stream = createReadStream(this.filePath);
86409
+ let lastSnapshotLineNumber = -1;
86410
+ const stream = createReadStream(this.filePath, { end: size - 1 });
86117
86411
  try {
86118
86412
  for await (const chunk of stream) {
86119
86413
  let searchFrom = 0;
86120
86414
  for (;;) {
86121
86415
  const newlineIndex = chunk.indexOf(10, searchFrom);
86416
+ const end = newlineIndex === -1 ? chunk.length : newlineIndex;
86417
+ if (end > searchFrom && headLength < prefix.length) {
86418
+ const take = Math.min(prefix.length - headLength, end - searchFrom);
86419
+ chunk.copy(head, headLength, searchFrom, searchFrom + take);
86420
+ headLength += take;
86421
+ }
86122
86422
  if (newlineIndex === -1) break;
86123
- const parts = openChunks.length > 0 ? [...openChunks, chunk.subarray(searchFrom, newlineIndex)] : [chunk.subarray(searchFrom, newlineIndex)];
86124
- let lineData = Buffer.concat(parts);
86125
- if (lineData.length > 0 && lineData.at(-1) === 13) lineData = lineData.subarray(0, lineData.length - 1);
86126
86423
  lineNumber++;
86127
- if (startsWithPrefix(lineData, SNAPSHOT_RECORD_LINE_PREFIX_BYTES)) lastSnapshotLineNumber = lineNumber;
86128
- lines.push({
86129
- data: lineData,
86130
- lineNumber,
86131
- allowTruncated: false
86132
- });
86424
+ if (headLength === prefix.length && head.equals(prefix)) lastSnapshotLineNumber = lineNumber;
86425
+ headLength = 0;
86426
+ searchFrom = newlineIndex + 1;
86427
+ }
86428
+ }
86429
+ } catch (error) {
86430
+ if (error.code === "ENOENT") return void 0;
86431
+ throw error;
86432
+ }
86433
+ return lastSnapshotLineNumber;
86434
+ }
86435
+ /**
86436
+ * Streaming replay pass. Folded records predating the last snapshot are
86437
+ * dropped WITHOUT being decoded — the restore fast-path discards them anyway
86438
+ * (records/index.ts snapshot branch), so the yielded stream is identical to a
86439
+ * full decode while neither the file nor the skipped lines are ever held in
86440
+ * memory. An unterminated trailing line is tolerated (see parseRecordLine):
86441
+ * the last write may have crashed mid-flush.
86442
+ */
86443
+ async *streamRecords(size, lastSnapshotLineNumber) {
86444
+ const head = Buffer.allocUnsafe(SNAPSHOT_FOLDED_MAX_PREFIX_BYTES);
86445
+ let headLength = 0;
86446
+ let openChunks = [];
86447
+ let decided = false;
86448
+ let retaining = false;
86449
+ let headerSeen = false;
86450
+ let skipFoldedBeforeSnapshot = false;
86451
+ let lineNumber = 0;
86452
+ let lineBytes = 0;
86453
+ let skippedBytes = 0;
86454
+ const retainedLine = () => openChunks.length === 1 ? openChunks[0] : Buffer.concat(openChunks);
86455
+ const keepLine = (currentLine) => !(skipFoldedBeforeSnapshot && currentLine < lastSnapshotLineNumber && headStartsWithAnyPrefix(head, headLength, SNAPSHOT_FOLDED_LINE_PREFIX_BYTES));
86456
+ const stream = createReadStream(this.filePath, { end: size - 1 });
86457
+ try {
86458
+ for await (const chunk of stream) {
86459
+ let searchFrom = 0;
86460
+ for (;;) {
86461
+ const newlineIndex = chunk.indexOf(10, searchFrom);
86462
+ const end = newlineIndex === -1 ? chunk.length : newlineIndex;
86463
+ if (!decided && end > searchFrom) {
86464
+ const take = Math.min(SNAPSHOT_FOLDED_MAX_PREFIX_BYTES - headLength, end - searchFrom);
86465
+ chunk.copy(head, headLength, searchFrom, searchFrom + take);
86466
+ headLength += take;
86467
+ searchFrom += take;
86468
+ lineBytes += take;
86469
+ if (headLength === SNAPSHOT_FOLDED_MAX_PREFIX_BYTES) {
86470
+ decided = true;
86471
+ retaining = keepLine(lineNumber + 1);
86472
+ if (retaining) openChunks.push(Buffer.from(head));
86473
+ }
86474
+ }
86475
+ if (retaining && end > searchFrom) openChunks.push(chunk.subarray(searchFrom, end));
86476
+ if (newlineIndex === -1) {
86477
+ lineBytes += end - searchFrom;
86478
+ break;
86479
+ }
86480
+ lineBytes += newlineIndex + 1 - searchFrom;
86481
+ lineNumber++;
86482
+ if (!decided) {
86483
+ decided = true;
86484
+ retaining = keepLine(lineNumber);
86485
+ if (retaining && headLength > 0) openChunks.push(Buffer.from(head.subarray(0, headLength)));
86486
+ }
86487
+ if (retaining) {
86488
+ let lineData = retainedLine();
86489
+ if (lineData.length > 0 && lineData.at(-1) === 13) lineData = lineData.subarray(0, lineData.length - 1);
86490
+ const lineText = lineData.toString("utf8");
86491
+ if (!headerSeen) {
86492
+ headerSeen = true;
86493
+ skipFoldedBeforeSnapshot = readSkipFoldedFlag(lineText);
86494
+ }
86495
+ const record = parseRecordLine(lineText, lineNumber, this.filePath, false);
86496
+ if (record !== void 0) yield record;
86497
+ }
86498
+ if (!retaining) skippedBytes += lineBytes;
86133
86499
  openChunks = [];
86500
+ headLength = 0;
86501
+ decided = false;
86502
+ retaining = false;
86503
+ lineBytes = 0;
86134
86504
  searchFrom = newlineIndex + 1;
86135
86505
  }
86136
- if (searchFrom < chunk.length) openChunks.push(chunk.subarray(searchFrom));
86137
86506
  }
86138
86507
  } catch (error) {
86139
86508
  if (error.code === "ENOENT") return;
86140
86509
  throw error;
86141
86510
  }
86142
- if (openChunks.length > 0) {
86143
- lineNumber++;
86144
- lines.push({
86145
- data: Buffer.concat(openChunks),
86146
- lineNumber,
86147
- allowTruncated: true
86148
- });
86511
+ if (!decided && headLength > 0) {
86512
+ decided = true;
86513
+ retaining = keepLine(lineNumber + 1);
86514
+ if (retaining) openChunks.push(Buffer.from(head.subarray(0, headLength)));
86149
86515
  }
86150
- let skipFoldedBeforeSnapshot = false;
86151
- if (lines.length > 0) try {
86152
- skipFoldedBeforeSnapshot = JSON.parse(lines[0].data.toString("utf8")).protocol_version === "1.4";
86153
- } catch {
86154
- skipFoldedBeforeSnapshot = false;
86155
- }
86156
- for (const entry of lines) {
86157
- if (skipFoldedBeforeSnapshot && entry.lineNumber < lastSnapshotLineNumber && startsWithAnyPrefix(entry.data, SNAPSHOT_FOLDED_LINE_PREFIX_BYTES)) continue;
86158
- const record = parseRecordLine(entry.data.toString("utf8"), entry.lineNumber, this.filePath, entry.allowTruncated);
86516
+ if (retaining && openChunks.length > 0) {
86517
+ lineNumber++;
86518
+ let lineData = retainedLine();
86519
+ if (lineData.length > 0 && lineData.at(-1) === 13) lineData = lineData.subarray(0, lineData.length - 1);
86520
+ const record = parseRecordLine(lineData.toString("utf8"), lineNumber, this.filePath, true);
86159
86521
  if (record !== void 0) yield record;
86522
+ } else if (!retaining) skippedBytes += lineBytes;
86523
+ this.droppedBytes = skippedBytes;
86524
+ }
86525
+ /** Folded-history bytes skipped by the most recent full read(). */
86526
+ droppedBytesOnLastRead() {
86527
+ return this.droppedBytes;
86528
+ }
86529
+ /** Whether the last read() skipped enough folded history to be worth reclaiming on disk. */
86530
+ shouldCompactOnResume() {
86531
+ return this.droppedBytes >= (this.options.compactThresholdBytes ?? 8388608);
86532
+ }
86533
+ /**
86534
+ * Physically drop every record that predates the last `context.snapshot`
86535
+ * (exactly the lines `read()` skips). Byte-preserving: retained lines are
86536
+ * copied verbatim, so unknown records, CRLF framing and blob references
86537
+ * survive untouched. Writes a temp file, fsyncs it, then atomically renames
86538
+ * it over the wire. While it runs, appends queue up instead of draining
86539
+ * (see compacting) and are flushed right after the swap.
86540
+ */
86541
+ async compact() {
86542
+ await this.flush();
86543
+ this.compacting = true;
86544
+ const tmpPath = `${this.filePath}.${process.pid}.${this.rewriteSeq++}.compact.tmp`;
86545
+ let swapped = false;
86546
+ try {
86547
+ const size = await fileSize(this.filePath);
86548
+ if (size === void 0 || size === 0) return;
86549
+ const lastSnapshotLineNumber = await this.scanLastSnapshotLine(size);
86550
+ if (lastSnapshotLineNumber === void 0) return;
86551
+ const directory = dirname$2(this.filePath);
86552
+ const tmp = await open(tmpPath, "w");
86553
+ let written = 0;
86554
+ try {
86555
+ written = await copyRetainingLines(this.filePath, tmp, size, lastSnapshotLineNumber);
86556
+ await tmp.sync();
86557
+ } finally {
86558
+ await tmp.close();
86559
+ }
86560
+ if (written === size) {
86561
+ this.droppedBytes = 0;
86562
+ return;
86563
+ }
86564
+ if (await fileSize(this.filePath) !== size) return;
86565
+ await rename(tmpPath, this.filePath);
86566
+ swapped = true;
86567
+ await syncDir(directory);
86568
+ this.directorySynced = true;
86569
+ this.droppedBytes = 0;
86570
+ } finally {
86571
+ if (!swapped) await rm(tmpPath, { force: true }).catch(() => {});
86572
+ this.compacting = false;
86573
+ if (this.shouldClear || this.pendingRecords.length > 0) this.scheduleFlush();
86160
86574
  }
86161
86575
  }
86162
86576
  append(input) {
@@ -86172,6 +86586,7 @@ var FileSystemAgentRecordPersistence = class {
86172
86586
  }
86173
86587
  async flush() {
86174
86588
  this.throwIfError();
86589
+ while (this.compacting) await new Promise((resolve) => setTimeout(resolve, 10));
86175
86590
  while (this.flushPromise !== void 0 || this.shouldClear || this.pendingRecords.length > 0) {
86176
86591
  await this.ensureFlush();
86177
86592
  this.throwIfError();
@@ -86186,6 +86601,7 @@ var FileSystemAgentRecordPersistence = class {
86186
86601
  });
86187
86602
  }
86188
86603
  ensureFlush() {
86604
+ if (this.compacting) return Promise.resolve();
86189
86605
  if (this.flushPromise !== void 0) return this.flushPromise;
86190
86606
  const promise = this.drainPendingRecords().catch((error) => {
86191
86607
  this.error = error;
@@ -86201,20 +86617,20 @@ var FileSystemAgentRecordPersistence = class {
86201
86617
  if (this.error !== void 0) throw this.error;
86202
86618
  }
86203
86619
  async drainPendingRecords() {
86204
- while (this.shouldClear || this.pendingRecords.length > 0) await this.drainBatch();
86620
+ while (!this.compacting && (this.shouldClear || this.pendingRecords.length > 0)) await this.drainBatch();
86205
86621
  }
86206
86622
  async drainBatch() {
86207
86623
  const shouldClear = this.shouldClear;
86208
86624
  const batch = this.pendingRecords.splice(0);
86209
86625
  this.shouldClear = false;
86210
- const content = (this.options.blobStore !== void 0 ? await Promise.all(batch.map((record) => this.options.blobStore.offload(record))) : batch).map((e) => JSON.stringify(e) + "\n").join("");
86626
+ const writable = this.options.blobStore !== void 0 ? await Promise.all(batch.map((record) => this.options.blobStore.offload(record))) : batch;
86211
86627
  const directory = dirname$2(this.filePath);
86212
86628
  await mkdir(directory, { recursive: true });
86213
86629
  if (shouldClear) {
86214
86630
  const tmpPath = `${this.filePath}.${process.pid}.${this.rewriteSeq++}.tmp`;
86215
86631
  const tmp = await open(tmpPath, "w");
86216
86632
  try {
86217
- if (content.length > 0) await tmp.writeFile(content, "utf8");
86633
+ await writeChunked(tmp, writable);
86218
86634
  await tmp.sync();
86219
86635
  } finally {
86220
86636
  await tmp.close();
@@ -86226,7 +86642,7 @@ var FileSystemAgentRecordPersistence = class {
86226
86642
  }
86227
86643
  const fh = await open(this.filePath, "a");
86228
86644
  try {
86229
- if (content.length > 0) await fh.writeFile(content, "utf8");
86645
+ await writeChunked(fh, writable);
86230
86646
  await fh.sync();
86231
86647
  } finally {
86232
86648
  await fh.close();
@@ -86237,6 +86653,94 @@ var FileSystemAgentRecordPersistence = class {
86237
86653
  }
86238
86654
  }
86239
86655
  };
86656
+ const MAX_WRITE_CHUNK_CHARS = 1e6;
86657
+ /**
86658
+ * Byte-preserving filter pass for compact(): copies every line that
86659
+ * streamRecords() would retain (same fold decision, same unterminated-tail
86660
+ * tolerance) into `out`, returning the number of bytes written. Kept in the
86661
+ * same shape as streamRecords() on purpose — the two must agree on which
86662
+ * lines survive; change both together.
86663
+ */
86664
+ async function copyRetainingLines(filePath, out, size, lastSnapshotLineNumber) {
86665
+ const head = Buffer.allocUnsafe(SNAPSHOT_FOLDED_MAX_PREFIX_BYTES);
86666
+ let headLength = 0;
86667
+ let openChunks = [];
86668
+ let decided = false;
86669
+ let retaining = false;
86670
+ let headerSeen = false;
86671
+ let skipFoldedBeforeSnapshot = false;
86672
+ let lineNumber = 0;
86673
+ let written = 0;
86674
+ const keepLine = (currentLine) => !(skipFoldedBeforeSnapshot && currentLine < lastSnapshotLineNumber && headStartsWithAnyPrefix(head, headLength, SNAPSHOT_FOLDED_LINE_PREFIX_BYTES));
86675
+ const writeRetainedLine = async () => {
86676
+ const data = openChunks.length === 1 ? openChunks[0] : Buffer.concat(openChunks);
86677
+ if (data.length > 0) {
86678
+ await out.write(data);
86679
+ written += data.length;
86680
+ }
86681
+ };
86682
+ const stream = createReadStream(filePath, { end: size - 1 });
86683
+ for await (const chunk of stream) {
86684
+ let searchFrom = 0;
86685
+ for (;;) {
86686
+ const newlineIndex = chunk.indexOf(10, searchFrom);
86687
+ const end = newlineIndex === -1 ? chunk.length : newlineIndex;
86688
+ if (!decided && end > searchFrom) {
86689
+ const take = Math.min(SNAPSHOT_FOLDED_MAX_PREFIX_BYTES - headLength, end - searchFrom);
86690
+ chunk.copy(head, headLength, searchFrom, searchFrom + take);
86691
+ headLength += take;
86692
+ searchFrom += take;
86693
+ if (headLength === SNAPSHOT_FOLDED_MAX_PREFIX_BYTES) {
86694
+ decided = true;
86695
+ retaining = keepLine(lineNumber + 1);
86696
+ if (retaining) openChunks.push(Buffer.from(head));
86697
+ }
86698
+ }
86699
+ if (retaining && end > searchFrom) openChunks.push(chunk.subarray(searchFrom, end));
86700
+ if (newlineIndex === -1) break;
86701
+ lineNumber++;
86702
+ if (!decided) {
86703
+ decided = true;
86704
+ retaining = keepLine(lineNumber);
86705
+ if (retaining && headLength > 0) openChunks.push(Buffer.from(head.subarray(0, headLength)));
86706
+ }
86707
+ if (retaining) {
86708
+ openChunks.push(chunk.subarray(newlineIndex, newlineIndex + 1));
86709
+ if (!headerSeen) {
86710
+ headerSeen = true;
86711
+ let lineData = openChunks.length === 1 ? openChunks[0] : Buffer.concat(openChunks);
86712
+ if (lineData.length > 0 && lineData.at(-1) === 10) lineData = lineData.subarray(0, lineData.length - 1);
86713
+ if (lineData.length > 0 && lineData.at(-1) === 13) lineData = lineData.subarray(0, lineData.length - 1);
86714
+ skipFoldedBeforeSnapshot = readSkipFoldedFlag(lineData.toString("utf8"));
86715
+ }
86716
+ await writeRetainedLine();
86717
+ }
86718
+ openChunks = [];
86719
+ headLength = 0;
86720
+ decided = false;
86721
+ retaining = false;
86722
+ searchFrom = newlineIndex + 1;
86723
+ }
86724
+ }
86725
+ if (!decided && headLength > 0) {
86726
+ decided = true;
86727
+ retaining = keepLine(lineNumber + 1);
86728
+ if (retaining) openChunks.push(Buffer.from(head.subarray(0, headLength)));
86729
+ }
86730
+ if (retaining && openChunks.length > 0) await writeRetainedLine();
86731
+ return written;
86732
+ }
86733
+ async function writeChunked(handle, records) {
86734
+ let chunk = "";
86735
+ for (const record of records) {
86736
+ chunk += JSON.stringify(record) + "\n";
86737
+ if (chunk.length >= MAX_WRITE_CHUNK_CHARS) {
86738
+ await handle.write(chunk, null, "utf8");
86739
+ chunk = "";
86740
+ }
86741
+ }
86742
+ if (chunk.length > 0) await handle.write(chunk, null, "utf8");
86743
+ }
86240
86744
  function parseRecordLine(line, lineNumber, filePath, allowTruncated) {
86241
86745
  if (line.length === 0) return void 0;
86242
86746
  try {
@@ -86570,6 +87074,12 @@ function restoreAgentRecord(agent, input) {
86570
87074
  case "context.append_message":
86571
87075
  agent.context.appendMessage(input.message);
86572
87076
  return;
87077
+ case "context.stream_draft":
87078
+ agent.replayBuilder.replacePartialDraft(input.turnId, {
87079
+ text: input.text,
87080
+ think: input.think
87081
+ });
87082
+ return;
86573
87083
  case "context.append_loop_event":
86574
87084
  agent.context.appendLoopEvent(input.event);
86575
87085
  return;
@@ -86651,6 +87161,7 @@ var AgentRecords = class {
86651
87161
  let hasMetadata = false;
86652
87162
  let shouldRewrite = false;
86653
87163
  let warning;
87164
+ let buffered = false;
86654
87165
  const replayedRecords = [];
86655
87166
  for await (const record of this.persistence.read()) {
86656
87167
  if (!hasMetadata) {
@@ -86665,38 +87176,45 @@ var AgentRecords = class {
86665
87176
  migrations = resolveWireMigrations(readVersion);
86666
87177
  shouldRewrite = readVersion !== "1.4";
86667
87178
  }
87179
+ buffered = shouldRewrite || warning !== void 0;
86668
87180
  }
86669
87181
  let migratedRecord = migrateWireRecord(record, migrations);
86670
87182
  if (migratedRecord.type === "metadata") migratedRecord = {
86671
87183
  ...migratedRecord,
86672
87184
  protocol_version: "1.4"
86673
87185
  };
86674
- replayedRecords.push(migratedRecord);
86675
- }
86676
- let snapshotIndex = -1;
86677
- for (let i = replayedRecords.length - 1; i >= 0; i--) if (replayedRecords[i]?.type === "context.snapshot") {
86678
- snapshotIndex = i;
86679
- break;
86680
- }
86681
- let foldedCompactionSummary;
86682
- for (let i = 0; i < replayedRecords.length; i++) {
86683
- const record = replayedRecords[i];
86684
- if (!record) continue;
86685
- if (i < snapshotIndex && isSnapshotFoldedContextRecord(record.type)) {
86686
- if (record.type === "context.apply_compaction") foldedCompactionSummary = record.summary;
87186
+ if (buffered) {
87187
+ replayedRecords.push(migratedRecord);
86687
87188
  continue;
86688
87189
  }
86689
- this.restore(record);
86690
- if (record.type === "context.snapshot" && foldedCompactionSummary !== void 0) {
86691
- recoverMemosFromCompactionSummary(this.agent, foldedCompactionSummary);
86692
- foldedCompactionSummary = void 0;
87190
+ this.restore(migratedRecord);
87191
+ }
87192
+ if (buffered) {
87193
+ let snapshotIndex = -1;
87194
+ for (let i = replayedRecords.length - 1; i >= 0; i--) if (replayedRecords[i]?.type === "context.snapshot") {
87195
+ snapshotIndex = i;
87196
+ break;
87197
+ }
87198
+ let foldedCompactionSummary;
87199
+ for (let i = 0; i < replayedRecords.length; i++) {
87200
+ const record = replayedRecords[i];
87201
+ if (!record) continue;
87202
+ if (i < snapshotIndex && isSnapshotFoldedContextRecord(record.type)) {
87203
+ if (record.type === "context.apply_compaction") foldedCompactionSummary = record.summary;
87204
+ continue;
87205
+ }
87206
+ this.restore(record);
87207
+ if (record.type === "context.snapshot" && foldedCompactionSummary !== void 0) {
87208
+ recoverMemosFromCompactionSummary(this.agent, foldedCompactionSummary);
87209
+ foldedCompactionSummary = void 0;
87210
+ }
86693
87211
  }
86694
87212
  }
86695
87213
  this.agent.context.dropVacuousOpenMessages();
86696
87214
  if (shouldRewrite) {
86697
87215
  this.persistence.rewrite(replayedRecords);
86698
87216
  await this.persistence.flush();
86699
- }
87217
+ } else if (this.persistence.shouldCompactOnResume?.() === true && this.persistence.compact !== void 0) await this.persistence.compact();
86700
87218
  if (this.agent.blobStore !== void 0) for (const msg of this.agent.context.history) await this.agent.blobStore.rehydrateParts(msg.content);
86701
87219
  return { warning };
86702
87220
  }
@@ -86723,6 +87241,8 @@ var ReplayBuilder = class {
86723
87241
  records = [];
86724
87242
  /** Indices (into `records`) of user-turn-start records, oldest first. */
86725
87243
  userTurnStarts = [];
87244
+ /** Index (into `records`) of the partial-draft record per turn, if present. */
87245
+ partialDraftIndices = /* @__PURE__ */ new Map();
86726
87246
  constructor(agent) {
86727
87247
  this.agent = agent;
86728
87248
  }
@@ -86735,6 +87255,43 @@ var ReplayBuilder = class {
86735
87255
  const dropBefore = this.userTurnStarts[this.userTurnStarts.length - 10];
86736
87256
  this.records.splice(0, dropBefore);
86737
87257
  this.userTurnStarts = this.userTurnStarts.slice(-10).map((i) => i - dropBefore);
87258
+ for (const [turnId, index] of this.partialDraftIndices) {
87259
+ const shifted = index - dropBefore;
87260
+ if (shifted < 0) this.partialDraftIndices.delete(turnId);
87261
+ else this.partialDraftIndices.set(turnId, shifted);
87262
+ }
87263
+ }
87264
+ /**
87265
+ * Track the latest surviving stream draft for a turn during restore: the
87266
+ * first draft pushes a `stream_draft_partial` record, later drafts replace
87267
+ * it in place (no index shifts), and a clearing draft (empty text+think)
87268
+ * removes it. Only used while `agent.records.restoring` is true.
87269
+ */
87270
+ replacePartialDraft(turnId, draft) {
87271
+ if (!this.agent.records.restoring) return;
87272
+ const existing = this.partialDraftIndices.get(turnId);
87273
+ if (draft.text.length === 0 && draft.think.length === 0) {
87274
+ if (existing !== void 0) {
87275
+ this.records.splice(existing, 1);
87276
+ this.partialDraftIndices.delete(turnId);
87277
+ for (const [id, index] of this.partialDraftIndices) if (index > existing) this.partialDraftIndices.set(id, index - 1);
87278
+ this.userTurnStarts = this.userTurnStarts.map((i) => i > existing ? i - 1 : i);
87279
+ }
87280
+ return;
87281
+ }
87282
+ const record = {
87283
+ type: "stream_draft_partial",
87284
+ turnId,
87285
+ text: draft.text,
87286
+ think: draft.think
87287
+ };
87288
+ if (existing !== void 0 && this.records[existing]?.type === "stream_draft_partial") {
87289
+ this.records[existing] = record;
87290
+ return;
87291
+ }
87292
+ if (existing !== void 0) this.partialDraftIndices.delete(turnId);
87293
+ this.records.push(record);
87294
+ this.partialDraftIndices.set(turnId, this.records.length - 1);
86738
87295
  }
86739
87296
  buildResult() {
86740
87297
  return this.records;
@@ -95041,7 +95598,8 @@ async function executeLoopStep(deps) {
95041
95598
  dispatchEvent,
95042
95599
  turnId,
95043
95600
  currentStep,
95044
- stepUuid
95601
+ stepUuid,
95602
+ onStreamingDraft: deps.onStreamingDraft ?? (() => {})
95045
95603
  })
95046
95604
  };
95047
95605
  let response;
@@ -95175,18 +95733,75 @@ function stepEndProviderDiagnostics(response, stopReason) {
95175
95733
  ...response.rawFinishReason !== void 0 ? { rawFinishReason: response.rawFinishReason } : {}
95176
95734
  };
95177
95735
  }
95736
+ /**
95737
+ * Accumulates the in-flight stream for the crash-recovery draft and flushes
95738
+ * it through `onDraft` at most once per 1.5s or 4k new characters, so a fast
95739
+ * stream does not write the wire on every delta while a slow stream still
95740
+ * checkpoints promptly. Failures in the sink never break the stream.
95741
+ */
95742
+ var StreamDraftTracker = class {
95743
+ onDraft;
95744
+ text = "";
95745
+ think = "";
95746
+ charsSinceFlush = 0;
95747
+ lastFlushAt = Date.now();
95748
+ wireHasDraft = false;
95749
+ constructor(onDraft) {
95750
+ this.onDraft = onDraft;
95751
+ }
95752
+ onText(delta) {
95753
+ this.text += delta;
95754
+ this.charsSinceFlush += delta.length;
95755
+ this.maybeFlush();
95756
+ }
95757
+ onThink(delta) {
95758
+ this.think += delta;
95759
+ this.charsSinceFlush += delta.length;
95760
+ this.maybeFlush();
95761
+ }
95762
+ /**
95763
+ * Real content parts are landing: drop the draft (persisted as empty) — but
95764
+ * only if the wire actually carries one. A stream that finished before any
95765
+ * draft flush (fast mock streams, short replies) must not pollute the wire
95766
+ * with an empty clear record.
95767
+ */
95768
+ clear() {
95769
+ if (!this.wireHasDraft) return;
95770
+ this.text = "";
95771
+ this.think = "";
95772
+ this.charsSinceFlush = 0;
95773
+ this.flush();
95774
+ }
95775
+ maybeFlush() {
95776
+ if (Date.now() - this.lastFlushAt < DRAFT_FLUSH_INTERVAL_MS && this.charsSinceFlush < DRAFT_FLUSH_MIN_CHARS) return;
95777
+ this.flush();
95778
+ }
95779
+ flush() {
95780
+ this.lastFlushAt = Date.now();
95781
+ this.charsSinceFlush = 0;
95782
+ try {
95783
+ this.onDraft(this.text, this.think);
95784
+ this.wireHasDraft = this.text.length > 0 || this.think.length > 0;
95785
+ } catch {}
95786
+ }
95787
+ };
95788
+ const DRAFT_FLUSH_INTERVAL_MS = 1500;
95789
+ const DRAFT_FLUSH_MIN_CHARS = 4096;
95178
95790
  function createChatStreamingCallbacks(deps) {
95179
- const { dispatchEvent, turnId, currentStep, stepUuid } = deps;
95791
+ const { dispatchEvent, turnId, currentStep, stepUuid, onStreamingDraft } = deps;
95180
95792
  let textIndex = 0;
95181
95793
  let thinkIndex = 0;
95794
+ const draftTracker = new StreamDraftTracker(onStreamingDraft);
95182
95795
  return {
95183
95796
  onTextDelta: (delta) => {
95797
+ draftTracker.onText(delta);
95184
95798
  dispatchEvent({
95185
95799
  type: "text.delta",
95186
95800
  delta
95187
95801
  });
95188
95802
  },
95189
95803
  onThinkDelta: (delta) => {
95804
+ draftTracker.onThink(delta);
95190
95805
  dispatchEvent({
95191
95806
  type: "thinking.delta",
95192
95807
  delta
@@ -95202,6 +95817,7 @@ function createChatStreamingCallbacks(deps) {
95202
95817
  },
95203
95818
  onTextPart: async (part) => {
95204
95819
  const index = textIndex++;
95820
+ draftTracker.clear();
95205
95821
  await dispatchEvent({
95206
95822
  type: "block.start",
95207
95823
  uuid: randomUUID(),
@@ -95231,6 +95847,7 @@ function createChatStreamingCallbacks(deps) {
95231
95847
  },
95232
95848
  onThinkPart: async (part) => {
95233
95849
  const index = thinkIndex++;
95850
+ draftTracker.clear();
95234
95851
  await dispatchEvent({
95235
95852
  type: "block.start",
95236
95853
  uuid: randomUUID(),
@@ -95310,7 +95927,8 @@ async function runTurn(input) {
95310
95927
  maxRetryAttempts,
95311
95928
  recordUsage: recordStepUsage,
95312
95929
  hasPendingSteer: input.hasPendingSteer,
95313
- mediaProjection
95930
+ mediaProjection,
95931
+ onStreamingDraft: input.onStreamingDraft
95314
95932
  });
95315
95933
  activeStep = void 0;
95316
95934
  if (stepResult.totalCalls > 0) {
@@ -101542,6 +102160,15 @@ var TurnFlow = class {
101542
102160
  maxSteps: loopControl?.maxStepsPerTurn,
101543
102161
  maxRetryAttempts: loopControl?.maxRetriesPerStep,
101544
102162
  hasPendingSteer: () => this.steerBuffer.some((steer) => steer.origin.kind === "user" && steer.interrupt !== false),
102163
+ onStreamingDraft: (text, think) => {
102164
+ this.agent.records.logRecord({
102165
+ type: "context.stream_draft",
102166
+ turnId: String(turnId),
102167
+ text,
102168
+ think,
102169
+ time: Date.now()
102170
+ });
102171
+ },
101545
102172
  hooks: {
101546
102173
  beforeStep: async ({ signal: stepSignal, stepNumber }) => {
101547
102174
  this.flushSteerBuffer();
@@ -102219,6 +102846,8 @@ var Agent = class {
102219
102846
  /** Read-only manifest of the core engine subsystems (see {@link AgentServices}). */
102220
102847
  services;
102221
102848
  lastLlmConfigLogSignature;
102849
+ /** Last rendered system prompt written to a request.header record. */
102850
+ lastRequestHeaderSystemPrompt;
102222
102851
  sharedEmbeddingEngine;
102223
102852
  resolveRuntimeSystemPrompt;
102224
102853
  constructor(options) {
@@ -102523,12 +103152,14 @@ var Agent = class {
102523
103152
  for (const message of history) if (message.partial === true) partialMessageCount += 1;
102524
103153
  const requestMetadata = { estimatedInputTokens: estimateTokens$1(systemPrompt) + estimateTokensForMessages(history) + estimateTokensForTools(tools) };
102525
103154
  if (partialMessageCount > 0) requestMetadata.partialMessageCount = partialMessageCount;
103155
+ const systemPromptReused = systemPrompt === this.lastRequestHeaderSystemPrompt;
103156
+ this.lastRequestHeaderSystemPrompt = systemPrompt;
102526
103157
  this.records.logRecord({
102527
103158
  type: "request.header",
102528
103159
  provider: provider.name,
102529
103160
  model: provider.modelName,
102530
103161
  modelAlias: this.config.modelAlias ?? "",
102531
- systemPrompt,
103162
+ ...systemPromptReused ? { systemPromptReused: true } : { systemPrompt },
102532
103163
  activeTools: tools.map((t) => t.name),
102533
103164
  messagesCount: history.length,
102534
103165
  estimatedInputTokens: requestMetadata.estimatedInputTokens ?? 0
@@ -121686,7 +122317,7 @@ var LocalFetchURLProvider = class {
121686
122317
  * `confident` distinguishes detection strength: a convertible Content-Type
121687
122318
  * means conversion failure is a real error (corrupt document), while an
121688
122319
  * extension-only guess (e.g. an HTML viewer page at a .pdf URL) falls back
121689
- * to the normal text extraction path instead of erroring (omp's behavior).
122320
+ * to the normal text extraction path instead of erroring.
121690
122321
  */
121691
122322
  async fetchDocument(response, extension, contentType, confident) {
121692
122323
  const bytes = new Uint8Array(await response.arrayBuffer());
@@ -121797,7 +122428,7 @@ const RESULT_TITLE_RE$1 = /<a\b[^>]*\bclass="[^"]*\bresult__a\b[^"]*"[^>]*\bhref
121797
122428
  const RESULT_SNIPPET_RE$1 = /<(?:a|div|span)\b[^>]*\bclass="[^"]*\bresult__snippet\b[^"]*"[^>]*>([\s\S]*?)<\/(?:a|div|span)>/;
121798
122429
  /** Strip inline tags (DDG wraps query terms in `<b>`) and decode entities. */
121799
122430
  function decodeHtmlText$2(value) {
121800
- return value.replace(/<[^>]*>/g, " ").replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code))).replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCharCode(Number.parseInt(code, 16))).replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, "\"").replace(/&#39;|&apos;/gi, "'").replace(/\s+/g, " ").trim();
122431
+ return value.replaceAll(/<[^>]*>/g, " ").replaceAll(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code))).replaceAll(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(Number.parseInt(code, 16))).replaceAll(/&nbsp;/gi, " ").replaceAll(/&amp;/gi, "&").replaceAll(/&lt;/gi, "<").replaceAll(/&gt;/gi, ">").replaceAll(/&quot;/gi, "\"").replaceAll(/&#39;|&apos;/gi, "'").replaceAll(/\s+/g, " ").trim();
121801
122432
  }
121802
122433
  /**
121803
122434
  * Resolve a DDG result href to the underlying target URL. DDG routes
@@ -121806,7 +122437,7 @@ function decodeHtmlText$2(value) {
121806
122437
  */
121807
122438
  function unwrapResultUrl(href) {
121808
122439
  if (href === "") return void 0;
121809
- const decoded = href.replace(/&amp;/gi, "&");
122440
+ const decoded = href.replaceAll(/&amp;/gi, "&");
121810
122441
  const wrapMatch = decoded.match(/[?&]uddg=([^&]+)/);
121811
122442
  if (wrapMatch?.[1] !== void 0) try {
121812
122443
  return decodeURIComponent(wrapMatch[1]);
@@ -122125,27 +122756,32 @@ function buildExportManifest(args) {
122125
122756
  }
122126
122757
  //#endregion
122127
122758
  //#region ../../packages/agent-core/src/session/export/wire-scan.ts
122759
+ /**
122760
+ * Scan a session's `wire.jsonl` for activity timestamps and the first user
122761
+ * input.
122762
+ *
122763
+ * The log is append-only and line-delimited, but a long-lived session can grow
122764
+ * it to gigabytes, so it is streamed rather than read into a single string: a
122765
+ * `StringDecoder` turns each chunk into text without splitting multi-byte
122766
+ * characters, complete lines are consumed as they arrive, and only the four
122767
+ * aggregated values are retained. Any read failure (including a missing file)
122768
+ * yields an empty scan.
122769
+ */
122128
122770
  async function scanSessionWire(sessionDir) {
122129
- let raw;
122130
- try {
122131
- raw = await readFile(join$1(sessionDir, "wire.jsonl"), "utf-8");
122132
- } catch {
122133
- return {};
122134
- }
122135
122771
  let firstActivityMs;
122136
122772
  let lastActivityMs;
122137
122773
  let lastUserMessageMs;
122138
122774
  let firstUserInput;
122139
- for (const line of raw.split("\n")) {
122775
+ const consumeLine = (line) => {
122140
122776
  const trimmed = line.trim();
122141
- if (trimmed.length === 0) continue;
122777
+ if (trimmed.length === 0) return;
122142
122778
  let parsed;
122143
122779
  try {
122144
122780
  parsed = JSON.parse(trimmed);
122145
122781
  } catch {
122146
- continue;
122782
+ return;
122147
122783
  }
122148
- if (typeof parsed !== "object" || parsed === null) continue;
122784
+ if (typeof parsed !== "object" || parsed === null) return;
122149
122785
  const record = parsed;
122150
122786
  const timeMs = typeof record.time === "number" ? normalizeTimestampMs(record.time) : void 0;
122151
122787
  if (timeMs !== void 0) {
@@ -122156,6 +122792,27 @@ async function scanSessionWire(sessionDir) {
122156
122792
  if (timeMs !== void 0) lastUserMessageMs = timeMs;
122157
122793
  if (firstUserInput === void 0 && typeof record.userInput === "string" && record.userInput.trim().length > 0) firstUserInput = record.userInput;
122158
122794
  }
122795
+ };
122796
+ try {
122797
+ const decoder = new StringDecoder("utf8");
122798
+ let pending = "";
122799
+ let scanned = 0;
122800
+ const stream = createReadStream(join$1(sessionDir, "wire.jsonl"));
122801
+ for await (const chunk of stream) {
122802
+ pending += decoder.write(chunk);
122803
+ let newlineIndex = pending.indexOf("\n", scanned);
122804
+ while (newlineIndex !== -1) {
122805
+ consumeLine(pending.slice(0, newlineIndex));
122806
+ pending = pending.slice(newlineIndex + 1);
122807
+ scanned = 0;
122808
+ newlineIndex = pending.indexOf("\n", scanned);
122809
+ }
122810
+ scanned = pending.length;
122811
+ }
122812
+ pending += decoder.end();
122813
+ if (pending.length > 0) consumeLine(pending);
122814
+ } catch {
122815
+ return {};
122159
122816
  }
122160
122817
  return {
122161
122818
  firstActivityMs,
@@ -123053,22 +123710,56 @@ async function writeExportZip(args) {
123053
123710
  await mkdir(dirname$2(args.outputPath), { recursive: true });
123054
123711
  const entries = ["manifest.json"];
123055
123712
  const zip = new import_yazl.ZipFile();
123713
+ const zipFailure = new Promise((_resolve, reject) => {
123714
+ zip.once("error", reject);
123715
+ });
123056
123716
  zip.addBuffer(Buffer.from(JSON.stringify(args.manifest, null, 2), "utf-8"), "manifest.json");
123057
123717
  for (const abs of args.sessionFiles) {
123058
123718
  const rel = relative$1(args.sessionDir, abs).split(/[\\/]/).join("/");
123059
- const data = await readFile(abs);
123060
- zip.addBuffer(data, rel);
123719
+ addStreamedFile(zip, abs, rel);
123061
123720
  entries.push(rel);
123062
123721
  }
123063
- for (const extra of args.extraEntries ?? []) try {
123064
- const data = "data" in extra ? extra.data : await readFile(extra.source);
123065
- zip.addBuffer(data, extra.target);
123722
+ for (const extra of args.extraEntries ?? []) {
123723
+ if ("data" in extra) {
123724
+ zip.addBuffer(extra.data, extra.target);
123725
+ entries.push(extra.target);
123726
+ continue;
123727
+ }
123728
+ try {
123729
+ if (!(await stat(extra.source)).isFile()) continue;
123730
+ } catch {
123731
+ continue;
123732
+ }
123733
+ addStreamedFile(zip, extra.source, extra.target);
123066
123734
  entries.push(extra.target);
123067
- } catch {}
123068
- zip.end();
123069
- await pipeline$1(zip.outputStream, createWriteStream$1(args.outputPath));
123735
+ }
123736
+ try {
123737
+ zip.end();
123738
+ await Promise.race([pipeline$1(zip.outputStream, createWriteStream$1(args.outputPath)), zipFailure]);
123739
+ } catch (error) {
123740
+ await rm(args.outputPath, { force: true }).catch(() => {});
123741
+ throw error;
123742
+ }
123070
123743
  return entries;
123071
123744
  }
123745
+ /**
123746
+ * Add a file as a lazily-streamed zip entry.
123747
+ *
123748
+ * `ZipFile.addFile` records the size from a stat and then asserts the byte count
123749
+ * while reading, so exporting a session that is still appending to its wire or
123750
+ * log files — or that rotates them mid-export — would abort the whole export.
123751
+ * Reading lazily without a declared size keeps the entry tolerant of growth,
123752
+ * opens one file at a time, and still never holds the file in memory.
123753
+ * (`addReadStreamLazy` is the entry point yazl's own `addFile` uses; the
123754
+ * published typings just do not declare it.)
123755
+ */
123756
+ function addStreamedFile(zip, source, target) {
123757
+ zip.addReadStreamLazy(target, (callback) => {
123758
+ const stream = createReadStream(source);
123759
+ stream.on("error", (error) => zip.emit("error", error));
123760
+ callback(null, stream);
123761
+ });
123762
+ }
123072
123763
  //#endregion
123073
123764
  //#region ../../packages/agent-core/src/session/export/session-export.ts
123074
123765
  const SESSION_LOG_REL = "logs/scream-code.log";
@@ -123086,10 +123777,9 @@ async function exportSessionDirectory(input) {
123086
123777
  let bundledGlobal = false;
123087
123778
  const globalPath = input.globalLogPath ?? (input.homeDir === void 0 ? void 0 : resolveGlobalLogPath(input.homeDir));
123088
123779
  if (input.request.includeGlobalLog === true && globalPath !== void 0) {
123089
- const data = await readOptionalFile(globalPath);
123090
- if (data !== void 0) {
123780
+ if (await pathIsFile(globalPath)) {
123091
123781
  extras.push({
123092
- data,
123782
+ source: globalPath,
123093
123783
  target: GLOBAL_LOG_REL
123094
123784
  });
123095
123785
  bundledGlobal = true;
@@ -123119,11 +123809,11 @@ async function exportSessionDirectory(input) {
123119
123809
  manifest
123120
123810
  };
123121
123811
  }
123122
- async function readOptionalFile(path) {
123812
+ async function pathIsFile(path) {
123123
123813
  try {
123124
- return await readFile(path);
123814
+ return (await stat(path)).isFile();
123125
123815
  } catch {
123126
- return;
123816
+ return false;
123127
123817
  }
123128
123818
  }
123129
123819
  //#endregion
@@ -123609,6 +124299,10 @@ function hasCustomTitle(metadata) {
123609
124299
  }
123610
124300
  //#endregion
123611
124301
  //#region ../../packages/agent-core/src/session/store/session-index.ts
124302
+ let sessionIndexCache;
124303
+ function invalidateSessionIndexCache() {
124304
+ sessionIndexCache = void 0;
124305
+ }
123612
124306
  function sessionIndexPath(homeDir) {
123613
124307
  return join$1(homeDir, "session_index.jsonl");
123614
124308
  }
@@ -123619,12 +124313,24 @@ async function appendSessionIndexEntry(homeDir, entry) {
123619
124313
  mode: 448
123620
124314
  });
123621
124315
  await appendFile(indexPath, `${JSON.stringify(entry)}\n`, "utf-8");
124316
+ invalidateSessionIndexCache();
123622
124317
  }
123623
124318
  async function readSessionIndex(homeDir, sessionsDir) {
124319
+ const indexPath = sessionIndexPath(homeDir);
124320
+ let stats;
124321
+ try {
124322
+ stats = await stat(indexPath);
124323
+ } catch {
124324
+ sessionIndexCache = void 0;
124325
+ return /* @__PURE__ */ new Map();
124326
+ }
124327
+ const cached = sessionIndexCache;
124328
+ if (cached !== void 0 && cached.homeDir === homeDir && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) return cached.map;
123624
124329
  let raw;
123625
124330
  try {
123626
- raw = await readFile(sessionIndexPath(homeDir), "utf-8");
124331
+ raw = await readFile(indexPath, "utf-8");
123627
124332
  } catch {
124333
+ sessionIndexCache = void 0;
123628
124334
  return /* @__PURE__ */ new Map();
123629
124335
  }
123630
124336
  const result = /* @__PURE__ */ new Map();
@@ -123644,6 +124350,12 @@ async function readSessionIndex(homeDir, sessionsDir) {
123644
124350
  workDir: resolve$2(entry.workDir)
123645
124351
  });
123646
124352
  }
124353
+ sessionIndexCache = {
124354
+ homeDir,
124355
+ mtimeMs: stats.mtimeMs,
124356
+ size: stats.size,
124357
+ map: result
124358
+ };
123647
124359
  return result;
123648
124360
  }
123649
124361
  async function removeSessionIndexEntry(homeDir, sessionId) {
@@ -123665,6 +124377,7 @@ async function removeSessionIndexEntry(homeDir, sessionId) {
123665
124377
  }
123666
124378
  if (kept.length === 0) await writeFile(indexPath, "", "utf-8");
123667
124379
  else await writeFile(indexPath, kept.join("\n") + "\n", "utf-8");
124380
+ invalidateSessionIndexCache();
123668
124381
  }
123669
124382
  function parseIndexLine(line) {
123670
124383
  try {
@@ -127740,7 +128453,7 @@ const backwardFrames = Array.from({ length: 6 }, (_, i) => ({
127740
128453
  const PULSE_WAVE_FRAMES = [...forwardFrames, ...backwardFrames];
127741
128454
  //#endregion
127742
128455
  //#region src/tui/commands/knowledge-store.ts
127743
- let knowledgeStoreInstance;
128456
+ let knowledgeStorePromise;
127744
128457
  let embeddingEngineInstance;
127745
128458
  let embeddingStatus = "idle";
127746
128459
  function getEmbeddingCacheDir() {
@@ -127751,24 +128464,40 @@ function getEmbeddingCacheDir() {
127751
128464
  /** Subdirectory fastembed uses for the BGESmallZH model inside the cache dir. */
127752
128465
  const EMBEDDING_MODEL_DIR = "fast-bge-small-zh-v1.5";
127753
128466
  /**
127754
- * Whether the embedding model has been downloaded to the local cache.
127755
- * Checks the ONNX weights plus the config and tokenizer sidecars — all
127756
- * required for FlagEmbedding.init to load without network access (a missing
127757
- * tokenizer would trigger a network fetch and violate the warm-up's
127758
- * offline-load guarantee).
128467
+ * Any usable cache: the model directory with its weights present.
128468
+ *
128469
+ * Such a cache can still be missing sidecars, which FlagEmbedding repairs from
128470
+ * the Hub while loading — so callers must not treat it as "no model here".
128471
+ * Flagging it as missing would block the very path that repairs it, and
128472
+ * fastembed itself skips downloading whenever the model directory exists.
127759
128473
  */
127760
- function isEmbeddingModelCached() {
128474
+ function hasEmbeddingModelCache() {
127761
128475
  const modelDir = join(getEmbeddingCacheDir(), EMBEDDING_MODEL_DIR);
127762
- return existsSync(join(modelDir, "model_optimized.onnx")) && existsSync(join(modelDir, "config.json")) && existsSync(join(modelDir, "tokenizer.json"));
127763
- }
127764
- async function getKnowledgeStore() {
127765
- if (knowledgeStoreInstance === void 0) {
127766
- knowledgeStoreInstance = new KnowledgeStore(getDataDir());
127767
- await knowledgeStoreInstance.init();
127768
- embeddingEngineInstance = createFastEmbedEngine(getEmbeddingCacheDir());
127769
- knowledgeStoreInstance.setEmbeddingEngine(embeddingEngineInstance);
127770
- }
127771
- return knowledgeStoreInstance;
128476
+ return existsSync(modelDir) && existsSync(join(modelDir, "model_optimized.onnx"));
128477
+ }
128478
+ /**
128479
+ * The store and its embedding engine are built together and published only once
128480
+ * both exist. Publishing the store first meant a single transient failure (a
128481
+ * locked knowledge.db, a full disk, a denied directory) left a store with no
128482
+ * engine that no later call could repair: every download then failed instantly
128483
+ * with "embedding engine not initialized" for the rest of the process.
128484
+ *
128485
+ * Concurrent callers share one build, and a failed build clears the slot so the
128486
+ * next caller retries from scratch.
128487
+ */
128488
+ function getKnowledgeStore() {
128489
+ knowledgeStorePromise ??= (async () => {
128490
+ const store = new KnowledgeStore(getDataDir());
128491
+ await store.init();
128492
+ const engine = createFastEmbedEngine(getEmbeddingCacheDir());
128493
+ store.setEmbeddingEngine(engine);
128494
+ embeddingEngineInstance = engine;
128495
+ return store;
128496
+ })().catch((error) => {
128497
+ knowledgeStorePromise = void 0;
128498
+ throw error;
128499
+ });
128500
+ return knowledgeStorePromise;
127772
128501
  }
127773
128502
  function getEmbeddingStatus() {
127774
128503
  return embeddingStatus;
@@ -127777,16 +128506,12 @@ function getEmbeddingStatus() {
127777
128506
  * Manually trigger the embedding model download/load.
127778
128507
  * Only mutates embeddingStatus; the actual download is delegated to
127779
128508
  * EmbeddingEngine.ensureReady() and saves the model to the shared cache dir.
127780
- * Returns { ok: true } on success, or { ok: false, error } on failure.
127781
128509
  * Concurrent calls join the in-flight download and share its result instead
127782
128510
  * of failing — the startup warm-up and a user-initiated download must never
127783
128511
  * race into a spurious "download already in progress" error.
127784
128512
  */
127785
128513
  async function startManualEmbeddingDownload() {
127786
- if (embeddingEngineInstance === void 0) return {
127787
- ok: false,
127788
- error: "embedding engine not initialized"
127789
- };
128514
+ if (embeddingEngineInstance === void 0) return failureResult("embedding engine not initialized");
127790
128515
  if (embeddingEngineInstance.available) {
127791
128516
  embeddingStatus = "ready";
127792
128517
  return {
@@ -127802,29 +128527,71 @@ async function startManualEmbeddingDownload() {
127802
128527
  return downloadPromise;
127803
128528
  }
127804
128529
  let downloadPromise;
128530
+ function failureResult(error) {
128531
+ return {
128532
+ ok: false,
128533
+ error,
128534
+ failureKind: classifyEmbeddingFailure(error)
128535
+ };
128536
+ }
127805
128537
  async function performDownload() {
127806
128538
  try {
128539
+ const support = await probeLocalEmbeddingSupport();
128540
+ if (!support.supported) {
128541
+ embeddingStatus = "failed";
128542
+ return {
128543
+ ok: false,
128544
+ error: support.error,
128545
+ failureKind: classifyEmbeddingFailure(support.error)
128546
+ };
128547
+ }
127807
128548
  let ok = await embeddingEngineInstance.ensureReady();
127808
128549
  let error = ok ? void 0 : embeddingEngineInstance.lastError;
127809
128550
  if (!ok) {
127810
- clearEmbeddingModelCache(getEmbeddingCacheDir());
127811
- ok = await embeddingEngineInstance.ensureReady();
127812
- error = ok ? void 0 : embeddingEngineInstance.lastError;
128551
+ if (classifyEmbeddingFailure(error) !== "platform") {
128552
+ clearEmbeddingModelCache(getEmbeddingCacheDir());
128553
+ ok = await embeddingEngineInstance.ensureReady();
128554
+ error = ok ? void 0 : embeddingEngineInstance.lastError;
128555
+ }
127813
128556
  }
127814
128557
  embeddingStatus = ok ? "ready" : "failed";
127815
- return {
127816
- ok,
127817
- error
127818
- };
128558
+ return ok ? { ok } : failureResult(error);
127819
128559
  } catch (error) {
127820
128560
  embeddingStatus = "failed";
127821
- return {
127822
- ok: false,
127823
- error: error instanceof Error ? error.message : String(error)
127824
- };
128561
+ return failureResult(error instanceof Error ? error.message : String(error));
127825
128562
  }
127826
128563
  }
127827
128564
  //#endregion
128565
+ //#region src/tui/commands/embedding-failure-hint.ts
128566
+ /**
128567
+ * The line shown above the raw error in a failed embedding download.
128568
+ *
128569
+ * Every cause used to share one hint that blamed the network and suggested a
128570
+ * proxy, which is wrong for a machine that simply has no local model binary —
128571
+ * that single sentence is what made four different failures look like the same
128572
+ * connectivity complaint. Kept apart from the store so the copy can be read and
128573
+ * tested on its own.
128574
+ */
128575
+ function embeddingFailureHint(kind) {
128576
+ switch (kind) {
128577
+ case "platform": return t("knowledge.embedding_platform_hint");
128578
+ case "archive": return t("knowledge.embedding_cache_corrupt_hint");
128579
+ case "network": return t("knowledge.embedding_network_hint");
128580
+ default: return t("knowledge.embedding_unknown_hint");
128581
+ }
128582
+ }
128583
+ /**
128584
+ * The detail block a failure notice shows: the hint, then the first line of the
128585
+ * raw error.
128586
+ *
128587
+ * Only the first line is displayed — Node appends a "Require stack:" block to
128588
+ * module errors, a local path dump no user needs (and the line we ask people to
128589
+ * send us) — while the engine keeps the full text for diagnosis.
128590
+ */
128591
+ function embeddingFailureDetail(kind, error) {
128592
+ return [embeddingFailureHint(kind), error?.split("\n")[0]].filter(Boolean).join("\n");
128593
+ }
128594
+ //#endregion
127828
128595
  //#region src/tui/commands/experimental-flags.ts
127829
128596
  let snapshot = {};
127830
128597
  /** Replace the cached flag snapshot. Call once after fetching via `harness.getExperimentalFlags()`. */
@@ -129573,8 +130340,6 @@ function contentPartsText(parts) {
129573
130340
  * Throws when the file is missing or contains no usable records.
129574
130341
  */
129575
130342
  function buildTraceCells({ wirePath }) {
129576
- const rows = readWireRows(wirePath);
129577
- if (rows.length === 0) throw new Error(`no wire records in ${wirePath}`);
129578
130343
  const cells = [];
129579
130344
  let lastTime;
129580
130345
  let nextIndex = 1;
@@ -129592,6 +130357,11 @@ function buildTraceCells({ wirePath }) {
129592
130357
  startedAt: time,
129593
130358
  ...fields
129594
130359
  };
130360
+ cell.text = truncateDetail(cell.text, MAX_TEXT) ?? "";
130361
+ if (cell.thinkingDetail) cell.thinkingDetail = truncateDetail(cell.thinkingDetail);
130362
+ if (cell.outputDetail) cell.outputDetail = truncateDetail(cell.outputDetail);
130363
+ if (cell.inputDetail) cell.inputDetail = truncateDetail(cell.inputDetail);
130364
+ if (cell.result) cell.result = truncateDetail(cell.result);
129595
130365
  cells.push(cell);
129596
130366
  lastCell = cell;
129597
130367
  return cell;
@@ -129755,99 +130525,102 @@ function buildTraceCells({ wirePath }) {
129755
130525
  default: break;
129756
130526
  }
129757
130527
  };
129758
- for (const { seq, time, record } of rows) switch (asString(record["type"])) {
129759
- case "context.append_loop_event": {
129760
- const event = asRecord(record["event"]);
129761
- if (!event) break;
129762
- handleLoopEvent(event, time, seq);
129763
- break;
129764
- }
129765
- case "turn.prompt": {
129766
- finalizeStep(time);
129767
- turnNo += 1;
129768
- flushPendingSystem(time);
129769
- const input = record["input"];
129770
- const text = contentPartsText(input).trim();
129771
- pushCell("user", text.replaceAll(/\s+/g, " ").slice(0, 80) || "(空输入)", {
129772
- opensTurn: true,
129773
- inputDetail: text || void 0,
129774
- sourceSeq: seq
129775
- }, time);
129776
- currentTurnStart = time;
129777
- break;
129778
- }
129779
- case "turn.steer": {
129780
- const input = record["input"];
129781
- const text = contentPartsText(input).trim();
129782
- pushCell("context", `转向: ${text.replaceAll(/\s+/g, " ").slice(0, 80)}`, {
129783
- inputDetail: text || void 0,
129784
- sourceSeq: seq
129785
- }, time);
129786
- break;
129787
- }
129788
- case "request.header": {
129789
- const provider = asString(record["provider"]) ?? "";
129790
- const model = asString(record["model"]) ?? "";
129791
- const tools = asRecordArray(record["activeTools"]).map((t) => asString(t["name"]) ?? "");
129792
- pushCell("system", `请求 ${provider ? `${provider}/` : ""}${model}`, {
129793
- requestOnly: true,
129794
- inputDetail: tools.length > 0 ? `工具: ${tools.join(", ")}` : void 0,
129795
- sourceSeq: seq
129796
- }, time);
129797
- break;
129798
- }
129799
- case "tools.set_active_tools": {
129800
- const names = asStringArray(record["names"]).length > 0 ? asStringArray(record["names"]) : asRecordArray(record["names"]).map((n) => asString(n["name"]) ?? "");
129801
- pendingSystem.push(`工具集: ${names.join(", ")}`);
129802
- lastSystemTime = time;
129803
- break;
129804
- }
129805
- case "config.update": {
129806
- const cfg = asRecord(record);
129807
- const bits = [];
129808
- if (asString(cfg?.["modelAlias"])) bits.push(`模型别名: ${cfg["modelAlias"]}`);
129809
- if (asString(cfg?.["systemPrompt"])) bits.push("系统提示词已更新");
129810
- if (bits.length === 0) break;
129811
- pendingSystem.push(bits.join(" · "));
129812
- lastSystemTime = time;
129813
- break;
129814
- }
129815
- case "usage.record":
129816
- if (currentStepUuid === void 0) {
129817
- const usage = asRecord(record["usage"]);
129818
- pushCell("context", "usage", {
129819
- input: asNumber(usage?.["inputOther"]),
129820
- cacheRead: asNumber(usage?.["inputCacheRead"]),
129821
- cacheWrite: asNumber(usage?.["inputCacheCreation"]),
129822
- output: asNumber(usage?.["output"]),
130528
+ const consumeRow = ({ seq, time, record }) => {
130529
+ switch (asString(record["type"])) {
130530
+ case "context.append_loop_event": {
130531
+ const event = asRecord(record["event"]);
130532
+ if (!event) break;
130533
+ handleLoopEvent(event, time, seq);
130534
+ break;
130535
+ }
130536
+ case "turn.prompt": {
130537
+ finalizeStep(time);
130538
+ turnNo += 1;
130539
+ flushPendingSystem(time);
130540
+ const input = record["input"];
130541
+ const text = contentPartsText(input).trim();
130542
+ pushCell("user", text.replaceAll(/\s+/g, " ").slice(0, 80) || "(空输入)", {
130543
+ opensTurn: true,
130544
+ inputDetail: text || void 0,
129823
130545
  sourceSeq: seq
129824
130546
  }, time);
130547
+ currentTurnStart = time;
130548
+ break;
129825
130549
  }
129826
- break;
129827
- case "full_compaction.begin": {
129828
- finalizeStep(time);
129829
- const reason = asString(record["reason"]);
129830
- const instruction = asString(record["instruction"]);
129831
- const source = asString(record["source"]);
129832
- pushCell("compacted", `压缩上下文${reason ? `(${reason})` : ""}`, {
129833
- sourceSeq: seq,
129834
- startedAt: currentTurnStart,
129835
- inputDetail: instruction || void 0,
129836
- result: source ? `来源: ${source}` : void 0
129837
- }, time);
129838
- break;
129839
- }
129840
- case "micro_compaction.apply": {
129841
- finalizeStep(time);
129842
- const reason = asString(record["reason"]);
129843
- pushCell("compacted", `微压缩${reason ? `(${reason})` : ""}`, {
129844
- sourceSeq: seq,
129845
- startedAt: currentTurnStart
129846
- }, time);
129847
- break;
130550
+ case "turn.steer": {
130551
+ const input = record["input"];
130552
+ const text = contentPartsText(input).trim();
130553
+ pushCell("context", `转向: ${text.replaceAll(/\s+/g, " ").slice(0, 80)}`, {
130554
+ inputDetail: text || void 0,
130555
+ sourceSeq: seq
130556
+ }, time);
130557
+ break;
130558
+ }
130559
+ case "request.header": {
130560
+ const provider = asString(record["provider"]) ?? "";
130561
+ const model = asString(record["model"]) ?? "";
130562
+ const tools = asRecordArray(record["activeTools"]).map((t) => asString(t["name"]) ?? "");
130563
+ pushCell("system", `请求 ${provider ? `${provider}/` : ""}${model}`, {
130564
+ requestOnly: true,
130565
+ inputDetail: tools.length > 0 ? `工具: ${tools.join(", ")}` : void 0,
130566
+ sourceSeq: seq
130567
+ }, time);
130568
+ break;
130569
+ }
130570
+ case "tools.set_active_tools": {
130571
+ const names = asStringArray(record["names"]).length > 0 ? asStringArray(record["names"]) : asRecordArray(record["names"]).map((n) => asString(n["name"]) ?? "");
130572
+ pendingSystem.push(`工具集: ${names.join(", ")}`);
130573
+ lastSystemTime = time;
130574
+ break;
130575
+ }
130576
+ case "config.update": {
130577
+ const cfg = asRecord(record);
130578
+ const bits = [];
130579
+ if (asString(cfg?.["modelAlias"])) bits.push(`模型别名: ${cfg["modelAlias"]}`);
130580
+ if (asString(cfg?.["systemPrompt"])) bits.push("系统提示词已更新");
130581
+ if (bits.length === 0) break;
130582
+ pendingSystem.push(bits.join(" · "));
130583
+ lastSystemTime = time;
130584
+ break;
130585
+ }
130586
+ case "usage.record":
130587
+ if (currentStepUuid === void 0) {
130588
+ const usage = asRecord(record["usage"]);
130589
+ pushCell("context", "usage", {
130590
+ input: asNumber(usage?.["inputOther"]),
130591
+ cacheRead: asNumber(usage?.["inputCacheRead"]),
130592
+ cacheWrite: asNumber(usage?.["inputCacheCreation"]),
130593
+ output: asNumber(usage?.["output"]),
130594
+ sourceSeq: seq
130595
+ }, time);
130596
+ }
130597
+ break;
130598
+ case "full_compaction.begin": {
130599
+ finalizeStep(time);
130600
+ const reason = asString(record["reason"]);
130601
+ const instruction = asString(record["instruction"]);
130602
+ const source = asString(record["source"]);
130603
+ pushCell("compacted", `压缩上下文${reason ? `(${reason})` : ""}`, {
130604
+ sourceSeq: seq,
130605
+ startedAt: currentTurnStart,
130606
+ inputDetail: instruction || void 0,
130607
+ result: source ? `来源: ${source}` : void 0
130608
+ }, time);
130609
+ break;
130610
+ }
130611
+ case "micro_compaction.apply": {
130612
+ finalizeStep(time);
130613
+ const reason = asString(record["reason"]);
130614
+ pushCell("compacted", `微压缩${reason ? `(${reason})` : ""}`, {
130615
+ sourceSeq: seq,
130616
+ startedAt: currentTurnStart
130617
+ }, time);
130618
+ break;
130619
+ }
130620
+ default: break;
129848
130621
  }
129849
- default: break;
129850
- }
130622
+ };
130623
+ if (forEachWireRow(wirePath, consumeRow) === 0) throw new Error(`no wire records in ${wirePath}`);
129851
130624
  finalizeStep(void 0);
129852
130625
  flushPendingSystem(void 0);
129853
130626
  return capTraceSize(cells);
@@ -129856,25 +130629,20 @@ function buildTraceCells({ wirePath }) {
129856
130629
  * Long sessions produce tens of thousands of cells with multi-MB detail
129857
130630
  * payloads, which previously bloated the trace HTML (up to ~60MB) and froze
129858
130631
  * the browser. Two mitigations, applied at build time:
129859
- * 1. Truncate per-cell detail text (thinking/output/input/result) to a cap.
130632
+ * 1. Truncate per-cell detail text (thinking/output/input/result) to a cap —
130633
+ * done in `pushCell`, so the raw payload never outlives the cell.
129860
130634
  * 2. Beyond a cell-count cap, collapse the oldest cells into per-turn
129861
130635
  * summary rows so the document stays bounded while early turns remain
129862
130636
  * visible in the ledger.
129863
130637
  */
129864
130638
  const MAX_DETAIL = 4e3;
130639
+ const MAX_TEXT = 240;
129865
130640
  const MAX_CELLS = 4e3;
129866
130641
  function truncateDetail(value, max = MAX_DETAIL) {
129867
130642
  if (!value || value.length <= max) return value;
129868
130643
  return `${value.slice(0, max)}\n…[已截断 ${value.length - max} 字符]`;
129869
130644
  }
129870
130645
  function capTraceSize(cells) {
129871
- for (const cell of cells) {
129872
- cell.text = truncateDetail(cell.text, 240) ?? "";
129873
- if (cell.thinkingDetail) cell.thinkingDetail = truncateDetail(cell.thinkingDetail);
129874
- if (cell.outputDetail) cell.outputDetail = truncateDetail(cell.outputDetail);
129875
- if (cell.inputDetail) cell.inputDetail = truncateDetail(cell.inputDetail);
129876
- if (cell.result) cell.result = truncateDetail(cell.result);
129877
- }
129878
130646
  if (cells.length <= MAX_CELLS) return cells;
129879
130647
  const keep = cells.slice(-4e3);
129880
130648
  const early = cells.slice(0, cells.length - MAX_CELLS);
@@ -129893,25 +130661,67 @@ function capTraceSize(cells) {
129893
130661
  startedAt: early.find((c) => c.startedAt !== void 0 && c.startedAt !== null)?.startedAt ?? null
129894
130662
  }, ...keep];
129895
130663
  }
129896
- function readWireRows(wirePath) {
129897
- const content = readFileSync(wirePath, "utf8");
129898
- const rows = [];
130664
+ /** Bytes read per `readSync` call while scanning a wire log. */
130665
+ const WIRE_READ_CHUNK_BYTES = 1024 * 1024;
130666
+ /**
130667
+ * Stream a wire log row by row without materializing the file.
130668
+ *
130669
+ * The log is append-only and line-delimited, but a long-lived session can grow
130670
+ * it to gigabytes, so the content is never held as a single string: fixed-size
130671
+ * chunks are read into one reusable buffer and split on "\n" (0x0A) at byte
130672
+ * level. Only the currently open line is carried across reads (as a small
130673
+ * copied buffer, because the chunk buffer is reused), and each parsed row is
130674
+ * handed to `onRow` and then dropped — neither the decoded file nor a
130675
+ * parsed-record array lives beyond the line being processed.
130676
+ *
130677
+ * Returns the number of parsed rows. Throws when the file cannot be opened;
130678
+ * callers rely on that to tell a missing session log apart from an empty one.
130679
+ */
130680
+ function forEachWireRow(wirePath, onRow) {
129899
130681
  let seq = 0;
129900
- for (const line of content.split("\n")) {
129901
- if (!line.trim()) continue;
130682
+ let parsedRows = 0;
130683
+ const pushLine = (line) => {
130684
+ if (!line.trim()) return;
129902
130685
  seq += 1;
130686
+ let parsed;
129903
130687
  try {
129904
- const rec = asRecord(JSON.parse(line));
129905
- if (!rec) continue;
129906
- const time = asNumber(rec["time"]);
129907
- rows.push({
129908
- seq,
129909
- time,
129910
- record: rec
129911
- });
129912
- } catch {}
130688
+ parsed = JSON.parse(line);
130689
+ } catch {
130690
+ return;
130691
+ }
130692
+ const rec = asRecord(parsed);
130693
+ if (!rec) return;
130694
+ onRow({
130695
+ seq,
130696
+ time: asNumber(rec["time"]),
130697
+ record: rec
130698
+ });
130699
+ parsedRows += 1;
130700
+ };
130701
+ const fd = openSync(wirePath, "r");
130702
+ try {
130703
+ const chunk = Buffer.allocUnsafe(WIRE_READ_CHUNK_BYTES);
130704
+ let openChunks = [];
130705
+ for (;;) {
130706
+ const bytesRead = readSync(fd, chunk, 0, chunk.length, null);
130707
+ if (bytesRead <= 0) break;
130708
+ const window = bytesRead === chunk.length ? chunk : chunk.subarray(0, bytesRead);
130709
+ let searchFrom = 0;
130710
+ for (;;) {
130711
+ const newlineIndex = window.indexOf(10, searchFrom);
130712
+ if (newlineIndex === -1) break;
130713
+ const tail = window.subarray(searchFrom, newlineIndex);
130714
+ pushLine(openChunks.length === 0 ? tail.toString("utf8") : Buffer.concat([...openChunks, tail]).toString("utf8"));
130715
+ openChunks = [];
130716
+ searchFrom = newlineIndex + 1;
130717
+ }
130718
+ if (searchFrom < window.length) openChunks.push(Buffer.from(window.subarray(searchFrom)));
130719
+ }
130720
+ if (openChunks.length > 0) pushLine(Buffer.concat(openChunks).toString("utf8"));
130721
+ } finally {
130722
+ closeSync(fd);
129913
130723
  }
129914
- return rows;
130724
+ return parsedRows;
129915
130725
  }
129916
130726
  //#endregion
129917
130727
  //#region src/utils/trace/render-trace-html.ts
@@ -131349,8 +132159,15 @@ const SPINNER_FRAMES$1 = [
131349
132159
  "◉"
131350
132160
  ];
131351
132161
  const SPINNER_TICK_MS = 60;
131352
- function buildStatusLine(streamingPhase, streamingStartTime) {
132162
+ function buildStatusLine(streamingPhase, streamingStartTime, statusFlags) {
132163
+ if (statusFlags.pendingUserAction === "approval") return t("status.awaiting_approval");
132164
+ if (statusFlags.pendingUserAction === "question") return t("status.awaiting_answer");
132165
+ if (statusFlags.isCompacting) return t("status.compacting");
131353
132166
  if (streamingPhase === "idle") return t("status.idle");
132167
+ if (statusFlags.reconnectAttempt > 0) return t("status.reconnecting_short", {
132168
+ attempt: String(statusFlags.reconnectAttempt),
132169
+ max: String(statusFlags.reconnectMaxAttempts ?? 0)
132170
+ });
131354
132171
  let label;
131355
132172
  if (streamingPhase === "tool") label = t("status.tool");
131356
132173
  else if (streamingPhase === "waiting") label = t("status.waiting");
@@ -131525,7 +132342,12 @@ var FooterComponent = class {
131525
132342
  let rightText;
131526
132343
  if (this.transientHint) rightText = chalk.hex(colors.warning).bold(this.transientHint);
131527
132344
  else {
131528
- const statusLine = buildStatusLine(state.streamingPhase, state.streamingStartTime);
132345
+ const statusLine = buildStatusLine(state.streamingPhase, state.streamingStartTime, {
132346
+ isCompacting: state.isCompacting === true,
132347
+ pendingUserAction: state.pendingUserAction ?? null,
132348
+ reconnectAttempt: state.reconnectAttempt ?? 0,
132349
+ reconnectMaxAttempts: state.reconnectMaxAttempts
132350
+ });
131529
132351
  const ccDot = state.ccConnectActive ? chalk.hex(colors.success)("●") : chalk.hex(colors.textDim)("●");
131530
132352
  const sessionUsage = state.sessionUsage ?? {
131531
132353
  inputOther: 0,
@@ -134065,7 +134887,7 @@ async function guidedGoalSetup(host) {
134065
134887
  host.showNotice(t("goal.storm_breaker"), t("goal.conflict_loop"));
134066
134888
  return;
134067
134889
  }
134068
- const { TextInputDialogComponent } = await import("./text-input-dialog-CNhL-3wm.mjs");
134890
+ const { TextInputDialogComponent } = await import("./text-input-dialog-Cj0egbsU.mjs");
134069
134891
  const initialDesc = await promptText(host, TextInputDialogComponent, {
134070
134892
  title: t("goal.setup_title_initial"),
134071
134893
  subtitle: t("goal.setup_desc_hint"),
@@ -134086,7 +134908,7 @@ async function guidedGoalSetup(host) {
134086
134908
  await showGoalConfigWizard(host, session, confirmed.trim() || objective, false);
134087
134909
  }
134088
134910
  async function showGoalConfigWizard(host, session, objective, replace) {
134089
- const { TextInputDialogComponent } = await import("./text-input-dialog-CNhL-3wm.mjs");
134911
+ const { TextInputDialogComponent } = await import("./text-input-dialog-Cj0egbsU.mjs");
134090
134912
  const turnInput = await promptNumber(host, TextInputDialogComponent, {
134091
134913
  title: t("goal.wizard_title", { objective }),
134092
134914
  subtitle: t("goal.budget_turns_hint"),
@@ -134940,9 +135762,9 @@ function formatTokens$1(n) {
134940
135762
  * Builds the bullet + text of a background task notice. Shared by the standalone
134941
135763
  * card and the activity block row so a notice looks the same in both places.
134942
135764
  */
134943
- function renderBackgroundStatus(data, colors, maxCells) {
135765
+ function renderBackgroundStatus(data, colors, maxCells, frameIndex = 0) {
134944
135766
  const tone = data.phase === "started" ? colors.primary : data.phase === "completed" ? colors.success : colors.error;
134945
- const bullet = data.phase === "completed" ? chalk.hex(colors.success)(DONE_MARK) : data.phase === "failed" ? chalk.hex(colors.error)(FAILURE_MARK) : chalk.hex(colors.textDim)(`${BRAILLE_SPINNER_FRAMES[0] ?? "⠋"} `);
135767
+ const bullet = data.phase === "completed" ? chalk.hex(colors.success)(DONE_MARK) : data.phase === "failed" ? chalk.hex(colors.error)(FAILURE_MARK) : chalk.hex(colors.textDim)(`${BRAILLE_SPINNER_FRAMES[frameIndex] ?? BRAILLE_SPINNER_FRAMES[0] ?? "⠋"} `);
134946
135768
  const hasDetail = data.detail !== void 0 && data.detail.length > 0;
134947
135769
  const plain = hasDetail ? `${data.headline} (${data.detail ?? ""})` : data.headline;
134948
135770
  if (maxCells !== void 0 && visibleWidth(plain) > maxCells) return {
@@ -134957,29 +135779,81 @@ function renderBackgroundStatus(data, colors, maxCells) {
134957
135779
  var BackgroundAgentStatusComponent = class {
134958
135780
  data;
134959
135781
  colors;
134960
- bullet;
135782
+ ui;
134961
135783
  textComponent;
134962
135784
  cachedWidth;
134963
135785
  cachedLines;
134964
- constructor(data, colors) {
135786
+ spinnerFrame = 0;
135787
+ spinnerTimer;
135788
+ lastSpinnerTickAt = 0;
135789
+ /** Only a notice that is still running animates; a finished one is static. */
135790
+ live;
135791
+ constructor(data, colors, ui) {
134965
135792
  this.data = data;
134966
135793
  this.colors = colors;
134967
- const view = renderBackgroundStatus(data, colors);
134968
- this.bullet = view.bullet;
134969
- this.textComponent = new Text(view.text, 0, 0);
135794
+ this.ui = ui;
135795
+ this.textComponent = new Text(renderBackgroundStatus(data, colors).text, 0, 0);
135796
+ this.live = data.phase === "started" && ui !== void 0;
135797
+ if (this.live) this.startSpinner();
134970
135798
  }
134971
135799
  invalidate() {
134972
135800
  this.cachedWidth = void 0;
134973
135801
  this.cachedLines = void 0;
134974
135802
  }
135803
+ dispose() {
135804
+ this.stopSpinner();
135805
+ }
135806
+ /**
135807
+ * The task this card announced is over: stop the ticker and fall back to the
135808
+ * first frame, then let the cache answer again. Without the reset a settled
135809
+ * card would keep whatever frame it froze on, which reads as a stuck spinner.
135810
+ */
135811
+ settle() {
135812
+ this.stopSpinner();
135813
+ this.live = false;
135814
+ if (this.spinnerFrame === 0) return;
135815
+ this.spinnerFrame = 0;
135816
+ this.invalidate();
135817
+ }
134975
135818
  render(width) {
134976
- if (this.cachedLines !== void 0 && this.cachedWidth === width) return this.cachedLines;
135819
+ if (!this.live && this.cachedLines !== void 0 && this.cachedWidth === width) return this.cachedLines;
135820
+ const view = renderBackgroundStatus(this.data, this.colors, void 0, this.spinnerFrame);
134977
135821
  const contentWidth = Math.max(1, width - 2);
134978
- const lines = ["", ...this.textComponent.render(contentWidth).map((line, index) => (index === 0 ? this.bullet : " ") + line)];
135822
+ const lines = ["", ...this.textComponent.render(contentWidth).map((line, index) => (index === 0 ? view.bullet : " ") + line)];
134979
135823
  this.cachedWidth = width;
134980
135824
  this.cachedLines = lines;
134981
135825
  return lines;
134982
135826
  }
135827
+ startSpinner() {
135828
+ if (this.spinnerTimer !== void 0) return;
135829
+ this.lastSpinnerTickAt = performance.now();
135830
+ this.scheduleSpinnerTick(80);
135831
+ }
135832
+ stopSpinner() {
135833
+ if (this.spinnerTimer === void 0) return;
135834
+ clearTimeout(this.spinnerTimer);
135835
+ this.spinnerTimer = void 0;
135836
+ }
135837
+ scheduleSpinnerTick(delayMs) {
135838
+ if (this.ui === void 0) return;
135839
+ const timer = setTimeout(() => {
135840
+ if (this.spinnerTimer !== timer) return;
135841
+ const startedAt = performance.now();
135842
+ const elapsed = startedAt - this.lastSpinnerTickAt;
135843
+ if (elapsed >= 80) {
135844
+ const steps = Math.floor(elapsed / 80);
135845
+ this.spinnerFrame = (this.spinnerFrame + steps) % BRAILLE_SPINNER_FRAMES.length;
135846
+ this.lastSpinnerTickAt += steps * 80;
135847
+ this.invalidate();
135848
+ this.ui?.requestRender();
135849
+ }
135850
+ const frameCostMs = performance.now() - startedAt;
135851
+ if (this.spinnerTimer !== timer) return;
135852
+ const cadenceDelayMs = Math.max(0, 80 - frameCostMs);
135853
+ this.scheduleSpinnerTick(Math.max(cadenceDelayMs, frameCostMs * 9));
135854
+ }, delayMs);
135855
+ this.spinnerTimer = timer;
135856
+ }
134983
135857
  };
134984
135858
  //#endregion
134985
135859
  //#region src/tui/utils/activity-lines.ts
@@ -135412,7 +136286,7 @@ var ActivityGroupComponent = class extends Container {
135412
136286
  this.addChild(this.bodyContainer);
135413
136287
  this.startSpinner();
135414
136288
  }
135415
- /** True when the group owns neither reasoning nor tool calls. */
136289
+ /** True when the group owns neither reasoning nor tool calls nor approvals. */
135416
136290
  isEmpty() {
135417
136291
  return this.segments.every((segment) => segment.kind === "thinking" && segment.text.trim().length === 0);
135418
136292
  }
@@ -135421,14 +136295,16 @@ var ActivityGroupComponent = class extends Container {
135421
136295
  }
135422
136296
  /**
135423
136297
  * Borrows a tool card as a hidden state container. Re-attaching the same
135424
- * card is a no-op.
136298
+ * card is a no-op. `callId` is what later lets an approval be filed under the
136299
+ * call it allowed.
135425
136300
  */
135426
- attachTool(tc, step) {
136301
+ attachTool(tc, step, callId) {
135427
136302
  if (this.segments.some((segment) => segment.kind === "tool" && segment.tc === tc)) return;
135428
136303
  this.segments.push({
135429
136304
  kind: "tool",
135430
136305
  tc,
135431
- step
136306
+ step,
136307
+ callId
135432
136308
  });
135433
136309
  if (step !== void 0) this.steps.add(step);
135434
136310
  tc.setExpanded(this.expanded);
@@ -135439,14 +136315,7 @@ var ActivityGroupComponent = class extends Container {
135439
136315
  }
135440
136316
  /** Adds a background task notice as the newest step of the timeline. */
135441
136317
  attachNotice(data) {
135442
- const last = this.segments.at(-1);
135443
- if (last !== void 0 && last.kind === "thinking" && last.live) {
135444
- last.live = false;
135445
- this.interruptedThinking = {
135446
- text: last.text,
135447
- length: last.text.length
135448
- };
135449
- }
136318
+ this.splitLiveThinking();
135450
136319
  this.segments.push({
135451
136320
  kind: "notice",
135452
136321
  data
@@ -135454,6 +136323,38 @@ var ActivityGroupComponent = class extends Container {
135454
136323
  this.flushNow();
135455
136324
  }
135456
136325
  /**
136326
+ * Adds an approval outcome as the step directly under the call it allowed, so
136327
+ * a run of approved calls reads as call/approval pairs. Returns false when
136328
+ * that call has no row here — a subagent's call, a card that stays standalone
136329
+ * — leaving the caller to decide where the outcome goes.
136330
+ */
136331
+ attachApproval(callId, label, detail, tone) {
136332
+ const index = this.segments.findIndex((segment) => segment.kind === "tool" && segment.callId === callId);
136333
+ if (index < 0) return false;
136334
+ this.segments.splice(index + 1, 0, {
136335
+ kind: "approval",
136336
+ label,
136337
+ detail,
136338
+ tone
136339
+ });
136340
+ this.flushNow();
136341
+ return true;
136342
+ }
136343
+ /**
136344
+ * A step landing mid-reasoning splits the run in two: the part that already
136345
+ * streamed is closed and its length remembered, so the continuation keeps only
136346
+ * what is new instead of repeating the whole run.
136347
+ */
136348
+ splitLiveThinking() {
136349
+ const last = this.segments.at(-1);
136350
+ if (last === void 0 || last.kind !== "thinking" || !last.live) return;
136351
+ last.live = false;
136352
+ this.interruptedThinking = {
136353
+ text: last.text,
136354
+ length: last.text.length
136355
+ };
136356
+ }
136357
+ /**
135457
136358
  * Appends to the reasoning run streaming right now, opening a new step of the
135458
136359
  * timeline when the last step is a tool call or a reasoning run that already
135459
136360
  * ended. `live` drives the streaming rate in the header.
@@ -135564,10 +136465,14 @@ var ActivityGroupComponent = class extends Container {
135564
136465
  * Aggregate diff of the group's finished, successful file mutations
135565
136466
  * (Edit/Write), shown in the header. Read-only tools contribute nothing;
135566
136467
  * failed attempts changed nothing, so only successful calls count.
136468
+ * `removed` stays undefined when any contributor could not report its
136469
+ * deletions (replayed records carry no display payload): summing a partial
136470
+ * number would claim a deletion count the group never measured.
135567
136471
  */
135568
136472
  diffTotals() {
135569
136473
  let added = 0;
135570
136474
  let removed = 0;
136475
+ let removedKnown = true;
135571
136476
  let seen = false;
135572
136477
  for (const segment of this.segments) {
135573
136478
  if (segment.kind !== "tool") continue;
@@ -135575,12 +136480,14 @@ var ActivityGroupComponent = class extends Container {
135575
136480
  if (contribution === void 0) continue;
135576
136481
  seen = true;
135577
136482
  added += contribution.added;
135578
- removed += contribution.removed;
136483
+ if (contribution.removed === void 0) removedKnown = false;
136484
+ else removed += contribution.removed;
135579
136485
  }
135580
- return seen ? {
136486
+ if (!seen) return void 0;
136487
+ return {
135581
136488
  added,
135582
- removed
135583
- } : void 0;
136489
+ removed: removedKnown ? removed : void 0
136490
+ };
135584
136491
  }
135585
136492
  buildHeader(width) {
135586
136493
  const colors = this.colors;
@@ -135593,10 +136500,10 @@ var ActivityGroupComponent = class extends Container {
135593
136500
  if (toolCount > 0) parts.push(t("activitygroup.tools", { count: String(toolCount) }));
135594
136501
  const stats = chalk.dim(SEPARATOR + parts.join(SEPARATOR));
135595
136502
  const diff = this.diffTotals();
135596
- const diffPart = diff ? SEPARATOR + t("activitygroup.diff", {
136503
+ const diffPart = diff ? SEPARATOR + (diff.removed === void 0 ? t("activitygroup.diffAddedOnly", { added: chalk.hex(colors.diffAdded)(`+${diff.added}`) }) : t("activitygroup.diff", {
135597
136504
  added: chalk.hex(colors.diffAdded)(`+${diff.added}`),
135598
136505
  removed: chalk.hex(colors.diffRemoved)(`-${diff.removed}`)
135599
- }) : "";
136506
+ })) : "";
135600
136507
  const speed = this.liveThinking() ? getSharedSpeedTracker().getSpeed() : 0;
135601
136508
  const rate = speed > MIN_RATE ? chalk.hex(lerpHex(colors.textDim, colors.primary, easeSpeedRatio(speed / 200)))(t("activitygroup.rate", { rate: speed.toFixed(1) })) : chalk.dim(t("activitygroup.rate", { rate: "-" }));
135602
136509
  const hint = chalk.dim(this.expanded ? t("activitygroup.hint_collapse") : t("activitygroup.hint_expand"));
@@ -135630,6 +136537,10 @@ var ActivityGroupComponent = class extends Container {
135630
136537
  this.bodyContainer.addChild(this.noticeRow(segment.data, width, isLast));
135631
136538
  return;
135632
136539
  }
136540
+ if (segment.kind === "approval") {
136541
+ this.bodyContainer.addChild(this.approvalRow(segment, width, isLast));
136542
+ return;
136543
+ }
135633
136544
  this.bodyContainer.addChild(this.thinkingSummaryRow(segment, width, isLast));
135634
136545
  });
135635
136546
  }
@@ -135675,15 +136586,30 @@ var ActivityGroupComponent = class extends Container {
135675
136586
  components: [this.noticeRow(segment.data, width, isLast)],
135676
136587
  cost: 1
135677
136588
  };
136589
+ if (segment.kind === "approval") return {
136590
+ components: [this.approvalRow(segment, width, isLast)],
136591
+ cost: 1
136592
+ };
135678
136593
  return this.thinkingSegmentRows(segment, width, isLast);
135679
136594
  }
135680
136595
  /** One background task notice, kept to a single row. */
135681
136596
  noticeRow(data, width, isLast) {
135682
136597
  const prefix = isLast ? BRANCH_LAST : BRANCH_FIRST;
135683
- const view = renderBackgroundStatus(data, this.colors, Math.max(1, width - visibleWidth(prefix) - NOTICE_BULLET_WIDTH));
136598
+ const view = renderBackgroundStatus(data, this.colors, Math.max(1, width - visibleWidth(prefix) - NOTICE_BULLET_WIDTH), this.spinnerFrame);
135684
136599
  return new Text(`${prefix}${view.bullet}${view.text}`, 0, 0);
135685
136600
  }
135686
136601
  /**
136602
+ * One approval outcome, kept to a single row. Approvals read as a quiet note
136603
+ * next to the work they allowed — dim, unlike a rejection, which keeps the
136604
+ * error tone so a denied call is never mistaken for an allowed one.
136605
+ */
136606
+ approvalRow(segment, width, isLast) {
136607
+ const prefix = isLast ? BRANCH_LAST : BRANCH_FIRST;
136608
+ const tone = segment.tone === "rejected" ? this.colors.error : this.colors.textDim;
136609
+ const plain = `${segment.label}${SEPARATOR}${segment.detail}`;
136610
+ return new Text(`${prefix}${chalk.hex(tone)(truncateToWidth(plain, Math.max(1, width - visibleWidth(prefix)), "…"))}`, 0, 0);
136611
+ }
136612
+ /**
135687
136613
  * One reasoning run: a label row plus as many of its lines as its own budget
135688
136614
  * allows. Lines keep their order; the oldest end of a live run (or the newest
135689
136615
  * end of a finished one) is dropped until the budget fits.
@@ -135696,7 +136622,7 @@ var ActivityGroupComponent = class extends Container {
135696
136622
  cost: 0
135697
136623
  };
135698
136624
  const continuation = isLast ? THINKING_BODY_PREFIX : BRANCH_PIPE;
135699
- const components = [new Text(`${isLast ? BRANCH_LAST : BRANCH_FIRST}${chalk.hex(colors.roleThinking)(t("activitygroup.thinking_label"))}`, 0, 0)];
136625
+ const components = [new Text(`${isLast ? BRANCH_LAST : BRANCH_FIRST}${this.thinkingGlyph(segment)}${chalk.hex(colors.roleThinking)(t("activitygroup.thinking_label"))}`, 0, 0)];
135700
136626
  let cost = 1;
135701
136627
  const budget = getActivityLines().expandedThinking;
135702
136628
  const inner = Math.max(1, width - visibleWidth(continuation));
@@ -135757,6 +136683,16 @@ var ActivityGroupComponent = class extends Container {
135757
136683
  aborted: cardMarkerChar(rawHeader) === "⊙"
135758
136684
  };
135759
136685
  }
136686
+ /** Mirrors a tool row's status prefix so reasoning and tool rows line up: a
136687
+ * live run spins with the block frame, a finished run takes the success mark
136688
+ * (`DONE_MARK` carries its own trailing space, so the column stays 2 cells). */
136689
+ thinkingGlyph(segment) {
136690
+ if (segment.live) {
136691
+ const frame = BRAILLE_SPINNER_FRAMES[this.spinnerFrame] ?? BRAILLE_SPINNER_FRAMES[0];
136692
+ return `${chalk.hex(this.colors.roleThinking)(frame)} `;
136693
+ }
136694
+ return chalk.hex(this.colors.success)(DONE_MARK);
136695
+ }
135760
136696
  /** Mirrors the card's own status: running spins, failed/aborted stay visible. */
135761
136697
  statusGlyph(row) {
135762
136698
  if (row.aborted) return chalk.hex(this.colors.error)("⊙");
@@ -135775,10 +136711,27 @@ var ActivityGroupComponent = class extends Container {
135775
136711
  thinkingSummaryRow(segment, width, isLast) {
135776
136712
  const first = thinkingLines(segment.text)[0] ?? "";
135777
136713
  const label = t("activitygroup.thinking_summary", { summary: "" });
135778
- const used = visibleWidth(isLast ? BRANCH_LAST : BRANCH_FIRST) + visibleWidth(label);
136714
+ const prefix = isLast ? BRANCH_LAST : BRANCH_FIRST;
136715
+ const used = visibleWidth(prefix) + visibleWidth(this.thinkingGlyph(segment)) + visibleWidth(label);
135779
136716
  const cells = Math.max(1, width - used - 1);
135780
136717
  const summary = chalk.hex(this.colors.roleThinking)(t("activitygroup.thinking_summary", { summary: truncateToWidth(first, cells, "…") }));
135781
- return new Text(`${isLast ? BRANCH_LAST : BRANCH_FIRST}${summary}`, 0, 0);
136718
+ return new Text(`${prefix}${this.thinkingGlyph(segment)}${summary}`, 0, 0);
136719
+ }
136720
+ /**
136721
+ * True while at least one visible row carries a live animation. Reasoning rows
136722
+ * spin too now, and a background task that is still running keeps its glyph
136723
+ * moving, so this can no longer be "collapsed with a pending tool": the only
136724
+ * question is whether repainting the body on this tick would show anything new.
136725
+ * Finished rows stay static by design.
136726
+ */
136727
+ needsRowAnimation() {
136728
+ if (!this.running) return false;
136729
+ for (const segment of this.segments) {
136730
+ if (segment.kind === "thinking" && segment.live) return true;
136731
+ if (segment.kind === "notice" && segment.data.phase === "started") return true;
136732
+ if (segment.kind === "tool" && segment.tc.resultView === void 0) return true;
136733
+ }
136734
+ return false;
135782
136735
  }
135783
136736
  scheduleSpinnerTick(delayMs) {
135784
136737
  if (this.ui === void 0) return;
@@ -135791,7 +136744,7 @@ var ActivityGroupComponent = class extends Container {
135791
136744
  this.spinnerFrame = (this.spinnerFrame + steps) % BRAILLE_SPINNER_FRAMES.length;
135792
136745
  this.lastSpinnerTickAt += steps * 80;
135793
136746
  const width = this.renderedWidth ?? 80;
135794
- if (!this.expanded && this.segments.some((segment) => segment.kind === "tool" && segment.tc.resultView === void 0)) this.rebuild(width);
136747
+ if (this.needsRowAnimation()) this.rebuild(width);
135795
136748
  else this.headerText.setText(this.buildHeader(width));
135796
136749
  this.ui?.requestRender();
135797
136750
  }
@@ -138008,9 +138961,24 @@ var ToolCallComponent = class ToolCallComponent extends CachedContainer {
138008
138961
  * additions), for the group-level diff stat in the activity block header.
138009
138962
  * Undefined for non-mutating tools, unfinished calls, and failed calls: a
138010
138963
  * failure changed nothing, so it must not count towards the group total.
138964
+ *
138965
+ * The mutation tools report their exact numbers (`display.file_diff`) computed
138966
+ * from the real before/after contents. Groups replayed from a resumed session
138967
+ * carry no such payload (the transcript keeps message text, not the UI
138968
+ * payload), so they fall back to deriving the numbers from the arguments — a
138969
+ * fallback that cannot know how many lines a Write replaced, so it reports
138970
+ * `removed: undefined` ("unknown") instead of claiming zero.
138011
138971
  */
138012
138972
  diffContribution() {
138013
138973
  if (this.result === void 0 || this.result.is_error) return void 0;
138974
+ const reported = this.result.display;
138975
+ if (reported?.kind === "file_diff") {
138976
+ if (reported.added === 0 && reported.removed === 0) return void 0;
138977
+ return {
138978
+ added: reported.added,
138979
+ removed: reported.removed
138980
+ };
138981
+ }
138014
138982
  if (this.toolCall.name === "Edit") {
138015
138983
  const stats = computeEditStats(this.toolCall.args);
138016
138984
  if (stats.added === 0 && stats.removed === 0) return void 0;
@@ -138021,7 +138989,7 @@ var ToolCallComponent = class ToolCallComponent extends CachedContainer {
138021
138989
  if (stats.lines === 0) return void 0;
138022
138990
  return {
138023
138991
  added: stats.lines,
138024
- removed: 0
138992
+ removed: void 0
138025
138993
  };
138026
138994
  }
138027
138995
  }
@@ -138493,7 +139461,7 @@ var ToolCallComponent = class ToolCallComponent extends CachedContainer {
138493
139461
  if (provider === void 0) return "";
138494
139462
  const text = provider(this.toolCall, result);
138495
139463
  if (text.length === 0) return "";
138496
- return (result.is_error ? chalk.hex(this.colors.error) : chalk.dim)(` · ${text}`);
139464
+ return chalk.dim(` · ${text}`);
138497
139465
  }
138498
139466
  rebuildContent() {
138499
139467
  this.markDirty();
@@ -138860,7 +139828,7 @@ var ToolCallComponent = class ToolCallComponent extends CachedContainer {
138860
139828
  else this.addChild(new Text(chalk.dim(plan), 2, 0));
138861
139829
  }
138862
139830
  computePlanBoxMaxContentLines() {
138863
- const rows = this.ui?.terminal.rows;
139831
+ const rows = this.ui?.terminal?.rows;
138864
139832
  if (rows === void 0 || !Number.isFinite(rows) || rows <= 0) return void 0;
138865
139833
  return Math.max(8, Math.floor(rows * .6) - 4);
138866
139834
  }
@@ -143736,18 +144704,18 @@ async function handleIngest(host) {
143736
144704
  async function ensureEmbeddingReadyInteractive(host) {
143737
144705
  const engine = (await getKnowledgeStore()).getEmbeddingEngine();
143738
144706
  if (engine !== void 0 && engine.available) return true;
143739
- if (!isEmbeddingModelCached()) {
144707
+ if (!hasEmbeddingModelCache()) {
143740
144708
  host.showError(t("knowledge.model_missing"));
143741
144709
  return false;
143742
144710
  }
143743
144711
  const spinner = host.showProgressSpinner(t("knowledge.loading_model"));
143744
- const { ok, error } = await startManualEmbeddingDownload();
144712
+ const { ok, error, failureKind } = await startManualEmbeddingDownload();
143745
144713
  spinner.stop({
143746
144714
  ok,
143747
144715
  label: ok ? t("kw.embedding_ready") : t("kw.embedding_failed")
143748
144716
  });
143749
144717
  if (!ok) {
143750
- const detail = [t("knowledge.download_model_retry_hint"), error].filter(Boolean).join("\n");
144718
+ const detail = embeddingFailureDetail(failureKind, error);
143751
144719
  host.showNotice(t("knowledge.model_load_failed"), detail);
143752
144720
  return false;
143753
144721
  }
@@ -143994,17 +144962,17 @@ async function handleReembed(host) {
143994
144962
  }
143995
144963
  }
143996
144964
  async function handleDownloadModel(host) {
143997
- await getKnowledgeStore();
143998
144965
  const spinner = host.showProgressSpinner(t("kw.embedding_downloading"));
143999
144966
  try {
144000
- const { ok, alreadyReady, error } = await startManualEmbeddingDownload();
144967
+ await getKnowledgeStore();
144968
+ const { ok, alreadyReady, error, failureKind } = await startManualEmbeddingDownload();
144001
144969
  spinner.stop({
144002
144970
  ok,
144003
144971
  label: ok ? t("kw.embedding_ready") : t("kw.embedding_failed")
144004
144972
  });
144005
144973
  if (ok) host.showStatus(alreadyReady ? t("kw.embedding_already_installed") : t("kw.embedding_ready"));
144006
144974
  else {
144007
- const detail = [t("knowledge.download_model_retry_hint"), error].filter(Boolean).join("\n");
144975
+ const detail = embeddingFailureDetail(failureKind, error);
144008
144976
  host.showNotice(t("kw.embedding_failed"), detail);
144009
144977
  }
144010
144978
  } catch (error) {
@@ -144772,4 +145740,4 @@ async function handleBuiltInSlashCommand(host, name, args) {
144772
145740
  }
144773
145741
  }
144774
145742
  //#endregion
144775
- export { handleExportMdCommand as $, readSessionIndex as $n, SubagentSlots as $t, langFromPath as A, startManualEmbeddingDownload as An, toggleEmptySessionHint as At, charsForTokenBudget as B, getLogDir as Bn, OSC11_QUERY as Bt, getDaemonInstructions as C, buildSkillSlashCommands as Cn, showStatusReport as Ct, renderDiffLines as D, setExperimentalFlags as Dn, createMarkdownTheme as Dt, ToolCallComponent as E, isExperimentalFlagEnabled as En, createEditorTheme as Et, replaceTabs as F, loadTuiConfig as Fn, parseOsc11BackgroundTheme as Ft, WelcomeComponent as G, PRODUCT_NAME as Gn, TERMINAL_FOCUS_IN as Gt, getSharedSpeedTracker as H, CLI_COMMAND_NAME as Hn, OSC11_RESPONSE_PREFIX as Ht, sanitizeShellOutput as I, saveTuiConfig as In, DISABLE_TERMINAL_FOCUS_REPORTING as It, resetBreathingClock as J, saveCatalogCache as Jn, TERMINAL_THEME_LIGHT as Jt, BREATHE_CYCLE_MS as K, DEFAULT_CATALOG_URL as Kn, TERMINAL_FOCUS_OUT as Kt, SkillActivationComponent as L, detectShellEnvironment as Ln, DISABLE_TERMINAL_THEME_REPORTING as Lt, ThinkingComponent as M, PULSE_WAVE_FRAMES as Mn, contrastTextHex as Mt, NoticeMessageComponent as N, TuiConfigParseError as Nn, getColorPalette as Nt, renderDiffLinesClustered as O, getKnowledgeStore as On, isEmptySessionHintDismissed as Ot, StatusMessageComponent as P, TuiLikePreferencesSchema as Pn, detectTerminalTheme as Pt, handleExportDebugZipCommand as Q, appendSessionIndexEntry as Qn, lerpGradient as Qt, AssistantMessageComponent as R, getDataDir as Rn, ENABLE_TERMINAL_FOCUS_REPORTING as Rt, handleCcCommand as S, getNoActiveSessionMessage as Sn, clearInfoPanelState as St, UserMessageComponent as T, sortSlashCommands as Tn, resolveThemeSync as Tt, BackgroundAgentStatusComponent as U, CLI_UI_MODE as Un, OSC11_RESPONSE_PREFIX_NO_ESC as Ut, estimateTokens as V, detectInstallSource as Vn, OSC11_RESPONSE as Vt, AgentGroupComponent as W, CLI_USER_AGENT_PRODUCT as Wn, QUERY_TERMINAL_THEME as Wt, clearGoalState as X, encodeWorkDirKey as Xn, isStreaming as Xt, CompactionComponent as Y, ScreamHarness as Yn, isBusy as Yt, refineGoal as Z, normalizeWorkDir as Zn, FooterComponent as Zt, selectUpdateTarget as _, SESSION_TIPS as _n, showModelPicker as _t, handleExtensionCommand as a, STATUS_BULLET as an, log as ar, changeThinkingLevel as at, appendJsonlLine as b, getCtrlDHint as bn, refreshProviderBalance as bt, hasDispose as c, formatErrorMessage as cn, isOrphanedToolCallError as cr, handleBotCommand as ct, MoonLoader as d, serializeToolResultOutput as dn, handleFusionPlanCommand as dt, handleTraceCommand as en, removeSessionIndexEntry as er, handleForkCommand as et, formatMemoryMemoForInjection as f, stringValue as fn, handleModelCommand as ft, handleUpdateCommand as g, MAIN_AGENT_ID$1 as gn, handleYoloCommand as gt, handleMcpCommand as h, EXIT_CONFIRM_WINDOW_MS as hn, handleWolfpackCommand as ht, buildRoleAdditionalText as i, printableChar as in, flushDiagnosticLogs as ir, toTerminalHyperlink as it, CachedContainer as j, PIXEL_PULSE_FRAMES as jn, createThemeStyles as jt, highlightLines as k, isEmbeddingModelCached as kn, isTurnElapsedEnabled as kt, isExpandable as l, isTodoItemShape as ln, ErrorCodes as lr, handleCompactCommand as lt, handleChannelCommand as m, EMPTY_SESSION_HINT_URL as mn, handleThemeCommand as mt, clearEvalPanelState as n, handleConnectCommand as nn, resolveScreamHome as nr, handleTitleCommand as nt, handleSkillCommand as o, appendStreamingArgsPreview as on, resolveGlobalLogPath as or, getModelCycleLevel as ot, handleMemoryCommand as p, truncateErrorMessage as pn, handlePlanCommand as pt, getBreathingFrame as q, fetchCatalog as qn, TERMINAL_THEME_DARK as qt, openUrl as r, handleLogoutCommand as rn, MemoryMemoStore as rr, isInternalMessage as rt, disposeChildren as s, argsRecord as sn, isScreamError as sr, handleAutoCommand as st, dispatchInput as t, handleSearchCommand as tn, MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE as tr, handleInitCommand as tt, isPlanExpandable as u, parseStreamingArgs as un, SCREAM_ERROR_INFO as ur, handleEditorCommand as ut, refreshUpdateCache as v, TIP_ROTATION_INTERVAL_MS as vn, showPermissionPicker as vt, handleRevokeCommand as w, BUILTIN_SLASH_COMMANDS as wn, showUsage as wt, readJsonlFile as x, getLlmNotSetMessage as xn, supportsBalance as xt, readUpdateCache as y, getCtrlCHint as yn, showSettingsSelector as yt, ActivityGroupComponent as z, getInputHistoryFile as zn, ENABLE_TERMINAL_THEME_REPORTING as zt };
145743
+ export { handleExportMdCommand as $, appendSessionIndexEntry as $n, SubagentSlots as $t, langFromPath as A, hasEmbeddingModelCache as An, toggleEmptySessionHint as At, charsForTokenBudget as B, getInputHistoryFile as Bn, OSC11_QUERY as Bt, getDaemonInstructions as C, buildSkillSlashCommands as Cn, showStatusReport as Ct, renderDiffLines as D, setExperimentalFlags as Dn, createMarkdownTheme as Dt, ToolCallComponent as E, isExperimentalFlagEnabled as En, createEditorTheme as Et, replaceTabs as F, TuiLikePreferencesSchema as Fn, parseOsc11BackgroundTheme as Ft, WelcomeComponent as G, CLI_USER_AGENT_PRODUCT as Gn, TERMINAL_FOCUS_IN as Gt, getSharedSpeedTracker as H, detectInstallSource as Hn, OSC11_RESPONSE_PREFIX as Ht, sanitizeShellOutput as I, loadTuiConfig as In, DISABLE_TERMINAL_FOCUS_REPORTING as It, resetBreathingClock as J, fetchCatalog as Jn, TERMINAL_THEME_LIGHT as Jt, BREATHE_CYCLE_MS as K, PRODUCT_NAME as Kn, TERMINAL_FOCUS_OUT as Kt, SkillActivationComponent as L, saveTuiConfig as Ln, DISABLE_TERMINAL_THEME_REPORTING as Lt, ThinkingComponent as M, PIXEL_PULSE_FRAMES as Mn, contrastTextHex as Mt, NoticeMessageComponent as N, PULSE_WAVE_FRAMES as Nn, getColorPalette as Nt, renderDiffLinesClustered as O, embeddingFailureDetail as On, isEmptySessionHintDismissed as Ot, StatusMessageComponent as P, TuiConfigParseError as Pn, detectTerminalTheme as Pt, handleExportDebugZipCommand as Q, normalizeWorkDir as Qn, lerpGradient as Qt, AssistantMessageComponent as R, detectShellEnvironment as Rn, ENABLE_TERMINAL_FOCUS_REPORTING as Rt, handleCcCommand as S, getNoActiveSessionMessage as Sn, clearInfoPanelState as St, UserMessageComponent as T, sortSlashCommands as Tn, resolveThemeSync as Tt, BackgroundAgentStatusComponent as U, CLI_COMMAND_NAME as Un, OSC11_RESPONSE_PREFIX_NO_ESC as Ut, estimateTokens as V, getLogDir as Vn, OSC11_RESPONSE as Vt, AgentGroupComponent as W, CLI_UI_MODE as Wn, QUERY_TERMINAL_THEME as Wt, clearGoalState as X, ScreamHarness as Xn, isStreaming as Xt, CompactionComponent as Y, saveCatalogCache as Yn, isBusy as Yt, refineGoal as Z, encodeWorkDirKey as Zn, FooterComponent as Zt, selectUpdateTarget as _, SESSION_TIPS as _n, showModelPicker as _t, handleExtensionCommand as a, STATUS_BULLET as an, flushDiagnosticLogs as ar, changeThinkingLevel as at, appendJsonlLine as b, getCtrlDHint as bn, refreshProviderBalance as bt, hasDispose as c, formatErrorMessage as cn, isScreamError as cr, handleBotCommand as ct, MoonLoader as d, serializeToolResultOutput as dn, SCREAM_ERROR_INFO as dr, handleFusionPlanCommand as dt, handleTraceCommand as en, readSessionIndex as er, handleForkCommand as et, formatMemoryMemoForInjection as f, stringValue as fn, handleModelCommand as ft, handleUpdateCommand as g, MAIN_AGENT_ID$1 as gn, handleYoloCommand as gt, handleMcpCommand as h, EXIT_CONFIRM_WINDOW_MS as hn, handleWolfpackCommand as ht, buildRoleAdditionalText as i, printableChar as in, MemoryMemoStore as ir, toTerminalHyperlink as it, CachedContainer as j, startManualEmbeddingDownload as jn, createThemeStyles as jt, highlightLines as k, getKnowledgeStore as kn, isTurnElapsedEnabled as kt, isExpandable as l, isTodoItemShape as ln, isOrphanedToolCallError as lr, handleCompactCommand as lt, handleChannelCommand as m, EMPTY_SESSION_HINT_URL as mn, handleThemeCommand as mt, clearEvalPanelState as n, handleConnectCommand as nn, MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE as nr, handleTitleCommand as nt, handleSkillCommand as o, appendStreamingArgsPreview as on, log as or, getModelCycleLevel as ot, handleMemoryCommand as p, truncateErrorMessage as pn, handlePlanCommand as pt, getBreathingFrame as q, DEFAULT_CATALOG_URL as qn, TERMINAL_THEME_DARK as qt, openUrl as r, handleLogoutCommand as rn, resolveScreamHome as rr, isInternalMessage as rt, disposeChildren as s, argsRecord as sn, resolveGlobalLogPath as sr, handleAutoCommand as st, dispatchInput as t, handleSearchCommand as tn, removeSessionIndexEntry as tr, handleInitCommand as tt, isPlanExpandable as u, parseStreamingArgs as un, ErrorCodes as ur, handleEditorCommand as ut, refreshUpdateCache as v, TIP_ROTATION_INTERVAL_MS as vn, showPermissionPicker as vt, handleRevokeCommand as w, BUILTIN_SLASH_COMMANDS as wn, showUsage as wt, readJsonlFile as x, getLlmNotSetMessage as xn, supportsBalance as xt, readUpdateCache as y, getCtrlCHint as yn, showSettingsSelector as yt, ActivityGroupComponent as z, getDataDir as zn, ENABLE_TERMINAL_THEME_REPORTING as zt };