scream-code 0.16.4 → 0.16.5
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-F12Iraus.mjs} +15 -4
- package/dist/{dispatch-C7ujqj4p.mjs → dispatch-BNVgLsrQ.mjs} +1 -1
- package/dist/{dispatch-La1sAZPF.mjs → dispatch-VY-K2oXV.mjs} +955 -230
- package/dist/main.mjs +1 -1
- package/dist/{text-input-dialog-CNhL-3wm.mjs → text-input-dialog-CQ6R2lns.mjs} +1 -1
- package/dist/{text-input-dialog--J3ngWFv.mjs → text-input-dialog-KaFgI_Ko.mjs} +4 -0
- 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-KaFgI_Ko.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);
|
|
@@ -69519,6 +69534,77 @@ function withTimeout$1(promise, timeoutMs, parentSignal) {
|
|
|
69519
69534
|
});
|
|
69520
69535
|
}
|
|
69521
69536
|
//#endregion
|
|
69537
|
+
//#region ../../packages/agent-core/src/tools/support/file-diff.ts
|
|
69538
|
+
/**
|
|
69539
|
+
* Line-diff statistics for the file-mutating tools.
|
|
69540
|
+
*
|
|
69541
|
+
* Edit and Write both hold the pre-write and post-write text of the file they
|
|
69542
|
+
* touch, so they can report exact `+added / -removed` line counts to the UI.
|
|
69543
|
+
* Deriving those counts from the tool arguments instead is systematically
|
|
69544
|
+
* wrong (a Write overwrite hides every removed line; an Edit with
|
|
69545
|
+
* `replace_all` sees only one occurrence), so the tools report them here.
|
|
69546
|
+
*
|
|
69547
|
+
* Only counts leave this module: file contents must never be embedded in the
|
|
69548
|
+
* display payload, which is persisted with the session transcript.
|
|
69549
|
+
*/
|
|
69550
|
+
/**
|
|
69551
|
+
* Upper bound applied to either side of the diff. Beyond it the line diff is
|
|
69552
|
+
* skipped (returns `undefined`) so an oversized file cannot stall the calling
|
|
69553
|
+
* tool; the caller then omits the display and the UI falls back to its
|
|
69554
|
+
* argument-derived counts.
|
|
69555
|
+
*/
|
|
69556
|
+
const MAX_DIFF_CHARS = 1e6;
|
|
69557
|
+
/**
|
|
69558
|
+
* Wall-clock budget for a single line diff. jsdiff's Myers implementation has
|
|
69559
|
+
* adversarial cases that stay quadratic-ish well inside {@link MAX_DIFF_CHARS}:
|
|
69560
|
+
* two ~400 KB blobs with no common lines measured minutes before aborting. Past
|
|
69561
|
+
* the budget jsdiff gives up and the result is treated exactly like the size
|
|
69562
|
+
* guard (no display, argument-derived fallback) — stalling the tool, and with it
|
|
69563
|
+
* the event loop, is never acceptable for a UI statistic.
|
|
69564
|
+
*/
|
|
69565
|
+
const DIFF_BUDGET_MS = 250;
|
|
69566
|
+
/**
|
|
69567
|
+
* `diffLines` with the runtime `timeout` option. jsdiff honours `timeout`
|
|
69568
|
+
* (returning `undefined` when it gives up) but its published typings omit it.
|
|
69569
|
+
*/
|
|
69570
|
+
const diffLinesWithBudget = diffLines;
|
|
69571
|
+
/**
|
|
69572
|
+
* Count the lines in a `diffLines` part value the way `git diff --numstat`
|
|
69573
|
+
* counts them: a trailing newline terminates the preceding line instead of
|
|
69574
|
+
* opening an empty one, and the empty string has no lines.
|
|
69575
|
+
*/
|
|
69576
|
+
function countLines$1(value) {
|
|
69577
|
+
if (value.length === 0) return 0;
|
|
69578
|
+
const lineBreaks = value.split("\n").length - 1;
|
|
69579
|
+
return value.endsWith("\n") ? lineBreaks : lineBreaks + 1;
|
|
69580
|
+
}
|
|
69581
|
+
/**
|
|
69582
|
+
* Exact line counts between `before` and `after`: every added / removed part
|
|
69583
|
+
* `diffLines` reports contributes its own line count, so a single replaced
|
|
69584
|
+
* line is one addition plus one removal. Whitespace is significant (jsdiff's
|
|
69585
|
+
* `ignoreWhitespace` is off by default) and `\r\n` endings are compared
|
|
69586
|
+
* literally.
|
|
69587
|
+
*
|
|
69588
|
+
* Returns `undefined` when either side exceeds {@link MAX_DIFF_CHARS} or the
|
|
69589
|
+
* diff does not finish within {@link DIFF_BUDGET_MS}.
|
|
69590
|
+
*/
|
|
69591
|
+
function fileDiffSummary(before, after) {
|
|
69592
|
+
if (before.length > MAX_DIFF_CHARS || after.length > MAX_DIFF_CHARS) return void 0;
|
|
69593
|
+
const parts = diffLinesWithBudget(before, after, { timeout: DIFF_BUDGET_MS });
|
|
69594
|
+
if (parts === void 0) return void 0;
|
|
69595
|
+
let added = 0;
|
|
69596
|
+
let removed = 0;
|
|
69597
|
+
for (const part of parts) {
|
|
69598
|
+
const lines = countLines$1(part.value);
|
|
69599
|
+
if (part.added) added += lines;
|
|
69600
|
+
else if (part.removed) removed += lines;
|
|
69601
|
+
}
|
|
69602
|
+
return {
|
|
69603
|
+
added,
|
|
69604
|
+
removed
|
|
69605
|
+
};
|
|
69606
|
+
}
|
|
69607
|
+
//#endregion
|
|
69522
69608
|
//#region ../../packages/agent-core/src/tools/support/scan-cache.ts
|
|
69523
69609
|
var FsScanCache = class {
|
|
69524
69610
|
cache = /* @__PURE__ */ new Map();
|
|
@@ -69842,6 +69928,29 @@ function replaceOnceLiteral(content, oldString, newString) {
|
|
|
69842
69928
|
if (index === -1) return content;
|
|
69843
69929
|
return content.slice(0, index) + newString + content.slice(index + oldString.length);
|
|
69844
69930
|
}
|
|
69931
|
+
/**
|
|
69932
|
+
* Attach the exact `+added / -removed` line counts to a successful result so
|
|
69933
|
+
* the UI can total them per activity group.
|
|
69934
|
+
*
|
|
69935
|
+
* The counts come from the real pre-edit and post-edit text, so a
|
|
69936
|
+
* `replace_all` edit reports the whole-file delta instead of one occurrence's
|
|
69937
|
+
* worth. Error results carry no display: nothing was applied, or the failure
|
|
69938
|
+
* path never established the post-edit text.
|
|
69939
|
+
*/
|
|
69940
|
+
function withFileDiffDisplay(result, before, after) {
|
|
69941
|
+
if (result.isError === true) return result;
|
|
69942
|
+
const summary = fileDiffSummary(before, after);
|
|
69943
|
+
if (summary === void 0) return result;
|
|
69944
|
+
const display = {
|
|
69945
|
+
kind: "file_diff",
|
|
69946
|
+
added: summary.added,
|
|
69947
|
+
removed: summary.removed
|
|
69948
|
+
};
|
|
69949
|
+
return {
|
|
69950
|
+
...result,
|
|
69951
|
+
display
|
|
69952
|
+
};
|
|
69953
|
+
}
|
|
69845
69954
|
var EditTool = class {
|
|
69846
69955
|
jian;
|
|
69847
69956
|
workspace;
|
|
@@ -69915,7 +70024,8 @@ var EditTool = class {
|
|
|
69915
70024
|
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
70025
|
};
|
|
69917
70026
|
try {
|
|
69918
|
-
const
|
|
70027
|
+
const raw = await this.jian.readText(safePath);
|
|
70028
|
+
const modelView = toModelTextView(raw);
|
|
69919
70029
|
const content = modelView.text;
|
|
69920
70030
|
const replaceAll = args.replace_all ?? false;
|
|
69921
70031
|
const existingBlocks = scanConflictLines(content.split("\n"));
|
|
@@ -69975,20 +70085,20 @@ var EditTool = class {
|
|
|
69975
70085
|
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
70086
|
};
|
|
69977
70087
|
}
|
|
69978
|
-
const
|
|
69979
|
-
await this.jian.writeText(safePath,
|
|
70088
|
+
const writtenText = materializeModelText(replaceOnceLiteral(content, args.old_string, args.new_string), modelView.lineEndingStyle);
|
|
70089
|
+
await this.jian.writeText(safePath, writtenText);
|
|
69980
70090
|
scanCache.clear();
|
|
69981
70091
|
const { notice, hasErrors } = await this.appendDiagnostics(safePath);
|
|
69982
70092
|
const output = `Replaced 1 occurrence in ${args.path}`;
|
|
69983
70093
|
const message = notice.length > 0 ? notice : void 0;
|
|
69984
|
-
return hasErrors ? {
|
|
70094
|
+
return withFileDiffDisplay(hasErrors ? {
|
|
69985
70095
|
isError: true,
|
|
69986
70096
|
output,
|
|
69987
70097
|
message
|
|
69988
70098
|
} : {
|
|
69989
70099
|
output,
|
|
69990
70100
|
message
|
|
69991
|
-
};
|
|
70101
|
+
}, raw, writtenText);
|
|
69992
70102
|
}
|
|
69993
70103
|
const parts = content.split(args.old_string);
|
|
69994
70104
|
const replacementCount = parts.length - 1;
|
|
@@ -69999,20 +70109,20 @@ var EditTool = class {
|
|
|
69999
70109
|
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
70110
|
};
|
|
70001
70111
|
}
|
|
70002
|
-
const
|
|
70003
|
-
await this.jian.writeText(safePath,
|
|
70112
|
+
const writtenText = materializeModelText(parts.join(args.new_string), modelView.lineEndingStyle);
|
|
70113
|
+
await this.jian.writeText(safePath, writtenText);
|
|
70004
70114
|
scanCache.clear();
|
|
70005
70115
|
const { notice, hasErrors } = await this.appendDiagnostics(safePath);
|
|
70006
70116
|
const output = `Replaced ${String(replacementCount)} occurrences in ${args.path}`;
|
|
70007
70117
|
const message = notice.length > 0 ? notice : void 0;
|
|
70008
|
-
return hasErrors ? {
|
|
70118
|
+
return withFileDiffDisplay(hasErrors ? {
|
|
70009
70119
|
isError: true,
|
|
70010
70120
|
output,
|
|
70011
70121
|
message
|
|
70012
70122
|
} : {
|
|
70013
70123
|
output,
|
|
70014
70124
|
message
|
|
70015
|
-
};
|
|
70125
|
+
}, raw, writtenText);
|
|
70016
70126
|
} catch (error) {
|
|
70017
70127
|
if (error?.code === "EISDIR") return {
|
|
70018
70128
|
isError: true,
|
|
@@ -75681,6 +75791,12 @@ var write_default = "Write does not preserve or infer the previous line-ending s
|
|
|
75681
75791
|
const S_IFMT$2 = 61440;
|
|
75682
75792
|
/** File-type bits of a directory. */
|
|
75683
75793
|
const S_IFDIR$1 = 16384;
|
|
75794
|
+
/**
|
|
75795
|
+
* Size ceiling (bytes, from `stat`) for reading the pre-write text that feeds
|
|
75796
|
+
* the `file_diff` display. Mirrors the guard inside `fileDiffSummary`: past it
|
|
75797
|
+
* the diff is skipped anyway, so the file is never read only to be discarded.
|
|
75798
|
+
*/
|
|
75799
|
+
const MAX_DIFF_SOURCE_BYTES = 1e6;
|
|
75684
75800
|
const WriteInputSchema = z.object({
|
|
75685
75801
|
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
75802
|
content: z.string().describe("Raw full file content to write exactly as provided. This does not use the Read/Edit text view."),
|
|
@@ -75739,6 +75855,8 @@ var WriteTool = class {
|
|
|
75739
75855
|
};
|
|
75740
75856
|
try {
|
|
75741
75857
|
const mode = args.mode ?? "overwrite";
|
|
75858
|
+
const existing = await this.readExistingText(safePath);
|
|
75859
|
+
const display = mode === "append" ? this.appendFileDiffDisplay(args.content, existing) : this.overwriteFileDiffDisplay(args.content, existing);
|
|
75742
75860
|
if (mode === "append") await this.jian.writeText(safePath, args.content, { mode: "a" });
|
|
75743
75861
|
else await this.jian.writeText(safePath, args.content);
|
|
75744
75862
|
scanCache.clear();
|
|
@@ -75746,13 +75864,18 @@ var WriteTool = class {
|
|
|
75746
75864
|
const { notice, hasErrors } = await this.appendDiagnostics(safePath);
|
|
75747
75865
|
const output = `${mode === "append" ? "Appended" : "Wrote"} ${String(bytesWritten)} bytes to ${args.path}`;
|
|
75748
75866
|
const message = notice.length > 0 ? notice : void 0;
|
|
75749
|
-
|
|
75867
|
+
if (hasErrors) return {
|
|
75750
75868
|
isError: true,
|
|
75751
75869
|
output,
|
|
75752
75870
|
message
|
|
75753
|
-
}
|
|
75871
|
+
};
|
|
75872
|
+
return display === void 0 ? {
|
|
75754
75873
|
output,
|
|
75755
75874
|
message
|
|
75875
|
+
} : {
|
|
75876
|
+
output,
|
|
75877
|
+
message,
|
|
75878
|
+
display
|
|
75756
75879
|
};
|
|
75757
75880
|
} catch (error) {
|
|
75758
75881
|
if (error?.code === "ENOENT") return {
|
|
@@ -75766,6 +75889,74 @@ var WriteTool = class {
|
|
|
75766
75889
|
}
|
|
75767
75890
|
}
|
|
75768
75891
|
/**
|
|
75892
|
+
* Compute the `file_diff` line counts for an append.
|
|
75893
|
+
*
|
|
75894
|
+
* Appending concatenates bytes, so a file that does not end with a newline has
|
|
75895
|
+
* its last line rewritten by the first appended line — counting only the new
|
|
75896
|
+
* content (`+N -0`) would hide that rewritten line. Diffing the real pre-write
|
|
75897
|
+
* text against that text plus the new content is exact; a target that does not
|
|
75898
|
+
* exist yet can only gain lines.
|
|
75899
|
+
*/
|
|
75900
|
+
appendFileDiffDisplay(content, existing) {
|
|
75901
|
+
if (existing.kind === "unavailable") return void 0;
|
|
75902
|
+
const before = existing.kind === "missing" ? "" : existing.text;
|
|
75903
|
+
const summary = fileDiffSummary(before, before + content);
|
|
75904
|
+
if (summary === void 0) return void 0;
|
|
75905
|
+
return {
|
|
75906
|
+
kind: "file_diff",
|
|
75907
|
+
added: summary.added,
|
|
75908
|
+
removed: summary.removed
|
|
75909
|
+
};
|
|
75910
|
+
}
|
|
75911
|
+
/**
|
|
75912
|
+
* Compute the `file_diff` line counts for an overwrite.
|
|
75913
|
+
*
|
|
75914
|
+
* A file that does not exist yet can only gain lines, so the argument-derived
|
|
75915
|
+
* count is exact there. Otherwise the previous text is diffed against the
|
|
75916
|
+
* new text, which is the only way to see the removed lines an overwrite
|
|
75917
|
+
* destroys. Returns `undefined` (no display) when the previous text is
|
|
75918
|
+
* unknown or too large to diff.
|
|
75919
|
+
*/
|
|
75920
|
+
overwriteFileDiffDisplay(content, existing) {
|
|
75921
|
+
if (existing.kind === "missing") return {
|
|
75922
|
+
kind: "file_diff",
|
|
75923
|
+
added: countLines$1(content),
|
|
75924
|
+
removed: 0
|
|
75925
|
+
};
|
|
75926
|
+
if (existing.kind === "unavailable") return void 0;
|
|
75927
|
+
const summary = fileDiffSummary(existing.text, content);
|
|
75928
|
+
if (summary === void 0) return void 0;
|
|
75929
|
+
return {
|
|
75930
|
+
kind: "file_diff",
|
|
75931
|
+
added: summary.added,
|
|
75932
|
+
removed: summary.removed
|
|
75933
|
+
};
|
|
75934
|
+
}
|
|
75935
|
+
/**
|
|
75936
|
+
* Best-effort read of the text this call is about to overwrite, for the
|
|
75937
|
+
* `file_diff` display only.
|
|
75938
|
+
*
|
|
75939
|
+
* The `stat` size check runs first so an oversized file is never pulled into
|
|
75940
|
+
* memory just to be diffed. A failing `stat` for any reason other than
|
|
75941
|
+
* `ENOENT` is inconclusive, so the read is still attempted.
|
|
75942
|
+
*/
|
|
75943
|
+
async readExistingText(safePath) {
|
|
75944
|
+
try {
|
|
75945
|
+
if ((await this.jian.stat(safePath)).stSize > MAX_DIFF_SOURCE_BYTES) return { kind: "unavailable" };
|
|
75946
|
+
} catch (error) {
|
|
75947
|
+
if (error?.code === "ENOENT") return { kind: "missing" };
|
|
75948
|
+
}
|
|
75949
|
+
try {
|
|
75950
|
+
return {
|
|
75951
|
+
kind: "text",
|
|
75952
|
+
text: await this.jian.readText(safePath)
|
|
75953
|
+
};
|
|
75954
|
+
} catch (error) {
|
|
75955
|
+
if (error?.code === "ENOENT") return { kind: "missing" };
|
|
75956
|
+
return { kind: "unavailable" };
|
|
75957
|
+
}
|
|
75958
|
+
}
|
|
75959
|
+
/**
|
|
75769
75960
|
* Best-effort check that the parent directory exists and is a directory.
|
|
75770
75961
|
*
|
|
75771
75962
|
* The path schema documents this precondition; probing it up front turns a
|
|
@@ -86069,7 +86260,8 @@ const SNAPSHOT_FOLDED_CONTEXT_TYPES = new Set([
|
|
|
86069
86260
|
"context.append_loop_event",
|
|
86070
86261
|
"context.apply_compaction",
|
|
86071
86262
|
"micro_compaction.apply",
|
|
86072
|
-
"full_compaction.complete"
|
|
86263
|
+
"full_compaction.complete",
|
|
86264
|
+
"request.header"
|
|
86073
86265
|
]);
|
|
86074
86266
|
/**
|
|
86075
86267
|
* Serialized-line prefix of a context.snapshot record. Persisted records
|
|
@@ -86088,11 +86280,42 @@ const SNAPSHOT_FOLDED_LINE_PREFIXES = [...SNAPSHOT_FOLDED_CONTEXT_TYPES].map((ty
|
|
|
86088
86280
|
const SNAPSHOT_RECORD_LINE_PREFIX_BYTES = Buffer.from(SNAPSHOT_RECORD_LINE_PREFIX, "utf8");
|
|
86089
86281
|
/** UTF-8 bytes of each entry of {@link SNAPSHOT_FOLDED_LINE_PREFIXES}. */
|
|
86090
86282
|
const SNAPSHOT_FOLDED_LINE_PREFIX_BYTES = SNAPSHOT_FOLDED_LINE_PREFIXES.map((prefix) => Buffer.from(prefix, "utf8"));
|
|
86091
|
-
|
|
86092
|
-
|
|
86283
|
+
/**
|
|
86284
|
+
* Longest folded-type line prefix: the number of leading bytes a line needs
|
|
86285
|
+
* before the parse filter can decide whether to drop it. Until that many bytes
|
|
86286
|
+
* are seen the head is the only part of the line held in memory.
|
|
86287
|
+
*/
|
|
86288
|
+
const SNAPSHOT_FOLDED_MAX_PREFIX_BYTES = SNAPSHOT_FOLDED_LINE_PREFIX_BYTES.reduce((longest, prefix) => Math.max(longest, prefix.length), 0);
|
|
86289
|
+
/** Bytes-level prefix test for a line head that may be shorter than the prefix. */
|
|
86290
|
+
function headStartsWithPrefix(head, headLength, prefix) {
|
|
86291
|
+
return headLength >= prefix.length && head.subarray(0, prefix.length).equals(prefix);
|
|
86093
86292
|
}
|
|
86094
|
-
function
|
|
86095
|
-
return prefixes.some((prefix) =>
|
|
86293
|
+
function headStartsWithAnyPrefix(head, headLength, prefixes) {
|
|
86294
|
+
return prefixes.some((prefix) => headStartsWithPrefix(head, headLength, prefix));
|
|
86295
|
+
}
|
|
86296
|
+
/**
|
|
86297
|
+
* Whether folded context records that predate the last snapshot can be skipped
|
|
86298
|
+
* without decoding. Safe only when no wire migration is needed: a version
|
|
86299
|
+
* mismatch triggers migrations/rewrite that must see EVERY record, so
|
|
86300
|
+
* old/new-version files fall back to full parsing. The version lives in the
|
|
86301
|
+
* first (metadata) line. A header that cannot be parsed returns `false` here —
|
|
86302
|
+
* the same line is parsed again below, where the error is thrown.
|
|
86303
|
+
*/
|
|
86304
|
+
function readSkipFoldedFlag(headerText) {
|
|
86305
|
+
try {
|
|
86306
|
+
return JSON.parse(headerText).protocol_version === "1.4";
|
|
86307
|
+
} catch {
|
|
86308
|
+
return false;
|
|
86309
|
+
}
|
|
86310
|
+
}
|
|
86311
|
+
/** File size in bytes, or `undefined` when the file does not exist yet. */
|
|
86312
|
+
async function fileSize(path) {
|
|
86313
|
+
try {
|
|
86314
|
+
return (await stat(path)).size;
|
|
86315
|
+
} catch (error) {
|
|
86316
|
+
if (error.code === "ENOENT") return void 0;
|
|
86317
|
+
throw error;
|
|
86318
|
+
}
|
|
86096
86319
|
}
|
|
86097
86320
|
var FileSystemAgentRecordPersistence = class {
|
|
86098
86321
|
filePath;
|
|
@@ -86102,6 +86325,10 @@ var FileSystemAgentRecordPersistence = class {
|
|
|
86102
86325
|
directorySynced = false;
|
|
86103
86326
|
rewriteSeq = 0;
|
|
86104
86327
|
flushPromise;
|
|
86328
|
+
/** Set while `compact()` runs; writes queue up instead of draining. */
|
|
86329
|
+
compacting = false;
|
|
86330
|
+
/** Folded-history bytes skipped by the most recent full read(). */
|
|
86331
|
+
droppedBytes = 0;
|
|
86105
86332
|
error;
|
|
86106
86333
|
constructor(filePath, options = {}) {
|
|
86107
86334
|
this.filePath = filePath;
|
|
@@ -86109,54 +86336,189 @@ var FileSystemAgentRecordPersistence = class {
|
|
|
86109
86336
|
}
|
|
86110
86337
|
async *read() {
|
|
86111
86338
|
await this.flush();
|
|
86112
|
-
const
|
|
86113
|
-
|
|
86339
|
+
const size = await fileSize(this.filePath);
|
|
86340
|
+
if (size === void 0 || size === 0) return;
|
|
86341
|
+
const lastSnapshotLineNumber = await this.scanLastSnapshotLine(size);
|
|
86342
|
+
if (lastSnapshotLineNumber === void 0) return;
|
|
86343
|
+
yield* this.streamRecords(size, lastSnapshotLineNumber);
|
|
86344
|
+
}
|
|
86345
|
+
/**
|
|
86346
|
+
* Byte-level pass that returns the line number of the last
|
|
86347
|
+
* `context.snapshot` line, or `-1` when the file has none. `undefined` means
|
|
86348
|
+
* the file does not exist (a brand-new session has no wire yet — not an
|
|
86349
|
+
* error). Nothing is decoded and no line is retained: memory stays at one
|
|
86350
|
+
* stream chunk.
|
|
86351
|
+
*/
|
|
86352
|
+
async scanLastSnapshotLine(size) {
|
|
86353
|
+
const prefix = SNAPSHOT_RECORD_LINE_PREFIX_BYTES;
|
|
86354
|
+
const head = Buffer.allocUnsafe(prefix.length);
|
|
86355
|
+
let headLength = 0;
|
|
86114
86356
|
let lineNumber = 0;
|
|
86115
|
-
let
|
|
86116
|
-
const stream = createReadStream(this.filePath);
|
|
86357
|
+
let lastSnapshotLineNumber = -1;
|
|
86358
|
+
const stream = createReadStream(this.filePath, { end: size - 1 });
|
|
86117
86359
|
try {
|
|
86118
86360
|
for await (const chunk of stream) {
|
|
86119
86361
|
let searchFrom = 0;
|
|
86120
86362
|
for (;;) {
|
|
86121
86363
|
const newlineIndex = chunk.indexOf(10, searchFrom);
|
|
86364
|
+
const end = newlineIndex === -1 ? chunk.length : newlineIndex;
|
|
86365
|
+
if (end > searchFrom && headLength < prefix.length) {
|
|
86366
|
+
const take = Math.min(prefix.length - headLength, end - searchFrom);
|
|
86367
|
+
chunk.copy(head, headLength, searchFrom, searchFrom + take);
|
|
86368
|
+
headLength += take;
|
|
86369
|
+
}
|
|
86122
86370
|
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
86371
|
lineNumber++;
|
|
86127
|
-
if (
|
|
86128
|
-
|
|
86129
|
-
|
|
86130
|
-
|
|
86131
|
-
|
|
86132
|
-
|
|
86372
|
+
if (headLength === prefix.length && head.equals(prefix)) lastSnapshotLineNumber = lineNumber;
|
|
86373
|
+
headLength = 0;
|
|
86374
|
+
searchFrom = newlineIndex + 1;
|
|
86375
|
+
}
|
|
86376
|
+
}
|
|
86377
|
+
} catch (error) {
|
|
86378
|
+
if (error.code === "ENOENT") return void 0;
|
|
86379
|
+
throw error;
|
|
86380
|
+
}
|
|
86381
|
+
return lastSnapshotLineNumber;
|
|
86382
|
+
}
|
|
86383
|
+
/**
|
|
86384
|
+
* Streaming replay pass. Folded records predating the last snapshot are
|
|
86385
|
+
* dropped WITHOUT being decoded — the restore fast-path discards them anyway
|
|
86386
|
+
* (records/index.ts snapshot branch), so the yielded stream is identical to a
|
|
86387
|
+
* full decode while neither the file nor the skipped lines are ever held in
|
|
86388
|
+
* memory. An unterminated trailing line is tolerated (see parseRecordLine):
|
|
86389
|
+
* the last write may have crashed mid-flush.
|
|
86390
|
+
*/
|
|
86391
|
+
async *streamRecords(size, lastSnapshotLineNumber) {
|
|
86392
|
+
const head = Buffer.allocUnsafe(SNAPSHOT_FOLDED_MAX_PREFIX_BYTES);
|
|
86393
|
+
let headLength = 0;
|
|
86394
|
+
let openChunks = [];
|
|
86395
|
+
let decided = false;
|
|
86396
|
+
let retaining = false;
|
|
86397
|
+
let headerSeen = false;
|
|
86398
|
+
let skipFoldedBeforeSnapshot = false;
|
|
86399
|
+
let lineNumber = 0;
|
|
86400
|
+
let lineBytes = 0;
|
|
86401
|
+
let skippedBytes = 0;
|
|
86402
|
+
const retainedLine = () => openChunks.length === 1 ? openChunks[0] : Buffer.concat(openChunks);
|
|
86403
|
+
const keepLine = (currentLine) => !(skipFoldedBeforeSnapshot && currentLine < lastSnapshotLineNumber && headStartsWithAnyPrefix(head, headLength, SNAPSHOT_FOLDED_LINE_PREFIX_BYTES));
|
|
86404
|
+
const stream = createReadStream(this.filePath, { end: size - 1 });
|
|
86405
|
+
try {
|
|
86406
|
+
for await (const chunk of stream) {
|
|
86407
|
+
let searchFrom = 0;
|
|
86408
|
+
for (;;) {
|
|
86409
|
+
const newlineIndex = chunk.indexOf(10, searchFrom);
|
|
86410
|
+
const end = newlineIndex === -1 ? chunk.length : newlineIndex;
|
|
86411
|
+
if (!decided && end > searchFrom) {
|
|
86412
|
+
const take = Math.min(SNAPSHOT_FOLDED_MAX_PREFIX_BYTES - headLength, end - searchFrom);
|
|
86413
|
+
chunk.copy(head, headLength, searchFrom, searchFrom + take);
|
|
86414
|
+
headLength += take;
|
|
86415
|
+
searchFrom += take;
|
|
86416
|
+
lineBytes += take;
|
|
86417
|
+
if (headLength === SNAPSHOT_FOLDED_MAX_PREFIX_BYTES) {
|
|
86418
|
+
decided = true;
|
|
86419
|
+
retaining = keepLine(lineNumber + 1);
|
|
86420
|
+
if (retaining) openChunks.push(Buffer.from(head));
|
|
86421
|
+
}
|
|
86422
|
+
}
|
|
86423
|
+
if (retaining && end > searchFrom) openChunks.push(chunk.subarray(searchFrom, end));
|
|
86424
|
+
if (newlineIndex === -1) {
|
|
86425
|
+
lineBytes += end - searchFrom;
|
|
86426
|
+
break;
|
|
86427
|
+
}
|
|
86428
|
+
lineBytes += newlineIndex + 1 - searchFrom;
|
|
86429
|
+
lineNumber++;
|
|
86430
|
+
if (!decided) {
|
|
86431
|
+
decided = true;
|
|
86432
|
+
retaining = keepLine(lineNumber);
|
|
86433
|
+
if (retaining && headLength > 0) openChunks.push(Buffer.from(head.subarray(0, headLength)));
|
|
86434
|
+
}
|
|
86435
|
+
if (retaining) {
|
|
86436
|
+
let lineData = retainedLine();
|
|
86437
|
+
if (lineData.length > 0 && lineData.at(-1) === 13) lineData = lineData.subarray(0, lineData.length - 1);
|
|
86438
|
+
const lineText = lineData.toString("utf8");
|
|
86439
|
+
if (!headerSeen) {
|
|
86440
|
+
headerSeen = true;
|
|
86441
|
+
skipFoldedBeforeSnapshot = readSkipFoldedFlag(lineText);
|
|
86442
|
+
}
|
|
86443
|
+
const record = parseRecordLine(lineText, lineNumber, this.filePath, false);
|
|
86444
|
+
if (record !== void 0) yield record;
|
|
86445
|
+
}
|
|
86446
|
+
if (!retaining) skippedBytes += lineBytes;
|
|
86133
86447
|
openChunks = [];
|
|
86448
|
+
headLength = 0;
|
|
86449
|
+
decided = false;
|
|
86450
|
+
retaining = false;
|
|
86451
|
+
lineBytes = 0;
|
|
86134
86452
|
searchFrom = newlineIndex + 1;
|
|
86135
86453
|
}
|
|
86136
|
-
if (searchFrom < chunk.length) openChunks.push(chunk.subarray(searchFrom));
|
|
86137
86454
|
}
|
|
86138
86455
|
} catch (error) {
|
|
86139
86456
|
if (error.code === "ENOENT") return;
|
|
86140
86457
|
throw error;
|
|
86141
86458
|
}
|
|
86142
|
-
if (
|
|
86143
|
-
|
|
86144
|
-
|
|
86145
|
-
|
|
86146
|
-
lineNumber,
|
|
86147
|
-
allowTruncated: true
|
|
86148
|
-
});
|
|
86459
|
+
if (!decided && headLength > 0) {
|
|
86460
|
+
decided = true;
|
|
86461
|
+
retaining = keepLine(lineNumber + 1);
|
|
86462
|
+
if (retaining) openChunks.push(Buffer.from(head.subarray(0, headLength)));
|
|
86149
86463
|
}
|
|
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);
|
|
86464
|
+
if (retaining && openChunks.length > 0) {
|
|
86465
|
+
lineNumber++;
|
|
86466
|
+
let lineData = retainedLine();
|
|
86467
|
+
if (lineData.length > 0 && lineData.at(-1) === 13) lineData = lineData.subarray(0, lineData.length - 1);
|
|
86468
|
+
const record = parseRecordLine(lineData.toString("utf8"), lineNumber, this.filePath, true);
|
|
86159
86469
|
if (record !== void 0) yield record;
|
|
86470
|
+
} else if (!retaining) skippedBytes += lineBytes;
|
|
86471
|
+
this.droppedBytes = skippedBytes;
|
|
86472
|
+
}
|
|
86473
|
+
/** Folded-history bytes skipped by the most recent full read(). */
|
|
86474
|
+
droppedBytesOnLastRead() {
|
|
86475
|
+
return this.droppedBytes;
|
|
86476
|
+
}
|
|
86477
|
+
/** Whether the last read() skipped enough folded history to be worth reclaiming on disk. */
|
|
86478
|
+
shouldCompactOnResume() {
|
|
86479
|
+
return this.droppedBytes >= (this.options.compactThresholdBytes ?? 8388608);
|
|
86480
|
+
}
|
|
86481
|
+
/**
|
|
86482
|
+
* Physically drop every record that predates the last `context.snapshot`
|
|
86483
|
+
* (exactly the lines `read()` skips). Byte-preserving: retained lines are
|
|
86484
|
+
* copied verbatim, so unknown records, CRLF framing and blob references
|
|
86485
|
+
* survive untouched. Writes a temp file, fsyncs it, then atomically renames
|
|
86486
|
+
* it over the wire. While it runs, appends queue up instead of draining
|
|
86487
|
+
* (see compacting) and are flushed right after the swap.
|
|
86488
|
+
*/
|
|
86489
|
+
async compact() {
|
|
86490
|
+
await this.flush();
|
|
86491
|
+
this.compacting = true;
|
|
86492
|
+
const tmpPath = `${this.filePath}.${process.pid}.${this.rewriteSeq++}.compact.tmp`;
|
|
86493
|
+
let swapped = false;
|
|
86494
|
+
try {
|
|
86495
|
+
const size = await fileSize(this.filePath);
|
|
86496
|
+
if (size === void 0 || size === 0) return;
|
|
86497
|
+
const lastSnapshotLineNumber = await this.scanLastSnapshotLine(size);
|
|
86498
|
+
if (lastSnapshotLineNumber === void 0) return;
|
|
86499
|
+
const directory = dirname$2(this.filePath);
|
|
86500
|
+
const tmp = await open(tmpPath, "w");
|
|
86501
|
+
let written = 0;
|
|
86502
|
+
try {
|
|
86503
|
+
written = await copyRetainingLines(this.filePath, tmp, size, lastSnapshotLineNumber);
|
|
86504
|
+
await tmp.sync();
|
|
86505
|
+
} finally {
|
|
86506
|
+
await tmp.close();
|
|
86507
|
+
}
|
|
86508
|
+
if (written === size) {
|
|
86509
|
+
this.droppedBytes = 0;
|
|
86510
|
+
return;
|
|
86511
|
+
}
|
|
86512
|
+
if (await fileSize(this.filePath) !== size) return;
|
|
86513
|
+
await rename(tmpPath, this.filePath);
|
|
86514
|
+
swapped = true;
|
|
86515
|
+
await syncDir(directory);
|
|
86516
|
+
this.directorySynced = true;
|
|
86517
|
+
this.droppedBytes = 0;
|
|
86518
|
+
} finally {
|
|
86519
|
+
if (!swapped) await rm(tmpPath, { force: true }).catch(() => {});
|
|
86520
|
+
this.compacting = false;
|
|
86521
|
+
if (this.shouldClear || this.pendingRecords.length > 0) this.scheduleFlush();
|
|
86160
86522
|
}
|
|
86161
86523
|
}
|
|
86162
86524
|
append(input) {
|
|
@@ -86172,6 +86534,7 @@ var FileSystemAgentRecordPersistence = class {
|
|
|
86172
86534
|
}
|
|
86173
86535
|
async flush() {
|
|
86174
86536
|
this.throwIfError();
|
|
86537
|
+
while (this.compacting) await new Promise((resolve) => setTimeout(resolve, 10));
|
|
86175
86538
|
while (this.flushPromise !== void 0 || this.shouldClear || this.pendingRecords.length > 0) {
|
|
86176
86539
|
await this.ensureFlush();
|
|
86177
86540
|
this.throwIfError();
|
|
@@ -86186,6 +86549,7 @@ var FileSystemAgentRecordPersistence = class {
|
|
|
86186
86549
|
});
|
|
86187
86550
|
}
|
|
86188
86551
|
ensureFlush() {
|
|
86552
|
+
if (this.compacting) return Promise.resolve();
|
|
86189
86553
|
if (this.flushPromise !== void 0) return this.flushPromise;
|
|
86190
86554
|
const promise = this.drainPendingRecords().catch((error) => {
|
|
86191
86555
|
this.error = error;
|
|
@@ -86201,20 +86565,20 @@ var FileSystemAgentRecordPersistence = class {
|
|
|
86201
86565
|
if (this.error !== void 0) throw this.error;
|
|
86202
86566
|
}
|
|
86203
86567
|
async drainPendingRecords() {
|
|
86204
|
-
while (this.shouldClear || this.pendingRecords.length > 0) await this.drainBatch();
|
|
86568
|
+
while (!this.compacting && (this.shouldClear || this.pendingRecords.length > 0)) await this.drainBatch();
|
|
86205
86569
|
}
|
|
86206
86570
|
async drainBatch() {
|
|
86207
86571
|
const shouldClear = this.shouldClear;
|
|
86208
86572
|
const batch = this.pendingRecords.splice(0);
|
|
86209
86573
|
this.shouldClear = false;
|
|
86210
|
-
const
|
|
86574
|
+
const writable = this.options.blobStore !== void 0 ? await Promise.all(batch.map((record) => this.options.blobStore.offload(record))) : batch;
|
|
86211
86575
|
const directory = dirname$2(this.filePath);
|
|
86212
86576
|
await mkdir(directory, { recursive: true });
|
|
86213
86577
|
if (shouldClear) {
|
|
86214
86578
|
const tmpPath = `${this.filePath}.${process.pid}.${this.rewriteSeq++}.tmp`;
|
|
86215
86579
|
const tmp = await open(tmpPath, "w");
|
|
86216
86580
|
try {
|
|
86217
|
-
|
|
86581
|
+
await writeChunked(tmp, writable);
|
|
86218
86582
|
await tmp.sync();
|
|
86219
86583
|
} finally {
|
|
86220
86584
|
await tmp.close();
|
|
@@ -86226,7 +86590,7 @@ var FileSystemAgentRecordPersistence = class {
|
|
|
86226
86590
|
}
|
|
86227
86591
|
const fh = await open(this.filePath, "a");
|
|
86228
86592
|
try {
|
|
86229
|
-
|
|
86593
|
+
await writeChunked(fh, writable);
|
|
86230
86594
|
await fh.sync();
|
|
86231
86595
|
} finally {
|
|
86232
86596
|
await fh.close();
|
|
@@ -86237,6 +86601,94 @@ var FileSystemAgentRecordPersistence = class {
|
|
|
86237
86601
|
}
|
|
86238
86602
|
}
|
|
86239
86603
|
};
|
|
86604
|
+
const MAX_WRITE_CHUNK_CHARS = 1e6;
|
|
86605
|
+
/**
|
|
86606
|
+
* Byte-preserving filter pass for compact(): copies every line that
|
|
86607
|
+
* streamRecords() would retain (same fold decision, same unterminated-tail
|
|
86608
|
+
* tolerance) into `out`, returning the number of bytes written. Kept in the
|
|
86609
|
+
* same shape as streamRecords() on purpose — the two must agree on which
|
|
86610
|
+
* lines survive; change both together.
|
|
86611
|
+
*/
|
|
86612
|
+
async function copyRetainingLines(filePath, out, size, lastSnapshotLineNumber) {
|
|
86613
|
+
const head = Buffer.allocUnsafe(SNAPSHOT_FOLDED_MAX_PREFIX_BYTES);
|
|
86614
|
+
let headLength = 0;
|
|
86615
|
+
let openChunks = [];
|
|
86616
|
+
let decided = false;
|
|
86617
|
+
let retaining = false;
|
|
86618
|
+
let headerSeen = false;
|
|
86619
|
+
let skipFoldedBeforeSnapshot = false;
|
|
86620
|
+
let lineNumber = 0;
|
|
86621
|
+
let written = 0;
|
|
86622
|
+
const keepLine = (currentLine) => !(skipFoldedBeforeSnapshot && currentLine < lastSnapshotLineNumber && headStartsWithAnyPrefix(head, headLength, SNAPSHOT_FOLDED_LINE_PREFIX_BYTES));
|
|
86623
|
+
const writeRetainedLine = async () => {
|
|
86624
|
+
const data = openChunks.length === 1 ? openChunks[0] : Buffer.concat(openChunks);
|
|
86625
|
+
if (data.length > 0) {
|
|
86626
|
+
await out.write(data);
|
|
86627
|
+
written += data.length;
|
|
86628
|
+
}
|
|
86629
|
+
};
|
|
86630
|
+
const stream = createReadStream(filePath, { end: size - 1 });
|
|
86631
|
+
for await (const chunk of stream) {
|
|
86632
|
+
let searchFrom = 0;
|
|
86633
|
+
for (;;) {
|
|
86634
|
+
const newlineIndex = chunk.indexOf(10, searchFrom);
|
|
86635
|
+
const end = newlineIndex === -1 ? chunk.length : newlineIndex;
|
|
86636
|
+
if (!decided && end > searchFrom) {
|
|
86637
|
+
const take = Math.min(SNAPSHOT_FOLDED_MAX_PREFIX_BYTES - headLength, end - searchFrom);
|
|
86638
|
+
chunk.copy(head, headLength, searchFrom, searchFrom + take);
|
|
86639
|
+
headLength += take;
|
|
86640
|
+
searchFrom += take;
|
|
86641
|
+
if (headLength === SNAPSHOT_FOLDED_MAX_PREFIX_BYTES) {
|
|
86642
|
+
decided = true;
|
|
86643
|
+
retaining = keepLine(lineNumber + 1);
|
|
86644
|
+
if (retaining) openChunks.push(Buffer.from(head));
|
|
86645
|
+
}
|
|
86646
|
+
}
|
|
86647
|
+
if (retaining && end > searchFrom) openChunks.push(chunk.subarray(searchFrom, end));
|
|
86648
|
+
if (newlineIndex === -1) break;
|
|
86649
|
+
lineNumber++;
|
|
86650
|
+
if (!decided) {
|
|
86651
|
+
decided = true;
|
|
86652
|
+
retaining = keepLine(lineNumber);
|
|
86653
|
+
if (retaining && headLength > 0) openChunks.push(Buffer.from(head.subarray(0, headLength)));
|
|
86654
|
+
}
|
|
86655
|
+
if (retaining) {
|
|
86656
|
+
openChunks.push(chunk.subarray(newlineIndex, newlineIndex + 1));
|
|
86657
|
+
if (!headerSeen) {
|
|
86658
|
+
headerSeen = true;
|
|
86659
|
+
let lineData = openChunks.length === 1 ? openChunks[0] : Buffer.concat(openChunks);
|
|
86660
|
+
if (lineData.length > 0 && lineData.at(-1) === 10) lineData = lineData.subarray(0, lineData.length - 1);
|
|
86661
|
+
if (lineData.length > 0 && lineData.at(-1) === 13) lineData = lineData.subarray(0, lineData.length - 1);
|
|
86662
|
+
skipFoldedBeforeSnapshot = readSkipFoldedFlag(lineData.toString("utf8"));
|
|
86663
|
+
}
|
|
86664
|
+
await writeRetainedLine();
|
|
86665
|
+
}
|
|
86666
|
+
openChunks = [];
|
|
86667
|
+
headLength = 0;
|
|
86668
|
+
decided = false;
|
|
86669
|
+
retaining = false;
|
|
86670
|
+
searchFrom = newlineIndex + 1;
|
|
86671
|
+
}
|
|
86672
|
+
}
|
|
86673
|
+
if (!decided && headLength > 0) {
|
|
86674
|
+
decided = true;
|
|
86675
|
+
retaining = keepLine(lineNumber + 1);
|
|
86676
|
+
if (retaining) openChunks.push(Buffer.from(head.subarray(0, headLength)));
|
|
86677
|
+
}
|
|
86678
|
+
if (retaining && openChunks.length > 0) await writeRetainedLine();
|
|
86679
|
+
return written;
|
|
86680
|
+
}
|
|
86681
|
+
async function writeChunked(handle, records) {
|
|
86682
|
+
let chunk = "";
|
|
86683
|
+
for (const record of records) {
|
|
86684
|
+
chunk += JSON.stringify(record) + "\n";
|
|
86685
|
+
if (chunk.length >= MAX_WRITE_CHUNK_CHARS) {
|
|
86686
|
+
await handle.write(chunk, null, "utf8");
|
|
86687
|
+
chunk = "";
|
|
86688
|
+
}
|
|
86689
|
+
}
|
|
86690
|
+
if (chunk.length > 0) await handle.write(chunk, null, "utf8");
|
|
86691
|
+
}
|
|
86240
86692
|
function parseRecordLine(line, lineNumber, filePath, allowTruncated) {
|
|
86241
86693
|
if (line.length === 0) return void 0;
|
|
86242
86694
|
try {
|
|
@@ -86570,6 +87022,12 @@ function restoreAgentRecord(agent, input) {
|
|
|
86570
87022
|
case "context.append_message":
|
|
86571
87023
|
agent.context.appendMessage(input.message);
|
|
86572
87024
|
return;
|
|
87025
|
+
case "context.stream_draft":
|
|
87026
|
+
agent.replayBuilder.replacePartialDraft(input.turnId, {
|
|
87027
|
+
text: input.text,
|
|
87028
|
+
think: input.think
|
|
87029
|
+
});
|
|
87030
|
+
return;
|
|
86573
87031
|
case "context.append_loop_event":
|
|
86574
87032
|
agent.context.appendLoopEvent(input.event);
|
|
86575
87033
|
return;
|
|
@@ -86651,6 +87109,7 @@ var AgentRecords = class {
|
|
|
86651
87109
|
let hasMetadata = false;
|
|
86652
87110
|
let shouldRewrite = false;
|
|
86653
87111
|
let warning;
|
|
87112
|
+
let buffered = false;
|
|
86654
87113
|
const replayedRecords = [];
|
|
86655
87114
|
for await (const record of this.persistence.read()) {
|
|
86656
87115
|
if (!hasMetadata) {
|
|
@@ -86665,38 +87124,45 @@ var AgentRecords = class {
|
|
|
86665
87124
|
migrations = resolveWireMigrations(readVersion);
|
|
86666
87125
|
shouldRewrite = readVersion !== "1.4";
|
|
86667
87126
|
}
|
|
87127
|
+
buffered = shouldRewrite || warning !== void 0;
|
|
86668
87128
|
}
|
|
86669
87129
|
let migratedRecord = migrateWireRecord(record, migrations);
|
|
86670
87130
|
if (migratedRecord.type === "metadata") migratedRecord = {
|
|
86671
87131
|
...migratedRecord,
|
|
86672
87132
|
protocol_version: "1.4"
|
|
86673
87133
|
};
|
|
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;
|
|
87134
|
+
if (buffered) {
|
|
87135
|
+
replayedRecords.push(migratedRecord);
|
|
86687
87136
|
continue;
|
|
86688
87137
|
}
|
|
86689
|
-
this.restore(
|
|
86690
|
-
|
|
86691
|
-
|
|
86692
|
-
|
|
87138
|
+
this.restore(migratedRecord);
|
|
87139
|
+
}
|
|
87140
|
+
if (buffered) {
|
|
87141
|
+
let snapshotIndex = -1;
|
|
87142
|
+
for (let i = replayedRecords.length - 1; i >= 0; i--) if (replayedRecords[i]?.type === "context.snapshot") {
|
|
87143
|
+
snapshotIndex = i;
|
|
87144
|
+
break;
|
|
87145
|
+
}
|
|
87146
|
+
let foldedCompactionSummary;
|
|
87147
|
+
for (let i = 0; i < replayedRecords.length; i++) {
|
|
87148
|
+
const record = replayedRecords[i];
|
|
87149
|
+
if (!record) continue;
|
|
87150
|
+
if (i < snapshotIndex && isSnapshotFoldedContextRecord(record.type)) {
|
|
87151
|
+
if (record.type === "context.apply_compaction") foldedCompactionSummary = record.summary;
|
|
87152
|
+
continue;
|
|
87153
|
+
}
|
|
87154
|
+
this.restore(record);
|
|
87155
|
+
if (record.type === "context.snapshot" && foldedCompactionSummary !== void 0) {
|
|
87156
|
+
recoverMemosFromCompactionSummary(this.agent, foldedCompactionSummary);
|
|
87157
|
+
foldedCompactionSummary = void 0;
|
|
87158
|
+
}
|
|
86693
87159
|
}
|
|
86694
87160
|
}
|
|
86695
87161
|
this.agent.context.dropVacuousOpenMessages();
|
|
86696
87162
|
if (shouldRewrite) {
|
|
86697
87163
|
this.persistence.rewrite(replayedRecords);
|
|
86698
87164
|
await this.persistence.flush();
|
|
86699
|
-
}
|
|
87165
|
+
} else if (this.persistence.shouldCompactOnResume?.() === true && this.persistence.compact !== void 0) await this.persistence.compact();
|
|
86700
87166
|
if (this.agent.blobStore !== void 0) for (const msg of this.agent.context.history) await this.agent.blobStore.rehydrateParts(msg.content);
|
|
86701
87167
|
return { warning };
|
|
86702
87168
|
}
|
|
@@ -86723,6 +87189,8 @@ var ReplayBuilder = class {
|
|
|
86723
87189
|
records = [];
|
|
86724
87190
|
/** Indices (into `records`) of user-turn-start records, oldest first. */
|
|
86725
87191
|
userTurnStarts = [];
|
|
87192
|
+
/** Index (into `records`) of the partial-draft record per turn, if present. */
|
|
87193
|
+
partialDraftIndices = /* @__PURE__ */ new Map();
|
|
86726
87194
|
constructor(agent) {
|
|
86727
87195
|
this.agent = agent;
|
|
86728
87196
|
}
|
|
@@ -86735,6 +87203,43 @@ var ReplayBuilder = class {
|
|
|
86735
87203
|
const dropBefore = this.userTurnStarts[this.userTurnStarts.length - 10];
|
|
86736
87204
|
this.records.splice(0, dropBefore);
|
|
86737
87205
|
this.userTurnStarts = this.userTurnStarts.slice(-10).map((i) => i - dropBefore);
|
|
87206
|
+
for (const [turnId, index] of this.partialDraftIndices) {
|
|
87207
|
+
const shifted = index - dropBefore;
|
|
87208
|
+
if (shifted < 0) this.partialDraftIndices.delete(turnId);
|
|
87209
|
+
else this.partialDraftIndices.set(turnId, shifted);
|
|
87210
|
+
}
|
|
87211
|
+
}
|
|
87212
|
+
/**
|
|
87213
|
+
* Track the latest surviving stream draft for a turn during restore: the
|
|
87214
|
+
* first draft pushes a `stream_draft_partial` record, later drafts replace
|
|
87215
|
+
* it in place (no index shifts), and a clearing draft (empty text+think)
|
|
87216
|
+
* removes it. Only used while `agent.records.restoring` is true.
|
|
87217
|
+
*/
|
|
87218
|
+
replacePartialDraft(turnId, draft) {
|
|
87219
|
+
if (!this.agent.records.restoring) return;
|
|
87220
|
+
const existing = this.partialDraftIndices.get(turnId);
|
|
87221
|
+
if (draft.text.length === 0 && draft.think.length === 0) {
|
|
87222
|
+
if (existing !== void 0) {
|
|
87223
|
+
this.records.splice(existing, 1);
|
|
87224
|
+
this.partialDraftIndices.delete(turnId);
|
|
87225
|
+
for (const [id, index] of this.partialDraftIndices) if (index > existing) this.partialDraftIndices.set(id, index - 1);
|
|
87226
|
+
this.userTurnStarts = this.userTurnStarts.map((i) => i > existing ? i - 1 : i);
|
|
87227
|
+
}
|
|
87228
|
+
return;
|
|
87229
|
+
}
|
|
87230
|
+
const record = {
|
|
87231
|
+
type: "stream_draft_partial",
|
|
87232
|
+
turnId,
|
|
87233
|
+
text: draft.text,
|
|
87234
|
+
think: draft.think
|
|
87235
|
+
};
|
|
87236
|
+
if (existing !== void 0 && this.records[existing]?.type === "stream_draft_partial") {
|
|
87237
|
+
this.records[existing] = record;
|
|
87238
|
+
return;
|
|
87239
|
+
}
|
|
87240
|
+
if (existing !== void 0) this.partialDraftIndices.delete(turnId);
|
|
87241
|
+
this.records.push(record);
|
|
87242
|
+
this.partialDraftIndices.set(turnId, this.records.length - 1);
|
|
86738
87243
|
}
|
|
86739
87244
|
buildResult() {
|
|
86740
87245
|
return this.records;
|
|
@@ -95041,7 +95546,8 @@ async function executeLoopStep(deps) {
|
|
|
95041
95546
|
dispatchEvent,
|
|
95042
95547
|
turnId,
|
|
95043
95548
|
currentStep,
|
|
95044
|
-
stepUuid
|
|
95549
|
+
stepUuid,
|
|
95550
|
+
onStreamingDraft: deps.onStreamingDraft ?? (() => {})
|
|
95045
95551
|
})
|
|
95046
95552
|
};
|
|
95047
95553
|
let response;
|
|
@@ -95175,18 +95681,75 @@ function stepEndProviderDiagnostics(response, stopReason) {
|
|
|
95175
95681
|
...response.rawFinishReason !== void 0 ? { rawFinishReason: response.rawFinishReason } : {}
|
|
95176
95682
|
};
|
|
95177
95683
|
}
|
|
95684
|
+
/**
|
|
95685
|
+
* Accumulates the in-flight stream for the crash-recovery draft and flushes
|
|
95686
|
+
* it through `onDraft` at most once per 1.5s or 4k new characters, so a fast
|
|
95687
|
+
* stream does not write the wire on every delta while a slow stream still
|
|
95688
|
+
* checkpoints promptly. Failures in the sink never break the stream.
|
|
95689
|
+
*/
|
|
95690
|
+
var StreamDraftTracker = class {
|
|
95691
|
+
onDraft;
|
|
95692
|
+
text = "";
|
|
95693
|
+
think = "";
|
|
95694
|
+
charsSinceFlush = 0;
|
|
95695
|
+
lastFlushAt = Date.now();
|
|
95696
|
+
wireHasDraft = false;
|
|
95697
|
+
constructor(onDraft) {
|
|
95698
|
+
this.onDraft = onDraft;
|
|
95699
|
+
}
|
|
95700
|
+
onText(delta) {
|
|
95701
|
+
this.text += delta;
|
|
95702
|
+
this.charsSinceFlush += delta.length;
|
|
95703
|
+
this.maybeFlush();
|
|
95704
|
+
}
|
|
95705
|
+
onThink(delta) {
|
|
95706
|
+
this.think += delta;
|
|
95707
|
+
this.charsSinceFlush += delta.length;
|
|
95708
|
+
this.maybeFlush();
|
|
95709
|
+
}
|
|
95710
|
+
/**
|
|
95711
|
+
* Real content parts are landing: drop the draft (persisted as empty) — but
|
|
95712
|
+
* only if the wire actually carries one. A stream that finished before any
|
|
95713
|
+
* draft flush (fast mock streams, short replies) must not pollute the wire
|
|
95714
|
+
* with an empty clear record.
|
|
95715
|
+
*/
|
|
95716
|
+
clear() {
|
|
95717
|
+
if (!this.wireHasDraft) return;
|
|
95718
|
+
this.text = "";
|
|
95719
|
+
this.think = "";
|
|
95720
|
+
this.charsSinceFlush = 0;
|
|
95721
|
+
this.flush();
|
|
95722
|
+
}
|
|
95723
|
+
maybeFlush() {
|
|
95724
|
+
if (Date.now() - this.lastFlushAt < DRAFT_FLUSH_INTERVAL_MS && this.charsSinceFlush < DRAFT_FLUSH_MIN_CHARS) return;
|
|
95725
|
+
this.flush();
|
|
95726
|
+
}
|
|
95727
|
+
flush() {
|
|
95728
|
+
this.lastFlushAt = Date.now();
|
|
95729
|
+
this.charsSinceFlush = 0;
|
|
95730
|
+
try {
|
|
95731
|
+
this.onDraft(this.text, this.think);
|
|
95732
|
+
this.wireHasDraft = this.text.length > 0 || this.think.length > 0;
|
|
95733
|
+
} catch {}
|
|
95734
|
+
}
|
|
95735
|
+
};
|
|
95736
|
+
const DRAFT_FLUSH_INTERVAL_MS = 1500;
|
|
95737
|
+
const DRAFT_FLUSH_MIN_CHARS = 4096;
|
|
95178
95738
|
function createChatStreamingCallbacks(deps) {
|
|
95179
|
-
const { dispatchEvent, turnId, currentStep, stepUuid } = deps;
|
|
95739
|
+
const { dispatchEvent, turnId, currentStep, stepUuid, onStreamingDraft } = deps;
|
|
95180
95740
|
let textIndex = 0;
|
|
95181
95741
|
let thinkIndex = 0;
|
|
95742
|
+
const draftTracker = new StreamDraftTracker(onStreamingDraft);
|
|
95182
95743
|
return {
|
|
95183
95744
|
onTextDelta: (delta) => {
|
|
95745
|
+
draftTracker.onText(delta);
|
|
95184
95746
|
dispatchEvent({
|
|
95185
95747
|
type: "text.delta",
|
|
95186
95748
|
delta
|
|
95187
95749
|
});
|
|
95188
95750
|
},
|
|
95189
95751
|
onThinkDelta: (delta) => {
|
|
95752
|
+
draftTracker.onThink(delta);
|
|
95190
95753
|
dispatchEvent({
|
|
95191
95754
|
type: "thinking.delta",
|
|
95192
95755
|
delta
|
|
@@ -95202,6 +95765,7 @@ function createChatStreamingCallbacks(deps) {
|
|
|
95202
95765
|
},
|
|
95203
95766
|
onTextPart: async (part) => {
|
|
95204
95767
|
const index = textIndex++;
|
|
95768
|
+
draftTracker.clear();
|
|
95205
95769
|
await dispatchEvent({
|
|
95206
95770
|
type: "block.start",
|
|
95207
95771
|
uuid: randomUUID(),
|
|
@@ -95231,6 +95795,7 @@ function createChatStreamingCallbacks(deps) {
|
|
|
95231
95795
|
},
|
|
95232
95796
|
onThinkPart: async (part) => {
|
|
95233
95797
|
const index = thinkIndex++;
|
|
95798
|
+
draftTracker.clear();
|
|
95234
95799
|
await dispatchEvent({
|
|
95235
95800
|
type: "block.start",
|
|
95236
95801
|
uuid: randomUUID(),
|
|
@@ -95310,7 +95875,8 @@ async function runTurn(input) {
|
|
|
95310
95875
|
maxRetryAttempts,
|
|
95311
95876
|
recordUsage: recordStepUsage,
|
|
95312
95877
|
hasPendingSteer: input.hasPendingSteer,
|
|
95313
|
-
mediaProjection
|
|
95878
|
+
mediaProjection,
|
|
95879
|
+
onStreamingDraft: input.onStreamingDraft
|
|
95314
95880
|
});
|
|
95315
95881
|
activeStep = void 0;
|
|
95316
95882
|
if (stepResult.totalCalls > 0) {
|
|
@@ -101542,6 +102108,15 @@ var TurnFlow = class {
|
|
|
101542
102108
|
maxSteps: loopControl?.maxStepsPerTurn,
|
|
101543
102109
|
maxRetryAttempts: loopControl?.maxRetriesPerStep,
|
|
101544
102110
|
hasPendingSteer: () => this.steerBuffer.some((steer) => steer.origin.kind === "user" && steer.interrupt !== false),
|
|
102111
|
+
onStreamingDraft: (text, think) => {
|
|
102112
|
+
this.agent.records.logRecord({
|
|
102113
|
+
type: "context.stream_draft",
|
|
102114
|
+
turnId: String(turnId),
|
|
102115
|
+
text,
|
|
102116
|
+
think,
|
|
102117
|
+
time: Date.now()
|
|
102118
|
+
});
|
|
102119
|
+
},
|
|
101545
102120
|
hooks: {
|
|
101546
102121
|
beforeStep: async ({ signal: stepSignal, stepNumber }) => {
|
|
101547
102122
|
this.flushSteerBuffer();
|
|
@@ -102219,6 +102794,8 @@ var Agent = class {
|
|
|
102219
102794
|
/** Read-only manifest of the core engine subsystems (see {@link AgentServices}). */
|
|
102220
102795
|
services;
|
|
102221
102796
|
lastLlmConfigLogSignature;
|
|
102797
|
+
/** Last rendered system prompt written to a request.header record. */
|
|
102798
|
+
lastRequestHeaderSystemPrompt;
|
|
102222
102799
|
sharedEmbeddingEngine;
|
|
102223
102800
|
resolveRuntimeSystemPrompt;
|
|
102224
102801
|
constructor(options) {
|
|
@@ -102523,12 +103100,14 @@ var Agent = class {
|
|
|
102523
103100
|
for (const message of history) if (message.partial === true) partialMessageCount += 1;
|
|
102524
103101
|
const requestMetadata = { estimatedInputTokens: estimateTokens$1(systemPrompt) + estimateTokensForMessages(history) + estimateTokensForTools(tools) };
|
|
102525
103102
|
if (partialMessageCount > 0) requestMetadata.partialMessageCount = partialMessageCount;
|
|
103103
|
+
const systemPromptReused = systemPrompt === this.lastRequestHeaderSystemPrompt;
|
|
103104
|
+
this.lastRequestHeaderSystemPrompt = systemPrompt;
|
|
102526
103105
|
this.records.logRecord({
|
|
102527
103106
|
type: "request.header",
|
|
102528
103107
|
provider: provider.name,
|
|
102529
103108
|
model: provider.modelName,
|
|
102530
103109
|
modelAlias: this.config.modelAlias ?? "",
|
|
102531
|
-
systemPrompt,
|
|
103110
|
+
...systemPromptReused ? { systemPromptReused: true } : { systemPrompt },
|
|
102532
103111
|
activeTools: tools.map((t) => t.name),
|
|
102533
103112
|
messagesCount: history.length,
|
|
102534
103113
|
estimatedInputTokens: requestMetadata.estimatedInputTokens ?? 0
|
|
@@ -121686,7 +122265,7 @@ var LocalFetchURLProvider = class {
|
|
|
121686
122265
|
* `confident` distinguishes detection strength: a convertible Content-Type
|
|
121687
122266
|
* means conversion failure is a real error (corrupt document), while an
|
|
121688
122267
|
* 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
|
|
122268
|
+
* to the normal text extraction path instead of erroring.
|
|
121690
122269
|
*/
|
|
121691
122270
|
async fetchDocument(response, extension, contentType, confident) {
|
|
121692
122271
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
@@ -121797,7 +122376,7 @@ const RESULT_TITLE_RE$1 = /<a\b[^>]*\bclass="[^"]*\bresult__a\b[^"]*"[^>]*\bhref
|
|
|
121797
122376
|
const RESULT_SNIPPET_RE$1 = /<(?:a|div|span)\b[^>]*\bclass="[^"]*\bresult__snippet\b[^"]*"[^>]*>([\s\S]*?)<\/(?:a|div|span)>/;
|
|
121798
122377
|
/** Strip inline tags (DDG wraps query terms in `<b>`) and decode entities. */
|
|
121799
122378
|
function decodeHtmlText$2(value) {
|
|
121800
|
-
return value.
|
|
122379
|
+
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
122380
|
}
|
|
121802
122381
|
/**
|
|
121803
122382
|
* Resolve a DDG result href to the underlying target URL. DDG routes
|
|
@@ -121806,7 +122385,7 @@ function decodeHtmlText$2(value) {
|
|
|
121806
122385
|
*/
|
|
121807
122386
|
function unwrapResultUrl(href) {
|
|
121808
122387
|
if (href === "") return void 0;
|
|
121809
|
-
const decoded = href.
|
|
122388
|
+
const decoded = href.replaceAll(/&/gi, "&");
|
|
121810
122389
|
const wrapMatch = decoded.match(/[?&]uddg=([^&]+)/);
|
|
121811
122390
|
if (wrapMatch?.[1] !== void 0) try {
|
|
121812
122391
|
return decodeURIComponent(wrapMatch[1]);
|
|
@@ -122125,27 +122704,32 @@ function buildExportManifest(args) {
|
|
|
122125
122704
|
}
|
|
122126
122705
|
//#endregion
|
|
122127
122706
|
//#region ../../packages/agent-core/src/session/export/wire-scan.ts
|
|
122707
|
+
/**
|
|
122708
|
+
* Scan a session's `wire.jsonl` for activity timestamps and the first user
|
|
122709
|
+
* input.
|
|
122710
|
+
*
|
|
122711
|
+
* The log is append-only and line-delimited, but a long-lived session can grow
|
|
122712
|
+
* it to gigabytes, so it is streamed rather than read into a single string: a
|
|
122713
|
+
* `StringDecoder` turns each chunk into text without splitting multi-byte
|
|
122714
|
+
* characters, complete lines are consumed as they arrive, and only the four
|
|
122715
|
+
* aggregated values are retained. Any read failure (including a missing file)
|
|
122716
|
+
* yields an empty scan.
|
|
122717
|
+
*/
|
|
122128
122718
|
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
122719
|
let firstActivityMs;
|
|
122136
122720
|
let lastActivityMs;
|
|
122137
122721
|
let lastUserMessageMs;
|
|
122138
122722
|
let firstUserInput;
|
|
122139
|
-
|
|
122723
|
+
const consumeLine = (line) => {
|
|
122140
122724
|
const trimmed = line.trim();
|
|
122141
|
-
if (trimmed.length === 0)
|
|
122725
|
+
if (trimmed.length === 0) return;
|
|
122142
122726
|
let parsed;
|
|
122143
122727
|
try {
|
|
122144
122728
|
parsed = JSON.parse(trimmed);
|
|
122145
122729
|
} catch {
|
|
122146
|
-
|
|
122730
|
+
return;
|
|
122147
122731
|
}
|
|
122148
|
-
if (typeof parsed !== "object" || parsed === null)
|
|
122732
|
+
if (typeof parsed !== "object" || parsed === null) return;
|
|
122149
122733
|
const record = parsed;
|
|
122150
122734
|
const timeMs = typeof record.time === "number" ? normalizeTimestampMs(record.time) : void 0;
|
|
122151
122735
|
if (timeMs !== void 0) {
|
|
@@ -122156,6 +122740,27 @@ async function scanSessionWire(sessionDir) {
|
|
|
122156
122740
|
if (timeMs !== void 0) lastUserMessageMs = timeMs;
|
|
122157
122741
|
if (firstUserInput === void 0 && typeof record.userInput === "string" && record.userInput.trim().length > 0) firstUserInput = record.userInput;
|
|
122158
122742
|
}
|
|
122743
|
+
};
|
|
122744
|
+
try {
|
|
122745
|
+
const decoder = new StringDecoder("utf8");
|
|
122746
|
+
let pending = "";
|
|
122747
|
+
let scanned = 0;
|
|
122748
|
+
const stream = createReadStream(join$1(sessionDir, "wire.jsonl"));
|
|
122749
|
+
for await (const chunk of stream) {
|
|
122750
|
+
pending += decoder.write(chunk);
|
|
122751
|
+
let newlineIndex = pending.indexOf("\n", scanned);
|
|
122752
|
+
while (newlineIndex !== -1) {
|
|
122753
|
+
consumeLine(pending.slice(0, newlineIndex));
|
|
122754
|
+
pending = pending.slice(newlineIndex + 1);
|
|
122755
|
+
scanned = 0;
|
|
122756
|
+
newlineIndex = pending.indexOf("\n", scanned);
|
|
122757
|
+
}
|
|
122758
|
+
scanned = pending.length;
|
|
122759
|
+
}
|
|
122760
|
+
pending += decoder.end();
|
|
122761
|
+
if (pending.length > 0) consumeLine(pending);
|
|
122762
|
+
} catch {
|
|
122763
|
+
return {};
|
|
122159
122764
|
}
|
|
122160
122765
|
return {
|
|
122161
122766
|
firstActivityMs,
|
|
@@ -123053,22 +123658,56 @@ async function writeExportZip(args) {
|
|
|
123053
123658
|
await mkdir(dirname$2(args.outputPath), { recursive: true });
|
|
123054
123659
|
const entries = ["manifest.json"];
|
|
123055
123660
|
const zip = new import_yazl.ZipFile();
|
|
123661
|
+
const zipFailure = new Promise((_resolve, reject) => {
|
|
123662
|
+
zip.once("error", reject);
|
|
123663
|
+
});
|
|
123056
123664
|
zip.addBuffer(Buffer.from(JSON.stringify(args.manifest, null, 2), "utf-8"), "manifest.json");
|
|
123057
123665
|
for (const abs of args.sessionFiles) {
|
|
123058
123666
|
const rel = relative$1(args.sessionDir, abs).split(/[\\/]/).join("/");
|
|
123059
|
-
|
|
123060
|
-
zip.addBuffer(data, rel);
|
|
123667
|
+
addStreamedFile(zip, abs, rel);
|
|
123061
123668
|
entries.push(rel);
|
|
123062
123669
|
}
|
|
123063
|
-
for (const extra of args.extraEntries ?? [])
|
|
123064
|
-
|
|
123065
|
-
|
|
123670
|
+
for (const extra of args.extraEntries ?? []) {
|
|
123671
|
+
if ("data" in extra) {
|
|
123672
|
+
zip.addBuffer(extra.data, extra.target);
|
|
123673
|
+
entries.push(extra.target);
|
|
123674
|
+
continue;
|
|
123675
|
+
}
|
|
123676
|
+
try {
|
|
123677
|
+
if (!(await stat(extra.source)).isFile()) continue;
|
|
123678
|
+
} catch {
|
|
123679
|
+
continue;
|
|
123680
|
+
}
|
|
123681
|
+
addStreamedFile(zip, extra.source, extra.target);
|
|
123066
123682
|
entries.push(extra.target);
|
|
123067
|
-
}
|
|
123068
|
-
|
|
123069
|
-
|
|
123683
|
+
}
|
|
123684
|
+
try {
|
|
123685
|
+
zip.end();
|
|
123686
|
+
await Promise.race([pipeline$1(zip.outputStream, createWriteStream$1(args.outputPath)), zipFailure]);
|
|
123687
|
+
} catch (error) {
|
|
123688
|
+
await rm(args.outputPath, { force: true }).catch(() => {});
|
|
123689
|
+
throw error;
|
|
123690
|
+
}
|
|
123070
123691
|
return entries;
|
|
123071
123692
|
}
|
|
123693
|
+
/**
|
|
123694
|
+
* Add a file as a lazily-streamed zip entry.
|
|
123695
|
+
*
|
|
123696
|
+
* `ZipFile.addFile` records the size from a stat and then asserts the byte count
|
|
123697
|
+
* while reading, so exporting a session that is still appending to its wire or
|
|
123698
|
+
* log files — or that rotates them mid-export — would abort the whole export.
|
|
123699
|
+
* Reading lazily without a declared size keeps the entry tolerant of growth,
|
|
123700
|
+
* opens one file at a time, and still never holds the file in memory.
|
|
123701
|
+
* (`addReadStreamLazy` is the entry point yazl's own `addFile` uses; the
|
|
123702
|
+
* published typings just do not declare it.)
|
|
123703
|
+
*/
|
|
123704
|
+
function addStreamedFile(zip, source, target) {
|
|
123705
|
+
zip.addReadStreamLazy(target, (callback) => {
|
|
123706
|
+
const stream = createReadStream(source);
|
|
123707
|
+
stream.on("error", (error) => zip.emit("error", error));
|
|
123708
|
+
callback(null, stream);
|
|
123709
|
+
});
|
|
123710
|
+
}
|
|
123072
123711
|
//#endregion
|
|
123073
123712
|
//#region ../../packages/agent-core/src/session/export/session-export.ts
|
|
123074
123713
|
const SESSION_LOG_REL = "logs/scream-code.log";
|
|
@@ -123086,10 +123725,9 @@ async function exportSessionDirectory(input) {
|
|
|
123086
123725
|
let bundledGlobal = false;
|
|
123087
123726
|
const globalPath = input.globalLogPath ?? (input.homeDir === void 0 ? void 0 : resolveGlobalLogPath(input.homeDir));
|
|
123088
123727
|
if (input.request.includeGlobalLog === true && globalPath !== void 0) {
|
|
123089
|
-
|
|
123090
|
-
if (data !== void 0) {
|
|
123728
|
+
if (await pathIsFile(globalPath)) {
|
|
123091
123729
|
extras.push({
|
|
123092
|
-
|
|
123730
|
+
source: globalPath,
|
|
123093
123731
|
target: GLOBAL_LOG_REL
|
|
123094
123732
|
});
|
|
123095
123733
|
bundledGlobal = true;
|
|
@@ -123119,11 +123757,11 @@ async function exportSessionDirectory(input) {
|
|
|
123119
123757
|
manifest
|
|
123120
123758
|
};
|
|
123121
123759
|
}
|
|
123122
|
-
async function
|
|
123760
|
+
async function pathIsFile(path) {
|
|
123123
123761
|
try {
|
|
123124
|
-
return await
|
|
123762
|
+
return (await stat(path)).isFile();
|
|
123125
123763
|
} catch {
|
|
123126
|
-
return;
|
|
123764
|
+
return false;
|
|
123127
123765
|
}
|
|
123128
123766
|
}
|
|
123129
123767
|
//#endregion
|
|
@@ -123609,6 +124247,10 @@ function hasCustomTitle(metadata) {
|
|
|
123609
124247
|
}
|
|
123610
124248
|
//#endregion
|
|
123611
124249
|
//#region ../../packages/agent-core/src/session/store/session-index.ts
|
|
124250
|
+
let sessionIndexCache;
|
|
124251
|
+
function invalidateSessionIndexCache() {
|
|
124252
|
+
sessionIndexCache = void 0;
|
|
124253
|
+
}
|
|
123612
124254
|
function sessionIndexPath(homeDir) {
|
|
123613
124255
|
return join$1(homeDir, "session_index.jsonl");
|
|
123614
124256
|
}
|
|
@@ -123619,12 +124261,24 @@ async function appendSessionIndexEntry(homeDir, entry) {
|
|
|
123619
124261
|
mode: 448
|
|
123620
124262
|
});
|
|
123621
124263
|
await appendFile(indexPath, `${JSON.stringify(entry)}\n`, "utf-8");
|
|
124264
|
+
invalidateSessionIndexCache();
|
|
123622
124265
|
}
|
|
123623
124266
|
async function readSessionIndex(homeDir, sessionsDir) {
|
|
124267
|
+
const indexPath = sessionIndexPath(homeDir);
|
|
124268
|
+
let stats;
|
|
124269
|
+
try {
|
|
124270
|
+
stats = await stat(indexPath);
|
|
124271
|
+
} catch {
|
|
124272
|
+
sessionIndexCache = void 0;
|
|
124273
|
+
return /* @__PURE__ */ new Map();
|
|
124274
|
+
}
|
|
124275
|
+
const cached = sessionIndexCache;
|
|
124276
|
+
if (cached !== void 0 && cached.homeDir === homeDir && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) return cached.map;
|
|
123624
124277
|
let raw;
|
|
123625
124278
|
try {
|
|
123626
|
-
raw = await readFile(
|
|
124279
|
+
raw = await readFile(indexPath, "utf-8");
|
|
123627
124280
|
} catch {
|
|
124281
|
+
sessionIndexCache = void 0;
|
|
123628
124282
|
return /* @__PURE__ */ new Map();
|
|
123629
124283
|
}
|
|
123630
124284
|
const result = /* @__PURE__ */ new Map();
|
|
@@ -123644,6 +124298,12 @@ async function readSessionIndex(homeDir, sessionsDir) {
|
|
|
123644
124298
|
workDir: resolve$2(entry.workDir)
|
|
123645
124299
|
});
|
|
123646
124300
|
}
|
|
124301
|
+
sessionIndexCache = {
|
|
124302
|
+
homeDir,
|
|
124303
|
+
mtimeMs: stats.mtimeMs,
|
|
124304
|
+
size: stats.size,
|
|
124305
|
+
map: result
|
|
124306
|
+
};
|
|
123647
124307
|
return result;
|
|
123648
124308
|
}
|
|
123649
124309
|
async function removeSessionIndexEntry(homeDir, sessionId) {
|
|
@@ -123665,6 +124325,7 @@ async function removeSessionIndexEntry(homeDir, sessionId) {
|
|
|
123665
124325
|
}
|
|
123666
124326
|
if (kept.length === 0) await writeFile(indexPath, "", "utf-8");
|
|
123667
124327
|
else await writeFile(indexPath, kept.join("\n") + "\n", "utf-8");
|
|
124328
|
+
invalidateSessionIndexCache();
|
|
123668
124329
|
}
|
|
123669
124330
|
function parseIndexLine(line) {
|
|
123670
124331
|
try {
|
|
@@ -129573,8 +130234,6 @@ function contentPartsText(parts) {
|
|
|
129573
130234
|
* Throws when the file is missing or contains no usable records.
|
|
129574
130235
|
*/
|
|
129575
130236
|
function buildTraceCells({ wirePath }) {
|
|
129576
|
-
const rows = readWireRows(wirePath);
|
|
129577
|
-
if (rows.length === 0) throw new Error(`no wire records in ${wirePath}`);
|
|
129578
130237
|
const cells = [];
|
|
129579
130238
|
let lastTime;
|
|
129580
130239
|
let nextIndex = 1;
|
|
@@ -129592,6 +130251,11 @@ function buildTraceCells({ wirePath }) {
|
|
|
129592
130251
|
startedAt: time,
|
|
129593
130252
|
...fields
|
|
129594
130253
|
};
|
|
130254
|
+
cell.text = truncateDetail(cell.text, MAX_TEXT) ?? "";
|
|
130255
|
+
if (cell.thinkingDetail) cell.thinkingDetail = truncateDetail(cell.thinkingDetail);
|
|
130256
|
+
if (cell.outputDetail) cell.outputDetail = truncateDetail(cell.outputDetail);
|
|
130257
|
+
if (cell.inputDetail) cell.inputDetail = truncateDetail(cell.inputDetail);
|
|
130258
|
+
if (cell.result) cell.result = truncateDetail(cell.result);
|
|
129595
130259
|
cells.push(cell);
|
|
129596
130260
|
lastCell = cell;
|
|
129597
130261
|
return cell;
|
|
@@ -129755,99 +130419,102 @@ function buildTraceCells({ wirePath }) {
|
|
|
129755
130419
|
default: break;
|
|
129756
130420
|
}
|
|
129757
130421
|
};
|
|
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"]),
|
|
130422
|
+
const consumeRow = ({ seq, time, record }) => {
|
|
130423
|
+
switch (asString(record["type"])) {
|
|
130424
|
+
case "context.append_loop_event": {
|
|
130425
|
+
const event = asRecord(record["event"]);
|
|
130426
|
+
if (!event) break;
|
|
130427
|
+
handleLoopEvent(event, time, seq);
|
|
130428
|
+
break;
|
|
130429
|
+
}
|
|
130430
|
+
case "turn.prompt": {
|
|
130431
|
+
finalizeStep(time);
|
|
130432
|
+
turnNo += 1;
|
|
130433
|
+
flushPendingSystem(time);
|
|
130434
|
+
const input = record["input"];
|
|
130435
|
+
const text = contentPartsText(input).trim();
|
|
130436
|
+
pushCell("user", text.replaceAll(/\s+/g, " ").slice(0, 80) || "(空输入)", {
|
|
130437
|
+
opensTurn: true,
|
|
130438
|
+
inputDetail: text || void 0,
|
|
129823
130439
|
sourceSeq: seq
|
|
129824
130440
|
}, time);
|
|
130441
|
+
currentTurnStart = time;
|
|
130442
|
+
break;
|
|
129825
130443
|
}
|
|
129826
|
-
|
|
129827
|
-
|
|
129828
|
-
|
|
129829
|
-
|
|
129830
|
-
|
|
129831
|
-
|
|
129832
|
-
|
|
129833
|
-
|
|
129834
|
-
|
|
129835
|
-
|
|
129836
|
-
|
|
129837
|
-
|
|
129838
|
-
|
|
129839
|
-
|
|
129840
|
-
|
|
129841
|
-
|
|
129842
|
-
|
|
129843
|
-
|
|
129844
|
-
|
|
129845
|
-
|
|
129846
|
-
|
|
129847
|
-
|
|
130444
|
+
case "turn.steer": {
|
|
130445
|
+
const input = record["input"];
|
|
130446
|
+
const text = contentPartsText(input).trim();
|
|
130447
|
+
pushCell("context", `转向: ${text.replaceAll(/\s+/g, " ").slice(0, 80)}`, {
|
|
130448
|
+
inputDetail: text || void 0,
|
|
130449
|
+
sourceSeq: seq
|
|
130450
|
+
}, time);
|
|
130451
|
+
break;
|
|
130452
|
+
}
|
|
130453
|
+
case "request.header": {
|
|
130454
|
+
const provider = asString(record["provider"]) ?? "";
|
|
130455
|
+
const model = asString(record["model"]) ?? "";
|
|
130456
|
+
const tools = asRecordArray(record["activeTools"]).map((t) => asString(t["name"]) ?? "");
|
|
130457
|
+
pushCell("system", `请求 ${provider ? `${provider}/` : ""}${model}`, {
|
|
130458
|
+
requestOnly: true,
|
|
130459
|
+
inputDetail: tools.length > 0 ? `工具: ${tools.join(", ")}` : void 0,
|
|
130460
|
+
sourceSeq: seq
|
|
130461
|
+
}, time);
|
|
130462
|
+
break;
|
|
130463
|
+
}
|
|
130464
|
+
case "tools.set_active_tools": {
|
|
130465
|
+
const names = asStringArray(record["names"]).length > 0 ? asStringArray(record["names"]) : asRecordArray(record["names"]).map((n) => asString(n["name"]) ?? "");
|
|
130466
|
+
pendingSystem.push(`工具集: ${names.join(", ")}`);
|
|
130467
|
+
lastSystemTime = time;
|
|
130468
|
+
break;
|
|
130469
|
+
}
|
|
130470
|
+
case "config.update": {
|
|
130471
|
+
const cfg = asRecord(record);
|
|
130472
|
+
const bits = [];
|
|
130473
|
+
if (asString(cfg?.["modelAlias"])) bits.push(`模型别名: ${cfg["modelAlias"]}`);
|
|
130474
|
+
if (asString(cfg?.["systemPrompt"])) bits.push("系统提示词已更新");
|
|
130475
|
+
if (bits.length === 0) break;
|
|
130476
|
+
pendingSystem.push(bits.join(" · "));
|
|
130477
|
+
lastSystemTime = time;
|
|
130478
|
+
break;
|
|
130479
|
+
}
|
|
130480
|
+
case "usage.record":
|
|
130481
|
+
if (currentStepUuid === void 0) {
|
|
130482
|
+
const usage = asRecord(record["usage"]);
|
|
130483
|
+
pushCell("context", "usage", {
|
|
130484
|
+
input: asNumber(usage?.["inputOther"]),
|
|
130485
|
+
cacheRead: asNumber(usage?.["inputCacheRead"]),
|
|
130486
|
+
cacheWrite: asNumber(usage?.["inputCacheCreation"]),
|
|
130487
|
+
output: asNumber(usage?.["output"]),
|
|
130488
|
+
sourceSeq: seq
|
|
130489
|
+
}, time);
|
|
130490
|
+
}
|
|
130491
|
+
break;
|
|
130492
|
+
case "full_compaction.begin": {
|
|
130493
|
+
finalizeStep(time);
|
|
130494
|
+
const reason = asString(record["reason"]);
|
|
130495
|
+
const instruction = asString(record["instruction"]);
|
|
130496
|
+
const source = asString(record["source"]);
|
|
130497
|
+
pushCell("compacted", `压缩上下文${reason ? `(${reason})` : ""}`, {
|
|
130498
|
+
sourceSeq: seq,
|
|
130499
|
+
startedAt: currentTurnStart,
|
|
130500
|
+
inputDetail: instruction || void 0,
|
|
130501
|
+
result: source ? `来源: ${source}` : void 0
|
|
130502
|
+
}, time);
|
|
130503
|
+
break;
|
|
130504
|
+
}
|
|
130505
|
+
case "micro_compaction.apply": {
|
|
130506
|
+
finalizeStep(time);
|
|
130507
|
+
const reason = asString(record["reason"]);
|
|
130508
|
+
pushCell("compacted", `微压缩${reason ? `(${reason})` : ""}`, {
|
|
130509
|
+
sourceSeq: seq,
|
|
130510
|
+
startedAt: currentTurnStart
|
|
130511
|
+
}, time);
|
|
130512
|
+
break;
|
|
130513
|
+
}
|
|
130514
|
+
default: break;
|
|
129848
130515
|
}
|
|
129849
|
-
|
|
129850
|
-
}
|
|
130516
|
+
};
|
|
130517
|
+
if (forEachWireRow(wirePath, consumeRow) === 0) throw new Error(`no wire records in ${wirePath}`);
|
|
129851
130518
|
finalizeStep(void 0);
|
|
129852
130519
|
flushPendingSystem(void 0);
|
|
129853
130520
|
return capTraceSize(cells);
|
|
@@ -129856,25 +130523,20 @@ function buildTraceCells({ wirePath }) {
|
|
|
129856
130523
|
* Long sessions produce tens of thousands of cells with multi-MB detail
|
|
129857
130524
|
* payloads, which previously bloated the trace HTML (up to ~60MB) and froze
|
|
129858
130525
|
* the browser. Two mitigations, applied at build time:
|
|
129859
|
-
* 1. Truncate per-cell detail text (thinking/output/input/result) to a cap
|
|
130526
|
+
* 1. Truncate per-cell detail text (thinking/output/input/result) to a cap —
|
|
130527
|
+
* done in `pushCell`, so the raw payload never outlives the cell.
|
|
129860
130528
|
* 2. Beyond a cell-count cap, collapse the oldest cells into per-turn
|
|
129861
130529
|
* summary rows so the document stays bounded while early turns remain
|
|
129862
130530
|
* visible in the ledger.
|
|
129863
130531
|
*/
|
|
129864
130532
|
const MAX_DETAIL = 4e3;
|
|
130533
|
+
const MAX_TEXT = 240;
|
|
129865
130534
|
const MAX_CELLS = 4e3;
|
|
129866
130535
|
function truncateDetail(value, max = MAX_DETAIL) {
|
|
129867
130536
|
if (!value || value.length <= max) return value;
|
|
129868
130537
|
return `${value.slice(0, max)}\n…[已截断 ${value.length - max} 字符]`;
|
|
129869
130538
|
}
|
|
129870
130539
|
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
130540
|
if (cells.length <= MAX_CELLS) return cells;
|
|
129879
130541
|
const keep = cells.slice(-4e3);
|
|
129880
130542
|
const early = cells.slice(0, cells.length - MAX_CELLS);
|
|
@@ -129893,25 +130555,67 @@ function capTraceSize(cells) {
|
|
|
129893
130555
|
startedAt: early.find((c) => c.startedAt !== void 0 && c.startedAt !== null)?.startedAt ?? null
|
|
129894
130556
|
}, ...keep];
|
|
129895
130557
|
}
|
|
129896
|
-
|
|
129897
|
-
|
|
129898
|
-
|
|
130558
|
+
/** Bytes read per `readSync` call while scanning a wire log. */
|
|
130559
|
+
const WIRE_READ_CHUNK_BYTES = 1024 * 1024;
|
|
130560
|
+
/**
|
|
130561
|
+
* Stream a wire log row by row without materializing the file.
|
|
130562
|
+
*
|
|
130563
|
+
* The log is append-only and line-delimited, but a long-lived session can grow
|
|
130564
|
+
* it to gigabytes, so the content is never held as a single string: fixed-size
|
|
130565
|
+
* chunks are read into one reusable buffer and split on "\n" (0x0A) at byte
|
|
130566
|
+
* level. Only the currently open line is carried across reads (as a small
|
|
130567
|
+
* copied buffer, because the chunk buffer is reused), and each parsed row is
|
|
130568
|
+
* handed to `onRow` and then dropped — neither the decoded file nor a
|
|
130569
|
+
* parsed-record array lives beyond the line being processed.
|
|
130570
|
+
*
|
|
130571
|
+
* Returns the number of parsed rows. Throws when the file cannot be opened;
|
|
130572
|
+
* callers rely on that to tell a missing session log apart from an empty one.
|
|
130573
|
+
*/
|
|
130574
|
+
function forEachWireRow(wirePath, onRow) {
|
|
129899
130575
|
let seq = 0;
|
|
129900
|
-
|
|
129901
|
-
|
|
130576
|
+
let parsedRows = 0;
|
|
130577
|
+
const pushLine = (line) => {
|
|
130578
|
+
if (!line.trim()) return;
|
|
129902
130579
|
seq += 1;
|
|
130580
|
+
let parsed;
|
|
129903
130581
|
try {
|
|
129904
|
-
|
|
129905
|
-
|
|
129906
|
-
|
|
129907
|
-
|
|
129908
|
-
|
|
129909
|
-
|
|
129910
|
-
|
|
129911
|
-
|
|
129912
|
-
|
|
130582
|
+
parsed = JSON.parse(line);
|
|
130583
|
+
} catch {
|
|
130584
|
+
return;
|
|
130585
|
+
}
|
|
130586
|
+
const rec = asRecord(parsed);
|
|
130587
|
+
if (!rec) return;
|
|
130588
|
+
onRow({
|
|
130589
|
+
seq,
|
|
130590
|
+
time: asNumber(rec["time"]),
|
|
130591
|
+
record: rec
|
|
130592
|
+
});
|
|
130593
|
+
parsedRows += 1;
|
|
130594
|
+
};
|
|
130595
|
+
const fd = openSync(wirePath, "r");
|
|
130596
|
+
try {
|
|
130597
|
+
const chunk = Buffer.allocUnsafe(WIRE_READ_CHUNK_BYTES);
|
|
130598
|
+
let openChunks = [];
|
|
130599
|
+
for (;;) {
|
|
130600
|
+
const bytesRead = readSync(fd, chunk, 0, chunk.length, null);
|
|
130601
|
+
if (bytesRead <= 0) break;
|
|
130602
|
+
const window = bytesRead === chunk.length ? chunk : chunk.subarray(0, bytesRead);
|
|
130603
|
+
let searchFrom = 0;
|
|
130604
|
+
for (;;) {
|
|
130605
|
+
const newlineIndex = window.indexOf(10, searchFrom);
|
|
130606
|
+
if (newlineIndex === -1) break;
|
|
130607
|
+
const tail = window.subarray(searchFrom, newlineIndex);
|
|
130608
|
+
pushLine(openChunks.length === 0 ? tail.toString("utf8") : Buffer.concat([...openChunks, tail]).toString("utf8"));
|
|
130609
|
+
openChunks = [];
|
|
130610
|
+
searchFrom = newlineIndex + 1;
|
|
130611
|
+
}
|
|
130612
|
+
if (searchFrom < window.length) openChunks.push(Buffer.from(window.subarray(searchFrom)));
|
|
130613
|
+
}
|
|
130614
|
+
if (openChunks.length > 0) pushLine(Buffer.concat(openChunks).toString("utf8"));
|
|
130615
|
+
} finally {
|
|
130616
|
+
closeSync(fd);
|
|
129913
130617
|
}
|
|
129914
|
-
return
|
|
130618
|
+
return parsedRows;
|
|
129915
130619
|
}
|
|
129916
130620
|
//#endregion
|
|
129917
130621
|
//#region src/utils/trace/render-trace-html.ts
|
|
@@ -134065,7 +134769,7 @@ async function guidedGoalSetup(host) {
|
|
|
134065
134769
|
host.showNotice(t("goal.storm_breaker"), t("goal.conflict_loop"));
|
|
134066
134770
|
return;
|
|
134067
134771
|
}
|
|
134068
|
-
const { TextInputDialogComponent } = await import("./text-input-dialog-
|
|
134772
|
+
const { TextInputDialogComponent } = await import("./text-input-dialog-CQ6R2lns.mjs");
|
|
134069
134773
|
const initialDesc = await promptText(host, TextInputDialogComponent, {
|
|
134070
134774
|
title: t("goal.setup_title_initial"),
|
|
134071
134775
|
subtitle: t("goal.setup_desc_hint"),
|
|
@@ -134086,7 +134790,7 @@ async function guidedGoalSetup(host) {
|
|
|
134086
134790
|
await showGoalConfigWizard(host, session, confirmed.trim() || objective, false);
|
|
134087
134791
|
}
|
|
134088
134792
|
async function showGoalConfigWizard(host, session, objective, replace) {
|
|
134089
|
-
const { TextInputDialogComponent } = await import("./text-input-dialog-
|
|
134793
|
+
const { TextInputDialogComponent } = await import("./text-input-dialog-CQ6R2lns.mjs");
|
|
134090
134794
|
const turnInput = await promptNumber(host, TextInputDialogComponent, {
|
|
134091
134795
|
title: t("goal.wizard_title", { objective }),
|
|
134092
134796
|
subtitle: t("goal.budget_turns_hint"),
|
|
@@ -135564,10 +136268,14 @@ var ActivityGroupComponent = class extends Container {
|
|
|
135564
136268
|
* Aggregate diff of the group's finished, successful file mutations
|
|
135565
136269
|
* (Edit/Write), shown in the header. Read-only tools contribute nothing;
|
|
135566
136270
|
* failed attempts changed nothing, so only successful calls count.
|
|
136271
|
+
* `removed` stays undefined when any contributor could not report its
|
|
136272
|
+
* deletions (replayed records carry no display payload): summing a partial
|
|
136273
|
+
* number would claim a deletion count the group never measured.
|
|
135567
136274
|
*/
|
|
135568
136275
|
diffTotals() {
|
|
135569
136276
|
let added = 0;
|
|
135570
136277
|
let removed = 0;
|
|
136278
|
+
let removedKnown = true;
|
|
135571
136279
|
let seen = false;
|
|
135572
136280
|
for (const segment of this.segments) {
|
|
135573
136281
|
if (segment.kind !== "tool") continue;
|
|
@@ -135575,12 +136283,14 @@ var ActivityGroupComponent = class extends Container {
|
|
|
135575
136283
|
if (contribution === void 0) continue;
|
|
135576
136284
|
seen = true;
|
|
135577
136285
|
added += contribution.added;
|
|
135578
|
-
|
|
136286
|
+
if (contribution.removed === void 0) removedKnown = false;
|
|
136287
|
+
else removed += contribution.removed;
|
|
135579
136288
|
}
|
|
135580
|
-
|
|
136289
|
+
if (!seen) return void 0;
|
|
136290
|
+
return {
|
|
135581
136291
|
added,
|
|
135582
|
-
removed
|
|
135583
|
-
}
|
|
136292
|
+
removed: removedKnown ? removed : void 0
|
|
136293
|
+
};
|
|
135584
136294
|
}
|
|
135585
136295
|
buildHeader(width) {
|
|
135586
136296
|
const colors = this.colors;
|
|
@@ -135593,10 +136303,10 @@ var ActivityGroupComponent = class extends Container {
|
|
|
135593
136303
|
if (toolCount > 0) parts.push(t("activitygroup.tools", { count: String(toolCount) }));
|
|
135594
136304
|
const stats = chalk.dim(SEPARATOR + parts.join(SEPARATOR));
|
|
135595
136305
|
const diff = this.diffTotals();
|
|
135596
|
-
const diffPart = diff ? SEPARATOR + t("activitygroup.diff", {
|
|
136306
|
+
const diffPart = diff ? SEPARATOR + (diff.removed === void 0 ? t("activitygroup.diffAddedOnly", { added: chalk.hex(colors.diffAdded)(`+${diff.added}`) }) : t("activitygroup.diff", {
|
|
135597
136307
|
added: chalk.hex(colors.diffAdded)(`+${diff.added}`),
|
|
135598
136308
|
removed: chalk.hex(colors.diffRemoved)(`-${diff.removed}`)
|
|
135599
|
-
}) : "";
|
|
136309
|
+
})) : "";
|
|
135600
136310
|
const speed = this.liveThinking() ? getSharedSpeedTracker().getSpeed() : 0;
|
|
135601
136311
|
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
136312
|
const hint = chalk.dim(this.expanded ? t("activitygroup.hint_collapse") : t("activitygroup.hint_expand"));
|
|
@@ -138008,9 +138718,24 @@ var ToolCallComponent = class ToolCallComponent extends CachedContainer {
|
|
|
138008
138718
|
* additions), for the group-level diff stat in the activity block header.
|
|
138009
138719
|
* Undefined for non-mutating tools, unfinished calls, and failed calls: a
|
|
138010
138720
|
* failure changed nothing, so it must not count towards the group total.
|
|
138721
|
+
*
|
|
138722
|
+
* The mutation tools report their exact numbers (`display.file_diff`) computed
|
|
138723
|
+
* from the real before/after contents. Groups replayed from a resumed session
|
|
138724
|
+
* carry no such payload (the transcript keeps message text, not the UI
|
|
138725
|
+
* payload), so they fall back to deriving the numbers from the arguments — a
|
|
138726
|
+
* fallback that cannot know how many lines a Write replaced, so it reports
|
|
138727
|
+
* `removed: undefined` ("unknown") instead of claiming zero.
|
|
138011
138728
|
*/
|
|
138012
138729
|
diffContribution() {
|
|
138013
138730
|
if (this.result === void 0 || this.result.is_error) return void 0;
|
|
138731
|
+
const reported = this.result.display;
|
|
138732
|
+
if (reported?.kind === "file_diff") {
|
|
138733
|
+
if (reported.added === 0 && reported.removed === 0) return void 0;
|
|
138734
|
+
return {
|
|
138735
|
+
added: reported.added,
|
|
138736
|
+
removed: reported.removed
|
|
138737
|
+
};
|
|
138738
|
+
}
|
|
138014
138739
|
if (this.toolCall.name === "Edit") {
|
|
138015
138740
|
const stats = computeEditStats(this.toolCall.args);
|
|
138016
138741
|
if (stats.added === 0 && stats.removed === 0) return void 0;
|
|
@@ -138021,7 +138746,7 @@ var ToolCallComponent = class ToolCallComponent extends CachedContainer {
|
|
|
138021
138746
|
if (stats.lines === 0) return void 0;
|
|
138022
138747
|
return {
|
|
138023
138748
|
added: stats.lines,
|
|
138024
|
-
removed: 0
|
|
138749
|
+
removed: void 0
|
|
138025
138750
|
};
|
|
138026
138751
|
}
|
|
138027
138752
|
}
|
|
@@ -138493,7 +139218,7 @@ var ToolCallComponent = class ToolCallComponent extends CachedContainer {
|
|
|
138493
139218
|
if (provider === void 0) return "";
|
|
138494
139219
|
const text = provider(this.toolCall, result);
|
|
138495
139220
|
if (text.length === 0) return "";
|
|
138496
|
-
return
|
|
139221
|
+
return chalk.dim(` · ${text}`);
|
|
138497
139222
|
}
|
|
138498
139223
|
rebuildContent() {
|
|
138499
139224
|
this.markDirty();
|