pi-agent-flow 2.0.0 → 2.0.2
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/README.md +126 -489
- package/agents/audit.md +7 -2
- package/agents/build.md +6 -2
- package/agents/craft.md +6 -3
- package/agents/debug.md +7 -3
- package/agents/ideas.md +8 -4
- package/agents/scout.md +8 -1
- package/dist/batch/apply-patch.d.ts +60 -0
- package/dist/batch/apply-patch.d.ts.map +1 -0
- package/dist/batch/apply-patch.js +477 -0
- package/dist/batch/apply-patch.js.map +1 -0
- package/dist/batch/batch-bash.d.ts +0 -6
- package/dist/batch/batch-bash.d.ts.map +1 -1
- package/dist/batch/batch-bash.js +52 -14
- package/dist/batch/batch-bash.js.map +1 -1
- package/dist/batch/constants.d.ts +45 -4
- package/dist/batch/constants.d.ts.map +1 -1
- package/dist/batch/constants.js +72 -4
- package/dist/batch/constants.js.map +1 -1
- package/dist/batch/execute.d.ts +8 -2
- package/dist/batch/execute.d.ts.map +1 -1
- package/dist/batch/execute.js +338 -70
- package/dist/batch/execute.js.map +1 -1
- package/dist/batch/fuzzy-edit.d.ts +4 -1
- package/dist/batch/fuzzy-edit.d.ts.map +1 -1
- package/dist/batch/fuzzy-edit.js +7 -2
- package/dist/batch/fuzzy-edit.js.map +1 -1
- package/dist/batch/index.d.ts +3 -15
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js +48 -78
- package/dist/batch/index.js.map +1 -1
- package/dist/batch/render.d.ts.map +1 -1
- package/dist/batch/render.js +30 -7
- package/dist/batch/render.js.map +1 -1
- package/dist/batch/shell-compress.d.ts +25 -0
- package/dist/batch/shell-compress.d.ts.map +1 -0
- package/dist/batch/shell-compress.js +602 -0
- package/dist/batch/shell-compress.js.map +1 -0
- package/dist/batch/summary.d.ts.map +1 -1
- package/dist/batch/summary.js +4 -0
- package/dist/batch/summary.js.map +1 -1
- package/dist/batch/symbols.d.ts.map +1 -1
- package/dist/batch/symbols.js +12 -7
- package/dist/batch/symbols.js.map +1 -1
- package/dist/config/config.d.ts +5 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +63 -0
- package/dist/config/config.js.map +1 -1
- package/dist/config/models.d.ts +2 -0
- package/dist/config/models.d.ts.map +1 -0
- package/dist/config/models.js +49 -0
- package/dist/config/models.js.map +1 -0
- package/dist/config/settings-resolver.js +2 -2
- package/dist/config/settings-resolver.js.map +1 -1
- package/dist/core/agents.js +2 -2
- package/dist/core/agents.js.map +1 -1
- package/dist/core/depth.d.ts +3 -3
- package/dist/core/depth.d.ts.map +1 -1
- package/dist/core/depth.js +5 -5
- package/dist/core/depth.js.map +1 -1
- package/dist/core/executor.d.ts +10 -3
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +7 -3
- package/dist/core/executor.js.map +1 -1
- package/dist/core/flow.d.ts +19 -3
- package/dist/core/flow.d.ts.map +1 -1
- package/dist/core/flow.js +97 -58
- package/dist/core/flow.js.map +1 -1
- package/dist/core/session-mode.d.ts +1 -1
- package/dist/core/session-mode.d.ts.map +1 -1
- package/dist/core/session-mode.js +2 -1
- package/dist/core/session-mode.js.map +1 -1
- package/dist/core/{delegation.d.ts → transition.d.ts} +9 -9
- package/dist/core/transition.d.ts.map +1 -0
- package/dist/core/{delegation.js → transition.js} +17 -24
- package/dist/core/transition.js.map +1 -0
- package/dist/flow/auto-warp.d.ts +12 -0
- package/dist/flow/auto-warp.d.ts.map +1 -0
- package/dist/flow/auto-warp.js +29 -0
- package/dist/flow/auto-warp.js.map +1 -0
- package/dist/flow/command.d.ts.map +1 -1
- package/dist/flow/command.js +7 -2
- package/dist/flow/command.js.map +1 -1
- package/dist/flow/continuation.d.ts.map +1 -1
- package/dist/flow/continuation.js +52 -15
- package/dist/flow/continuation.js.map +1 -1
- package/dist/flow/index.d.ts +5 -2
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +6 -3
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/loop-command.d.ts +8 -0
- package/dist/flow/loop-command.d.ts.map +1 -0
- package/dist/flow/loop-command.js +102 -0
- package/dist/flow/loop-command.js.map +1 -0
- package/dist/flow/loop-templates.d.ts +7 -0
- package/dist/flow/loop-templates.d.ts.map +1 -0
- package/dist/flow/loop-templates.js +38 -0
- package/dist/flow/loop-templates.js.map +1 -0
- package/dist/flow/loop.d.ts +19 -0
- package/dist/flow/loop.d.ts.map +1 -0
- package/dist/flow/loop.js +95 -0
- package/dist/flow/loop.js.map +1 -0
- package/dist/flow/settings-command.d.ts.map +1 -1
- package/dist/flow/settings-command.js +93 -4
- package/dist/flow/settings-command.js.map +1 -1
- package/dist/flow/store.d.ts +3 -3
- package/dist/flow/store.d.ts.map +1 -1
- package/dist/flow/store.js +24 -16
- package/dist/flow/store.js.map +1 -1
- package/dist/flow/template-shared.d.ts +9 -0
- package/dist/flow/template-shared.d.ts.map +1 -0
- package/dist/flow/template-shared.js +13 -0
- package/dist/flow/template-shared.js.map +1 -0
- package/dist/flow/template-strings.d.ts.map +1 -1
- package/dist/flow/template-strings.js +2 -5
- package/dist/flow/template-strings.js.map +1 -1
- package/dist/flow/types.d.ts +15 -9
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/flow/warp.d.ts +15 -0
- package/dist/flow/warp.d.ts.map +1 -0
- package/dist/flow/warp.js +207 -0
- package/dist/flow/warp.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +185 -28
- package/dist/index.js.map +1 -1
- package/dist/snapshot/cli-args.d.ts +1 -0
- package/dist/snapshot/cli-args.d.ts.map +1 -1
- package/dist/snapshot/cli-args.js +12 -0
- package/dist/snapshot/cli-args.js.map +1 -1
- package/dist/snapshot/runner-events.d.ts +5 -0
- package/dist/snapshot/runner-events.d.ts.map +1 -1
- package/dist/snapshot/runner-events.js +89 -15
- package/dist/snapshot/runner-events.js.map +1 -1
- package/dist/snapshot/snapshot.d.ts +22 -19
- package/dist/snapshot/snapshot.d.ts.map +1 -1
- package/dist/snapshot/snapshot.js +1063 -167
- package/dist/snapshot/snapshot.js.map +1 -1
- package/dist/steering/flow-prompt.d.ts +2 -2
- package/dist/steering/flow-prompt.d.ts.map +1 -1
- package/dist/steering/flow-prompt.js +4 -4
- package/dist/steering/flow-prompt.js.map +1 -1
- package/dist/steering/sliding-prompt.d.ts.map +1 -1
- package/dist/steering/sliding-prompt.js +9 -6
- package/dist/steering/sliding-prompt.js.map +1 -1
- package/dist/steering/tool-utils.d.ts +31 -8
- package/dist/steering/tool-utils.d.ts.map +1 -1
- package/dist/steering/tool-utils.js +63 -30
- package/dist/steering/tool-utils.js.map +1 -1
- package/dist/tools/ask-user.d.ts +0 -17
- package/dist/tools/ask-user.d.ts.map +1 -1
- package/dist/tools/ask-user.js +13 -37
- package/dist/tools/ask-user.js.map +1 -1
- package/dist/tools/timed-bash.d.ts +1 -1
- package/dist/tools/timed-bash.d.ts.map +1 -1
- package/dist/tools/timed-bash.js +19 -8
- package/dist/tools/timed-bash.js.map +1 -1
- package/dist/tools/web-tool.d.ts.map +1 -1
- package/dist/tools/web-tool.js +11 -13
- package/dist/tools/web-tool.js.map +1 -1
- package/dist/tui/render-utils.d.ts +5 -1
- package/dist/tui/render-utils.d.ts.map +1 -1
- package/dist/tui/render-utils.js +36 -10
- package/dist/tui/render-utils.js.map +1 -1
- package/dist/tui/render.d.ts +9 -0
- package/dist/tui/render.d.ts.map +1 -1
- package/dist/tui/render.js +112 -100
- package/dist/tui/render.js.map +1 -1
- package/dist/tui/scramble/constants.d.ts +8 -8
- package/dist/tui/scramble/constants.d.ts.map +1 -1
- package/dist/tui/scramble/constants.js +7 -7
- package/dist/tui/scramble/constants.js.map +1 -1
- package/dist/tui/scramble/index.d.ts +1 -1
- package/dist/tui/scramble/index.d.ts.map +1 -1
- package/dist/tui/scramble/index.js +1 -1
- package/dist/tui/scramble/index.js.map +1 -1
- package/dist/tui/scramble/manager.d.ts +1 -5
- package/dist/tui/scramble/manager.d.ts.map +1 -1
- package/dist/tui/scramble/manager.js +16 -64
- package/dist/tui/scramble/manager.js.map +1 -1
- package/dist/tui/scramble/utils.js +1 -1
- package/dist/tui/scramble/utils.js.map +1 -1
- package/dist/types/flow.d.ts +2 -0
- package/dist/types/flow.d.ts.map +1 -1
- package/dist/types/flow.js +11 -2
- package/dist/types/flow.js.map +1 -1
- package/dist/types/output.d.ts +6 -0
- package/dist/types/output.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/core/delegation.d.ts.map +0 -1
- package/dist/core/delegation.js.map +0 -1
- package/dist/flow/warp-command.d.ts +0 -9
- package/dist/flow/warp-command.d.ts.map +0 -1
- package/dist/flow/warp-command.js +0 -405
- package/dist/flow/warp-command.js.map +0 -1
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Two JSONL protocols are used in this codebase:
|
|
3
3
|
*
|
|
4
4
|
* 1. Fork Snapshot Protocol (snapshot.ts):
|
|
5
|
-
* Types: session, model_change, thinking_level_change,
|
|
6
|
-
* compression-stats
|
|
5
|
+
* Types: session, model_change, thinking_level_change, message
|
|
7
6
|
* Purpose: Serialized session state passed to child flows via --session.
|
|
8
7
|
* Emitted by buildForkSessionSnapshotJsonl() and consumed by
|
|
9
8
|
* sanitizeForkSnapshot() before forking.
|
|
@@ -18,7 +17,8 @@
|
|
|
18
17
|
import { stripReasoningFromAssistantMessage } from "./reasoning-strip.js";
|
|
19
18
|
import { stripSteeringHintFromContent, contentContainsSteeringHintTag, isJsonEqual, } from "../steering/sliding-prompt.js";
|
|
20
19
|
import { stripStrategicHintsFromContent } from "../steering/tool-utils.js";
|
|
21
|
-
import { logError } from "../config/log.js";
|
|
20
|
+
import { logWarn, logError } from "../config/log.js";
|
|
21
|
+
import * as path from "node:path";
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
23
|
// Session snapshot serialization
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
@@ -26,6 +26,27 @@ export function buildForkSessionSnapshotJsonl(sessionManager) {
|
|
|
26
26
|
const header = sessionManager.getHeader();
|
|
27
27
|
if (!header || typeof header !== "object")
|
|
28
28
|
return null;
|
|
29
|
+
// Compress cwd in session header: relative to repo root if under it,
|
|
30
|
+
// otherwise basename only. Saves ~50-100 bytes per snapshot.
|
|
31
|
+
const repoRoot = process.cwd();
|
|
32
|
+
let compressedHeader = header;
|
|
33
|
+
if (typeof compressedHeader.cwd === "string") {
|
|
34
|
+
const cwd = compressedHeader.cwd;
|
|
35
|
+
let compressedCwd;
|
|
36
|
+
if (cwd === repoRoot) {
|
|
37
|
+
compressedCwd = ".";
|
|
38
|
+
}
|
|
39
|
+
else if (cwd.startsWith(repoRoot + "/") || cwd.startsWith(repoRoot + "\\")) {
|
|
40
|
+
compressedCwd = cwd.slice(repoRoot.length + 1);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const lastSep = Math.max(cwd.lastIndexOf("/"), cwd.lastIndexOf("\\"));
|
|
44
|
+
compressedCwd = lastSep >= 0 ? cwd.slice(lastSep + 1) : cwd;
|
|
45
|
+
}
|
|
46
|
+
if (compressedCwd !== cwd) {
|
|
47
|
+
compressedHeader = { ...compressedHeader, cwd: compressedCwd };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
29
50
|
const branchEntries = sessionManager.getBranch();
|
|
30
51
|
const lines = [];
|
|
31
52
|
// Emit session header once, unless getBranch() already includes it as the
|
|
@@ -38,13 +59,7 @@ export function buildForkSessionSnapshotJsonl(sessionManager) {
|
|
|
38
59
|
typeof firstBranch !== "object" ||
|
|
39
60
|
(firstType !== "session" && firstType !== "header") ||
|
|
40
61
|
firstId !== headerId) {
|
|
41
|
-
lines.push(JSON.stringify(
|
|
42
|
-
}
|
|
43
|
-
// Emit system event so the JSONL is self-contained — parsers can reconstruct
|
|
44
|
-
// full context without needing the markdown section.
|
|
45
|
-
const systemPrompt = header.systemPrompt;
|
|
46
|
-
if (typeof systemPrompt === "string" && systemPrompt) {
|
|
47
|
-
lines.push(JSON.stringify({ type: "system", content: systemPrompt }));
|
|
62
|
+
lines.push(JSON.stringify(compressedHeader));
|
|
48
63
|
}
|
|
49
64
|
for (const entry of branchEntries)
|
|
50
65
|
lines.push(JSON.stringify(entry));
|
|
@@ -111,50 +126,18 @@ export function renderCompressedFlowResult(r) {
|
|
|
111
126
|
}
|
|
112
127
|
if (r.error)
|
|
113
128
|
parts.push(`Error: ${r.error}`);
|
|
114
|
-
|
|
115
|
-
|
|
129
|
+
// Safety net: reject malformed runtime data where required fields are missing.
|
|
130
|
+
// This is more precise than a substring search that would false-positive on
|
|
131
|
+
// legitimate content containing the word "undefined".
|
|
132
|
+
if (!r.type || !r.status)
|
|
116
133
|
return undefined;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
* Handles both { o: [...] } and bare array argument formats.
|
|
125
|
-
*/
|
|
126
|
-
function extractBatchReadPaths(args) {
|
|
127
|
-
if (!args || typeof args !== "object")
|
|
128
|
-
return [];
|
|
129
|
-
let ops;
|
|
130
|
-
if (Array.isArray(args)) {
|
|
131
|
-
ops = args;
|
|
132
|
-
}
|
|
133
|
-
else if (Array.isArray(args.o)) {
|
|
134
|
-
ops = args.o;
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
return [];
|
|
138
|
-
}
|
|
139
|
-
const paths = [];
|
|
140
|
-
for (const op of ops) {
|
|
141
|
-
if (!op || typeof op !== "object")
|
|
142
|
-
continue;
|
|
143
|
-
const p = op.p;
|
|
144
|
-
if (typeof p === "string" && p)
|
|
145
|
-
paths.push(p);
|
|
146
|
-
}
|
|
147
|
-
return paths;
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Render a compressed batch_read result as compact metadata for child context.
|
|
151
|
-
* Format: [batch_read] N ops → paths: file1.ts, file2.ts, …
|
|
152
|
-
*/
|
|
153
|
-
function renderCompressedBatchReadResult(paths) {
|
|
154
|
-
const MAX_PATHS_DISPLAY = 10;
|
|
155
|
-
const display = paths.slice(0, MAX_PATHS_DISPLAY);
|
|
156
|
-
const suffix = paths.length > MAX_PATHS_DISPLAY ? `, … +${paths.length - MAX_PATHS_DISPLAY} more` : "";
|
|
157
|
-
return `[batch_read] ${paths.length} ops → paths: ${display.join(", ")}${suffix}`;
|
|
134
|
+
if (r.actions?.some((a) => !a.type || !a.description))
|
|
135
|
+
return undefined;
|
|
136
|
+
if (r.commands?.some((c) => !c.command))
|
|
137
|
+
return undefined;
|
|
138
|
+
if (r.notDone?.some((n) => !n.item))
|
|
139
|
+
return undefined;
|
|
140
|
+
return parts.join("\n");
|
|
158
141
|
}
|
|
159
142
|
// ---------------------------------------------------------------------------
|
|
160
143
|
// Additional tool result compressors
|
|
@@ -170,29 +153,444 @@ const KNOWN_SECTION_HEADERS = [
|
|
|
170
153
|
/^--- (.+) \((\d+) lines\) ---$/,
|
|
171
154
|
/^--- (.+) (context map|file summary) ---$/,
|
|
172
155
|
/^--- bash \[.+\] exit (\d+) ---$/,
|
|
156
|
+
/^--- bash \[.+\] pending ---$/,
|
|
157
|
+
/^--- bash \[.+\] error ---$/,
|
|
173
158
|
/^--- edit: .+ ---$/,
|
|
174
159
|
/^--- write: .+ ---$/,
|
|
175
160
|
/^--- delete: .+ ---$/,
|
|
176
161
|
/^--- read: .+ ---$/,
|
|
177
|
-
/^---
|
|
162
|
+
/^--- rg: .+ ---$/,
|
|
163
|
+
/^--- (?!bash \[|edit:|write:|delete:|read:|rg:)(.+) ---$/,
|
|
178
164
|
];
|
|
179
165
|
function isKnownSectionHeader(line) {
|
|
180
166
|
return KNOWN_SECTION_HEADERS.some((re) => re.test(line));
|
|
181
167
|
}
|
|
182
|
-
/** Compress
|
|
183
|
-
|
|
168
|
+
/** Compress a single bash block into the X1 compact format. */
|
|
169
|
+
/** Convert legacy depth number to DepthPolicy. */
|
|
170
|
+
export function depthToPolicy(depth) {
|
|
171
|
+
const isDepth1 = depth < 2;
|
|
172
|
+
return { showPreviews: isDepth1, showBytes: isDepth1, showSupersededBreadcrumbs: isDepth1, showEditBlocks: isDepth1 };
|
|
173
|
+
}
|
|
174
|
+
function compressBashSection(bashId, status, exitCode, timingTier, stdoutLines, stderrLines, policy) {
|
|
175
|
+
const isDepth1 = policy.showPreviews;
|
|
176
|
+
const tier = timingTier ? ` · ${timingTier}` : "";
|
|
177
|
+
// Trim trailing empty lines inserted by multi-bash formatting
|
|
178
|
+
while (stdoutLines.length > 0 && stdoutLines[stdoutLines.length - 1] === "")
|
|
179
|
+
stdoutLines.pop();
|
|
180
|
+
while (stderrLines.length > 0 && stderrLines[stderrLines.length - 1] === "")
|
|
181
|
+
stderrLines.pop();
|
|
182
|
+
if (status === "ok") {
|
|
183
|
+
const lineCount = stdoutLines.length;
|
|
184
|
+
const linesLabel = lineCount === 1 ? "1 line" : `${lineCount} lines`;
|
|
185
|
+
if (isDepth1) {
|
|
186
|
+
if (lineCount === 0) {
|
|
187
|
+
return `[bash:ok] ${bashId} · exit ${exitCode}${tier} · 0 lines`;
|
|
188
|
+
}
|
|
189
|
+
const head = stdoutLines.slice(0, 3).join("\n");
|
|
190
|
+
return `[bash:ok] ${bashId} · exit ${exitCode}${tier} · ${linesLabel}\n> head:\n${head}`;
|
|
191
|
+
}
|
|
192
|
+
return `[bash:ok] ${bashId} · exit ${exitCode}`;
|
|
193
|
+
}
|
|
194
|
+
if (status === "pending") {
|
|
195
|
+
const lineCount = stdoutLines.length;
|
|
196
|
+
const linesLabel = lineCount === 1 ? "1 line partial" : `${lineCount} lines partial`;
|
|
197
|
+
if (isDepth1) {
|
|
198
|
+
if (lineCount === 0) {
|
|
199
|
+
return `[bash:pending] ${bashId} · still running · 0 lines partial`;
|
|
200
|
+
}
|
|
201
|
+
const head = stdoutLines.slice(0, 3).join("\n");
|
|
202
|
+
return `[bash:pending] ${bashId} · still running · ${linesLabel}\n> head:\n${head}`;
|
|
203
|
+
}
|
|
204
|
+
return `[bash:pending] ${bashId} · still running`;
|
|
205
|
+
}
|
|
206
|
+
if (status === "error") {
|
|
207
|
+
const lineCount = stderrLines.length;
|
|
208
|
+
const linesLabel = lineCount === 1 ? "1 line stderr" : `${lineCount} lines stderr`;
|
|
209
|
+
const exit = exitCode !== undefined ? ` · exit ${exitCode}` : "";
|
|
210
|
+
if (isDepth1) {
|
|
211
|
+
if (lineCount === 0) {
|
|
212
|
+
return `[bash:err] ${bashId}${exit}${tier} · 0 lines`;
|
|
213
|
+
}
|
|
214
|
+
const head = stderrLines.slice(0, 3).join("\n");
|
|
215
|
+
return `[bash:err] ${bashId}${exit}${tier} · ${linesLabel}\n> stderr:\n${head}`;
|
|
216
|
+
}
|
|
217
|
+
return `[bash:err] ${bashId}${exit}`;
|
|
218
|
+
}
|
|
219
|
+
return `[bash] ${bashId} · status unknown`;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Normalize a bash command string for use as a dedup key.
|
|
223
|
+
*/
|
|
224
|
+
function normalizeBashCommand(cmd) {
|
|
225
|
+
return cmd.trim().replace(/\s+/g, " ");
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Normalize a file path for use as a dedup key.
|
|
229
|
+
*/
|
|
230
|
+
function normalizeDedupPath(rawPath, cwd) {
|
|
231
|
+
let p = rawPath.replace(/\\/g, "/");
|
|
232
|
+
if (p.startsWith("./")) {
|
|
233
|
+
p = p.slice(2);
|
|
234
|
+
}
|
|
235
|
+
p = path.resolve(cwd, p);
|
|
236
|
+
p = path.normalize(p);
|
|
237
|
+
return p;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Scan all batch tool results in the snapshot and build a DedupIndex.
|
|
241
|
+
* Only successful writes/edits/deletes are tracked; error operations are exempt.
|
|
242
|
+
*/
|
|
243
|
+
function buildDedupIndex(lines, toolCallIdToName, toolCallIdToArgs, sessionCwd) {
|
|
244
|
+
const cwd = sessionCwd ?? process.cwd();
|
|
245
|
+
const latestWrite = new Map();
|
|
246
|
+
const latestEdit = new Map();
|
|
247
|
+
const latestDelete = new Map();
|
|
248
|
+
const latestWebSearch = new Map();
|
|
249
|
+
const latestWebFetch = new Map();
|
|
250
|
+
const latestAskUser = new Map();
|
|
251
|
+
const bashIdToCommand = new Map();
|
|
252
|
+
const latestBash = new Map();
|
|
253
|
+
for (const line of lines) {
|
|
254
|
+
let entry;
|
|
255
|
+
try {
|
|
256
|
+
entry = JSON.parse(line);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (entry?.type !== "message" || (entry.message?.role !== "tool" && entry.message?.role !== "toolResult"))
|
|
262
|
+
continue;
|
|
263
|
+
const toolCallId = entry.message?.toolCallId;
|
|
264
|
+
if (typeof toolCallId !== "string")
|
|
265
|
+
continue;
|
|
266
|
+
const toolName = toolCallIdToName.get(toolCallId);
|
|
267
|
+
if (toolName === "batch") {
|
|
268
|
+
const text = extractToolResultText(entry) ?? "";
|
|
269
|
+
const textLines = text.replace(/\r\n/g, "\n").split("\n");
|
|
270
|
+
for (let i = 0; i < textLines.length; i++) {
|
|
271
|
+
const l = textLines[i];
|
|
272
|
+
// Successful write
|
|
273
|
+
const writeMatch = l.match(/^--- write: (.+) \((\d+) bytes\) ---$/);
|
|
274
|
+
if (writeMatch) {
|
|
275
|
+
const normPath = normalizeDedupPath(writeMatch[1].trim(), cwd);
|
|
276
|
+
latestWrite.set(normPath, toolCallId);
|
|
277
|
+
latestEdit.delete(normPath); // write supersedes earlier edits
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
// Error write — exempt from dedup
|
|
281
|
+
const errorWriteMatch = l.match(/^--- write: (.+) ---$/);
|
|
282
|
+
if (errorWriteMatch) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
// Successful edit
|
|
286
|
+
const editMatch = l.match(/^--- edit: (.+) \(([^)]*)\) ---$/);
|
|
287
|
+
if (editMatch) {
|
|
288
|
+
const normPath = normalizeDedupPath(editMatch[1].trim(), cwd);
|
|
289
|
+
latestEdit.set(normPath, toolCallId);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
// Error edit — exempt from dedup
|
|
293
|
+
const errorEditMatch = l.match(/^--- edit: (.+) ---$/);
|
|
294
|
+
if (errorEditMatch) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
// Delete
|
|
298
|
+
const deleteMatch = l.match(/^--- delete: (.+) ---$/);
|
|
299
|
+
if (deleteMatch) {
|
|
300
|
+
const normPath = normalizeDedupPath(deleteMatch[1].trim(), cwd);
|
|
301
|
+
latestDelete.set(normPath, toolCallId);
|
|
302
|
+
latestWrite.delete(normPath); // delete supersedes earlier writes
|
|
303
|
+
latestEdit.delete(normPath); // delete supersedes earlier edits
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// B1: Extract bash commands from batch args for cross-turn dedup
|
|
307
|
+
const args = toolCallIdToArgs.get(toolCallId);
|
|
308
|
+
if (args && typeof args === "object") {
|
|
309
|
+
const a = args;
|
|
310
|
+
const ops = Array.isArray(a.o) ? a.o : Array.isArray(a.op) ? a.op : undefined;
|
|
311
|
+
if (ops) {
|
|
312
|
+
for (const op of ops) {
|
|
313
|
+
if (op && typeof op === "object") {
|
|
314
|
+
const opObj = op;
|
|
315
|
+
if (opObj.o === "bash" || opObj.op === "bash") {
|
|
316
|
+
const cmd = typeof opObj.c === "string" ? opObj.c : "";
|
|
317
|
+
const id = typeof opObj.i === "string" ? opObj.i : "";
|
|
318
|
+
if (cmd && id) {
|
|
319
|
+
const normCmd = normalizeBashCommand(cmd);
|
|
320
|
+
bashIdToCommand.set(id, normCmd);
|
|
321
|
+
latestBash.set(normCmd, id);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Ask_user tool results — build A1 dedup index
|
|
330
|
+
if (toolName === "ask_user") {
|
|
331
|
+
const args = toolCallIdToArgs.get(toolCallId);
|
|
332
|
+
if (args && typeof args === "object") {
|
|
333
|
+
const question = args.question;
|
|
334
|
+
if (typeof question === "string") {
|
|
335
|
+
const norm = question.trim().toLowerCase().slice(0, 120);
|
|
336
|
+
if (norm) {
|
|
337
|
+
latestAskUser.set(norm, toolCallId);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Web tool results — build Q1 dedup index
|
|
343
|
+
if (toolName === "web") {
|
|
344
|
+
const args = toolCallIdToArgs.get(toolCallId);
|
|
345
|
+
if (args && typeof args === "object") {
|
|
346
|
+
const a = args;
|
|
347
|
+
const ops = Array.isArray(a.o) ? a.o : Array.isArray(a.op) ? a.op : undefined;
|
|
348
|
+
if (ops && ops.length > 0) {
|
|
349
|
+
const firstOp = ops[0];
|
|
350
|
+
if (firstOp && typeof firstOp === "object") {
|
|
351
|
+
const query = typeof firstOp.q === "string" ? firstOp.q.trim().toLowerCase() : undefined;
|
|
352
|
+
const url = typeof firstOp.u === "string" ? firstOp.u.trim().replace(/\/$/, "") : undefined;
|
|
353
|
+
if (query)
|
|
354
|
+
latestWebSearch.set(query, toolCallId);
|
|
355
|
+
if (url)
|
|
356
|
+
latestWebFetch.set(url, toolCallId);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return { latestWrite, latestEdit, latestDelete, latestWebSearch, latestWebFetch, latestAskUser, bashIdToCommand, latestBash };
|
|
363
|
+
}
|
|
364
|
+
/** Check if a web tool result is superseded by a later result with the same query or URL. */
|
|
365
|
+
function checkWebDedup(args, toolCallId, dedupIndex) {
|
|
366
|
+
if (!args || typeof args !== "object")
|
|
367
|
+
return { isSuperseded: false };
|
|
368
|
+
const a = args;
|
|
369
|
+
const ops = Array.isArray(a.o) ? a.o : Array.isArray(a.op) ? a.op : undefined;
|
|
370
|
+
if (!ops || ops.length === 0)
|
|
371
|
+
return { isSuperseded: false };
|
|
372
|
+
const firstOp = ops[0];
|
|
373
|
+
if (!firstOp || typeof firstOp !== "object")
|
|
374
|
+
return { isSuperseded: false };
|
|
375
|
+
if (typeof firstOp.q === "string") {
|
|
376
|
+
const normQuery = firstOp.q.trim().toLowerCase();
|
|
377
|
+
if (normQuery) {
|
|
378
|
+
const latestTc = dedupIndex.latestWebSearch.get(normQuery);
|
|
379
|
+
if (latestTc !== toolCallId) {
|
|
380
|
+
return {
|
|
381
|
+
isSuperseded: true,
|
|
382
|
+
marker: `[web:search] "${firstOp.q}" (superseded by later search)`,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (typeof firstOp.u === "string") {
|
|
388
|
+
const normUrl = firstOp.u.trim().replace(/\/$/, "");
|
|
389
|
+
if (normUrl) {
|
|
390
|
+
const latestTc = dedupIndex.latestWebFetch.get(normUrl);
|
|
391
|
+
if (latestTc !== toolCallId) {
|
|
392
|
+
return {
|
|
393
|
+
isSuperseded: true,
|
|
394
|
+
marker: `[web:fetch] ${firstOp.u} (superseded by later fetch)`,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return { isSuperseded: false };
|
|
400
|
+
}
|
|
401
|
+
/** Compress batch tool result: compress bash sections, truncate read content, dedup writes/edits/deletes (W1 + E1). */
|
|
402
|
+
function compressBatchResult(text, options = {}) {
|
|
403
|
+
const policy = options.depthPolicy ?? depthToPolicy(1);
|
|
404
|
+
const { toolCallId, latestWrite, latestEdit, latestDelete, bashIdToCommand, latestBash } = options;
|
|
405
|
+
const cwd = options.cwd ?? process.cwd();
|
|
184
406
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
407
|
+
// Pre-scan to find the last occurrence of each operation within this result.
|
|
408
|
+
// This handles the edge case where a single batch result contains multiple
|
|
409
|
+
// writes/edits/deletes to the same path.
|
|
410
|
+
const lastWriteIndex = new Map();
|
|
411
|
+
const lastEditIndex = new Map();
|
|
412
|
+
const lastDeleteIndex = new Map();
|
|
413
|
+
for (let j = 0; j < lines.length; j++) {
|
|
414
|
+
const w = lines[j].match(/^--- write: (.+) \((\d+) bytes\) ---$/);
|
|
415
|
+
if (w)
|
|
416
|
+
lastWriteIndex.set(normalizeDedupPath(w[1].trim(), cwd), j);
|
|
417
|
+
const e = lines[j].match(/^--- edit: (.+) \(([^)]*)\) ---$/);
|
|
418
|
+
if (e)
|
|
419
|
+
lastEditIndex.set(normalizeDedupPath(e[1].trim(), cwd), j);
|
|
420
|
+
const d = lines[j].match(/^--- delete: (.+) ---$/);
|
|
421
|
+
if (d)
|
|
422
|
+
lastDeleteIndex.set(normalizeDedupPath(d[1].trim(), cwd), j);
|
|
423
|
+
}
|
|
185
424
|
const out = [];
|
|
186
425
|
let i = 0;
|
|
426
|
+
const isSupersededWrite = (normPath, index) => {
|
|
427
|
+
if (!toolCallId)
|
|
428
|
+
return false;
|
|
429
|
+
const latestTc = latestWrite?.get(normPath);
|
|
430
|
+
if (latestTc !== toolCallId)
|
|
431
|
+
return true;
|
|
432
|
+
return lastWriteIndex.get(normPath) !== index;
|
|
433
|
+
};
|
|
434
|
+
const isSupersededEdit = (normPath, index) => {
|
|
435
|
+
if (!toolCallId)
|
|
436
|
+
return false;
|
|
437
|
+
const latestTc = latestEdit?.get(normPath);
|
|
438
|
+
if (latestTc !== toolCallId)
|
|
439
|
+
return true;
|
|
440
|
+
return lastEditIndex.get(normPath) !== index;
|
|
441
|
+
};
|
|
442
|
+
const isSupersededDelete = (normPath, index) => {
|
|
443
|
+
if (!toolCallId)
|
|
444
|
+
return false;
|
|
445
|
+
const latestTc = latestDelete?.get(normPath);
|
|
446
|
+
if (latestTc !== toolCallId)
|
|
447
|
+
return true;
|
|
448
|
+
return lastDeleteIndex.get(normPath) !== index;
|
|
449
|
+
};
|
|
187
450
|
while (i < lines.length) {
|
|
188
451
|
const line = lines[i];
|
|
189
|
-
//
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
192
|
-
|
|
452
|
+
// Bash section — compress with X1 protocol
|
|
453
|
+
const bashMatch = line.match(/^--- bash \[([^\]]+)\] (exit (\d+)|pending|error) ---$/);
|
|
454
|
+
if (bashMatch) {
|
|
455
|
+
const bashId = bashMatch[1];
|
|
456
|
+
const rawStatus = bashMatch[2];
|
|
457
|
+
const status = rawStatus.startsWith("exit") ? "ok" : rawStatus;
|
|
458
|
+
const exitCode = bashMatch[3] !== undefined ? Number(bashMatch[3]) : undefined;
|
|
459
|
+
// Stricter section-end check for bash content: don't treat generic
|
|
460
|
+
// `--- text ---` lines as section headers (they could be bash output).
|
|
461
|
+
const isBashSectionEnd = (l) => /^--- bash \[.+\]/.test(l) ||
|
|
462
|
+
/^--- (.+) \((\d+) lines\) ---$/.test(l) ||
|
|
463
|
+
/^--- (.+) (context map|file summary) ---$/.test(l) ||
|
|
464
|
+
/^--- edit: .+ ---$/.test(l) ||
|
|
465
|
+
/^--- write: .+ ---$/.test(l) ||
|
|
466
|
+
/^--- delete: .+ ---$/.test(l) ||
|
|
467
|
+
/^--- read: .+ ---$/.test(l) ||
|
|
468
|
+
/^--- rg: .+ ---$/.test(l);
|
|
469
|
+
// B1 cross-turn bash dedup
|
|
470
|
+
const normCmd = bashIdToCommand?.get(bashId);
|
|
471
|
+
const isSupersededBash = normCmd ? latestBash?.get(normCmd) !== bashId : false;
|
|
472
|
+
if (isSupersededBash) {
|
|
473
|
+
if (policy.showSupersededBreadcrumbs) {
|
|
474
|
+
const statusTag = status === "ok" ? "ok" : status === "pending" ? "pending" : "err";
|
|
475
|
+
out.push(`[bash:${statusTag}] ${bashId} (superseded)`);
|
|
476
|
+
}
|
|
477
|
+
i++;
|
|
478
|
+
while (i < lines.length && !isBashSectionEnd(lines[i])) {
|
|
479
|
+
i++;
|
|
480
|
+
}
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
193
483
|
i++;
|
|
484
|
+
let timingTier;
|
|
485
|
+
const stdoutLines = [];
|
|
486
|
+
let stderrLines = [];
|
|
487
|
+
let inStderr = false;
|
|
488
|
+
while (i < lines.length && !isBashSectionEnd(lines[i])) {
|
|
489
|
+
const contentLine = lines[i];
|
|
490
|
+
const timingMatch = contentLine.match(/^\[Execution time: (.+)\]$/);
|
|
491
|
+
if (timingMatch) {
|
|
492
|
+
timingTier = timingMatch[1];
|
|
493
|
+
}
|
|
494
|
+
else if (contentLine === "[stderr]") {
|
|
495
|
+
inStderr = true;
|
|
496
|
+
}
|
|
497
|
+
else if (contentLine === "[partial output]") {
|
|
498
|
+
// pending partial output marker — stdout follows
|
|
499
|
+
inStderr = false;
|
|
500
|
+
}
|
|
501
|
+
else if (contentLine.startsWith("[Use batch_bash_poll")) {
|
|
502
|
+
// skip poll hint lines
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
if (inStderr) {
|
|
506
|
+
stderrLines.push(contentLine);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
stdoutLines.push(contentLine);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
i++;
|
|
513
|
+
}
|
|
514
|
+
// If error bash has no stderr but produced stdout, preserve the stdout
|
|
515
|
+
// as the error output so it isn't silently lost.
|
|
516
|
+
if (status === "error" && stderrLines.length === 0 && stdoutLines.length > 0) {
|
|
517
|
+
stderrLines = stdoutLines;
|
|
518
|
+
}
|
|
519
|
+
out.push(compressBashSection(bashId, status, exitCode, timingTier, stdoutLines, stderrLines, policy));
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
// R1: rg output compression
|
|
523
|
+
const rgMatch = line.match(/^--- rg: (.+) ---$/);
|
|
524
|
+
if (rgMatch) {
|
|
525
|
+
const rgPath = rgMatch[1].trim();
|
|
526
|
+
i++;
|
|
527
|
+
const rgLines = [];
|
|
194
528
|
while (i < lines.length && !isKnownSectionHeader(lines[i])) {
|
|
529
|
+
rgLines.push(lines[i]);
|
|
530
|
+
i++;
|
|
531
|
+
}
|
|
532
|
+
// Trim trailing empty lines
|
|
533
|
+
while (rgLines.length > 0 && rgLines[rgLines.length - 1] === "")
|
|
534
|
+
rgLines.pop();
|
|
535
|
+
const matchCount = rgLines.length;
|
|
536
|
+
// Detect error: batch tool outputs "Error: <msg>" for failed rg ops
|
|
537
|
+
const firstNonEmpty = rgLines.find((l) => l.trim() !== "");
|
|
538
|
+
const isError = firstNonEmpty?.startsWith("Error:") ?? false;
|
|
539
|
+
if (isError) {
|
|
540
|
+
const lineCount = rgLines.filter((l) => l.trim() !== "").length;
|
|
541
|
+
const linesLabel = lineCount === 1 ? "1 line" : `${lineCount} lines`;
|
|
542
|
+
out.push(`[rg:err] ${rgPath} · ${linesLabel}`);
|
|
543
|
+
}
|
|
544
|
+
else if (matchCount === 0) {
|
|
545
|
+
out.push(`[rg:ok] ${rgPath} · 0 matches`);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
// Extract unique file paths from rg output (format: path:line:content)
|
|
549
|
+
const fileSet = new Set(rgLines
|
|
550
|
+
.map((l) => {
|
|
551
|
+
const colonIdx = l.indexOf(":");
|
|
552
|
+
return colonIdx > 0 ? l.slice(0, colonIdx) : "";
|
|
553
|
+
})
|
|
554
|
+
.filter(Boolean));
|
|
555
|
+
const fileCount = fileSet.size;
|
|
556
|
+
if (policy.showPreviews) {
|
|
557
|
+
const head = rgLines.slice(0, 3).join("\n");
|
|
558
|
+
out.push(`[rg:ok] ${rgPath} · ${matchCount} matches · ${fileCount} files\n> head:\n${head}`);
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
out.push(`[rg:ok] ${rgPath} · ${matchCount} matches · ${fileCount} files`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
i--; // will be incremented by loop
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
// File read section with content — preview or truncate
|
|
568
|
+
const readMatch = line.match(/^--- (.+) \((\d+) lines\) ---$/);
|
|
569
|
+
if (readMatch) {
|
|
570
|
+
if (policy.showPreviews) {
|
|
571
|
+
i++;
|
|
572
|
+
const contentLines = [];
|
|
573
|
+
while (i < lines.length && !isKnownSectionHeader(lines[i])) {
|
|
574
|
+
contentLines.push(lines[i]);
|
|
575
|
+
i++;
|
|
576
|
+
}
|
|
577
|
+
const head = contentLines.slice(0, 2).join("\n");
|
|
578
|
+
const tail = contentLines.slice(-2).join("\n");
|
|
579
|
+
let previewText;
|
|
580
|
+
if (contentLines.length > 4) {
|
|
581
|
+
previewText = `${head}\n[...${contentLines.length - 4} lines truncated...]\n${tail}`;
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
previewText = contentLines.join("\n");
|
|
585
|
+
}
|
|
586
|
+
out.push(`--- ${readMatch[1]} (${readMatch[2]} lines, preview) ---\n${previewText}`);
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
out.push(`--- ${readMatch[1]} (${readMatch[2]} lines, content truncated) ---`);
|
|
195
590
|
i++;
|
|
591
|
+
while (i < lines.length && !isKnownSectionHeader(lines[i])) {
|
|
592
|
+
i++;
|
|
593
|
+
}
|
|
196
594
|
}
|
|
197
595
|
continue;
|
|
198
596
|
}
|
|
@@ -206,21 +604,160 @@ function compressBatchResult(text) {
|
|
|
206
604
|
}
|
|
207
605
|
continue;
|
|
208
606
|
}
|
|
209
|
-
// File read without line count — truncate
|
|
607
|
+
// File read without line count — preview or truncate
|
|
210
608
|
// Negative lookahead excludes bash/edit/write/delete/read-error sections that should be kept verbatim
|
|
211
609
|
const fallbackReadMatch = line.match(/^--- (?!bash \[|edit:|write:|delete:|read:)(.+) ---$/);
|
|
212
610
|
if (fallbackReadMatch) {
|
|
213
|
-
|
|
611
|
+
if (policy.showPreviews) {
|
|
612
|
+
i++;
|
|
613
|
+
const contentLines = [];
|
|
614
|
+
while (i < lines.length && !isKnownSectionHeader(lines[i])) {
|
|
615
|
+
contentLines.push(lines[i]);
|
|
616
|
+
i++;
|
|
617
|
+
}
|
|
618
|
+
const head = contentLines.slice(0, 2).join("\n");
|
|
619
|
+
const tail = contentLines.slice(-2).join("\n");
|
|
620
|
+
let previewText;
|
|
621
|
+
if (contentLines.length > 4) {
|
|
622
|
+
previewText = `${head}\n[...${contentLines.length - 4} lines truncated...]\n${tail}`;
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
previewText = contentLines.join("\n");
|
|
626
|
+
}
|
|
627
|
+
out.push(`--- ${fallbackReadMatch[1]} (preview) ---\n${previewText}`);
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
out.push(`--- ${fallbackReadMatch[1]} (content truncated) ---`);
|
|
631
|
+
i++;
|
|
632
|
+
while (i < lines.length && !isKnownSectionHeader(lines[i])) {
|
|
633
|
+
i++;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
// Write section — W1 dedup and compression
|
|
639
|
+
const writeMatch = line.match(/^--- write: (.+) \((\d+) bytes\) ---$/);
|
|
640
|
+
if (writeMatch) {
|
|
641
|
+
const rawPath = writeMatch[1].trim();
|
|
642
|
+
const normPath = normalizeDedupPath(rawPath, cwd);
|
|
643
|
+
const bytes = writeMatch[2];
|
|
644
|
+
if (isSupersededWrite(normPath, i)) {
|
|
645
|
+
if (policy.showSupersededBreadcrumbs) {
|
|
646
|
+
out.push(`[batch:write] ${rawPath} (superseded)`);
|
|
647
|
+
}
|
|
648
|
+
i++;
|
|
649
|
+
while (i < lines.length && !isKnownSectionHeader(lines[i])) {
|
|
650
|
+
i++;
|
|
651
|
+
}
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (policy.showBytes) {
|
|
655
|
+
out.push(`[batch:write] ${rawPath} (${bytes} bytes)`);
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
out.push(`[batch:write] ${rawPath}`);
|
|
659
|
+
}
|
|
660
|
+
i++;
|
|
661
|
+
while (i < lines.length && !isKnownSectionHeader(lines[i])) {
|
|
662
|
+
i++;
|
|
663
|
+
}
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
// Error write — exempt from dedup, keep verbatim
|
|
667
|
+
const errorWriteMatch = line.match(/^--- write: (.+) ---$/);
|
|
668
|
+
if (errorWriteMatch) {
|
|
669
|
+
out.push(line);
|
|
670
|
+
i++;
|
|
671
|
+
while (i < lines.length && !isKnownSectionHeader(lines[i])) {
|
|
672
|
+
out.push(lines[i]);
|
|
673
|
+
i++;
|
|
674
|
+
}
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
// Edit section — E1 dedup and compression
|
|
678
|
+
const editMatch = line.match(/^--- edit: (.+) \(([^)]*)\) ---$/);
|
|
679
|
+
if (editMatch) {
|
|
680
|
+
const rawPath = editMatch[1].trim();
|
|
681
|
+
const normPath = normalizeDedupPath(rawPath, cwd);
|
|
682
|
+
const blockInfo = editMatch[2];
|
|
683
|
+
if (isSupersededEdit(normPath, i)) {
|
|
684
|
+
if (policy.showSupersededBreadcrumbs) {
|
|
685
|
+
out.push(`[batch:edit] ${rawPath} (superseded)`);
|
|
686
|
+
}
|
|
687
|
+
i++;
|
|
688
|
+
while (i < lines.length && !isKnownSectionHeader(lines[i])) {
|
|
689
|
+
i++;
|
|
690
|
+
}
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (policy.showEditBlocks) {
|
|
694
|
+
const blocksLabel = blockInfo ? ` (${blockInfo})` : "";
|
|
695
|
+
out.push(`[batch:edit] ${rawPath}${blocksLabel}`);
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
out.push(`[batch:edit] ${rawPath}`);
|
|
699
|
+
}
|
|
700
|
+
i++;
|
|
701
|
+
while (i < lines.length && !isKnownSectionHeader(lines[i])) {
|
|
702
|
+
i++;
|
|
703
|
+
}
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
// Error edit — exempt from dedup, keep verbatim
|
|
707
|
+
const errorEditMatch = line.match(/^--- edit: (.+) ---$/);
|
|
708
|
+
if (errorEditMatch) {
|
|
709
|
+
out.push(line);
|
|
710
|
+
i++;
|
|
711
|
+
while (i < lines.length && !isKnownSectionHeader(lines[i])) {
|
|
712
|
+
out.push(lines[i]);
|
|
713
|
+
i++;
|
|
714
|
+
}
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
// Delete section — keep existing format for non-superseded, skip superseded
|
|
718
|
+
const deleteMatch = line.match(/^--- delete: (.+) ---$/);
|
|
719
|
+
if (deleteMatch) {
|
|
720
|
+
const rawPath = deleteMatch[1].trim();
|
|
721
|
+
const normPath = normalizeDedupPath(rawPath, cwd);
|
|
722
|
+
if (isSupersededDelete(normPath, i)) {
|
|
723
|
+
if (policy.showSupersededBreadcrumbs) {
|
|
724
|
+
out.push(`[batch:delete] ${rawPath} (superseded)`);
|
|
725
|
+
}
|
|
726
|
+
i++;
|
|
727
|
+
while (i < lines.length && !isKnownSectionHeader(lines[i])) {
|
|
728
|
+
i++;
|
|
729
|
+
}
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
out.push(line);
|
|
214
733
|
i++;
|
|
215
734
|
while (i < lines.length && !isKnownSectionHeader(lines[i])) {
|
|
735
|
+
out.push(lines[i]);
|
|
216
736
|
i++;
|
|
217
737
|
}
|
|
218
738
|
continue;
|
|
219
739
|
}
|
|
220
|
-
// Everything else (
|
|
740
|
+
// Everything else (summary, error generic, etc.) — keep as-is
|
|
221
741
|
out.push(line);
|
|
222
742
|
i++;
|
|
223
743
|
}
|
|
744
|
+
// B1: If every line in out is superseded or truncated noise, collapse to single summary
|
|
745
|
+
const meaningfulOut = out.filter((l) => l.trim() !== "");
|
|
746
|
+
// Single-pass rollup check: matches superseded breadcrumbs, truncated reads,
|
|
747
|
+
// and compact bash/rg lines. Equivalent to the previous multi-check logic.
|
|
748
|
+
const SUPERSEDED_OR_TRUNCATED_RE = /\(superseded\)|\(content truncated\)|\(context map, truncated\)|^\[bash:(ok|pending|err)\] |^\[bash:poll\] |^\[rg:(ok|err)\] /;
|
|
749
|
+
const isAllSupersededOrTruncated = meaningfulOut.length > 0 && meaningfulOut.every((l) => SUPERSEDED_OR_TRUNCATED_RE.test(l));
|
|
750
|
+
// At depth 2+, superseded writes/edits are dropped entirely (no breadcrumbs),
|
|
751
|
+
// leaving out empty. If the original text had only section headers and no
|
|
752
|
+
// summary or other kept content, rollup to a single line.
|
|
753
|
+
const meaningfulLines = lines.filter((l) => l.trim() !== "");
|
|
754
|
+
const allLinesWereSectionHeaders = meaningfulLines.length > 0 && meaningfulLines.every((l) => isKnownSectionHeader(l));
|
|
755
|
+
if (isAllSupersededOrTruncated || (allLinesWereSectionHeaders && meaningfulOut.length === 0)) {
|
|
756
|
+
const opCount = meaningfulLines.length > 0 ? String(meaningfulLines.length) : "0";
|
|
757
|
+
return policy.showSupersededBreadcrumbs
|
|
758
|
+
? `[batch] ${opCount} ops (all superseded or truncated by later operations)`
|
|
759
|
+
: `[batch] ${opCount} ops (superseded)`;
|
|
760
|
+
}
|
|
224
761
|
return out.join("\n");
|
|
225
762
|
}
|
|
226
763
|
/** Compress web tool result into compact metadata. */
|
|
@@ -233,8 +770,10 @@ function compressWebResult(text, args) {
|
|
|
233
770
|
const ops = Array.isArray(a.o) ? a.o : Array.isArray(a.op) ? a.op : undefined;
|
|
234
771
|
if (ops && ops.length > 0) {
|
|
235
772
|
const firstOp = ops[0];
|
|
236
|
-
|
|
237
|
-
|
|
773
|
+
if (firstOp && typeof firstOp === "object") {
|
|
774
|
+
query = typeof firstOp.q === "string" ? firstOp.q : undefined;
|
|
775
|
+
url = typeof firstOp.u === "string" ? firstOp.u : undefined;
|
|
776
|
+
}
|
|
238
777
|
}
|
|
239
778
|
}
|
|
240
779
|
// Search result format: numbered list
|
|
@@ -277,6 +816,113 @@ function compressAskUserResult(text, args) {
|
|
|
277
816
|
}
|
|
278
817
|
return `[ask_user] · ${text.length} chars`;
|
|
279
818
|
}
|
|
819
|
+
/** Compress batch_bash_poll tool result into compact metadata (S4 + B1). */
|
|
820
|
+
function compressBatchBashPollResult(text, policy, options) {
|
|
821
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
822
|
+
const out = [];
|
|
823
|
+
let i = 0;
|
|
824
|
+
const isDepth1 = policy.showPreviews;
|
|
825
|
+
const isPollSectionEnd = (l) => /^--- \[.+\]/.test(l);
|
|
826
|
+
while (i < lines.length) {
|
|
827
|
+
const line = lines[i];
|
|
828
|
+
const completedMatch = line.match(/^--- \[([^\]]+)\] (exit (\d+)|interrupted) ---$/);
|
|
829
|
+
const pendingMatch = line.match(/^--- \[([^\]]+)\] still running ---$/);
|
|
830
|
+
if (completedMatch || pendingMatch) {
|
|
831
|
+
const id = (completedMatch ?? pendingMatch)[1];
|
|
832
|
+
const isCompleted = !!completedMatch;
|
|
833
|
+
const exitCode = completedMatch?.[3] !== undefined ? Number(completedMatch[3]) : undefined;
|
|
834
|
+
// B1 cross-turn bash dedup for poll results
|
|
835
|
+
const normCmd = options?.bashIdToCommand?.get(id);
|
|
836
|
+
const isSuperseded = normCmd ? options?.latestBash?.get(normCmd) !== id : false;
|
|
837
|
+
if (isSuperseded) {
|
|
838
|
+
if (policy.showSupersededBreadcrumbs) {
|
|
839
|
+
out.push(`[bash:poll] ${id} (superseded)`);
|
|
840
|
+
}
|
|
841
|
+
i++;
|
|
842
|
+
while (i < lines.length && !isPollSectionEnd(lines[i])) {
|
|
843
|
+
i++;
|
|
844
|
+
}
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
i++;
|
|
848
|
+
let timingTier;
|
|
849
|
+
const stdoutLines = [];
|
|
850
|
+
let stderrLines = [];
|
|
851
|
+
let inStderr = false;
|
|
852
|
+
while (i < lines.length && !isPollSectionEnd(lines[i])) {
|
|
853
|
+
const contentLine = lines[i];
|
|
854
|
+
const timingMatch = contentLine.match(/^\[Execution time: (.+)\]$/);
|
|
855
|
+
if (timingMatch) {
|
|
856
|
+
timingTier = timingMatch[1];
|
|
857
|
+
}
|
|
858
|
+
else if (contentLine === "[stderr]") {
|
|
859
|
+
inStderr = true;
|
|
860
|
+
}
|
|
861
|
+
else if (contentLine === "[output so far]") {
|
|
862
|
+
inStderr = false;
|
|
863
|
+
}
|
|
864
|
+
else if (contentLine.trim() === "") {
|
|
865
|
+
// skip empty lines between sections
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
if (inStderr) {
|
|
869
|
+
stderrLines.push(contentLine);
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
stdoutLines.push(contentLine);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
i++;
|
|
876
|
+
}
|
|
877
|
+
const tier = timingTier ? ` · ${timingTier}` : "";
|
|
878
|
+
if (isCompleted) {
|
|
879
|
+
const statusTag = exitCode !== undefined ? `exit ${exitCode}` : "interrupted";
|
|
880
|
+
const statusLabel = exitCode === 0 ? "ok" : "error";
|
|
881
|
+
if (statusLabel === "error" && stderrLines.length === 0 && stdoutLines.length > 0) {
|
|
882
|
+
stderrLines = stdoutLines;
|
|
883
|
+
}
|
|
884
|
+
const targetLines = statusLabel === "error" ? stderrLines : stdoutLines;
|
|
885
|
+
const lineCount = targetLines.length;
|
|
886
|
+
if (isDepth1) {
|
|
887
|
+
if (lineCount === 0) {
|
|
888
|
+
out.push(`[bash:poll] ${id} · ${statusTag}${tier} · 0 lines`);
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
const linesLabel = statusLabel === "error"
|
|
892
|
+
? (lineCount === 1 ? "1 line stderr" : `${lineCount} lines stderr`)
|
|
893
|
+
: (lineCount === 1 ? "1 line" : `${lineCount} lines`);
|
|
894
|
+
const headPrefix = statusLabel === "error" ? "> stderr:" : "> head:";
|
|
895
|
+
const head = targetLines.slice(0, 3).join("\n");
|
|
896
|
+
out.push(`[bash:poll] ${id} · ${statusTag}${tier} · ${linesLabel}\n${headPrefix}\n${head}`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
out.push(`[bash:poll] ${id} · ${statusTag}${tier}`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
const lineCount = stdoutLines.length;
|
|
905
|
+
const linesLabel = lineCount === 1 ? "1 line partial" : `${lineCount} lines partial`;
|
|
906
|
+
if (isDepth1) {
|
|
907
|
+
if (lineCount === 0) {
|
|
908
|
+
out.push(`[bash:poll] ${id} · still running · 0 lines partial`);
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
const head = stdoutLines.slice(0, 3).join("\n");
|
|
912
|
+
out.push(`[bash:poll] ${id} · still running · ${linesLabel}\n> head:\n${head}`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
out.push(`[bash:poll] ${id} · still running`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
out.push(line);
|
|
922
|
+
i++;
|
|
923
|
+
}
|
|
924
|
+
return out.join("\n");
|
|
925
|
+
}
|
|
280
926
|
// ---------------------------------------------------------------------------
|
|
281
927
|
// Shared: toolCallId → toolName mapping
|
|
282
928
|
// ---------------------------------------------------------------------------
|
|
@@ -320,7 +966,8 @@ function buildToolCallIdToNameMap(lines) {
|
|
|
320
966
|
* - `batch_read` results: replaced with compact metadata (paths + op count)
|
|
321
967
|
* since children have `batch` and can re-read files themselves.
|
|
322
968
|
*/
|
|
323
|
-
export function compressToolResults(snapshot, cache) {
|
|
969
|
+
export function compressToolResults(snapshot, cache, depthPolicy) {
|
|
970
|
+
const policy = depthPolicy ?? depthToPolicy(1);
|
|
324
971
|
const lines = snapshot.trimEnd().split("\n");
|
|
325
972
|
// Quick check: if there are no flow cache entries and no compressible tool calls,
|
|
326
973
|
// nothing to compress — return early.
|
|
@@ -331,7 +978,7 @@ export function compressToolResults(snapshot, cache) {
|
|
|
331
978
|
return entry?.type === "message" && entry.message?.role === "assistant" &&
|
|
332
979
|
Array.isArray(entry.message.content) &&
|
|
333
980
|
entry.message.content.some((p) => p.type === "toolCall" &&
|
|
334
|
-
["batch_read", "batch", "web", "ask_user"].includes(p.name));
|
|
981
|
+
["batch_read", "batch", "web", "ask_user", "batch_bash_poll"].includes(p.name));
|
|
335
982
|
}
|
|
336
983
|
catch {
|
|
337
984
|
return false;
|
|
@@ -371,11 +1018,31 @@ export function compressToolResults(snapshot, cache) {
|
|
|
371
1018
|
continue;
|
|
372
1019
|
for (const part of content) {
|
|
373
1020
|
if (part.type === "toolCall" && (part.id || part.toolCallId) && part.arguments) {
|
|
374
|
-
toolCallIdToArgs.set(part.id ?? part.toolCallId, part.arguments);
|
|
1021
|
+
toolCallIdToArgs.set((part.id ?? part.toolCallId), part.arguments);
|
|
375
1022
|
}
|
|
376
1023
|
}
|
|
377
1024
|
}
|
|
1025
|
+
// Extract session cwd from header for path normalization
|
|
1026
|
+
let sessionCwd = process.cwd();
|
|
1027
|
+
for (const line of lines) {
|
|
1028
|
+
let entry;
|
|
1029
|
+
try {
|
|
1030
|
+
entry = JSON.parse(line);
|
|
1031
|
+
}
|
|
1032
|
+
catch {
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
if (entry?.type === "session" || entry?.type === "header") {
|
|
1036
|
+
if (typeof entry.cwd === "string") {
|
|
1037
|
+
sessionCwd = entry.cwd;
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// === PASS 1 (pre-scan): Build DedupIndex for batch and web tool results (W1 + E1 + Q1) ===
|
|
1043
|
+
const dedupIndex = buildDedupIndex(lines, toolCallIdToName, toolCallIdToArgs, sessionCwd);
|
|
378
1044
|
const result = [];
|
|
1045
|
+
let webSummaryEmitted = false;
|
|
379
1046
|
// Second pass: compress matching tool results
|
|
380
1047
|
for (const line of lines) {
|
|
381
1048
|
let entry;
|
|
@@ -430,51 +1097,115 @@ export function compressToolResults(snapshot, cache) {
|
|
|
430
1097
|
// Cache miss (never populated or evicted) — do NOT pass megabytes of raw
|
|
431
1098
|
// flow output verbatim into child context. Render a minimal placeholder.
|
|
432
1099
|
originalText = extractToolResultText(entry) ?? "";
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
1100
|
+
const flowArgs = toolCallIdToArgs.get(toolCallId);
|
|
1101
|
+
let flowTypeSuffix = '';
|
|
1102
|
+
if (flowArgs && typeof flowArgs === 'object') {
|
|
1103
|
+
const flowArr = Array.isArray(flowArgs.flow)
|
|
1104
|
+
? flowArgs.flow
|
|
1105
|
+
: undefined;
|
|
1106
|
+
if (flowArr && flowArr.length > 0 && typeof flowArr[0].type === 'string') {
|
|
1107
|
+
flowTypeSuffix = `:${flowArr[0].type}`;
|
|
1108
|
+
}
|
|
1109
|
+
else if (typeof flowArgs.type === 'string') {
|
|
1110
|
+
flowTypeSuffix = `:${flowArgs.type}`;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
const statusLabel = entry.message.isError ? 'failed' : 'completed';
|
|
1114
|
+
rendered = `[flow${flowTypeSuffix}] ${statusLabel} · see prior session`;
|
|
439
1115
|
}
|
|
440
1116
|
else {
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
1117
|
+
const renderedParts = [];
|
|
1118
|
+
for (const r of compressed) {
|
|
1119
|
+
const renderedResult = renderCompressedFlowResult(r);
|
|
1120
|
+
if (renderedResult === undefined) {
|
|
1121
|
+
// Granular fallback: only this element is malformed, don't waste
|
|
1122
|
+
// valid siblings by falling back the entire array to raw text.
|
|
1123
|
+
const flowType = r.type ?? "unknown";
|
|
1124
|
+
const status = r.status ?? "unknown";
|
|
1125
|
+
renderedParts.push(`[flow:${flowType}] ${status} (cache miss)`);
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
renderedParts.push(renderedResult);
|
|
1129
|
+
}
|
|
453
1130
|
}
|
|
1131
|
+
rendered = renderedParts.join("\n\n");
|
|
454
1132
|
}
|
|
455
1133
|
}
|
|
456
|
-
//
|
|
457
|
-
// before compressToolResults runs, so this branch is no longer needed.
|
|
458
|
-
// Kept as a no-op safety net for any edge cases.
|
|
459
|
-
else if (toolName === "batch_read") {
|
|
460
|
-
rendered = undefined; // handled upstream
|
|
461
|
-
}
|
|
462
|
-
// --- Compress batch tool results (selective: keep bash, truncate reads) ---
|
|
1134
|
+
// --- Compress batch tool results (selective: compress bash, truncate reads, dedup writes/edits/deletes) ---
|
|
463
1135
|
else if (toolName === "batch") {
|
|
464
1136
|
originalText = extractToolResultText(entry) ?? "";
|
|
465
|
-
rendered = compressBatchResult(originalText
|
|
1137
|
+
rendered = compressBatchResult(originalText, {
|
|
1138
|
+
depthPolicy: policy,
|
|
1139
|
+
toolCallId,
|
|
1140
|
+
latestWrite: dedupIndex.latestWrite,
|
|
1141
|
+
latestEdit: dedupIndex.latestEdit,
|
|
1142
|
+
latestDelete: dedupIndex.latestDelete,
|
|
1143
|
+
bashIdToCommand: dedupIndex.bashIdToCommand,
|
|
1144
|
+
latestBash: dedupIndex.latestBash,
|
|
1145
|
+
cwd: sessionCwd,
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
// --- Compress batch_bash_poll tool results (S4 + B1) ---
|
|
1149
|
+
else if (toolName === "batch_bash_poll") {
|
|
1150
|
+
originalText = extractToolResultText(entry) ?? "";
|
|
1151
|
+
rendered = compressBatchBashPollResult(originalText, policy, {
|
|
1152
|
+
bashIdToCommand: dedupIndex.bashIdToCommand,
|
|
1153
|
+
latestBash: dedupIndex.latestBash,
|
|
1154
|
+
});
|
|
466
1155
|
}
|
|
467
|
-
// --- Compress web tool results ---
|
|
1156
|
+
// --- Compress web tool results (Q1 dedup) ---
|
|
468
1157
|
else if (toolName === "web") {
|
|
469
1158
|
originalText = extractToolResultText(entry) ?? "";
|
|
470
1159
|
const args = toolCallIdToArgs.get(toolCallId);
|
|
471
|
-
|
|
1160
|
+
const { isSuperseded, marker } = checkWebDedup(args, toolCallId, dedupIndex);
|
|
1161
|
+
if (isSuperseded) {
|
|
1162
|
+
if (policy.showSupersededBreadcrumbs) {
|
|
1163
|
+
rendered = marker;
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
// At depth 2+, drop superseded web results entirely
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
else {
|
|
1171
|
+
rendered = compressWebResult(originalText, args);
|
|
1172
|
+
if (!policy.showSupersededBreadcrumbs && !webSummaryEmitted) {
|
|
1173
|
+
const searchCount = dedupIndex.latestWebSearch.size;
|
|
1174
|
+
const fetchCount = dedupIndex.latestWebFetch.size;
|
|
1175
|
+
const total = searchCount + fetchCount;
|
|
1176
|
+
if (total > 0) {
|
|
1177
|
+
const fetchLabel = fetchCount === 1 ? "fetch" : "fetches";
|
|
1178
|
+
rendered = `[web] ${total} unique queries (${searchCount} searches, ${fetchCount} ${fetchLabel}) · latest per query below\n${rendered}`;
|
|
1179
|
+
webSummaryEmitted = true;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
472
1183
|
}
|
|
473
|
-
// --- Compress ask_user tool results ---
|
|
1184
|
+
// --- Compress ask_user tool results (A1 dedup) ---
|
|
474
1185
|
else if (toolName === "ask_user") {
|
|
475
1186
|
originalText = extractToolResultText(entry) ?? "";
|
|
476
1187
|
const args = toolCallIdToArgs.get(toolCallId);
|
|
477
|
-
|
|
1188
|
+
let question = "";
|
|
1189
|
+
if (args && typeof args === "object") {
|
|
1190
|
+
const q = args.question;
|
|
1191
|
+
if (typeof q === "string") {
|
|
1192
|
+
question = q;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
const normQuestion = question.trim().toLowerCase().slice(0, 120);
|
|
1196
|
+
const latestTc = normQuestion ? dedupIndex.latestAskUser.get(normQuestion) : undefined;
|
|
1197
|
+
if (latestTc && latestTc !== toolCallId) {
|
|
1198
|
+
if (policy.showSupersededBreadcrumbs) {
|
|
1199
|
+
rendered = `[ask_user] "${question}" (superseded by later ask_user)`;
|
|
1200
|
+
}
|
|
1201
|
+
else {
|
|
1202
|
+
// At depth 2+, drop superseded ask_user results entirely
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
else {
|
|
1207
|
+
rendered = compressAskUserResult(originalText, args);
|
|
1208
|
+
}
|
|
478
1209
|
}
|
|
479
1210
|
if (rendered !== undefined) {
|
|
480
1211
|
logCompress(toolName ?? "unknown", originalText.length || line.length, rendered.length);
|
|
@@ -523,13 +1254,6 @@ function extractToolResultText(entry) {
|
|
|
523
1254
|
}
|
|
524
1255
|
return undefined;
|
|
525
1256
|
}
|
|
526
|
-
/**
|
|
527
|
-
* Backward-compatible alias for compressToolResults.
|
|
528
|
-
* @deprecated Use compressToolResults instead.
|
|
529
|
-
*/
|
|
530
|
-
export function compressFlowToolResults(snapshot, cache) {
|
|
531
|
-
return compressToolResults(snapshot, cache);
|
|
532
|
-
}
|
|
533
1257
|
// ---------------------------------------------------------------------------
|
|
534
1258
|
// batch_read tool call stripping
|
|
535
1259
|
// ---------------------------------------------------------------------------
|
|
@@ -543,6 +1267,44 @@ export function compressFlowToolResults(snapshot, cache) {
|
|
|
543
1267
|
* results causes strict API providers (e.g. kimi-coding, DeepSeek) to reject
|
|
544
1268
|
* the request with `tool_call_id is not found`.
|
|
545
1269
|
*/
|
|
1270
|
+
/**
|
|
1271
|
+
* Check if an assistant message is empty (continuation marker with no semantic value).
|
|
1272
|
+
* Empty means: no substantive text, no tool calls.
|
|
1273
|
+
*/
|
|
1274
|
+
function isEmptyAssistantMessage(message) {
|
|
1275
|
+
if (message.role !== "assistant")
|
|
1276
|
+
return false;
|
|
1277
|
+
const content = message.content;
|
|
1278
|
+
// Null/undefined/empty string
|
|
1279
|
+
if (content === null || content === undefined || content === "")
|
|
1280
|
+
return true;
|
|
1281
|
+
// Whitespace-only string
|
|
1282
|
+
if (typeof content === "string" && content.trim() === "")
|
|
1283
|
+
return true;
|
|
1284
|
+
// Array content: check for no text parts or only whitespace text parts, and NO tool calls
|
|
1285
|
+
if (Array.isArray(content)) {
|
|
1286
|
+
const hasToolCall = content.some((p) => p.type === "toolCall");
|
|
1287
|
+
if (hasToolCall)
|
|
1288
|
+
return false;
|
|
1289
|
+
const textParts = content.filter((p) => p.type === "text" && typeof p.text === "string");
|
|
1290
|
+
if (textParts.length === 0)
|
|
1291
|
+
return true;
|
|
1292
|
+
const allWhitespace = textParts.every((p) => p.text.trim() === "");
|
|
1293
|
+
if (allWhitespace)
|
|
1294
|
+
return true;
|
|
1295
|
+
// Low-signal detection: short text with no actionable markers
|
|
1296
|
+
const fullText = textParts.map((p) => p.text).join("");
|
|
1297
|
+
if (fullText.length < 300) {
|
|
1298
|
+
const hasFilePath = /\w+\.\w+/.test(fullText);
|
|
1299
|
+
const hasToolReference = /\[[a-z_]+[^\]]*\]/.test(fullText);
|
|
1300
|
+
const hasCodeBlock = fullText.includes("```");
|
|
1301
|
+
const hasActionableMarkers = hasFilePath || hasToolReference || hasCodeBlock;
|
|
1302
|
+
if (!hasActionableMarkers)
|
|
1303
|
+
return true;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
546
1308
|
export function stripBatchReadToolCalls(snapshot) {
|
|
547
1309
|
const lines = snapshot.trimEnd().split("\n");
|
|
548
1310
|
// Pass 1: Collect all batch_read toolCallIds from assistant messages.
|
|
@@ -562,7 +1324,7 @@ export function stripBatchReadToolCalls(snapshot) {
|
|
|
562
1324
|
continue;
|
|
563
1325
|
for (const part of content) {
|
|
564
1326
|
if (part.type === "toolCall" && part.name === "batch_read" && (part.id || part.toolCallId)) {
|
|
565
|
-
batchReadToolCallIds.add(part.id ?? part.toolCallId);
|
|
1327
|
+
batchReadToolCallIds.add((part.id ?? part.toolCallId));
|
|
566
1328
|
}
|
|
567
1329
|
}
|
|
568
1330
|
}
|
|
@@ -607,7 +1369,9 @@ export function stripBatchReadToolCalls(snapshot) {
|
|
|
607
1369
|
}
|
|
608
1370
|
const filteredContent = content.filter((part) => !(part.type === "toolCall" && part.name === "batch_read"));
|
|
609
1371
|
if (filteredContent.length === 0) {
|
|
610
|
-
|
|
1372
|
+
// Skip assistant messages that have no content after stripping batch_read
|
|
1373
|
+
// — an empty text placeholder wastes tokens and conveys nothing.
|
|
1374
|
+
continue;
|
|
611
1375
|
}
|
|
612
1376
|
result.push(JSON.stringify({
|
|
613
1377
|
...entry,
|
|
@@ -620,6 +1384,75 @@ export function stripBatchReadToolCalls(snapshot) {
|
|
|
620
1384
|
return `${result.join("\n")}\n`;
|
|
621
1385
|
}
|
|
622
1386
|
// ---------------------------------------------------------------------------
|
|
1387
|
+
// Flow tool call argument compression
|
|
1388
|
+
// ---------------------------------------------------------------------------
|
|
1389
|
+
/**
|
|
1390
|
+
* Compress verbose `flow` tool call arguments in assistant messages.
|
|
1391
|
+
*
|
|
1392
|
+
* The child flow already receives its own `-p` activation prompt, so the full
|
|
1393
|
+
* mission text inside the JSONL assistant message is pure duplication.
|
|
1394
|
+
* Replaces the arguments with a compact summary `{type, aim, steps}`.
|
|
1395
|
+
*/
|
|
1396
|
+
export function compressFlowToolCallArgs(snapshot) {
|
|
1397
|
+
const lines = snapshot.trimEnd().split("\n");
|
|
1398
|
+
const result = [];
|
|
1399
|
+
for (const line of lines) {
|
|
1400
|
+
let entry;
|
|
1401
|
+
try {
|
|
1402
|
+
entry = JSON.parse(line);
|
|
1403
|
+
}
|
|
1404
|
+
catch {
|
|
1405
|
+
result.push(line);
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
if (entry?.type !== "message" || entry.message?.role !== "assistant") {
|
|
1409
|
+
result.push(line);
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
const content = entry.message.content;
|
|
1413
|
+
if (!Array.isArray(content)) {
|
|
1414
|
+
result.push(line);
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
let modified = false;
|
|
1418
|
+
const newContent = content.map((part) => {
|
|
1419
|
+
if (part.type !== "toolCall" || part.name !== "flow")
|
|
1420
|
+
return part;
|
|
1421
|
+
const args = part.arguments;
|
|
1422
|
+
if (!args || typeof args !== "object")
|
|
1423
|
+
return part;
|
|
1424
|
+
const flowArr = Array.isArray(args.flow)
|
|
1425
|
+
? args.flow
|
|
1426
|
+
: undefined;
|
|
1427
|
+
if (!flowArr || flowArr.length === 0)
|
|
1428
|
+
return part;
|
|
1429
|
+
const firstFlow = flowArr[0];
|
|
1430
|
+
const type = typeof firstFlow?.type === "string" ? firstFlow.type : undefined;
|
|
1431
|
+
const aim = typeof firstFlow?.aim === "string" ? firstFlow.aim : undefined;
|
|
1432
|
+
const steps = Array.isArray(firstFlow?.steps) ? firstFlow.steps.length : undefined;
|
|
1433
|
+
if (!type && !aim && steps === undefined)
|
|
1434
|
+
return part;
|
|
1435
|
+
modified = true;
|
|
1436
|
+
return {
|
|
1437
|
+
...part,
|
|
1438
|
+
arguments: { type, aim, steps },
|
|
1439
|
+
};
|
|
1440
|
+
});
|
|
1441
|
+
if (!modified) {
|
|
1442
|
+
result.push(line);
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
result.push(JSON.stringify({
|
|
1446
|
+
...entry,
|
|
1447
|
+
message: {
|
|
1448
|
+
...entry.message,
|
|
1449
|
+
content: newContent,
|
|
1450
|
+
},
|
|
1451
|
+
}));
|
|
1452
|
+
}
|
|
1453
|
+
return `${result.join("\n")}\n`;
|
|
1454
|
+
}
|
|
1455
|
+
// ---------------------------------------------------------------------------
|
|
623
1456
|
// Reparent orphans
|
|
624
1457
|
// ---------------------------------------------------------------------------
|
|
625
1458
|
/**
|
|
@@ -635,42 +1468,58 @@ function reparentOrphans(snapshot) {
|
|
|
635
1468
|
const id = entry?.message?.id ?? entry?.message?.messageId ?? entry?.id;
|
|
636
1469
|
if (typeof id === "string" && id)
|
|
637
1470
|
survivingIds.add(id);
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1471
|
+
// Only ids of actual entries should be in survivingIds; parentId refs
|
|
1472
|
+
// are checked in the second pass, not added here.
|
|
1473
|
+
}
|
|
1474
|
+
catch (err) {
|
|
1475
|
+
logWarn(`[pi-agent-flow] reparentOrphans id scan failed: ${err}`);
|
|
641
1476
|
}
|
|
642
|
-
catch { /* ignore */ }
|
|
643
1477
|
}
|
|
644
1478
|
for (let i = 0; i < lines.length; i++) {
|
|
645
1479
|
try {
|
|
646
1480
|
let entry = JSON.parse(lines[i]);
|
|
647
|
-
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
if (
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const { parentId: _pid, parentMessageId: _pmid, ...restEntry } = entry;
|
|
1481
|
+
let modified = false;
|
|
1482
|
+
const isMessageEntry = entry?.type === "message";
|
|
1483
|
+
// Fix top-level parentId only for message entries (not session headers).
|
|
1484
|
+
if (isMessageEntry) {
|
|
1485
|
+
if (typeof entry.parentId === "string" && entry.parentId && !survivingIds.has(entry.parentId)) {
|
|
1486
|
+
const { parentId: _pid, ...restEntry } = entry;
|
|
654
1487
|
entry = restEntry;
|
|
655
1488
|
modified = true;
|
|
656
1489
|
}
|
|
657
|
-
if (
|
|
658
|
-
const {
|
|
1490
|
+
if (typeof entry.parentMessageId === "string" && entry.parentMessageId && !survivingIds.has(entry.parentMessageId)) {
|
|
1491
|
+
const { parentMessageId: _pmid, ...restEntry } = entry;
|
|
1492
|
+
entry = restEntry;
|
|
1493
|
+
modified = true;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
// Fix message-level parentId for all entries.
|
|
1497
|
+
const msg = entry.message;
|
|
1498
|
+
if (msg) {
|
|
1499
|
+
if (typeof msg.parentId === "string" && msg.parentId && !survivingIds.has(msg.parentId)) {
|
|
1500
|
+
const { parentId: _pid, ...restMessage } = msg;
|
|
659
1501
|
entry = { ...entry, message: restMessage };
|
|
660
1502
|
modified = true;
|
|
661
1503
|
}
|
|
662
|
-
if (
|
|
663
|
-
|
|
1504
|
+
if (typeof msg.parentMessageId === "string" && msg.parentMessageId && !survivingIds.has(msg.parentMessageId)) {
|
|
1505
|
+
const { parentMessageId: _pmid, ...restMessage } = msg;
|
|
1506
|
+
entry = { ...entry, message: restMessage };
|
|
1507
|
+
modified = true;
|
|
664
1508
|
}
|
|
665
1509
|
}
|
|
1510
|
+
if (modified) {
|
|
1511
|
+
lines[i] = JSON.stringify(entry);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
catch (err) {
|
|
1515
|
+
logWarn(`[pi-agent-flow] reparentOrphans breadcrumb fix failed: ${err}`);
|
|
666
1516
|
}
|
|
667
|
-
catch { /* ignore */ }
|
|
668
1517
|
}
|
|
669
1518
|
return `${lines.join("\n")}\n`;
|
|
670
1519
|
}
|
|
671
1520
|
export function sanitizeForkSnapshot(snapshot, cache = new Map(), options) {
|
|
672
1521
|
if (!snapshot)
|
|
673
|
-
return { result: snapshot, passesApplied: [] };
|
|
1522
|
+
return { result: snapshot, passesApplied: [], stats: null };
|
|
674
1523
|
const preBytes = snapshot.length;
|
|
675
1524
|
const lines = snapshot.trimEnd().split("\n");
|
|
676
1525
|
const sanitizedLines = [];
|
|
@@ -681,29 +1530,26 @@ export function sanitizeForkSnapshot(snapshot, cache = new Map(), options) {
|
|
|
681
1530
|
try {
|
|
682
1531
|
entry = JSON.parse(line);
|
|
683
1532
|
}
|
|
684
|
-
catch {
|
|
1533
|
+
catch (err) {
|
|
1534
|
+
logWarn(`[pi-agent-flow] sanitizeForkSnapshot parse failed: ${err}`);
|
|
685
1535
|
sanitizedLines.push(line);
|
|
686
1536
|
continue;
|
|
687
1537
|
}
|
|
688
1538
|
let changed = false;
|
|
689
|
-
//
|
|
1539
|
+
// Strip outer entry timestamp from all entries — child replay doesn't need it
|
|
1540
|
+
// (JSONL line ordering is sufficient).
|
|
1541
|
+
if ("timestamp" in entry) {
|
|
1542
|
+
const { timestamp, ...restEntry } = entry;
|
|
1543
|
+
entry = restEntry;
|
|
1544
|
+
changed = true;
|
|
1545
|
+
subPasses.add("stripTimestamps");
|
|
1546
|
+
}
|
|
1547
|
+
// Header (first line): replace parent system prompt.
|
|
690
1548
|
if (i === 0 && entry && typeof entry === "object") {
|
|
691
|
-
//
|
|
692
|
-
if (options && (options.forkedFrom || options.forkedAt || options.parentFlow || options.depth !== undefined)) {
|
|
693
|
-
entry = {
|
|
694
|
-
...entry,
|
|
695
|
-
...(options.forkedFrom !== undefined ? { forkedFrom: options.forkedFrom } : {}),
|
|
696
|
-
...(options.forkedAt !== undefined ? { forkedAt: options.forkedAt } : {}),
|
|
697
|
-
...(options.parentFlow !== undefined ? { parentFlow: options.parentFlow } : {}),
|
|
698
|
-
...(options.depth !== undefined ? { depth: options.depth } : {}),
|
|
699
|
-
};
|
|
700
|
-
changed = true;
|
|
701
|
-
subPasses.add("forkMetadataInjection");
|
|
702
|
-
}
|
|
703
|
-
// Replace the parent orchestrator system prompt with a brief note.
|
|
1549
|
+
// Replace the parent root state system prompt with a brief note.
|
|
704
1550
|
// Children receive their own directive in the <activation> block.
|
|
705
1551
|
if (entry.systemPrompt && typeof entry.systemPrompt === "string") {
|
|
706
|
-
entry = { ...entry, systemPrompt: "[parent
|
|
1552
|
+
entry = { ...entry, systemPrompt: "[parent root state system prompt stripped — child receives its own directive]" };
|
|
707
1553
|
changed = true;
|
|
708
1554
|
subPasses.add("stripSystemPrompt");
|
|
709
1555
|
}
|
|
@@ -717,14 +1563,36 @@ export function sanitizeForkSnapshot(snapshot, cache = new Map(), options) {
|
|
|
717
1563
|
subPasses.add('stripSessionId');
|
|
718
1564
|
}
|
|
719
1565
|
}
|
|
720
|
-
//
|
|
1566
|
+
// Whitelist session entry fields to prevent unknown metadata leaks.
|
|
1567
|
+
const isSessionHeader = i === 0 || entry?.type === "session";
|
|
1568
|
+
if (isSessionHeader && entry && typeof entry === "object") {
|
|
1569
|
+
const allowedHeaderKeys = new Set([
|
|
1570
|
+
"type", "systemPrompt", "version", "cwd",
|
|
1571
|
+
"forkedFrom", "forkedAt", "parentFlow", "depth", "parentId",
|
|
1572
|
+
"meta",
|
|
1573
|
+
]);
|
|
1574
|
+
const entryKeys = Object.keys(entry);
|
|
1575
|
+
const hasUnknownHeaderField = entryKeys.some((k) => !allowedHeaderKeys.has(k));
|
|
1576
|
+
if (hasUnknownHeaderField) {
|
|
1577
|
+
const whitelisted = {};
|
|
1578
|
+
for (const key of entryKeys) {
|
|
1579
|
+
if (allowedHeaderKeys.has(key)) {
|
|
1580
|
+
whitelisted[key] = entry[key];
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
entry = whitelisted;
|
|
1584
|
+
changed = true;
|
|
1585
|
+
subPasses.add("stripUnknownHeaderFields");
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
// Drop type: "system" entries — the parent root state system prompt was already
|
|
721
1589
|
// stripped from the header above. Standalone system events leak the full prompt.
|
|
722
1590
|
// Children receive their own directive in the <activation> block.
|
|
723
1591
|
if (entry?.type === "system") {
|
|
724
1592
|
subPasses.add("dropSystemEvents");
|
|
725
1593
|
continue;
|
|
726
1594
|
}
|
|
727
|
-
// Drop custom_message entries — hidden
|
|
1595
|
+
// Drop custom_message entries — hidden root state instructions (e.g.
|
|
728
1596
|
// flow continuation hook messages with display:false) that children
|
|
729
1597
|
// should never see.
|
|
730
1598
|
if (entry?.type === "custom_message") {
|
|
@@ -788,10 +1656,11 @@ export function sanitizeForkSnapshot(snapshot, cache = new Map(), options) {
|
|
|
788
1656
|
subPasses.add("stripTimestamps");
|
|
789
1657
|
}
|
|
790
1658
|
// Strip API metadata fields that children don't need (~5-7 KB per assistant message).
|
|
791
|
-
// IMPORTANT: keep `usage`
|
|
1659
|
+
// IMPORTANT: keep `usage.totalTokens` ONLY. The child `pi` process replays
|
|
792
1660
|
// this JSONL and core/session code reads `message.usage.totalTokens`; stripping
|
|
793
1661
|
// `usage` causes: Cannot read properties of undefined (reading 'totalTokens').
|
|
794
|
-
//
|
|
1662
|
+
// Other fields (input, output, cacheRead, cacheWrite) are consumed only from
|
|
1663
|
+
// live child stdout events (runner-events.ts), never from fork snapshot replay.
|
|
795
1664
|
if (message.role === "assistant") {
|
|
796
1665
|
const { api, provider, model, stopReason, responseId, responseModel, usage, ...rest } = message;
|
|
797
1666
|
let stripped = false;
|
|
@@ -799,12 +1668,16 @@ export function sanitizeForkSnapshot(snapshot, cache = new Map(), options) {
|
|
|
799
1668
|
stopReason !== undefined || responseId !== undefined || responseModel !== undefined) {
|
|
800
1669
|
stripped = true;
|
|
801
1670
|
}
|
|
802
|
-
//
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1671
|
+
// Compress usage to totalTokens only — child pi replay requires totalTokens.
|
|
1672
|
+
// Other fields (input, output, cacheRead, cacheWrite) are consumed only from
|
|
1673
|
+
// live child stdout events (runner-events.ts), never from fork snapshot replay.
|
|
1674
|
+
let cleanedUsage;
|
|
1675
|
+
if (usage && typeof usage === "object") {
|
|
1676
|
+
const ttl = usage.totalTokens;
|
|
1677
|
+
if (typeof ttl === "number") {
|
|
1678
|
+
cleanedUsage = { totalTokens: ttl };
|
|
1679
|
+
stripped = true;
|
|
1680
|
+
}
|
|
808
1681
|
}
|
|
809
1682
|
if (stripped) {
|
|
810
1683
|
message = { ...rest, ...(cleanedUsage !== undefined ? { usage: cleanedUsage } : {}) };
|
|
@@ -812,6 +1685,20 @@ export function sanitizeForkSnapshot(snapshot, cache = new Map(), options) {
|
|
|
812
1685
|
subPasses.add("stripApiMetadata");
|
|
813
1686
|
}
|
|
814
1687
|
}
|
|
1688
|
+
// Collapse empty/low-signal assistant messages to a minimal continuation marker.
|
|
1689
|
+
if (message.role === "assistant" && isEmptyAssistantMessage(message)) {
|
|
1690
|
+
const totalTokens = message.usage?.totalTokens;
|
|
1691
|
+
const { usage: _usage, ...rest } = message;
|
|
1692
|
+
message = {
|
|
1693
|
+
...rest,
|
|
1694
|
+
...(totalTokens !== undefined ? { usage: { totalTokens } } : {}),
|
|
1695
|
+
content: totalTokens !== undefined
|
|
1696
|
+
? `[assistant: ${totalTokens} tokens, no action]`
|
|
1697
|
+
: "[assistant:continuation]",
|
|
1698
|
+
};
|
|
1699
|
+
changed = true;
|
|
1700
|
+
subPasses.add("collapseEmptyAssistantMessages");
|
|
1701
|
+
}
|
|
815
1702
|
// Strip `details` from tool/toolResult messages — carries FlowDetails UI metadata
|
|
816
1703
|
// (mode, flowStyle, projectAgentsDir, results) that children never need.
|
|
817
1704
|
if (message.role === "tool" || message.role === "toolResult") {
|
|
@@ -831,14 +1718,12 @@ export function sanitizeForkSnapshot(snapshot, cache = new Map(), options) {
|
|
|
831
1718
|
changed = true;
|
|
832
1719
|
subPasses.add("stripSteeringHints");
|
|
833
1720
|
}
|
|
834
|
-
// Strip strategic hints from
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
subPasses.add("stripStrategicHints");
|
|
841
|
-
}
|
|
1721
|
+
// Strip strategic hints from all messages
|
|
1722
|
+
const afterHints = stripStrategicHintsFromContent(modifiedContent);
|
|
1723
|
+
if (!isJsonEqual(afterHints, modifiedContent)) {
|
|
1724
|
+
modifiedContent = afterHints;
|
|
1725
|
+
changed = true;
|
|
1726
|
+
subPasses.add("stripStrategicHints");
|
|
842
1727
|
}
|
|
843
1728
|
// Compress parent activation prompts in nested snapshot JSONL
|
|
844
1729
|
// (detect user messages containing <context-seal> at depth >= 2).
|
|
@@ -892,37 +1777,48 @@ export function sanitizeForkSnapshot(snapshot, cache = new Map(), options) {
|
|
|
892
1777
|
sanitizedLines.push(outLine);
|
|
893
1778
|
}
|
|
894
1779
|
const passesApplied = [];
|
|
1780
|
+
const passDeltas = {};
|
|
1781
|
+
const measureBytes = (s) => new TextEncoder().encode(s).length;
|
|
895
1782
|
let sanitized = `${sanitizedLines.join("\n")}\n`;
|
|
896
1783
|
passesApplied.push(...subPasses);
|
|
1784
|
+
passDeltas["mainLoop"] = measureBytes(sanitized);
|
|
897
1785
|
// Reparent orphaned parentIds after steering-hint messages were dropped.
|
|
898
1786
|
sanitized = reparentOrphans(sanitized);
|
|
899
1787
|
passesApplied.push("reparentOrphans");
|
|
1788
|
+
passDeltas["reparentOrphans1"] = measureBytes(sanitized);
|
|
900
1789
|
// Strip batch_read tool calls from assistant messages.
|
|
901
1790
|
// Children don't have batch_read in their active tools.
|
|
902
1791
|
sanitized = stripBatchReadToolCalls(sanitized);
|
|
903
1792
|
passesApplied.push("stripBatchRead");
|
|
1793
|
+
passDeltas["stripBatchRead"] = measureBytes(sanitized);
|
|
1794
|
+
// Compress verbose flow tool call arguments in assistant messages.
|
|
1795
|
+
sanitized = compressFlowToolCallArgs(sanitized);
|
|
1796
|
+
passesApplied.push("compressFlowToolCallArgs");
|
|
1797
|
+
passDeltas["compressFlowToolCallArgs"] = measureBytes(sanitized);
|
|
904
1798
|
// Compress tool results (flow, batch, web, ask_user).
|
|
905
|
-
sanitized = compressToolResults(sanitized, cache);
|
|
1799
|
+
sanitized = compressToolResults(sanitized, cache, depthToPolicy(options?.depth ?? 1));
|
|
906
1800
|
passesApplied.push("compressToolResults");
|
|
1801
|
+
passDeltas["compressToolResults"] = measureBytes(sanitized);
|
|
907
1802
|
// Reparent again after stripBatchRead and compressToolResults may have
|
|
908
1803
|
// dropped additional messages, leaving new orphaned parentIds.
|
|
909
1804
|
sanitized = reparentOrphans(sanitized);
|
|
910
1805
|
passesApplied.push("reparentOrphans");
|
|
1806
|
+
passDeltas["reparentOrphans2"] = measureBytes(sanitized);
|
|
911
1807
|
// Telemetry: measure total delta across sanitization, stripping, and compression.
|
|
912
1808
|
const postBytes = sanitized.length;
|
|
913
|
-
const reduction = preBytes > 0 ? ((1 - postBytes / preBytes) *
|
|
1809
|
+
const reduction = preBytes > 0 ? Math.round((1 - postBytes / preBytes) * 1000) / 10 : 0;
|
|
914
1810
|
if (DEBUG_CONTEXT) {
|
|
915
1811
|
logError(`[context-snapshot] pre: ${preBytes} → post: ${postBytes} bytes (${reduction}% reduction)`);
|
|
916
1812
|
}
|
|
917
|
-
//
|
|
918
|
-
//
|
|
919
|
-
|
|
920
|
-
type: "compression-stats",
|
|
1813
|
+
// Stats are returned out-of-band for dump consumers only.
|
|
1814
|
+
// Do NOT append to child-visible JSONL — it's telemetry noise for the model.
|
|
1815
|
+
const stats = {
|
|
921
1816
|
preBytes,
|
|
922
1817
|
postBytes,
|
|
923
|
-
reductionPercent:
|
|
1818
|
+
reductionPercent: reduction,
|
|
924
1819
|
passesApplied,
|
|
925
|
-
|
|
926
|
-
|
|
1820
|
+
passDeltas,
|
|
1821
|
+
};
|
|
1822
|
+
return { result: sanitized, passesApplied, stats };
|
|
927
1823
|
}
|
|
928
1824
|
//# sourceMappingURL=snapshot.js.map
|