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.
- package/dist/{app-C0OLgL_b.mjs → app-D9oJ-Y8W.mjs} +224 -49
- package/dist/{dispatch-C7ujqj4p.mjs → dispatch-dDbUBD7J.mjs} +1 -1
- package/dist/{dispatch-La1sAZPF.mjs → dispatch-fjwbqaYI.mjs} +1275 -307
- package/dist/main.mjs +1 -1
- package/dist/{text-input-dialog-CNhL-3wm.mjs → text-input-dialog-Cj0egbsU.mjs} +1 -1
- package/dist/{text-input-dialog--J3ngWFv.mjs → text-input-dialog-DBlx8pMR.mjs} +24 -6
- package/package.json +2 -2
|
@@ -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
|
|
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
|
-
|
|
48282
|
-
|
|
48283
|
-
if (
|
|
48284
|
-
|
|
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
|
-
*
|
|
53616
|
-
*
|
|
53617
|
-
|
|
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
|
|
53621
|
-
|
|
53622
|
-
|
|
53623
|
-
|
|
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
|
|
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
|
|
69979
|
-
await this.jian.writeText(safePath,
|
|
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
|
|
70003
|
-
await this.jian.writeText(safePath,
|
|
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
|
-
|
|
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
|
-
|
|
86092
|
-
|
|
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
|
-
|
|
86095
|
-
|
|
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
|
|
86113
|
-
|
|
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
|
|
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 (
|
|
86128
|
-
|
|
86129
|
-
|
|
86130
|
-
|
|
86131
|
-
|
|
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 (
|
|
86143
|
-
|
|
86144
|
-
|
|
86145
|
-
|
|
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
|
-
|
|
86151
|
-
|
|
86152
|
-
|
|
86153
|
-
|
|
86154
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
86690
|
-
|
|
86691
|
-
|
|
86692
|
-
|
|
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
|
|
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.
|
|
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(/ /gi, " ").replaceAll(/&/gi, "&").replaceAll(/</gi, "<").replaceAll(/>/gi, ">").replaceAll(/"/gi, "\"").replaceAll(/'|'/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.
|
|
122440
|
+
const decoded = href.replaceAll(/&/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
|
-
|
|
122775
|
+
const consumeLine = (line) => {
|
|
122140
122776
|
const trimmed = line.trim();
|
|
122141
|
-
if (trimmed.length === 0)
|
|
122777
|
+
if (trimmed.length === 0) return;
|
|
122142
122778
|
let parsed;
|
|
122143
122779
|
try {
|
|
122144
122780
|
parsed = JSON.parse(trimmed);
|
|
122145
122781
|
} catch {
|
|
122146
|
-
|
|
122782
|
+
return;
|
|
122147
122783
|
}
|
|
122148
|
-
if (typeof parsed !== "object" || parsed === null)
|
|
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
|
-
|
|
123060
|
-
zip.addBuffer(data, rel);
|
|
123719
|
+
addStreamedFile(zip, abs, rel);
|
|
123061
123720
|
entries.push(rel);
|
|
123062
123721
|
}
|
|
123063
|
-
for (const extra of args.extraEntries ?? [])
|
|
123064
|
-
|
|
123065
|
-
|
|
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
|
-
}
|
|
123068
|
-
|
|
123069
|
-
|
|
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
|
-
|
|
123090
|
-
if (data !== void 0) {
|
|
123780
|
+
if (await pathIsFile(globalPath)) {
|
|
123091
123781
|
extras.push({
|
|
123092
|
-
|
|
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
|
|
123812
|
+
async function pathIsFile(path) {
|
|
123123
123813
|
try {
|
|
123124
|
-
return await
|
|
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(
|
|
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
|
|
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
|
-
*
|
|
127755
|
-
*
|
|
127756
|
-
*
|
|
127757
|
-
*
|
|
127758
|
-
*
|
|
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
|
|
128474
|
+
function hasEmbeddingModelCache() {
|
|
127761
128475
|
const modelDir = join(getEmbeddingCacheDir(), EMBEDDING_MODEL_DIR);
|
|
127762
|
-
return existsSync(
|
|
127763
|
-
}
|
|
127764
|
-
|
|
127765
|
-
|
|
127766
|
-
|
|
127767
|
-
|
|
127768
|
-
|
|
127769
|
-
|
|
127770
|
-
|
|
127771
|
-
|
|
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
|
-
|
|
127811
|
-
|
|
127812
|
-
|
|
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
|
-
|
|
129759
|
-
|
|
129760
|
-
|
|
129761
|
-
|
|
129762
|
-
|
|
129763
|
-
|
|
129764
|
-
|
|
129765
|
-
|
|
129766
|
-
|
|
129767
|
-
|
|
129768
|
-
|
|
129769
|
-
|
|
129770
|
-
|
|
129771
|
-
|
|
129772
|
-
|
|
129773
|
-
|
|
129774
|
-
|
|
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
|
-
|
|
129827
|
-
|
|
129828
|
-
|
|
129829
|
-
|
|
129830
|
-
|
|
129831
|
-
|
|
129832
|
-
|
|
129833
|
-
|
|
129834
|
-
|
|
129835
|
-
|
|
129836
|
-
|
|
129837
|
-
|
|
129838
|
-
|
|
129839
|
-
|
|
129840
|
-
|
|
129841
|
-
|
|
129842
|
-
|
|
129843
|
-
|
|
129844
|
-
|
|
129845
|
-
|
|
129846
|
-
|
|
129847
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129897
|
-
|
|
129898
|
-
|
|
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
|
-
|
|
129901
|
-
|
|
130682
|
+
let parsedRows = 0;
|
|
130683
|
+
const pushLine = (line) => {
|
|
130684
|
+
if (!line.trim()) return;
|
|
129902
130685
|
seq += 1;
|
|
130686
|
+
let parsed;
|
|
129903
130687
|
try {
|
|
129904
|
-
|
|
129905
|
-
|
|
129906
|
-
|
|
129907
|
-
|
|
129908
|
-
|
|
129909
|
-
|
|
129910
|
-
|
|
129911
|
-
|
|
129912
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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
|
-
|
|
135782
|
+
ui;
|
|
134961
135783
|
textComponent;
|
|
134962
135784
|
cachedWidth;
|
|
134963
135785
|
cachedLines;
|
|
134964
|
-
|
|
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
|
-
|
|
134968
|
-
this.
|
|
134969
|
-
this.
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
136483
|
+
if (contribution.removed === void 0) removedKnown = false;
|
|
136484
|
+
else removed += contribution.removed;
|
|
135579
136485
|
}
|
|
135580
|
-
|
|
136486
|
+
if (!seen) return void 0;
|
|
136487
|
+
return {
|
|
135581
136488
|
added,
|
|
135582
|
-
removed
|
|
135583
|
-
}
|
|
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
|
|
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(`${
|
|
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 (
|
|
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
|
|
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
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 $,
|
|
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 };
|