token-goat 2.8.2 → 2.8.4

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.
@@ -23,7 +23,7 @@ import {
23
23
  summarizeOutputDelta,
24
24
  summarizeResidentContext,
25
25
  taskListPruneHint
26
- } from "./token-goat-chunk-5MLXFSRI.mjs";
26
+ } from "./token-goat-chunk-IZRXU64B.mjs";
27
27
  import {
28
28
  BODY_FIRST_TOOL_RESPONSE_KEYS,
29
29
  OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
@@ -41,10 +41,10 @@ import {
41
41
  contextOutput,
42
42
  countSymbols,
43
43
  denyOutput,
44
+ emitRewrite,
44
45
  enqueueDirtyPathSafe,
45
46
  ensureWorkerAlive,
46
47
  estimateTokens,
47
- estimateTokensFromLength,
48
48
  extractCompactFromMarker,
49
49
  extractToolResponseField,
50
50
  extractToolResultText,
@@ -88,6 +88,7 @@ import {
88
88
  matchesDenyPattern,
89
89
  meetsSavingsFloor,
90
90
  passOutput,
91
+ probeImageMeta,
91
92
  recordBashOutput,
92
93
  recordBashRerun,
93
94
  recordCliRead,
@@ -109,16 +110,19 @@ import {
109
110
  saveSessionState,
110
111
  scanForInjectionPatterns,
111
112
  serializeOutput,
113
+ sessionOutputBodyBytes,
112
114
  sessionSidecarPath,
113
115
  setLastTabContext,
114
116
  shrinkImage,
115
117
  storeBlob,
116
118
  storeOutput,
117
119
  takePendingLargeFileHint,
120
+ visionTokens,
121
+ visionTokensSaved,
118
122
  wasCliReadThisSession,
119
123
  wasFileReadThisSession,
120
124
  wasHintShown
121
- } from "./token-goat-chunk-AM23GDIS.mjs";
125
+ } from "./token-goat-chunk-4OTIB7SB.mjs";
122
126
  import {
123
127
  canRunWrappedShell,
124
128
  compressOutput,
@@ -129,8 +133,9 @@ import {
129
133
  isRewriteWorthwhile,
130
134
  resolveMinNetSavingsBytes,
131
135
  shlexSplit
132
- } from "./token-goat-chunk-TF5NT3H5.mjs";
136
+ } from "./token-goat-chunk-CZALRRGN.mjs";
133
137
  import {
138
+ PER_FILE_COUNTERFACTUAL_CEILING,
134
139
  VERSION,
135
140
  detectHarness,
136
141
  detectLanguage,
@@ -147,9 +152,10 @@ import {
147
152
  redactSecrets,
148
153
  resolveIndexPath,
149
154
  runGit,
155
+ savedTokensFromBytes,
150
156
  shortFingerprint,
151
157
  toKB
152
- } from "./token-goat-chunk-IVCTQPZD.mjs";
158
+ } from "./token-goat-chunk-E76UNTVK.mjs";
153
159
 
154
160
  // src/hooks_grep.ts
155
161
  function grepIntInput(toolInput, key) {
@@ -164,7 +170,7 @@ function grepIntInput(toolInput, key) {
164
170
  function grepSignature(toolInput) {
165
171
  const pattern = toolInput["pattern"];
166
172
  if (typeof pattern !== "string" || pattern === "") return null;
167
- const path2 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
173
+ const path3 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
168
174
  const outputMode = typeof toolInput["output_mode"] === "string" ? toolInput["output_mode"] : "files_with_matches";
169
175
  const glob = typeof toolInput["glob"] === "string" ? toolInput["glob"] : "";
170
176
  const type = typeof toolInput["type"] === "string" ? toolInput["type"] : "";
@@ -179,7 +185,7 @@ function grepSignature(toolInput) {
179
185
  const offset = grepIntInput(toolInput, "offset");
180
186
  return JSON.stringify([
181
187
  pattern,
182
- path2,
188
+ path3,
183
189
  outputMode,
184
190
  glob,
185
191
  type,
@@ -213,8 +219,9 @@ function foldGrepContentHandler(event) {
213
219
  return passOutput();
214
220
  }
215
221
  if (toolInput["-n"] === false) return passOutput();
216
- const text = extractToolResponseField(event.raw, OUTPUT_FIRST_TOOL_RESPONSE_KEYS);
217
- if (!text) return passOutput();
222
+ const rawText = extractToolResponseField(event.raw, OUTPUT_FIRST_TOOL_RESPONSE_KEYS);
223
+ if (!rawText) return passOutput();
224
+ const text = redactSecrets(rawText).text;
218
225
  const rawLines = text.split(/\r\n|\r|\n/);
219
226
  const parsed = [];
220
227
  for (const line of rawLines) {
@@ -255,9 +262,7 @@ function foldGrepContentHandler(event) {
255
262
  })) {
256
263
  return passOutput();
257
264
  }
258
- const bytesDelta = originalBytes - rewrittenBytes;
259
- recordStat("grep:fold", bytesDelta, Math.round(bytesDelta / 4));
260
- return { hookType: "rewriteOutput", updatedOutput: rewritten };
265
+ return emitRewrite(rewritten, "grep", { kind: "grep:fold", originalBytes });
261
266
  } catch {
262
267
  return passOutput();
263
268
  }
@@ -275,8 +280,8 @@ registerHook("post_tool_use", postGrepHandler, { toolName: "Grep" });
275
280
  function globSignature(toolInput) {
276
281
  const pattern = toolInput["pattern"];
277
282
  if (typeof pattern !== "string" || pattern === "") return null;
278
- const path2 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
279
- return JSON.stringify([pattern, path2]);
283
+ const path3 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
284
+ return JSON.stringify([pattern, path3]);
280
285
  }
281
286
  var { post: postGlobHandler, pre: preGlobDedupHandler } = makeDedupHintHandlers({
282
287
  toolName: "Glob",
@@ -424,6 +429,9 @@ function renderReadRow(entry) {
424
429
  const edited = entry.wasEdited ? ", edited" : "";
425
430
  return `- ${entry.path} (${kb}kb, ${entry.readCount} ${plural}${edited})`;
426
431
  }
432
+ function renderSymbolReadRow(entry) {
433
+ return `- ${entry.path} (symbols: ${(entry.symbols_read ?? []).join(", ")})`;
434
+ }
427
435
  function mergeManifestFiles(parent, siblingFiles) {
428
436
  const byPath = /* @__PURE__ */ new Map();
429
437
  for (const f of parent) byPath.set(foldPath(f.path), f);
@@ -451,6 +459,7 @@ function buildManifest(sessionId, cwd) {
451
459
  const files = siblingFiles.length > 0 ? mergeManifestFiles(ownFiles, siblingFiles) : ownFiles;
452
460
  const editedFiles = files.filter((f) => f.wasEdited);
453
461
  const readFiles = files.filter((f) => f.readCount > 0 && !f.wasEdited);
462
+ const symbolOnlyFiles = files.filter((f) => f.readCount === 0 && !f.wasEdited && (f.symbols_read?.length ?? 0) > 0);
454
463
  const webFetches = [...getSessionWebFetches().entries()];
455
464
  const lines = [];
456
465
  lines.push("## Session context");
@@ -463,6 +472,7 @@ function buildManifest(sessionId, cwd) {
463
472
  editedFiles.map((entry) => `- ${entry.path}`),
464
473
  MAX_ROWS
465
474
  );
475
+ appendCappedSection(lines, "### Surgically read files (symbol/section reads, never read whole)", symbolOnlyFiles.map(renderSymbolReadRow), MAX_ROWS);
466
476
  appendCappedSection(
467
477
  lines,
468
478
  "### Web URLs fetched",
@@ -607,7 +617,7 @@ function postCompactHandler(event) {
607
617
  0,
608
618
  0,
609
619
  void 0,
610
- `trigger=${trigger} bytes=${bytes} est_tokens=${estimateTokensFromLength(summary.length)} manifest_paths=${survived}/${sample.length}`
620
+ `trigger=${trigger} bytes=${bytes} est_tokens=${savedTokensFromBytes(bytes)} manifest_paths=${survived}/${sample.length}`
611
621
  );
612
622
  return passOutput();
613
623
  }
@@ -620,7 +630,7 @@ import crypto from "node:crypto";
620
630
  var TRACKED_SKILL = "token-goat";
621
631
  var MAX_COMMANDS_SHOWN = 8;
622
632
  async function currentCommandNames() {
623
- const { buildProgram } = await import("./token-goat-chunk-SE6V7LUZ.mjs");
633
+ const { buildProgram } = await import("./token-goat-chunk-HNODKNWO.mjs");
624
634
  return flattenCommandNames(buildCommandManifest(buildProgram()));
625
635
  }
626
636
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -1464,11 +1474,11 @@ function mapInner(r, f) {
1464
1474
  function mapOuter(r, f) {
1465
1475
  return r.matched ? f(r) : r;
1466
1476
  }
1467
- function ab(pa, pb, join) {
1468
- return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join(ma.value, vb, data, i, j)));
1477
+ function ab(pa, pb, join2) {
1478
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join2(ma.value, vb, data, i, j)));
1469
1479
  }
1470
- function abc(pa, pb, pc, join) {
1471
- return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join(ma.value, mb.value, vc, data, i, j))));
1480
+ function abc(pa, pb, pc, join2) {
1481
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join2(ma.value, mb.value, vc, data, i, j))));
1472
1482
  }
1473
1483
  function ahead(p) {
1474
1484
  return (data, i) => mapOuter(p(data, i), (m1) => ({
@@ -5385,8 +5395,8 @@ function trimCharacterEnd(str, char) {
5385
5395
  function unicodeEscape(str) {
5386
5396
  return str.replace(/[\s\S]/g, (c) => "\\u" + c.charCodeAt().toString(16).padStart(4, "0"));
5387
5397
  }
5388
- function get(obj, path2) {
5389
- for (const key of path2) {
5398
+ function get(obj, path3) {
5399
+ for (const key of path3) {
5390
5400
  if (!obj) {
5391
5401
  return void 0;
5392
5402
  }
@@ -6587,8 +6597,8 @@ function withBrackets(str, brackets) {
6587
6597
  const rbr = typeof brackets[1] === "string" ? brackets[1] : "]";
6588
6598
  return lbr + str + rbr;
6589
6599
  }
6590
- function pathRewrite(path2, rewriter, baseUrl, metadata, elem) {
6591
- const modifiedPath = typeof rewriter === "function" ? rewriter(path2, metadata, elem) : path2;
6600
+ function pathRewrite(path3, rewriter, baseUrl, metadata, elem) {
6601
+ const modifiedPath = typeof rewriter === "function" ? rewriter(path3, metadata, elem) : path3;
6592
6602
  return modifiedPath[0] === "/" && baseUrl ? trimCharacterEnd(baseUrl, "/") + modifiedPath : modifiedPath;
6593
6603
  }
6594
6604
  function formatImage(elem, walk, builder, formatOptions) {
@@ -6912,9 +6922,9 @@ function handleDeprecatedOptions(options) {
6912
6922
  options.selectors.push(...tagDefinitions);
6913
6923
  options.selectors = mergeDuplicatesPreferLast(options.selectors, ((s) => s.selector));
6914
6924
  }
6915
- function set(obj, path2, value) {
6916
- const valueKey = path2.pop();
6917
- for (const key of path2) {
6925
+ function set(obj, path3, value) {
6926
+ const valueKey = path3.pop();
6927
+ for (const key of path3) {
6918
6928
  let nested = obj[key];
6919
6929
  if (!nested) {
6920
6930
  nested = {};
@@ -7005,7 +7015,7 @@ function preFetchHandler(event) {
7005
7015
  if (cached !== null) {
7006
7016
  const cachedBytes = Buffer.byteLength(cached, "utf-8");
7007
7017
  if (cachedBytes >= loadConfig().hints.web_dedup_min_bytes) {
7008
- recordStat("webfetch:recall", cachedBytes, Math.round(cachedBytes / 4));
7018
+ recordStat("webfetch:recall", cachedBytes, savedTokensFromBytes(cachedBytes));
7009
7019
  return denyOutput(
7010
7020
  "Already fetched this URL with this prompt; the response is cached. Use `token-goat web-output " + cacheId + "` to recall it (append `--grep PATTERN` to filter or `--section Heading` for a markdown section) instead of re-fetching."
7011
7021
  );
@@ -7030,15 +7040,22 @@ function postFetchHandler(event) {
7030
7040
  if (injectionMatches.length > 0) {
7031
7041
  recordStat("injection_detected", 0, 0, void 0, injectionMatches.join(","));
7032
7042
  }
7043
+ const bodyRedacted = body ? redactSecrets(body) : { text: body, count: 0 };
7033
7044
  if (!event.sessionId) {
7034
7045
  if (injectionMatches.length > 0) {
7035
- return { hookType: "rewriteOutput", updatedOutput: fenceUntrustedContent(body, injectionMatches) };
7046
+ return emitRewrite(fenceUntrustedContent(bodyRedacted.text, injectionMatches), "fetch");
7047
+ }
7048
+ if (bodyRedacted.count > 0) {
7049
+ return emitRewrite(bodyRedacted.text, "fetch");
7036
7050
  }
7037
7051
  return passOutput();
7038
7052
  }
7039
7053
  if (!body || body.length < 1024) {
7040
7054
  if (injectionMatches.length > 0) {
7041
- return { hookType: "rewriteOutput", updatedOutput: fenceUntrustedContent(body, injectionMatches) };
7055
+ return emitRewrite(fenceUntrustedContent(bodyRedacted.text, injectionMatches), "fetch");
7056
+ }
7057
+ if (bodyRedacted.count > 0) {
7058
+ return emitRewrite(bodyRedacted.text, "fetch");
7042
7059
  }
7043
7060
  return passOutput();
7044
7061
  }
@@ -7054,8 +7071,9 @@ function postFetchHandler(event) {
7054
7071
  }
7055
7072
  const cacheId = storeWebOutput(url, storedBody, `${url}\0${prompt}`, storedBody !== body ? body : void 0);
7056
7073
  recordWebFetch(url, prompt, cacheId);
7074
+ const storedRedacted = storedBody === body ? bodyRedacted : redactSecrets(storedBody);
7057
7075
  if (injectionMatches.length > 0) {
7058
- return { hookType: "rewriteOutput", updatedOutput: fenceUntrustedContent(storedBody, injectionMatches) };
7076
+ return emitRewrite(fenceUntrustedContent(storedRedacted.text, injectionMatches), "fetch");
7059
7077
  }
7060
7078
  if (storedBody !== body) {
7061
7079
  const noticeFor = (id) => `
@@ -7070,11 +7088,12 @@ function postFetchHandler(event) {
7070
7088
  noticeBytes,
7071
7089
  minNetSavingsBytes: resolveMinNetSavingsBytes()
7072
7090
  })) {
7073
- const bytesDelta = originalBytes - rewrittenBytes;
7074
- recordStat("webfetch:compress", bytesDelta, Math.round(bytesDelta / 4));
7075
- return { hookType: "rewriteOutput", updatedOutput: storedBody + notice };
7091
+ return emitRewrite(storedRedacted.text + notice, "fetch", { kind: "webfetch:compress", originalBytes });
7076
7092
  }
7077
7093
  }
7094
+ if (storedRedacted.count > 0) {
7095
+ return emitRewrite(storedRedacted.text, "fetch");
7096
+ }
7078
7097
  return passOutput();
7079
7098
  } catch {
7080
7099
  return passOutput();
@@ -7127,7 +7146,9 @@ async function preSkillHandler(event) {
7127
7146
  return passOutput();
7128
7147
  }
7129
7148
  if (await hasSessionOutput(event.sessionId, skillName)) {
7130
- recordStat("session_hint");
7149
+ const cachedBytes = await sessionOutputBodyBytes(event.sessionId, skillName);
7150
+ const denyCredit = cachedBytes !== null ? Math.min(cachedBytes, PER_FILE_COUNTERFACTUAL_CEILING) : 0;
7151
+ recordStat("session_hint", denyCredit, savedTokensFromBytes(denyCredit));
7131
7152
  return denyOutput(
7132
7153
  "Skill `" + skillName + "` was already loaded this session and is cached. Use `token-goat skill-body " + skillName + " --compact` to recall the compact slice (or `token-goat skill-body " + skillName + "` for the full body) instead of re-loading it."
7133
7154
  );
@@ -7142,7 +7163,7 @@ async function preSkillHandler(event) {
7142
7163
  const compactBytes = Buffer.byteLength(compact, "utf-8");
7143
7164
  if (compactBytes * 2 <= bodyBytes && compactBytes <= COMPACT_INLINE_MAX_BYTES) {
7144
7165
  const savedBytes = bodyBytes - compactBytes;
7145
- recordStat("skill_compact_inlined", savedBytes, Math.round(savedBytes / 4));
7166
+ recordStat("skill_compact_inlined", savedBytes, savedTokensFromBytes(savedBytes));
7146
7167
  return denyOutput(
7147
7168
  "Skill `" + skillName + "` is large (" + bodyBytes + " bytes); its compact slice (" + compactBytes + " bytes) is inlined below instead of the full body. Run `token-goat skill-body " + skillName + "` if you need the full body.\n\n" + compact
7148
7169
  );
@@ -7466,16 +7487,16 @@ function isTempPath(fp) {
7466
7487
  return /^\/tmp\//i.test(norm) || /\/var\/folders\//i.test(norm) || /AppData\/Local\/Temp\//i.test(norm) || norm.startsWith("/c/Users/") && norm.includes("/AppData/Local/Temp/") || isUnderSystemTemp(fp);
7467
7488
  }
7468
7489
  function isOrchestratorStateFile(filePath) {
7469
- const basename2 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
7470
- return /^\.improve-state-/.test(basename2);
7490
+ const basename3 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
7491
+ return /^\.improve-state-/.test(basename3);
7471
7492
  }
7472
7493
  function extractCatSourceFile(cmd) {
7473
7494
  const m = /^cat\s+(\S+\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|css|scss|sass|less))\s*$/.exec(cmd);
7474
7495
  return m?.[1] ?? null;
7475
7496
  }
7476
7497
  function classifyFileExtensions(filePath) {
7477
- const basename2 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
7478
- const isEnvFile = /^\.env(\.\w+)?$/i.test(basename2);
7498
+ const basename3 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
7499
+ const isEnvFile = /^\.env(\.\w+)?$/i.test(basename3);
7479
7500
  const hasKnownExt = /\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|css|scss|sass|less|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties|sql|ps1|psm1|env)$/i.test(filePath);
7480
7501
  if (!hasKnownExt && !isEnvFile) return null;
7481
7502
  const isSql = /\.sql$/i.test(filePath);
@@ -7497,12 +7518,14 @@ function classifyCatPath(filePath, cmd0) {
7497
7518
  return { filePath, ...flags, cmd0 };
7498
7519
  }
7499
7520
  function extractCatFile(cmd) {
7500
- const m = /^(cat|bat|type|Get-Content|gc)(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+-[a-zA-Z].*)?\s*$/i.exec(cmd);
7521
+ const m = /^(cat|bat|type|Get-Content|gc)(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+-[a-zA-Z].*?)?(?:\s+2>(&1|\/dev\/null))?\s*$/i.exec(cmd);
7501
7522
  if (!m) return null;
7502
7523
  const cmd0 = m[1];
7503
7524
  const filePath = m[2] ?? m[3] ?? m[4];
7504
7525
  if (filePath === void 0) return null;
7505
- return classifyCatPath(filePath, cmd0);
7526
+ const r = classifyCatPath(filePath, cmd0);
7527
+ if (r === null) return null;
7528
+ return { ...r, advisoryOnly: m[5] === "/dev/null" };
7506
7529
  }
7507
7530
  function extractCatFilesMulti(cmd) {
7508
7531
  if (/[|<>;&`]/.test(cmd) || cmd.includes("$(")) return null;
@@ -7750,11 +7773,12 @@ function extractPythonFileRead(cmd) {
7750
7773
  return null;
7751
7774
  }
7752
7775
  function extractHeadFile(cmd) {
7753
- const m = /^head(?:\s+-n\s+(\d+)|\s+-(\d+))?\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
7754
- if (!m) return null;
7755
- const n = parseInt(m[1] ?? m[2] ?? "0", 10);
7776
+ const direct = /^head(?:\s+-n\s+(\d+)|\s+-(\d+))?\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
7777
+ const piped = direct === null ? /^cat(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+2>(?:&1|\/dev\/null))?\s*\|\s*head(?:\s+-n\s+(\d+)|\s+-(\d+))?\s*$/.exec(cmd) : null;
7778
+ if (direct === null && piped === null) return null;
7779
+ const n = parseInt((direct !== null ? direct[1] ?? direct[2] : piped[4] ?? piped[5]) ?? "0", 10);
7756
7780
  if (n <= 10) return null;
7757
- const filePath = m[3] ?? m[4] ?? m[5];
7781
+ const filePath = direct !== null ? direct[3] ?? direct[4] ?? direct[5] : piped[1] ?? piped[2] ?? piped[3];
7758
7782
  if (filePath === void 0) return null;
7759
7783
  if (isTempPath(filePath)) return null;
7760
7784
  if (!/\.(?:ts|tsx|js|jsx|py|go|java|rs|rb|cs|md|mdx|rst|txt|json|yaml|yml|toml|sql|sh)$/i.test(filePath)) return null;
@@ -7762,12 +7786,12 @@ function extractHeadFile(cmd) {
7762
7786
  return { filePath, isDoc, isConfig, isSql, n };
7763
7787
  }
7764
7788
  function extractSedRange(cmd) {
7765
- const m = /^sed\s+-n\s+['"](?:\d+,\d+p)(?:;\d+,\d+p)*['"]\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>\/dev\/null)?\s*$/.exec(cmd);
7789
+ const m = /^sed\s+-n\s+(?:['"]((?:\d+,\d+p)(?:;\d+,\d+p)*)['"]|(\d+,\d+p))\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>(?:\/dev\/null|&1))?\s*$/.exec(cmd);
7766
7790
  if (!m) return null;
7767
- const quotedAddress = /['"]([^'"]+)['"]/.exec(cmd);
7768
- if (!quotedAddress) return null;
7791
+ const addressList = m[1] ?? m[2];
7792
+ if (addressList === void 0) return null;
7769
7793
  const ranges = [];
7770
- for (const clause of quotedAddress[1].split(";")) {
7794
+ for (const clause of addressList.split(";")) {
7771
7795
  const cm = /^(\d+),(\d+)p$/.exec(clause ?? "");
7772
7796
  if (!cm) return null;
7773
7797
  const start = parseInt(cm[1], 10);
@@ -7776,19 +7800,21 @@ function extractSedRange(cmd) {
7776
7800
  ranges.push([start, end]);
7777
7801
  }
7778
7802
  if (ranges.length === 0) return null;
7779
- const filePath = m[1] ?? m[2] ?? m[3];
7803
+ const filePath = m[3] ?? m[4] ?? m[5];
7780
7804
  if (filePath === void 0) return null;
7781
7805
  if (isTempPath(filePath)) return null;
7782
7806
  return { filePath, ranges };
7783
7807
  }
7784
7808
  function extractAwkRange(cmd) {
7785
- const m = /^awk\s+(?:'([^']+)'|"([^"]+)")\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>\/dev\/null)?\s*$/.exec(cmd);
7809
+ const m = /^awk\s+(?:'([^']+)'|"([^"]+)")\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>(?:\/dev\/null|&1))?\s*$/.exec(cmd);
7786
7810
  if (!m) return null;
7787
7811
  const program = m[1] ?? m[2];
7788
7812
  if (program === void 0) return null;
7789
- const cmp = /^\s*NR\s*>=\s*(\d+)\s*&&\s*NR\s*<=\s*(\d+)\s*$/.exec(program);
7790
- const rng = /^\s*NR\s*==\s*(\d+)\s*,\s*NR\s*==\s*(\d+)\s*$/.exec(program);
7813
+ const cmp = /^\s*NR\s*>=\s*(\d+)\s*&&\s*NR\s*<=\s*(\d+)\s*(\{.*\})?\s*$/.exec(program);
7814
+ const rng = /^\s*NR\s*==\s*(\d+)\s*,\s*NR\s*==\s*(\d+)\s*(\{.*\})?\s*$/.exec(program);
7791
7815
  const hit = cmp ?? rng;
7816
+ const action = hit?.[3];
7817
+ if (action !== void 0 && !/^\{\s*(?:print(?:\s+NR\s*"[^"%]*"\s*\$0|\s+\$0)?|printf\s*"%d[^%"]*%s\\n"\s*,\s*NR\s*,\s*\$0)\s*\}$/.test(action)) return null;
7792
7818
  if (!hit) return null;
7793
7819
  const start = parseInt(hit[1], 10);
7794
7820
  const end = parseInt(hit[2], 10);
@@ -7798,13 +7824,47 @@ function extractAwkRange(cmd) {
7798
7824
  if (isTempPath(filePath)) return null;
7799
7825
  return { filePath, ranges: [[start, end]] };
7800
7826
  }
7827
+ function normalizeCatSedPipe(cmd) {
7828
+ const m = /^cat(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+2>(?:&1|\/dev\/null))?\s*\|\s*sed\s+-n\s+('[^']+'|"[^"]+"|\d+,\d+p)\s*$/.exec(cmd);
7829
+ if (!m) return null;
7830
+ const filePath = m[1] ?? m[2] ?? m[3];
7831
+ if (filePath === void 0 || filePath.includes('"')) return null;
7832
+ return "sed -n " + m[4] + ' "' + filePath + '"';
7833
+ }
7801
7834
  function extractLineRangeRead(cmd) {
7835
+ const catSed = normalizeCatSedPipe(cmd);
7836
+ if (catSed !== null) cmd = catSed;
7802
7837
  const sed = extractSedRange(cmd);
7803
7838
  if (sed !== null) return { ...sed, tool: "sed" };
7804
7839
  const awk = extractAwkRange(cmd);
7805
7840
  if (awk !== null) return { ...awk, tool: "awk" };
7806
7841
  return null;
7807
7842
  }
7843
+ var FORMATTING_STAGE_RE = /^(?:fold|cut|cat|nl|head|tail)(?:\s+(?:-\S+|[\d,+-]+))*\s*$/;
7844
+ function extractLineRangeReadsCompound(cmd) {
7845
+ const cleaned = cmd.replace(/\s2>(?:&1|\/dev\/null)/g, "");
7846
+ const segments = splitShellSegments(cleaned);
7847
+ if (segments.length < 2) return null;
7848
+ const reads = [];
7849
+ for (const seg of segments) {
7850
+ const r = extractLineRangeRead(seg);
7851
+ if (r !== null) {
7852
+ reads.push(r);
7853
+ continue;
7854
+ }
7855
+ if (/^echo\b[^<>]*$/.test(seg)) continue;
7856
+ if (FORMATTING_STAGE_RE.test(seg)) continue;
7857
+ return null;
7858
+ }
7859
+ if (reads.length === 0) return null;
7860
+ const merged = /* @__PURE__ */ new Map();
7861
+ for (const r of reads) {
7862
+ const prev = merged.get(r.filePath);
7863
+ if (prev !== void 0) prev.ranges.push(...r.ranges);
7864
+ else merged.set(r.filePath, { filePath: r.filePath, ranges: [...r.ranges], tool: r.tool });
7865
+ }
7866
+ return [...merged.values()];
7867
+ }
7808
7868
  var SYMBOL_BEARING_LANGUAGES = /* @__PURE__ */ new Set([
7809
7869
  "python",
7810
7870
  "typescript",
@@ -7904,11 +7964,12 @@ function extractTailFile(cmd) {
7904
7964
  if (/-f\b/.test(cmd)) return null;
7905
7965
  if (/-c\b/.test(cmd)) return null;
7906
7966
  if (/-n\s*\+/.test(cmd)) return null;
7907
- const m = /^tail(?:\s+-n\s+(\d+)|\s+-(\d+))?\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
7908
- if (!m) return null;
7909
- const n = parseInt(m[1] ?? m[2] ?? "0", 10);
7967
+ const direct = /^tail(?:\s+-n\s+(\d+)|\s+-(\d+))?\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
7968
+ const piped = direct === null ? /^cat(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+2>(?:&1|\/dev\/null))?\s*\|\s*tail(?:\s+-n\s+(\d+)|\s+-(\d+))?\s*$/.exec(cmd) : null;
7969
+ if (direct === null && piped === null) return null;
7970
+ const n = parseInt((direct !== null ? direct[1] ?? direct[2] : piped[4] ?? piped[5]) ?? "0", 10);
7910
7971
  if (n <= 10) return null;
7911
- const filePath = m[3] ?? m[4] ?? m[5];
7972
+ const filePath = direct !== null ? direct[3] ?? direct[4] ?? direct[5] : piped[1] ?? piped[2] ?? piped[3];
7912
7973
  if (!filePath) return null;
7913
7974
  if (isTempPath(filePath)) return null;
7914
7975
  if (!/\.(?:ts|tsx|js|jsx|py|go|java|rs|rb|cs|md|mdx|rst|txt|json|yaml|yml|toml|sql|sh)$/i.test(filePath)) return null;
@@ -8362,10 +8423,19 @@ async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinB
8362
8423
  });
8363
8424
  const minNet = resolveMinNetSavingsBytes();
8364
8425
  if (!compressed.worthApplying(minNet)) return null;
8365
- const id = await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
8366
- recordStat("bash_compress:generic", compressed.bytesSaved, compressed.tokensSaved);
8426
+ const id = await commandHash(cmd, cwd);
8367
8427
  const body = compressed.withMarker(minNet) + "\n[token-goat] full output: bash-output " + id + " --full";
8368
- return { hookType: "rewriteOutput", updatedOutput: body };
8428
+ const emittedBytes = Buffer.byteLength(body, "utf-8");
8429
+ if (!isRewriteWorthwhile({
8430
+ originalBytes: compressed.originalBytes,
8431
+ rewrittenBytes: emittedBytes,
8432
+ noticeBytes: 0,
8433
+ minNetSavingsBytes: minNet
8434
+ })) {
8435
+ return null;
8436
+ }
8437
+ await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
8438
+ return emitRewrite(body, "bash", { kind: "bash_compress:generic", originalBytes: compressed.originalBytes });
8369
8439
  }
8370
8440
  function unwrapCompressCommand(executed) {
8371
8441
  const t = executed.trim();
@@ -8591,25 +8661,28 @@ function preBashHandlerInner(event) {
8591
8661
  "Use `token-goat outline <file>` to see symbol names and line counts without loading files."
8592
8662
  );
8593
8663
  }
8594
- const sedRange = extractLineRangeRead(cmd);
8595
- if (sedRange !== null) {
8596
- const { filePath, ranges, tool } = sedRange;
8597
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
8664
+ const singleLineRangeRead = extractLineRangeRead(cmd);
8665
+ const sedReads = singleLineRangeRead !== null ? [singleLineRangeRead] : extractLineRangeReadsCompound(cmd);
8666
+ if (sedReads !== null) {
8598
8667
  recordStat("session_hint", 0, 0);
8599
- const sedDedupKey = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
8600
- const overlapHints = [];
8601
- const freshRanges = [];
8602
- for (const [start, end] of ranges) {
8603
- const priorOverlap = findRangeOverlap(getFileLineRanges(sedDedupKey), start, end);
8604
- recordFileLineRange(sedDedupKey, start, end);
8605
- if (priorOverlap !== null) {
8606
- overlapHints.push(sedOverlapHint(hintPath, priorOverlap, start, end));
8607
- } else {
8608
- freshRanges.push([start, end]);
8668
+ const hints = [];
8669
+ for (const { filePath, ranges, tool } of sedReads) {
8670
+ const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
8671
+ const sedDedupKey = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
8672
+ const overlapHints = [];
8673
+ const freshRanges = [];
8674
+ for (const [start, end] of ranges) {
8675
+ const priorOverlap = findRangeOverlap(getFileLineRanges(sedDedupKey), start, end);
8676
+ recordFileLineRange(sedDedupKey, start, end);
8677
+ if (priorOverlap !== null) {
8678
+ overlapHints.push(sedOverlapHint(hintPath, priorOverlap, start, end));
8679
+ } else {
8680
+ freshRanges.push([start, end]);
8681
+ }
8609
8682
  }
8683
+ hints.push(...overlapHints);
8684
+ if (freshRanges.length > 0) hints.push(sedRangeHint(hintPath, freshRanges, tool));
8610
8685
  }
8611
- const hints = [...overlapHints];
8612
- if (freshRanges.length > 0) hints.push(sedRangeHint(hintPath, freshRanges, tool));
8613
8686
  return contextOutput(hints.join(" "));
8614
8687
  }
8615
8688
  const catJsonPipe = extractCatJsonPipe(cmd);
@@ -8623,7 +8696,7 @@ function preBashHandlerInner(event) {
8623
8696
  }
8624
8697
  const catResult = extractCatFile(cmd);
8625
8698
  if (catResult !== null) {
8626
- const { filePath, isDoc, isEnv, isConfig, isSql, cmd0 } = catResult;
8699
+ const { filePath, isDoc, isEnv, isConfig, isSql, cmd0, advisoryOnly } = catResult;
8627
8700
  const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
8628
8701
  recordStat("session_hint", 0, 0);
8629
8702
  if (isSql) {
@@ -8632,7 +8705,7 @@ function preBashHandlerInner(event) {
8632
8705
  );
8633
8706
  }
8634
8707
  const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc);
8635
- return cdStripped ? contextOutput("`" + cmd0 + "` loads the entire file into context. " + hint) : denyOutput("`" + cmd0 + "` loads the entire file into context. " + hint);
8708
+ return cdStripped || advisoryOnly ? contextOutput("`" + cmd0 + "` loads the entire file into context. " + hint) : denyOutput("`" + cmd0 + "` loads the entire file into context. " + hint);
8636
8709
  }
8637
8710
  const catMulti = extractCatFilesMulti(cmd);
8638
8711
  if (catMulti !== null) {
@@ -8771,13 +8844,13 @@ function preBashHandlerInner(event) {
8771
8844
  const monBytes = monEntry.sizeBytes;
8772
8845
  const catFile = extractCatSourceFile(cmd);
8773
8846
  if (catFile !== null) {
8774
- recordStat("bash_compress:recall", monBytes, Math.round(monBytes / 4));
8847
+ recordStat("bash_compress:recall", monBytes, savedTokensFromBytes(monBytes));
8775
8848
  return contextOutput(
8776
8849
  "Prior output from `" + cmd + "`" + pipelineDivergenceNote(cmd, monEntry.command) + " is cached. Use `token-goat bash-output " + monOutputId + "` to recall the full file, or `token-goat read '" + catFile + "::SymbolName'` to extract only the symbol you need."
8777
8850
  );
8778
8851
  }
8779
8852
  const cmdSummary = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
8780
- recordStat("bash_compress:recall", monBytes, Math.round(monBytes / 4));
8853
+ recordStat("bash_compress:recall", monBytes, savedTokensFromBytes(monBytes));
8781
8854
  return contextOutput(
8782
8855
  "Prior output from `" + cmdSummary + "`" + pipelineDivergenceNote(cmd, monEntry.command) + " is cached.\nUse `token-goat bash-output " + monOutputId + " " + monitoringHint + "` to re-inspect without re-running."
8783
8856
  );
@@ -8804,7 +8877,7 @@ function preBashHandlerInner(event) {
8804
8877
  const curlEntry = curlEntryRaw !== null && !isBashEntryStale(curlEntryRaw, cmd, preHookCwd) ? curlEntryRaw : null;
8805
8878
  if (curlOutputId !== null && curlEntry !== null && curlEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(curlEntry.sizeBytes)) {
8806
8879
  const curlBytes = curlEntry.sizeBytes;
8807
- recordStat("bash_compress:recall", curlBytes, Math.round(curlBytes / 4));
8880
+ recordStat("bash_compress:recall", curlBytes, savedTokensFromBytes(curlBytes));
8808
8881
  const curlPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
8809
8882
  return contextOutput(
8810
8883
  "curl response cached (`" + curlPreview + "`)." + pipelineDivergenceNote(cmd, curlEntry.command) + " Use `token-goat bash-output " + curlOutputId + "` to recall it. Append `--grep PATTERN` to filter or `--section HeadingName` for a markdown section."
@@ -8818,7 +8891,7 @@ function preBashHandlerInner(event) {
8818
8891
  const ghEntry = ghEntryRaw !== null && !isBashEntryStale(ghEntryRaw, cmd, preHookCwd) ? ghEntryRaw : null;
8819
8892
  if (ghOutputId !== null && ghEntry !== null && ghEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(ghEntry.sizeBytes)) {
8820
8893
  const ghBytes = ghEntry.sizeBytes;
8821
- recordStat("bash_compress:recall", ghBytes, Math.round(ghBytes / 4));
8894
+ recordStat("bash_compress:recall", ghBytes, savedTokensFromBytes(ghBytes));
8822
8895
  const ghPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
8823
8896
  return contextOutput(
8824
8897
  "gh api response cached (`" + ghPreview + "`)." + pipelineDivergenceNote(cmd, ghEntry.command) + " Use `token-goat bash-output " + ghOutputId + "` to recall it. Append `--jq '.field'` on the original call, or `--grep PATTERN` / `--max-matches N` here, to narrow it."
@@ -8832,7 +8905,7 @@ function preBashHandlerInner(event) {
8832
8905
  const gitScopedEntry = gitScopedEntryRaw !== null && !isBashEntryStale(gitScopedEntryRaw, cmd, preHookCwd) ? gitScopedEntryRaw : null;
8833
8906
  if (gitScopedOutputId !== null && gitScopedEntry !== null && gitScopedEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(gitScopedEntry.sizeBytes)) {
8834
8907
  const gitScopedBytes = gitScopedEntry.sizeBytes;
8835
- recordStat("bash_compress:recall", gitScopedBytes, Math.round(gitScopedBytes / 4));
8908
+ recordStat("bash_compress:recall", gitScopedBytes, savedTokensFromBytes(gitScopedBytes));
8836
8909
  const gitScopedPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
8837
8910
  return contextOutput(
8838
8911
  "Output from `" + gitScopedPreview + "`" + pipelineDivergenceNote(cmd, gitScopedEntry.command) + " is cached and unchanged (no edits to that path or HEAD since). Use `token-goat bash-output " + gitScopedOutputId + "` to recall it instead of re-running."
@@ -8874,7 +8947,7 @@ function preBashHandlerInner(event) {
8874
8947
  const entry = entryRaw !== null && !isBashEntryStale(entryRaw, cmd, preHookCwd) ? entryRaw : null;
8875
8948
  if (outputId !== null && entry !== null && entry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(entry.sizeBytes)) {
8876
8949
  const bytes = entry.sizeBytes;
8877
- recordStat("bash_compress:recall", bytes, Math.round(bytes / 4));
8950
+ recordStat("bash_compress:recall", bytes, savedTokensFromBytes(bytes));
8878
8951
  return contextOutput(buildRecallHint(cmd, outputId));
8879
8952
  }
8880
8953
  return maybeCompressRewrite(event, rawCmd, cmd) ?? passOutput();
@@ -9121,10 +9194,7 @@ function postBashOutputHandler(event) {
9121
9194
  })) {
9122
9195
  return passOutput();
9123
9196
  }
9124
- return {
9125
- hookType: "rewriteOutput",
9126
- updatedOutput: unchangedNotice
9127
- };
9197
+ return emitRewrite(unchangedNotice, "bashoutput", { kind: "bashoutput:unchanged", originalBytes: Buffer.byteLength(rawOutput, "utf-8") });
9128
9198
  }
9129
9199
  if (!output.startsWith(prior.output)) {
9130
9200
  storePollSnapshot(event.sessionId, bashId, output);
@@ -9143,10 +9213,7 @@ function postBashOutputHandler(event) {
9143
9213
  })) {
9144
9214
  return passOutput();
9145
9215
  }
9146
- return {
9147
- hookType: "rewriteOutput",
9148
- updatedOutput: `${deltaNotice}${delta}`
9149
- };
9216
+ return emitRewrite(`${deltaNotice}${delta}`, "bashoutput", { kind: "bashoutput:delta", originalBytes: Buffer.byteLength(rawOutput, "utf-8") });
9150
9217
  } catch {
9151
9218
  return passOutput();
9152
9219
  }
@@ -9176,12 +9243,23 @@ function getTaskId(toolInput) {
9176
9243
  const value = toolInput["task_id"];
9177
9244
  return typeof value === "string" && value !== "" ? value : void 0;
9178
9245
  }
9246
+ function extractTaskOutputText(raw) {
9247
+ const tr = raw["tool_response"];
9248
+ if (tr !== null && typeof tr === "object" && !Array.isArray(tr)) {
9249
+ const task = tr["task"];
9250
+ if (task !== null && typeof task === "object" && !Array.isArray(task)) {
9251
+ const output = task["output"];
9252
+ if (typeof output === "string") return output;
9253
+ }
9254
+ }
9255
+ return extractToolResultText(raw);
9256
+ }
9179
9257
  function postTaskOutputHandler(event) {
9180
9258
  try {
9181
9259
  if (getToolName(event) !== "TaskOutput" || !event.sessionId) return passOutput();
9182
9260
  const taskId = getTaskId(getToolInput(event));
9183
9261
  if (taskId === void 0) return passOutput();
9184
- const rawOutput = extractToolResultText(event.raw);
9262
+ const rawOutput = extractTaskOutputText(event.raw);
9185
9263
  if (!rawOutput) return passOutput();
9186
9264
  const output = redactSecrets(rawOutput).text;
9187
9265
  if (!output) return passOutput();
@@ -9201,10 +9279,7 @@ function postTaskOutputHandler(event) {
9201
9279
  })) {
9202
9280
  return passOutput();
9203
9281
  }
9204
- return {
9205
- hookType: "rewriteOutput",
9206
- updatedOutput: collapsed
9207
- };
9282
+ return emitRewrite(collapsed, "taskoutput", { kind: "taskoutput:collapse", originalBytes: Buffer.byteLength(rawOutput, "utf-8") });
9208
9283
  }
9209
9284
  if (output === prior.output) {
9210
9285
  storePollSnapshot2(event.sessionId, taskId, output);
@@ -9218,10 +9293,7 @@ function postTaskOutputHandler(event) {
9218
9293
  })) {
9219
9294
  return passOutput();
9220
9295
  }
9221
- return {
9222
- hookType: "rewriteOutput",
9223
- updatedOutput: unchangedNotice
9224
- };
9296
+ return emitRewrite(unchangedNotice, "taskoutput", { kind: "taskoutput:unchanged", originalBytes: Buffer.byteLength(rawOutput, "utf-8") });
9225
9297
  }
9226
9298
  if (!output.startsWith(prior.output)) {
9227
9299
  storePollSnapshot2(event.sessionId, taskId, output);
@@ -9241,10 +9313,7 @@ function postTaskOutputHandler(event) {
9241
9313
  })) {
9242
9314
  return passOutput();
9243
9315
  }
9244
- return {
9245
- hookType: "rewriteOutput",
9246
- updatedOutput: `${deltaNotice}${collapsedDelta}`
9247
- };
9316
+ return emitRewrite(`${deltaNotice}${collapsedDelta}`, "taskoutput", { kind: "taskoutput:delta", originalBytes: Buffer.byteLength(rawOutput, "utf-8") });
9248
9317
  } catch {
9249
9318
  return passOutput();
9250
9319
  }
@@ -9340,8 +9409,10 @@ var APPROVED_PLAN_MARKER = "## Approved Plan:";
9340
9409
  function postExitPlanModeHandler(event) {
9341
9410
  try {
9342
9411
  if (getToolName(event) !== "ExitPlanMode") return passOutput();
9343
- const output = extractToolResultText(event.raw);
9344
- if (!output) return passOutput();
9412
+ const rawOutput = extractToolResultText(event.raw);
9413
+ if (!rawOutput) return passOutput();
9414
+ const redacted = redactSecrets(rawOutput);
9415
+ const output = redacted.text;
9345
9416
  const markerIndex = output.indexOf(APPROVED_PLAN_MARKER);
9346
9417
  if (markerIndex === -1) return passOutput();
9347
9418
  const planValue = getToolInput(event)["plan"];
@@ -9360,10 +9431,10 @@ ${PLAN_OMIT_POINTER}`;
9360
9431
  })) {
9361
9432
  return passOutput();
9362
9433
  }
9363
- return {
9364
- hookType: "rewriteOutput",
9365
- updatedOutput: `${prefix}${notice}`
9366
- };
9434
+ return emitRewrite(`${prefix}${notice}`, "exitplanmode", {
9435
+ kind: "plan_echo_collapse",
9436
+ originalBytes: Buffer.byteLength(output, "utf-8")
9437
+ });
9367
9438
  } catch {
9368
9439
  return passOutput();
9369
9440
  }
@@ -9431,6 +9502,9 @@ function storeMcpOutput(sessionId, toolName, toolInput, resultText) {
9431
9502
  ${redactedOutput}`, entry.storedAt);
9432
9503
  return id;
9433
9504
  }
9505
+ function mcpOutputBytes(id) {
9506
+ return getBashOutput(id)?.sizeBytes ?? 0;
9507
+ }
9434
9508
  function getMcpOutput(sessionId, toolName, toolInput, ttlMs = Number.POSITIVE_INFINITY) {
9435
9509
  if (!sessionId) return null;
9436
9510
  const id = mcpOutputId(sessionId, mcpHash(toolName, toolInput));
@@ -9753,6 +9827,8 @@ function preMcpHandler(event) {
9753
9827
  const ttlMs = loadConfig().hints.mcp_dedup_ttl_secs * 1e3;
9754
9828
  const id = getMcpOutput(event.sessionId, toolName, toolInput, ttlMs);
9755
9829
  if (!id) return passOutput();
9830
+ const denyCredit = Math.min(mcpOutputBytes(id), PER_FILE_COUNTERFACTUAL_CEILING);
9831
+ recordStat("mcp:recall", denyCredit, savedTokensFromBytes(denyCredit));
9756
9832
  return denyOutput(
9757
9833
  "Identical read-only MCP call already cached this session. Use `token-goat bash-output " + id + "` to recall the result (add `--grep PATTERN`, `--tail N`, or `--head N` to slice) instead of repeating the call."
9758
9834
  );
@@ -9770,11 +9846,16 @@ function postMcpHandler(event) {
9770
9846
  injectionMatches = [];
9771
9847
  }
9772
9848
  if (injectionMatches.length > 0) recordStat("injection_detected", 0, 0, void 0, injectionMatches.join(","));
9773
- const fenced = () => ({
9774
- hookType: "rewriteOutput",
9775
- updatedOutput: fenceUntrustedContent(redactSecrets(resultText).text, injectionMatches, UNTRUSTED_TOOL_TAG)
9776
- });
9777
- const passOrFence = () => injectionMatches.length > 0 ? fenced() : passOutput();
9849
+ const redactedResult = redactSecrets(resultText);
9850
+ const fenced = () => emitRewrite(
9851
+ fenceUntrustedContent(redactedResult.text, injectionMatches, UNTRUSTED_TOOL_TAG),
9852
+ "mcp"
9853
+ );
9854
+ const passOrFence = () => {
9855
+ if (injectionMatches.length > 0) return fenced();
9856
+ if (redactedResult.count > 0) return emitRewrite(redactedResult.text, "mcp");
9857
+ return passOutput();
9858
+ };
9778
9859
  if (!event.sessionId) return passOrFence();
9779
9860
  if (isMcpErrorResponse(event.raw)) return passOrFence();
9780
9861
  const readOnly = isMcpReadOnly(toolName, toolInput);
@@ -9792,17 +9873,19 @@ function postMcpHandler(event) {
9792
9873
  const redactedBody = redactSecrets(compressed).text;
9793
9874
  const notice = `[token-goat: compressed, full via mcp-output ${id}]
9794
9875
  `;
9876
+ const originalBytes = Buffer.byteLength(resultText, "utf-8");
9795
9877
  const worthwhile = isRewriteWorthwhile({
9796
- originalBytes: Buffer.byteLength(resultText, "utf-8"),
9878
+ originalBytes,
9797
9879
  rewrittenBytes: Buffer.byteLength(redactedBody, "utf-8"),
9798
9880
  noticeBytes: Buffer.byteLength(notice, "utf-8"),
9799
9881
  minNetSavingsBytes: resolveMinNetSavingsBytes()
9800
9882
  });
9801
9883
  if (worthwhile) {
9802
- return {
9803
- hookType: "rewriteOutput",
9804
- updatedOutput: injectionMatches.length > 0 ? `${notice}${fenceUntrustedContent(redactedBody, injectionMatches, UNTRUSTED_TOOL_TAG)}` : `${notice}${redactedBody}`
9805
- };
9884
+ return emitRewrite(
9885
+ injectionMatches.length > 0 ? `${notice}${fenceUntrustedContent(redactedBody, injectionMatches, UNTRUSTED_TOOL_TAG)}` : `${notice}${redactedBody}`,
9886
+ "mcp",
9887
+ { kind: "mcp:compress", originalBytes }
9888
+ );
9806
9889
  }
9807
9890
  }
9808
9891
  }
@@ -9848,14 +9931,32 @@ function preWebSearchDedupHandler(event) {
9848
9931
  }
9849
9932
  function postWebSearchHandler(event) {
9850
9933
  try {
9851
- if (getToolName(event) !== "WebSearch" || !event.sessionId) return passOutput();
9852
- const signature = webSearchSignatureInput(getToolInput(event));
9853
- if (signature === null) return passOutput();
9854
- if (isMcpErrorResponse(event.raw)) return passOutput();
9934
+ if (getToolName(event) !== "WebSearch") return passOutput();
9855
9935
  const resultText = extractToolResultText(event.raw);
9856
9936
  if (!resultText) return passOutput();
9937
+ let injectionMatches = [];
9938
+ try {
9939
+ if (loadConfig().injection.enabled) injectionMatches = scanForInjectionPatterns(resultText);
9940
+ } catch {
9941
+ injectionMatches = [];
9942
+ }
9943
+ if (injectionMatches.length > 0) recordStat("injection_detected", 0, 0, void 0, injectionMatches.join(","));
9944
+ const redacted = redactSecrets(resultText);
9945
+ const passOrFence = () => {
9946
+ if (injectionMatches.length > 0) {
9947
+ return emitRewrite(fenceUntrustedContent(redacted.text, injectionMatches), "websearch");
9948
+ }
9949
+ if (redacted.count > 0) {
9950
+ return emitRewrite(redacted.text, "websearch");
9951
+ }
9952
+ return passOutput();
9953
+ };
9954
+ if (!event.sessionId) return passOrFence();
9955
+ const signature = webSearchSignatureInput(getToolInput(event));
9956
+ if (signature === null) return passOrFence();
9957
+ if (isMcpErrorResponse(event.raw)) return passOrFence();
9857
9958
  storeMcpOutput(event.sessionId, "WebSearch", signature, resultText);
9858
- return passOutput();
9959
+ return passOrFence();
9859
9960
  } catch {
9860
9961
  return passOutput();
9861
9962
  }
@@ -9905,13 +10006,15 @@ async function shrinkImageBlock(block) {
9905
10006
  const mediaType = typeof source?.media_type === "string" ? source.media_type : "image/png";
9906
10007
  const data = typeof source?.data === "string" ? source.data : "";
9907
10008
  const originalDataUrl = `data:${mediaType};base64,${data}`;
9908
- if (source?.type !== "base64" || data === "") return { text: originalDataUrl, changed: false, savedBytes: 0 };
10009
+ if (source?.type !== "base64" || data === "") return { text: originalDataUrl, changed: false, savedBytes: 0, savedTokens: 0 };
9909
10010
  let buffer;
9910
10011
  try {
9911
10012
  buffer = Buffer.from(data, "base64");
9912
10013
  } catch {
9913
- return { text: originalDataUrl, changed: false, savedBytes: 0 };
10014
+ return { text: originalDataUrl, changed: false, savedBytes: 0, savedTokens: 0 };
9914
10015
  }
10016
+ const meta = await probeImageMeta(buffer);
10017
+ const tier = loadConfig().image_shrink.vision_tier;
9915
10018
  const fingerprint = screenshotFingerprint(data);
9916
10019
  const alreadyShown = hasSeenImage(fingerprint);
9917
10020
  recordSeenImage(fingerprint);
@@ -9926,16 +10029,23 @@ async function shrinkImageBlock(block) {
9926
10029
  return {
9927
10030
  text: SCREENSHOT_REPEAT_NOTICE,
9928
10031
  changed: true,
9929
- savedBytes: originalDataUrl.length - SCREENSHOT_REPEAT_NOTICE.length
10032
+ // Decoded image bytes, not base64 characters. Both branches of this function file under the same `image_shrink` kind, and the shrink branch below credits `result.originalBytes - result.shrunkBytes` -- decoded bytes, the convention documented at image_shrink.ts's own recordStat call. Base64 inflates by 4/3, so crediting `originalDataUrl.length` here booked roughly 33% more for a repeat screenshot than an identical shrink of the same image, and the ledger summed the two units into one row. The gate above deliberately still measures the data URL: it decides whether the rewrite pays off on the wire, where base64 characters are what is actually sent.
10033
+ savedBytes: buffer.length - Buffer.byteLength(SCREENSHOT_REPEAT_NOTICE, "utf-8"),
10034
+ // The whole image is withheld here rather than resized, so the visual tokens saved are its
10035
+ // entire billed cost less the notice standing in for it. Billed cost, not raw patch count:
10036
+ // an oversized screenshot would have been capped by the API's own downscale, so pricing the
10037
+ // untouched dimensions would credit a bill that was never going to be sent.
10038
+ savedTokens: Math.max(0, visionTokens(meta?.width ?? 0, meta?.height ?? 0, tier) - savedTokensFromBytes(Buffer.byteLength(SCREENSHOT_REPEAT_NOTICE, "utf-8")))
9930
10039
  };
9931
10040
  }
9932
10041
  }
9933
10042
  const result = await imageQualifiesForShrink(buffer) ? await shrinkImage(buffer, { sizeThresholdBytes: 0 }) : null;
9934
- if (result === null) return { text: originalDataUrl, changed: false, savedBytes: 0 };
10043
+ if (result === null) return { text: originalDataUrl, changed: false, savedBytes: 0, savedTokens: 0 };
9935
10044
  const saved = result.originalBytes - result.shrunkBytes;
9936
10045
  const { summary, dataUrl } = formatShrinkSummary(result, "an inline browser screenshot");
10046
+ const savedTokens = visionTokensSaved(result.originalWidth, result.originalHeight, result.width, result.height, tier);
9937
10047
  return { text: `${summary}
9938
- ${dataUrl}`, changed: true, savedBytes: saved };
10048
+ ${dataUrl}`, changed: true, savedBytes: saved, savedTokens };
9939
10049
  }
9940
10050
  var TAB_CONTEXT_UNCHANGED_NOTICE = "(tabs unchanged since last check)";
9941
10051
  function dedupTabContext(text) {
@@ -9963,15 +10073,19 @@ async function postBrowserImageHandler(event) {
9963
10073
  const parts = [];
9964
10074
  for (const block of blocks) {
9965
10075
  if (block.type === "image") {
9966
- const { text, changed, savedBytes } = await shrinkImageBlock(block);
10076
+ const { text, changed, savedBytes, savedTokens } = await shrinkImageBlock(block);
9967
10077
  if (changed) {
9968
10078
  anyChanged = true;
9969
- recordStat("image_shrink", savedBytes, Math.round(savedBytes / 4), void 0, toolName);
10079
+ recordStat("image_shrink", savedBytes, savedTokens, void 0, toolName);
9970
10080
  }
9971
10081
  parts.push(text);
9972
10082
  } else if (block.type === "text" && typeof block.text === "string") {
9973
10083
  const { text, changed } = dedupTabContext(block.text);
9974
- if (changed) anyChanged = true;
10084
+ if (changed) {
10085
+ anyChanged = true;
10086
+ const savedBytes = Buffer.byteLength(block.text, "utf-8") - Buffer.byteLength(text, "utf-8");
10087
+ if (savedBytes > 0) recordStat("browser_tab_dedup", savedBytes, savedTokensFromBytes(savedBytes), void 0, toolName);
10088
+ }
9975
10089
  parts.push(text);
9976
10090
  } else {
9977
10091
  try {
@@ -9982,7 +10096,7 @@ async function postBrowserImageHandler(event) {
9982
10096
  }
9983
10097
  }
9984
10098
  if (!anyChanged) return passOutput();
9985
- return { hookType: "rewriteOutput", updatedOutput: parts.join("\n") };
10099
+ return emitRewrite(parts.join("\n"), toolName);
9986
10100
  } catch {
9987
10101
  return passOutput();
9988
10102
  }
@@ -9991,6 +10105,9 @@ registerHook("post_tool_use", postBrowserImageHandler, { toolPattern: "^mcp__" }
9991
10105
 
9992
10106
  // src/hooks_agent_spawn.ts
9993
10107
  import { createHash as createHash2 } from "node:crypto";
10108
+ import * as fs from "node:fs";
10109
+ import * as os from "node:os";
10110
+ import * as path2 from "node:path";
9994
10111
  var BRIEFING_TARGET_TOKENS = 550;
9995
10112
  function buildSubagentBriefing() {
9996
10113
  try {
@@ -10062,8 +10179,11 @@ function findDuplicateOutstandingPrompt(prompt) {
10062
10179
  function truncateForWarning(text, max) {
10063
10180
  return text.length > max ? text.slice(0, max) + "..." : text;
10064
10181
  }
10182
+ function isAgentTool(toolName) {
10183
+ return toolName === "Agent" || toolName === "task" || toolName === "Task";
10184
+ }
10065
10185
  function preAgentHandler(event) {
10066
- if (event.toolName !== "Agent") return passOutput();
10186
+ if (!isAgentTool(event.toolName)) return passOutput();
10067
10187
  const toolInput = event.toolInput;
10068
10188
  const prompt = toolInput["prompt"];
10069
10189
  if (typeof prompt !== "string" || prompt.trim() === "") return passOutput();
@@ -10085,7 +10205,7 @@ function preAgentHandler(event) {
10085
10205
  return passOutput();
10086
10206
  }
10087
10207
  }
10088
- var FENCE_LINE_RE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
10208
+ var FENCE_LINE_RE = /^ {0,3}(`{3,}|~{3,})([^\n]*)$/;
10089
10209
  function findFencedBlockLines(lines) {
10090
10210
  const blocks = [];
10091
10211
  let fenceStart = -1;
@@ -10185,21 +10305,118 @@ function collapseBlankRunsInFences(text) {
10185
10305
  out.push(...lines.slice(cursor));
10186
10306
  return changedAny ? out.join("\n") : text;
10187
10307
  }
10308
+ var SPAWN_RESTRICT_HINT_KEY = "agent-spawn-restrict-hint";
10309
+ var SPAWN_RESTRICT_MAX_NAMES = 3;
10310
+ var ROSTER_WALK_MAX_DEPTH = 4;
10311
+ var ROSTER_WALK_MAX_FILES = 400;
10312
+ function parseAgentDefinition(text, fallbackName) {
10313
+ const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
10314
+ if (!fmMatch) return null;
10315
+ const fm = fmMatch[1];
10316
+ const nameMatch = /^name:(.*)$/m.exec(fm);
10317
+ const rawName = nameMatch ? nameMatch[1].trim().replace(/^["']|["']$/g, "") : "";
10318
+ const name2 = rawName !== "" ? rawName : fallbackName;
10319
+ const toolsMatch = /^tools:(.*)$/m.exec(fm);
10320
+ if (!toolsMatch) return { name: name2, restricted: false };
10321
+ const inline = toolsMatch[1].trim();
10322
+ if (inline === "*" || inline === '"*"' || inline === "'*'") return { name: name2, restricted: false };
10323
+ if (inline !== "") return { name: name2, restricted: true };
10324
+ const after = fm.slice(toolsMatch.index + toolsMatch[0].length);
10325
+ for (const line of after.split(/\r?\n/)) {
10326
+ if (/^\s+-\s*\S/.test(line)) return { name: name2, restricted: true };
10327
+ if (/^\S/.test(line)) break;
10328
+ }
10329
+ return { name: name2, restricted: false };
10330
+ }
10331
+ function findRestrictedAgentNames(roots) {
10332
+ const scanRoots = roots ?? [path2.join(os.homedir(), ".claude", "agents")];
10333
+ const names = /* @__PURE__ */ new Set();
10334
+ const visited = /* @__PURE__ */ new Set();
10335
+ let filesSeen = 0;
10336
+ const walk = (dir, depth) => {
10337
+ if (depth > ROSTER_WALK_MAX_DEPTH || filesSeen >= ROSTER_WALK_MAX_FILES) return;
10338
+ let real;
10339
+ try {
10340
+ real = fs.realpathSync(dir);
10341
+ } catch {
10342
+ return;
10343
+ }
10344
+ const key = process.platform === "win32" ? real.toLocaleLowerCase() : real;
10345
+ if (visited.has(key)) return;
10346
+ visited.add(key);
10347
+ let entries;
10348
+ try {
10349
+ entries = fs.readdirSync(dir, { withFileTypes: true });
10350
+ } catch {
10351
+ return;
10352
+ }
10353
+ for (const entry of entries) {
10354
+ if (filesSeen >= ROSTER_WALK_MAX_FILES) return;
10355
+ const full = path2.join(dir, entry.name);
10356
+ let stat;
10357
+ try {
10358
+ stat = fs.statSync(full);
10359
+ } catch {
10360
+ continue;
10361
+ }
10362
+ if (stat.isDirectory()) {
10363
+ walk(full, depth + 1);
10364
+ continue;
10365
+ }
10366
+ if (!stat.isFile() || !entry.name.toLowerCase().endsWith(".md")) continue;
10367
+ filesSeen++;
10368
+ let text;
10369
+ try {
10370
+ text = fs.readFileSync(full, "utf-8");
10371
+ } catch {
10372
+ continue;
10373
+ }
10374
+ const parsed = parseAgentDefinition(text, path2.basename(entry.name, path2.extname(entry.name)));
10375
+ if (parsed !== null && parsed.restricted) names.add(parsed.name);
10376
+ }
10377
+ };
10378
+ for (const root of scanRoots) walk(root, 0);
10379
+ return Array.from(names).sort();
10380
+ }
10381
+ function buildUnrestrictedSpawnAdvisory(toolInput) {
10382
+ try {
10383
+ if (getHarnessName() === "copilot_cli") return "";
10384
+ const rawType = toolInput["subagent_type"];
10385
+ const spawnType = typeof rawType === "string" ? rawType.trim() : "";
10386
+ if (spawnType !== "" && spawnType !== "general-purpose") return "";
10387
+ if (wasHintShown(SPAWN_RESTRICT_HINT_KEY)) return "";
10388
+ const names = findRestrictedAgentNames();
10389
+ if (names.length === 0) return "";
10390
+ markHintShown(SPAWN_RESTRICT_HINT_KEY);
10391
+ recordStat("session_hint", 0, 0, void 0, "agent-spawn-restrict");
10392
+ const shown = names.slice(0, SPAWN_RESTRICT_MAX_NAMES).join(", ");
10393
+ const more = names.length > SPAWN_RESTRICT_MAX_NAMES ? ` and ${names.length - SPAWN_RESTRICT_MAX_NAMES} more` : "";
10394
+ return `[token-goat] This spawn ran as general-purpose (the default when subagent_type is omitted), which is unrestricted: its lane starts by paying for every tool and MCP schema on the machine. Tools-restricted agent definitions exist here: ${shown}${more}. A future spawn that fits one of them can pass that name as subagent_type to start with a much smaller prefix. Advisory only: this spawn has already run, and this notice saved nothing.`;
10395
+ } catch {
10396
+ return "";
10397
+ }
10398
+ }
10188
10399
  function postAgentHandler(event) {
10189
10400
  try {
10190
- if (event.toolName !== "Agent" || !event.sessionId) return passOutput();
10401
+ if (!isAgentTool(event.toolName) || !event.sessionId) return passOutput();
10191
10402
  const finishedPrompt = event.toolInput["prompt"];
10192
10403
  if (typeof finishedPrompt === "string" && finishedPrompt !== "") {
10193
10404
  removeOutstandingAgentSpawn(finishedPrompt);
10194
10405
  }
10195
- const resultText = extractToolResultText(event.raw);
10406
+ const redactedReport = redactSecrets(extractToolResultText(event.raw));
10407
+ const resultText = redactedReport.text;
10196
10408
  const agentReportCfg = loadConfig().agent_report;
10197
- if (!resultText || resultText.length < agentReportCfg.min_bytes) return passOutput();
10198
- const id = storeMcpOutput(event.sessionId, "Agent", event.toolInput, resultText);
10199
- if (id === null) return passOutput();
10409
+ const spawnAdvisory = buildUnrestrictedSpawnAdvisory(event.toolInput);
10410
+ if (!resultText || resultText.length < agentReportCfg.min_bytes) {
10411
+ return spawnAdvisory === "" ? passOutput() : contextOutput(spawnAdvisory);
10412
+ }
10413
+ const id = storeMcpOutput(event.sessionId, event.toolName ?? "Agent", event.toolInput, resultText);
10414
+ if (id === null) return spawnAdvisory === "" ? passOutput() : contextOutput(spawnAdvisory);
10415
+ if (redactedReport.count > 0) recordStat("secret_redacted", 0, redactedReport.count, void 0, "agent");
10200
10416
  recordStat("session_hint", 0, 0);
10201
10417
  const recallHint = `token-goat mcp-output ${id} --full`;
10202
- const notice = `[token-goat] This subagent report (${toKB(resultText.length)}KB) is cached for later recall: ${recallHint}`;
10418
+ const notice = `[token-goat] This subagent report (${toKB(resultText.length)}KB) is cached for later recall: ${recallHint}${spawnAdvisory === "" ? "" : `
10419
+ ${spawnAdvisory}`}`;
10203
10420
  const collapsed = collapseFencedBlocks(resultText, recallHint, agentReportCfg.fence_collapse_min_lines, agentReportCfg.fence_collapse_keep_lines);
10204
10421
  const deduped = dedupeFencedBlocks(collapsed, resultText, recallHint);
10205
10422
  const final = collapseBlankRunsInFences(deduped);
@@ -10216,8 +10433,8 @@ function postAgentHandler(event) {
10216
10433
 
10217
10434
  ${notice}`;
10218
10435
  const savedBytes = originalBytes - Buffer.byteLength(updatedOutput, "utf-8");
10219
- if (savedBytes > 0) recordStat("agent_report_compact", savedBytes, Math.round(savedBytes / 4));
10220
- return { hookType: "rewriteOutput", updatedOutput };
10436
+ if (savedBytes > 0) recordStat("agent_report_compact", savedBytes, savedTokensFromBytes(savedBytes));
10437
+ return emitRewrite(updatedOutput, "agent", void 0, "counted-elsewhere");
10221
10438
  }
10222
10439
  recordStat("agent_report_compact_declined", 0, 0);
10223
10440
  }
@@ -10227,7 +10444,11 @@ ${notice}`;
10227
10444
  }
10228
10445
  }
10229
10446
  registerHook("pre_tool_use", preAgentHandler, { toolName: "Agent" });
10447
+ registerHook("pre_tool_use", preAgentHandler, { toolName: "task" });
10448
+ registerHook("pre_tool_use", preAgentHandler, { toolName: "Task" });
10230
10449
  registerHook("post_tool_use", postAgentHandler, { toolName: "Agent" });
10450
+ registerHook("post_tool_use", postAgentHandler, { toolName: "task" });
10451
+ registerHook("post_tool_use", postAgentHandler, { toolName: "Task" });
10231
10452
 
10232
10453
  // src/relay.ts
10233
10454
  function isHookEventName(name2) {
@@ -10243,7 +10464,11 @@ function buildEvent(eventName, payload) {
10243
10464
  const sessionId = typeof rawSession === "string" ? rawSession : "";
10244
10465
  const rawAgentId = obj["agent_id"] ?? obj["agentId"];
10245
10466
  const agentId = typeof rawAgentId === "string" && rawAgentId !== "" ? rawAgentId : void 0;
10246
- return { eventName, toolName, toolInput, sessionId, agentId, raw: obj };
10467
+ const rawTraceparent = obj["traceparent"] ?? obj["traceParent"] ?? process.env["TRACEPARENT"] ?? process.env["traceparent"];
10468
+ const traceparent = typeof rawTraceparent === "string" && rawTraceparent.trim() !== "" ? rawTraceparent.trim() : void 0;
10469
+ const rawTracestate = obj["tracestate"] ?? obj["traceState"] ?? process.env["TRACESTATE"] ?? process.env["tracestate"];
10470
+ const tracestate = typeof rawTracestate === "string" && rawTracestate.trim() !== "" ? rawTracestate.trim() : void 0;
10471
+ return { eventName, toolName, toolInput, sessionId, agentId, traceparent, tracestate, raw: obj };
10247
10472
  }
10248
10473
  function sessionStateKey(event) {
10249
10474
  return event.agentId !== void 0 ? `${event.sessionId}:agent:${event.agentId}` : event.sessionId;
@@ -10254,6 +10479,7 @@ function harnessForNormalization() {
10254
10479
  if (detected === "gemini") return "gemini";
10255
10480
  if (detected === "grok") return "grok";
10256
10481
  if (detected === "kimi") return "kimi";
10482
+ if (detected === "qwen") return "qwen";
10257
10483
  if (detected === "copilot_cli") return "copilot_cli";
10258
10484
  return "claude";
10259
10485
  }
@@ -10268,6 +10494,12 @@ async function relayInProcess(eventName, rawPayload) {
10268
10494
  if (!process.env["CLAUDE_CODE_SESSION_ID"] && event.sessionId) {
10269
10495
  process.env["CLAUDE_CODE_SESSION_ID"] = event.sessionId;
10270
10496
  }
10497
+ if (!process.env["TRACEPARENT"] && event.traceparent) {
10498
+ process.env["TRACEPARENT"] = event.traceparent;
10499
+ }
10500
+ if (!process.env["TRACESTATE"] && event.tracestate) {
10501
+ process.env["TRACESTATE"] = event.tracestate;
10502
+ }
10271
10503
  const stateKey = sessionStateKey(event);
10272
10504
  try {
10273
10505
  loadSessionState(stateKey);