token-goat 2.8.4 → 2.9.1

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.
@@ -1,32 +1,26 @@
1
1
  import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
3
  import {
4
- BASH_OUTPUT_SUBDIR,
4
+ DEFAULT_RECONCILE_BUDGET_MS,
5
5
  HOOK_EVENTS,
6
6
  accumulateResidentLines,
7
7
  buildCommandManifest,
8
8
  checkSymbolBodySize,
9
- commandHash,
10
9
  flattenCommandNames,
11
- getBashOutput,
12
10
  getWebOutput,
13
- indexRecallEntry,
14
- isBashEntryStale,
15
- isScopedGitStatusOrDiffStatCommand,
11
+ isReconcileClean,
16
12
  lineMayCarryResidentSignal,
17
13
  normalizePayload,
18
14
  readStdinJson,
19
15
  readTranscriptTail,
16
+ reconcileProject,
20
17
  repeatedSkillBodyHint,
21
- storeBashOutput,
22
18
  storeWebOutput,
23
- summarizeOutputDelta,
24
19
  summarizeResidentContext,
25
20
  taskListPruneHint
26
- } from "./token-goat-chunk-IZRXU64B.mjs";
21
+ } from "./token-goat-chunk-MR7MNSDR.mjs";
27
22
  import {
28
- BODY_FIRST_TOOL_RESPONSE_KEYS,
29
- OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
23
+ BASH_OUTPUT_SUBDIR,
30
24
  UNTRUSTED_TOOL_TAG,
31
25
  WEB_FETCH_KEY_SEP,
32
26
  appendDirtyPath,
@@ -36,27 +30,23 @@ import {
36
30
  classifyBashHint,
37
31
  classifyEditHint,
38
32
  clearCurlDownload,
33
+ commandHash,
39
34
  compactPathFor,
40
35
  computeAdaptiveBudget,
41
- contextOutput,
42
36
  countSymbols,
43
- denyOutput,
44
- emitRewrite,
45
37
  enqueueDirtyPathSafe,
46
38
  ensureWorkerAlive,
47
39
  estimateTokens,
48
40
  extractCompactFromMarker,
49
- extractToolResponseField,
50
- extractToolResultText,
51
- fenceUntrustedContent,
41
+ fenceWithMatches,
52
42
  formatProjectMap,
53
43
  formatShrinkSummary,
44
+ getBashOutput,
54
45
  getBashOutputId,
55
46
  getContextPressure,
56
47
  getCurlDownloadPath,
57
- getCwd,
58
48
  getFileLineRanges,
59
- getFilePath,
49
+ getFileServedOutputs,
60
50
  getGlobMatchCount,
61
51
  getGrepMatchCount,
62
52
  getLastTabContext,
@@ -66,20 +56,19 @@ import {
66
56
  getSessionBashReruns,
67
57
  getSessionFiles,
68
58
  getSessionWebFetches,
69
- getToolInput,
70
- getToolName,
71
59
  hasSeenImage,
72
60
  hasSessionOutput,
73
61
  imageQualifiesForShrink,
74
62
  incrementSkillHit,
63
+ indexRecallEntry,
75
64
  installedSkillPath,
65
+ isBashEntryStale,
76
66
  isBuildCommand,
77
- isMcpErrorResponse,
67
+ isScopedGitStatusOrDiffStatCommand,
78
68
  isTestRunnerCommand,
79
69
  listSiblingSessionStates,
80
70
  loadSessionCache,
81
71
  loadSessionState,
82
- makeDedupHintHandlers,
83
72
  markCompactStale,
84
73
  markCompacted,
85
74
  markFileTruncated,
@@ -87,7 +76,6 @@ import {
87
76
  matchesAllowPattern,
88
77
  matchesDenyPattern,
89
78
  meetsSavingsFloor,
90
- passOutput,
91
79
  probeImageMeta,
92
80
  recordBashOutput,
93
81
  recordBashRerun,
@@ -96,6 +84,7 @@ import {
96
84
  recordFileEdit,
97
85
  recordFileLineRange,
98
86
  recordFileRead,
87
+ recordFileServedOutput,
99
88
  recordGlobQuery,
100
89
  recordGrepQuery,
101
90
  recordKnownRootThrottled,
@@ -104,58 +93,84 @@ import {
104
93
  recordSeenImage,
105
94
  recordSymbolRead,
106
95
  recordWebFetch,
107
- registerHook,
108
96
  removeOutstandingAgentSpawn,
109
- runHook,
110
97
  saveSessionState,
111
- scanForInjectionPatterns,
112
- serializeOutput,
98
+ scanAndRecord,
113
99
  sessionOutputBodyBytes,
114
100
  sessionSidecarPath,
115
101
  setLastTabContext,
116
102
  shrinkImage,
103
+ storeBashOutput,
117
104
  storeBlob,
118
105
  storeOutput,
106
+ summarizeOutputDelta,
119
107
  takePendingLargeFileHint,
120
108
  visionTokens,
121
109
  visionTokensSaved,
122
110
  wasCliReadThisSession,
123
111
  wasFileReadThisSession,
124
112
  wasHintShown
125
- } from "./token-goat-chunk-4OTIB7SB.mjs";
113
+ } from "./token-goat-chunk-FDXINYK4.mjs";
126
114
  import {
127
115
  canRunWrappedShell,
128
- compressOutput,
129
- dedupeConsecutive,
130
- detectFromCommand,
131
- filterByName,
132
- hasBareBackgroundOrNewline,
133
- isRewriteWorthwhile,
134
- resolveMinNetSavingsBytes,
135
- shlexSplit
136
- } from "./token-goat-chunk-CZALRRGN.mjs";
116
+ deliveredOutputBytes
117
+ } from "./token-goat-chunk-LHMC5ENL.mjs";
137
118
  import {
119
+ BODY_FIRST_TOOL_RESPONSE_KEYS,
120
+ ENV_KEYS,
121
+ IDENTICAL_READ_MIN_BODY_BYTES,
122
+ OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
138
123
  PER_FILE_COUNTERFACTUAL_CEILING,
139
124
  VERSION,
125
+ compressOutput,
126
+ containsLineRun,
127
+ contextOutput,
128
+ countNoun,
129
+ dedupeConsecutive,
130
+ denyOutput,
131
+ detectFromCommand,
140
132
  detectHarness,
141
133
  detectLanguage,
142
134
  displaySafePath,
135
+ emitRewrite,
136
+ emitRewriteIfChanged,
137
+ envBool,
138
+ envInt,
143
139
  extractErrorMessage,
140
+ extractToolResponseField,
141
+ extractToolResultText,
142
+ filterByName,
144
143
  foldPath,
144
+ getCwd,
145
145
  getDb,
146
+ getFilePath,
146
147
  getHarnessName,
148
+ getToolInput,
149
+ getToolName,
147
150
  globalDbPath,
151
+ hasBareBackgroundOrNewline,
152
+ isMcpErrorResponse,
153
+ isRewriteWorthwhile,
148
154
  isUnderSystemTemp,
149
155
  loadConfig,
156
+ makeDedupHintHandlers,
150
157
  normalizePath,
158
+ passOutput,
151
159
  recordStat,
152
160
  redactSecrets,
161
+ registerHook,
153
162
  resolveIndexPath,
163
+ resolveMinNetSavingsBytes,
154
164
  runGit,
165
+ runHook,
155
166
  savedTokensFromBytes,
167
+ serializeOutput,
168
+ sessionStateKey,
169
+ shlexSplit,
156
170
  shortFingerprint,
171
+ stripAnsiEscapes,
157
172
  toKB
158
- } from "./token-goat-chunk-E76UNTVK.mjs";
173
+ } from "./token-goat-chunk-4ZDRTNGV.mjs";
159
174
 
160
175
  // src/hooks_grep.ts
161
176
  function grepIntInput(toolInput, key) {
@@ -630,7 +645,7 @@ import crypto from "node:crypto";
630
645
  var TRACKED_SKILL = "token-goat";
631
646
  var MAX_COMMANDS_SHOWN = 8;
632
647
  async function currentCommandNames() {
633
- const { buildProgram } = await import("./token-goat-chunk-HNODKNWO.mjs");
648
+ const { buildProgram } = await import("./token-goat-chunk-V5T3YNYR.mjs");
634
649
  return flattenCommandNames(buildCommandManifest(buildProgram()));
635
650
  }
636
651
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -886,23 +901,47 @@ registerHook("post_tool_use", pendingContextHandler, { advisory: true, followsMa
886
901
  // src/hooks_session_start.ts
887
902
  var GENERIC_REMINDER = 'token-goat: prefer surgical reads over the Read/Grep tools on this codebase; shell commands like `rg`, `grep`, `fd`, `sed`, `cat`, `find`, and `ls` are just commands, not tool names -- `token-goat symbol <name>`, `token-goat read "file::symbol"`, `token-goat section "file::Heading"`, `token-goat semantic "description"`, `token-goat outline <file>`. Run `token-goat index .` if this project is not indexed yet.';
888
903
  var INDEXED_REMINDER = 'token-goat: this project is indexed. Prefer `symbol <name>`, `read "file::symbol"`, `section "file::Heading"`, `semantic "description"`, or `outline <file>` over a full Read/Grep tool call; for JSON/YAML use `json-query file \'a.b.c\'` or `yaml-query` (nested keys are not symbols); shell commands like `rg`, `grep`, `fd`, `sed`, `cat`, `find`, and `ls` are still just commands.';
889
- function buildReminder(cwd) {
890
- if (cwd === void 0) return GENERIC_REMINDER;
891
- let symbolCount;
904
+ function isIndexedProject(cwd) {
905
+ if (cwd === void 0) return false;
892
906
  try {
893
- symbolCount = countSymbols({ rootDir: cwd }, globalDbPath());
907
+ return countSymbols({ rootDir: cwd }, globalDbPath()) > 0;
894
908
  } catch {
895
- return GENERIC_REMINDER;
909
+ return false;
910
+ }
911
+ }
912
+ function buildReminder(indexed) {
913
+ return indexed ? INDEXED_REMINDER : GENERIC_REMINDER;
914
+ }
915
+ function reconcileNote(cwd, indexed) {
916
+ if (!indexed) return null;
917
+ if (!envBool(ENV_KEYS.RECONCILE, true)) return null;
918
+ try {
919
+ const budgetMs = envInt(ENV_KEYS.RECONCILE_BUDGET_MS, DEFAULT_RECONCILE_BUDGET_MS, 0, 6e4);
920
+ const result = reconcileProject({ cwd, budgetMs });
921
+ if (isReconcileClean(result)) return null;
922
+ const parts = [];
923
+ if (result.changed.length > 0) parts.push(`${result.changed.length} changed`);
924
+ if (result.added.length > 0) parts.push(`${result.added.length} new`);
925
+ if (result.removed.length > 0) parts.push(`${result.removed.length} removed`);
926
+ const breakdown = parts.length > 1 ? ` (${parts.join(", ")})` : "";
927
+ const truncated = result.budgetExhausted ? ` (sweep stopped at its time budget with ${countNoun(result.unscanned, "file")} unchecked, so there may be more)` : "";
928
+ const total = result.changed.length + result.added.length + result.removed.length;
929
+ return `token-goat: reindexing ${countNoun(total, "file")} that changed outside this session${breakdown}${truncated}. Symbol lookups may be briefly stale.`;
930
+ } catch {
931
+ return null;
896
932
  }
897
- if (symbolCount <= 0) return GENERIC_REMINDER;
898
- return INDEXED_REMINDER;
899
933
  }
900
934
  function sessionStartHandler(event) {
901
935
  try {
902
936
  if (!loadConfig().hints.session_start_reminder) return passOutput();
903
937
  const cwd = getCwd(event);
904
- let context = buildReminder(cwd);
938
+ const indexed = isIndexedProject(cwd);
939
+ let context = buildReminder(indexed);
905
940
  if (cwd !== void 0) {
941
+ const drift = reconcileNote(cwd, indexed);
942
+ if (drift !== null) context += `
943
+
944
+ ${drift}`;
906
945
  const capsule = buildDeltaCapsule(cwd);
907
946
  if (capsule !== null) context += `
908
947
 
@@ -7035,29 +7074,13 @@ function postFetchHandler(event) {
7035
7074
  const { toolInput, url } = urlCtx;
7036
7075
  recordStat("web_fetch");
7037
7076
  const body = extractToolResponse(event.raw);
7038
- const injCfg = loadConfig().injection;
7039
- const injectionMatches = injCfg.enabled && body ? scanForInjectionPatterns(body) : [];
7040
- if (injectionMatches.length > 0) {
7041
- recordStat("injection_detected", 0, 0, void 0, injectionMatches.join(","));
7042
- }
7077
+ const injectionMatches = body ? scanAndRecord(body) : [];
7043
7078
  const bodyRedacted = body ? redactSecrets(body) : { text: body, count: 0 };
7044
7079
  if (!event.sessionId) {
7045
- if (injectionMatches.length > 0) {
7046
- return emitRewrite(fenceUntrustedContent(bodyRedacted.text, injectionMatches), "fetch");
7047
- }
7048
- if (bodyRedacted.count > 0) {
7049
- return emitRewrite(bodyRedacted.text, "fetch");
7050
- }
7051
- return passOutput();
7080
+ return emitRewriteIfChanged(body, fenceWithMatches(bodyRedacted.text, injectionMatches), "fetch");
7052
7081
  }
7053
7082
  if (!body || body.length < 1024) {
7054
- if (injectionMatches.length > 0) {
7055
- return emitRewrite(fenceUntrustedContent(bodyRedacted.text, injectionMatches), "fetch");
7056
- }
7057
- if (bodyRedacted.count > 0) {
7058
- return emitRewrite(bodyRedacted.text, "fetch");
7059
- }
7060
- return passOutput();
7083
+ return emitRewriteIfChanged(body, fenceWithMatches(bodyRedacted.text, injectionMatches), "fetch");
7061
7084
  }
7062
7085
  const prompt = typeof toolInput["prompt"] === "string" ? toolInput["prompt"] : "";
7063
7086
  let storedBody = body;
@@ -7072,29 +7095,24 @@ function postFetchHandler(event) {
7072
7095
  const cacheId = storeWebOutput(url, storedBody, `${url}\0${prompt}`, storedBody !== body ? body : void 0);
7073
7096
  recordWebFetch(url, prompt, cacheId);
7074
7097
  const storedRedacted = storedBody === body ? bodyRedacted : redactSecrets(storedBody);
7075
- if (injectionMatches.length > 0) {
7076
- return emitRewrite(fenceUntrustedContent(storedRedacted.text, injectionMatches), "fetch");
7077
- }
7098
+ const fencedStored = fenceWithMatches(storedRedacted.text, injectionMatches);
7078
7099
  if (storedBody !== body) {
7079
7100
  const noticeFor = (id) => `
7080
7101
  [token-goat: WebFetch body compressed via extractCleanText; use \`token-goat web-output ${id} --raw\` to recall it]`;
7081
7102
  const notice = noticeFor(cacheId);
7082
7103
  const noticeBytes = Buffer.byteLength(notice, "utf-8");
7083
7104
  const originalBytes = Buffer.byteLength(body, "utf-8");
7084
- const rewrittenBytes = Buffer.byteLength(storedBody, "utf-8");
7105
+ const rewrittenBytes = Buffer.byteLength(fencedStored, "utf-8");
7085
7106
  if (isRewriteWorthwhile({
7086
7107
  originalBytes,
7087
7108
  rewrittenBytes,
7088
7109
  noticeBytes,
7089
7110
  minNetSavingsBytes: resolveMinNetSavingsBytes()
7090
7111
  })) {
7091
- return emitRewrite(storedRedacted.text + notice, "fetch", { kind: "webfetch:compress", originalBytes });
7112
+ return emitRewrite(fencedStored + notice, "fetch", { kind: "webfetch:compress", originalBytes });
7092
7113
  }
7093
7114
  }
7094
- if (storedRedacted.count > 0) {
7095
- return emitRewrite(storedRedacted.text, "fetch");
7096
- }
7097
- return passOutput();
7115
+ return emitRewriteIfChanged(body, fencedStored, "fetch");
7098
7116
  } catch {
7099
7117
  return passOutput();
7100
7118
  }
@@ -7150,7 +7168,7 @@ async function preSkillHandler(event) {
7150
7168
  const denyCredit = cachedBytes !== null ? Math.min(cachedBytes, PER_FILE_COUNTERFACTUAL_CEILING) : 0;
7151
7169
  recordStat("session_hint", denyCredit, savedTokensFromBytes(denyCredit));
7152
7170
  return denyOutput(
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."
7171
+ "Skill `" + skillName + "` was already loaded this session and is cached. Use `token-goat skill-section " + skillName + " '<heading>'` to recall a section, `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."
7154
7172
  );
7155
7173
  }
7156
7174
  const sourcePath = await installedSkillPath(skillName);
@@ -7165,12 +7183,12 @@ async function preSkillHandler(event) {
7165
7183
  const savedBytes = bodyBytes - compactBytes;
7166
7184
  recordStat("skill_compact_inlined", savedBytes, savedTokensFromBytes(savedBytes));
7167
7185
  return denyOutput(
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
7186
+ "Skill `" + skillName + "` is large (" + bodyBytes + " bytes); its compact slice (" + compactBytes + " bytes) is inlined below instead of the full body. For a specific section, run `token-goat skill-section " + skillName + " '<heading>'`, or `token-goat skill-body " + skillName + "` if you need the full body.\n\n" + compact
7169
7187
  );
7170
7188
  }
7171
7189
  recordStat("skill_oversized_first_load");
7172
7190
  return denyOutput(
7173
- "Skill `" + skillName + "` is large (" + bodyBytes + " bytes) and has a compact slice available. Use `token-goat skill-body " + skillName + " --compact` to load the compact slice instead of the full body."
7191
+ "Skill `" + skillName + "` is large (" + bodyBytes + " bytes) and has a compact slice available. Use `token-goat skill-section " + skillName + " '<heading>'` to load a specific section, `token-goat skill-body " + skillName + " --compact` to load the compact slice, or `token-goat skill-body " + skillName + "` for the full body."
7174
7192
  );
7175
7193
  }
7176
7194
  } catch {
@@ -8403,6 +8421,40 @@ function maybeCompressRewrite(event, rawCmd, cmd) {
8403
8421
  const wrapped = `token-goat compress -f ${filterName} --timeout ${cfg.timeout_seconds} -c ${shellQuoteSingle(rawCmd)}`;
8404
8422
  return { hookType: "rewriteInput", updatedInput: { ...event.toolInput, command: wrapped } };
8405
8423
  }
8424
+ function pureFileReadPath(cmd) {
8425
+ return extractCatFile(cmd)?.filePath ?? extractHeadFile(cmd)?.filePath ?? extractTailFile(cmd)?.filePath ?? extractLineRangeRead(cmd)?.filePath ?? null;
8426
+ }
8427
+ async function maybeCollapseIdenticalRead(cmd, output, exitCode, cwd, cacheMinBytes) {
8428
+ if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
8429
+ if (exitCode !== null && exitCode !== 0) return null;
8430
+ const filePath = pureFileReadPath(cmd);
8431
+ if (filePath === null) return null;
8432
+ const originalBytes = Buffer.byteLength(output, "utf-8");
8433
+ if (originalBytes < Math.max(cacheMinBytes, IDENTICAL_READ_MIN_BODY_BYTES)) return null;
8434
+ const fileKey = resolveIndexPath(filePath, cwd ?? process.cwd());
8435
+ const priorIds = getFileServedOutputs(fileKey);
8436
+ let containerId = null;
8437
+ let identical = false;
8438
+ for (let i = priorIds.length - 1; i >= 0; i--) {
8439
+ const id = priorIds[i];
8440
+ if (id === void 0) continue;
8441
+ const prior = getBashOutput(id);
8442
+ if (prior === null || !containsLineRun(prior.output, output)) continue;
8443
+ containerId = id;
8444
+ identical = prior.output === output;
8445
+ break;
8446
+ }
8447
+ const sessionKey = shortFingerprint(stripOutputPipeline(cmd));
8448
+ if (containerId === null) {
8449
+ const storedId = await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
8450
+ recordBashOutput(sessionKey, storedId, originalBytes);
8451
+ recordFileServedOutput(fileKey, storedId);
8452
+ return null;
8453
+ }
8454
+ const pointer = identical ? "[token-goat] Identical to an earlier run of this command in this session; the file has not changed since. " + originalBytes + " bytes withheld -- recall them with `token-goat bash-output " + containerId + "`." : "[token-goat] These " + originalBytes + " bytes already appear verbatim inside a wider read of " + filePath + " served earlier in this session. Withheld -- recall the full earlier output with `token-goat bash-output " + containerId + "`.";
8455
+ if (!isRewriteWorthwhile({ originalBytes, rewrittenBytes: Buffer.byteLength(pointer, "utf-8"), noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() })) return null;
8456
+ return emitRewrite(pointer, identical ? "identical file re-read collapsed" : "already-served file lines collapsed", { kind: identical ? "bash_compress:identical-reread" : "bash_compress:contained-reread", originalBytes: deliveredOutputBytes(originalBytes) });
8457
+ }
8406
8458
  async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinBytes) {
8407
8459
  if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
8408
8460
  if (isCompressibleSingleCommand(cmd)) return null;
@@ -8435,7 +8487,28 @@ async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinB
8435
8487
  return null;
8436
8488
  }
8437
8489
  await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
8438
- return emitRewrite(body, "bash", { kind: "bash_compress:generic", originalBytes: compressed.originalBytes });
8490
+ return emitRewrite(body, "bash", { kind: "bash_compress:generic", originalBytes: deliveredOutputBytes(compressed.originalBytes) });
8491
+ }
8492
+ function maybeStripAnsiOnly(output) {
8493
+ if (!output.includes("\x1B")) return null;
8494
+ let cfg;
8495
+ try {
8496
+ cfg = loadConfig().bash_compress;
8497
+ } catch {
8498
+ return null;
8499
+ }
8500
+ if (!cfg.enabled || cfg.disabled_filters.includes("ansi")) return null;
8501
+ const stripped = stripAnsiEscapes(output);
8502
+ const originalBytes = Buffer.byteLength(output, "utf-8");
8503
+ if (!isRewriteWorthwhile({
8504
+ originalBytes,
8505
+ rewrittenBytes: Buffer.byteLength(stripped, "utf-8"),
8506
+ noticeBytes: 0,
8507
+ minNetSavingsBytes: resolveMinNetSavingsBytes()
8508
+ })) {
8509
+ return null;
8510
+ }
8511
+ return emitRewrite(stripped, "ansi escapes stripped", { kind: "bash_compress:ansi", originalBytes: deliveredOutputBytes(originalBytes) }, "counted-elsewhere");
8439
8512
  }
8440
8513
  function unwrapCompressCommand(executed) {
8441
8514
  const t = executed.trim();
@@ -9122,9 +9195,11 @@ async function postBashHandler(event) {
9122
9195
  }
9123
9196
  const isMonitoring = getMonitoringRecallHint(cmd) !== null;
9124
9197
  if (!isMonitoring && !isBuildCommand(cmd) && !isCurlGetCommand(cmd)) {
9198
+ const identical = await maybeCollapseIdenticalRead(cmd, output, exitCode, cwd, cacheMinBytes);
9199
+ if (identical !== null) return identical;
9125
9200
  const compound = await maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinBytes);
9126
9201
  if (compound !== null) return compound;
9127
- return passOutput();
9202
+ return maybeStripAnsiOnly(output) ?? passOutput();
9128
9203
  }
9129
9204
  if (Buffer.byteLength(output, "utf-8") < cacheMinBytes) return passOutput();
9130
9205
  const cacheKey = isCurlGetCommand(cmd) ? extractCurlUrl(cmd) ?? cmd : stripOutputPipeline(cmd);
@@ -9141,6 +9216,8 @@ async function postBashHandler(event) {
9141
9216
  return contextOutput(delta + " \u2014 full output: bash-output " + id);
9142
9217
  }
9143
9218
  }
9219
+ const ansiOnly = maybeStripAnsiOnly(output);
9220
+ if (ansiOnly !== null) return ansiOnly;
9144
9221
  } catch {
9145
9222
  }
9146
9223
  return passOutput();
@@ -9839,23 +9916,14 @@ function postMcpHandler(event) {
9839
9916
  const toolInput = getToolInput(event);
9840
9917
  const resultText = extractToolResultText(event.raw);
9841
9918
  if (!resultText) return passOutput();
9842
- let injectionMatches = [];
9843
- try {
9844
- if (loadConfig().injection.enabled) injectionMatches = scanForInjectionPatterns(resultText);
9845
- } catch {
9846
- injectionMatches = [];
9847
- }
9848
- if (injectionMatches.length > 0) recordStat("injection_detected", 0, 0, void 0, injectionMatches.join(","));
9919
+ const injectionMatches = scanAndRecord(resultText);
9849
9920
  const redactedResult = redactSecrets(resultText);
9850
- const fenced = () => emitRewrite(
9851
- fenceUntrustedContent(redactedResult.text, injectionMatches, UNTRUSTED_TOOL_TAG),
9921
+ const fenced = () => emitRewriteIfChanged(
9922
+ resultText,
9923
+ fenceWithMatches(redactedResult.text, injectionMatches, UNTRUSTED_TOOL_TAG),
9852
9924
  "mcp"
9853
9925
  );
9854
- const passOrFence = () => {
9855
- if (injectionMatches.length > 0) return fenced();
9856
- if (redactedResult.count > 0) return emitRewrite(redactedResult.text, "mcp");
9857
- return passOutput();
9858
- };
9926
+ const passOrFence = () => fenced();
9859
9927
  if (!event.sessionId) return passOrFence();
9860
9928
  if (isMcpErrorResponse(event.raw)) return passOrFence();
9861
9929
  const readOnly = isMcpReadOnly(toolName, toolInput);
@@ -9882,7 +9950,7 @@ function postMcpHandler(event) {
9882
9950
  });
9883
9951
  if (worthwhile) {
9884
9952
  return emitRewrite(
9885
- injectionMatches.length > 0 ? `${notice}${fenceUntrustedContent(redactedBody, injectionMatches, UNTRUSTED_TOOL_TAG)}` : `${notice}${redactedBody}`,
9953
+ `${notice}${fenceWithMatches(redactedBody, injectionMatches, UNTRUSTED_TOOL_TAG)}`,
9886
9954
  "mcp",
9887
9955
  { kind: "mcp:compress", originalBytes }
9888
9956
  );
@@ -9934,23 +10002,9 @@ function postWebSearchHandler(event) {
9934
10002
  if (getToolName(event) !== "WebSearch") return passOutput();
9935
10003
  const resultText = extractToolResultText(event.raw);
9936
10004
  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(","));
10005
+ const injectionMatches = scanAndRecord(resultText);
9944
10006
  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
- };
10007
+ const passOrFence = () => emitRewriteIfChanged(resultText, fenceWithMatches(redacted.text, injectionMatches), "websearch");
9954
10008
  if (!event.sessionId) return passOrFence();
9955
10009
  const signature = webSearchSignatureInput(getToolInput(event));
9956
10010
  if (signature === null) return passOrFence();
@@ -10470,9 +10524,6 @@ function buildEvent(eventName, payload) {
10470
10524
  const tracestate = typeof rawTracestate === "string" && rawTracestate.trim() !== "" ? rawTracestate.trim() : void 0;
10471
10525
  return { eventName, toolName, toolInput, sessionId, agentId, traceparent, tracestate, raw: obj };
10472
10526
  }
10473
- function sessionStateKey(event) {
10474
- return event.agentId !== void 0 ? `${event.sessionId}:agent:${event.agentId}` : event.sessionId;
10475
- }
10476
10527
  function harnessForNormalization() {
10477
10528
  const detected = detectHarness();
10478
10529
  if (detected === "codex") return "codex";
@@ -10510,7 +10561,7 @@ async function relayInProcess(eventName, rawPayload) {
10510
10561
  saveSessionState(stateKey);
10511
10562
  } catch {
10512
10563
  }
10513
- return serializeOutput(output, event.eventName, harness);
10564
+ return serializeOutput(output, event.eventName, harness, event);
10514
10565
  } catch {
10515
10566
  return "{}";
10516
10567
  }