@royalcat/opencode-dcp-rc 4.0.0 → 4.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.
Files changed (55) hide show
  1. package/README.md +21 -1
  2. package/dist/index.js +1402 -27
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/commands/stats.d.ts +3 -1
  5. package/dist/lib/commands/stats.d.ts.map +1 -1
  6. package/dist/lib/compress/rc.d.ts.map +1 -1
  7. package/dist/lib/compress/summary.d.ts +11 -1
  8. package/dist/lib/compress/summary.d.ts.map +1 -1
  9. package/dist/lib/compress/types.d.ts +10 -1
  10. package/dist/lib/compress/types.d.ts.map +1 -1
  11. package/dist/lib/compress/usage.d.ts +45 -0
  12. package/dist/lib/compress/usage.d.ts.map +1 -0
  13. package/dist/lib/config.d.ts +2 -0
  14. package/dist/lib/config.d.ts.map +1 -1
  15. package/dist/lib/prompts/context-limit-nudge.d.ts +1 -1
  16. package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -1
  17. package/dist/lib/prompts/extensions/system.d.ts +2 -2
  18. package/dist/lib/prompts/extensions/system.d.ts.map +1 -1
  19. package/dist/lib/prompts/iteration-nudge.d.ts +1 -1
  20. package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -1
  21. package/dist/lib/prompts/turn-nudge.d.ts +1 -1
  22. package/dist/lib/prompts/turn-nudge.d.ts.map +1 -1
  23. package/dist/lib/state/persistence.d.ts +2 -0
  24. package/dist/lib/state/persistence.d.ts.map +1 -1
  25. package/dist/lib/state/state.d.ts.map +1 -1
  26. package/dist/lib/state/types.d.ts +3 -0
  27. package/dist/lib/state/types.d.ts.map +1 -1
  28. package/dist/lib/tui/dialogs.d.ts.map +1 -1
  29. package/dist/lib/v2/index.d.ts.map +1 -1
  30. package/dist/lib/v2/rpc.d.ts +20 -0
  31. package/dist/lib/v2/rpc.d.ts.map +1 -1
  32. package/dist/lib/v2/scrub.d.ts +94 -0
  33. package/dist/lib/v2/scrub.d.ts.map +1 -0
  34. package/dist/lib/v2/usage.d.ts +45 -0
  35. package/dist/lib/v2/usage.d.ts.map +1 -0
  36. package/lib/commands/stats.ts +21 -0
  37. package/lib/compress/rc.ts +43 -7
  38. package/lib/compress/summary.ts +28 -1
  39. package/lib/compress/types.ts +14 -1
  40. package/lib/compress/usage.ts +168 -0
  41. package/lib/config.ts +30 -0
  42. package/lib/prompts/context-limit-nudge.ts +2 -0
  43. package/lib/prompts/extensions/system.ts +4 -0
  44. package/lib/prompts/iteration-nudge.ts +2 -0
  45. package/lib/prompts/system.ts +1 -1
  46. package/lib/prompts/turn-nudge.ts +2 -0
  47. package/lib/state/persistence.ts +18 -2
  48. package/lib/state/state.ts +4 -0
  49. package/lib/state/types.ts +3 -0
  50. package/lib/tui/dialogs.tsx +24 -0
  51. package/lib/v2/index.ts +58 -5
  52. package/lib/v2/rpc.ts +12 -0
  53. package/lib/v2/scrub.ts +954 -0
  54. package/lib/v2/usage.ts +450 -0
  55. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -913,6 +913,8 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
913
913
  "compress.protectedTools",
914
914
  "compress.protectTags",
915
915
  "compress.protectUserMessages",
916
+ "compress.scrubModelOutput",
917
+ "compress.scrubMessageIds",
916
918
  "strategies",
917
919
  "strategies.deduplication",
918
920
  "strategies.deduplication.enabled",
@@ -1146,6 +1148,20 @@ function validateConfigTypes(config) {
1146
1148
  actual: typeof compress.protectUserMessages
1147
1149
  });
1148
1150
  }
1151
+ if (compress.scrubModelOutput !== void 0 && typeof compress.scrubModelOutput !== "boolean") {
1152
+ errors.push({
1153
+ key: "compress.scrubModelOutput",
1154
+ expected: "boolean",
1155
+ actual: typeof compress.scrubModelOutput
1156
+ });
1157
+ }
1158
+ if (compress.scrubMessageIds !== void 0 && typeof compress.scrubMessageIds !== "boolean") {
1159
+ errors.push({
1160
+ key: "compress.scrubMessageIds",
1161
+ expected: "boolean",
1162
+ actual: typeof compress.scrubMessageIds
1163
+ });
1164
+ }
1149
1165
  if (typeof compress.iterationNudgeThreshold === "number" && compress.iterationNudgeThreshold < 1) {
1150
1166
  errors.push({
1151
1167
  key: "compress.iterationNudgeThreshold",
@@ -1331,7 +1347,9 @@ var defaultConfig = {
1331
1347
  nudgeForce: "soft",
1332
1348
  protectedTools: [...COMPRESS_DEFAULT_PROTECTED_TOOLS],
1333
1349
  protectTags: false,
1334
- protectUserMessages: false
1350
+ protectUserMessages: false,
1351
+ scrubModelOutput: true,
1352
+ scrubMessageIds: true
1335
1353
  },
1336
1354
  strategies: {
1337
1355
  deduplication: {
@@ -1456,7 +1474,9 @@ function mergeCompress(base, override) {
1456
1474
  nudgeForce: override.nudgeForce ?? base.nudgeForce,
1457
1475
  protectedTools: [.../* @__PURE__ */ new Set([...base.protectedTools, ...override.protectedTools ?? []])],
1458
1476
  protectTags: override.protectTags ?? base.protectTags,
1459
- protectUserMessages: override.protectUserMessages ?? base.protectUserMessages
1477
+ protectUserMessages: override.protectUserMessages ?? base.protectUserMessages,
1478
+ scrubModelOutput: override.scrubModelOutput ?? base.scrubModelOutput,
1479
+ scrubMessageIds: override.scrubMessageIds ?? base.scrubMessageIds
1460
1480
  };
1461
1481
  }
1462
1482
  function mergeCommands(base, override) {
@@ -1791,7 +1811,7 @@ You operate in a context-constrained environment. Manage context continuously to
1791
1811
 
1792
1812
  The ONLY tool you have for context management is \`compress\`. It replaces older conversation content with compact technical summaries.
1793
1813
 
1794
- ${markers} and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
1814
+ ${markers} and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output, quote, or mention them in your replies or reasoning.
1795
1815
 
1796
1816
  THE PHILOSOPHY OF COMPRESS
1797
1817
  \`compress\` transforms conversation content into dense, high-fidelity summaries. This is not cleanup - it is crystallization. Your summary becomes the authoritative record of what transpired.
@@ -1863,6 +1883,8 @@ Avoid the newest active working messages unless it is clearly closed.
1863
1883
  SUMMARY REQUIREMENTS
1864
1884
  Your summary MUST cover all essential details from the selected messages so work can continue.
1865
1885
  If the compressed range includes user messages, preserve user intent exactly. Prefer direct quotes for short user messages to avoid semantic drift.
1886
+
1887
+ Never repeat or quote this reminder in your reply.
1866
1888
  </dcp-system-reminder>
1867
1889
  `;
1868
1890
 
@@ -1875,6 +1897,8 @@ If direction has shifted, compress earlier ranges that are now less relevant.
1875
1897
 
1876
1898
  The goal is to filter noise and distill key information so context accumulation stays under control.
1877
1899
  Keep active context uncompressed.
1900
+
1901
+ Never repeat or quote this reminder in your reply.
1878
1902
  </dcp-system-reminder>
1879
1903
  `;
1880
1904
 
@@ -1883,6 +1907,8 @@ var ITERATION_NUDGE = `<dcp-system-reminder>
1883
1907
  You've been iterating for a while after the last user message.
1884
1908
 
1885
1909
  If there is a closed portion that is unlikely to be referenced immediately (for example, finished research before implementation), use the compress tool on it now.
1910
+
1911
+ Never repeat or quote this reminder in your reply.
1886
1912
  </dcp-system-reminder>
1887
1913
  `;
1888
1914
 
@@ -1895,6 +1921,8 @@ Only use the compress tool after seeing \`<compress triggered manually>\` in the
1895
1921
  Issue exactly ONE compress tool per manual trigger. Do NOT launch multiple compress tools in parallel. Each trigger grants a single compression; after it completes, wait for the next trigger.
1896
1922
 
1897
1923
  After completing a manually triggered context-management action, STOP IMMEDIATELY. Do NOT continue with any task execution. End your response right after the tool use completes and wait for the next user input.
1924
+
1925
+ Never repeat this notice in your reply.
1898
1926
  </dcp-system-reminder>
1899
1927
  `;
1900
1928
  var SUBAGENT_SYSTEM_EXTENSION = `<dcp-system-reminder>
@@ -1903,6 +1931,8 @@ You are operating in a subagent environment.
1903
1931
  The initial subagent instruction is imperative and must be followed exactly.
1904
1932
  It is the only user message intentionally not assigned a message ID, and therefore is not eligible for compression.
1905
1933
  All subsequent messages in the session will have IDs.
1934
+
1935
+ Never repeat this notice in your reply.
1906
1936
  </dcp-system-reminder>
1907
1937
  `;
1908
1938
  function buildProtectedToolsExtension(protectedTools) {
@@ -2250,6 +2280,102 @@ import { existsSync as existsSync4 } from "fs";
2250
2280
  import { homedir as homedir4 } from "os";
2251
2281
  import { join as join4 } from "path";
2252
2282
 
2283
+ // lib/compress/usage.ts
2284
+ function emptyCompressionUsage() {
2285
+ return {
2286
+ calls: 0,
2287
+ providerCalls: 0,
2288
+ estimatedCalls: 0,
2289
+ inputTokens: 0,
2290
+ outputTokens: 0,
2291
+ cacheReadTokens: 0,
2292
+ cacheWriteTokens: 0,
2293
+ reasoningTokens: 0
2294
+ };
2295
+ }
2296
+ function toCount(value, fallback = 0) {
2297
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
2298
+ return fallback;
2299
+ }
2300
+ return Math.round(value);
2301
+ }
2302
+ function normalizeProviderUsage(raw) {
2303
+ if (!raw || typeof raw !== "object") {
2304
+ return void 0;
2305
+ }
2306
+ const value = raw;
2307
+ const input = value.inputTokens && typeof value.inputTokens === "object" ? value.inputTokens : {};
2308
+ const output2 = value.outputTokens && typeof value.outputTokens === "object" ? value.outputTokens : {};
2309
+ const cacheRead = toCount(input.cacheRead);
2310
+ const cacheWrite = toCount(input.cacheWrite);
2311
+ const uncached = toCount(input.noCache);
2312
+ const inputFromParts = uncached + cacheRead + cacheWrite;
2313
+ const inputTotal = Math.max(toCount(input.total, inputFromParts), inputFromParts);
2314
+ const reasoning = toCount(output2.reasoning);
2315
+ const outputTotal = Math.max(toCount(output2.total, toCount(output2.text) + reasoning), reasoning);
2316
+ if (inputTotal === 0 && outputTotal === 0 && cacheRead === 0 && cacheWrite === 0 && reasoning === 0) {
2317
+ return void 0;
2318
+ }
2319
+ return {
2320
+ inputTokens: inputTotal,
2321
+ outputTokens: outputTotal,
2322
+ cacheReadTokens: cacheRead,
2323
+ cacheWriteTokens: cacheWrite,
2324
+ reasoningTokens: reasoning,
2325
+ source: "provider"
2326
+ };
2327
+ }
2328
+ function estimatedCompressionUsage(inputTokens, outputTokens) {
2329
+ return {
2330
+ inputTokens: toCount(inputTokens),
2331
+ outputTokens: toCount(outputTokens),
2332
+ cacheReadTokens: 0,
2333
+ cacheWriteTokens: 0,
2334
+ reasoningTokens: 0,
2335
+ source: "estimated"
2336
+ };
2337
+ }
2338
+ function addCompressionUsage(totals, usage) {
2339
+ return {
2340
+ calls: totals.calls + 1,
2341
+ providerCalls: totals.providerCalls + (usage.source === "provider" ? 1 : 0),
2342
+ estimatedCalls: totals.estimatedCalls + (usage.source === "estimated" ? 1 : 0),
2343
+ inputTokens: totals.inputTokens + usage.inputTokens,
2344
+ outputTokens: totals.outputTokens + usage.outputTokens,
2345
+ cacheReadTokens: totals.cacheReadTokens + usage.cacheReadTokens,
2346
+ cacheWriteTokens: totals.cacheWriteTokens + usage.cacheWriteTokens,
2347
+ reasoningTokens: totals.reasoningTokens + usage.reasoningTokens
2348
+ };
2349
+ }
2350
+ function addCompressionUsageTotals(totals, other) {
2351
+ return {
2352
+ calls: totals.calls + other.calls,
2353
+ providerCalls: totals.providerCalls + other.providerCalls,
2354
+ estimatedCalls: totals.estimatedCalls + other.estimatedCalls,
2355
+ inputTokens: totals.inputTokens + other.inputTokens,
2356
+ outputTokens: totals.outputTokens + other.outputTokens,
2357
+ cacheReadTokens: totals.cacheReadTokens + other.cacheReadTokens,
2358
+ cacheWriteTokens: totals.cacheWriteTokens + other.cacheWriteTokens,
2359
+ reasoningTokens: totals.reasoningTokens + other.reasoningTokens
2360
+ };
2361
+ }
2362
+ function normalizeCompressionUsageTotals(value) {
2363
+ if (!value || typeof value !== "object") {
2364
+ return emptyCompressionUsage();
2365
+ }
2366
+ const raw = value;
2367
+ return {
2368
+ calls: toCount(raw.calls),
2369
+ providerCalls: toCount(raw.providerCalls),
2370
+ estimatedCalls: toCount(raw.estimatedCalls),
2371
+ inputTokens: toCount(raw.inputTokens),
2372
+ outputTokens: toCount(raw.outputTokens),
2373
+ cacheReadTokens: toCount(raw.cacheReadTokens),
2374
+ cacheWriteTokens: toCount(raw.cacheWriteTokens),
2375
+ reasoningTokens: toCount(raw.reasoningTokens)
2376
+ };
2377
+ }
2378
+
2253
2379
  // lib/messages/shape.ts
2254
2380
  function isMessageWithInfo(message) {
2255
2381
  if (!message || typeof message !== "object") {
@@ -2824,7 +2950,8 @@ function emptyPersistedState(manualMode) {
2824
2950
  },
2825
2951
  stats: {
2826
2952
  pruneTokenCounter: 0,
2827
- totalPruneTokens: 0
2953
+ totalPruneTokens: 0,
2954
+ compressionUsage: emptyCompressionUsage()
2828
2955
  },
2829
2956
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
2830
2957
  };
@@ -2845,7 +2972,8 @@ async function loadAllSessionStats(logger) {
2845
2972
  totalTokens: 0,
2846
2973
  totalTools: 0,
2847
2974
  totalMessages: 0,
2848
- sessionCount: 0
2975
+ sessionCount: 0,
2976
+ compressionUsage: emptyCompressionUsage()
2849
2977
  };
2850
2978
  try {
2851
2979
  if (!existsSync4(STORAGE_DIR)) {
@@ -2858,10 +2986,16 @@ async function loadAllSessionStats(logger) {
2858
2986
  const filePath = join4(STORAGE_DIR, file);
2859
2987
  const content = await fs.readFile(filePath, "utf-8");
2860
2988
  const state = JSON.parse(content);
2861
- if (state?.stats?.totalPruneTokens && state?.prune) {
2862
- result.totalTokens += state.stats.totalPruneTokens;
2989
+ const usage = normalizeCompressionUsageTotals(state?.stats?.compressionUsage);
2990
+ const hasUsage = usage.calls > 0;
2991
+ if (state?.prune && (state?.stats?.totalPruneTokens || hasUsage)) {
2992
+ result.totalTokens += state.stats?.totalPruneTokens || 0;
2863
2993
  result.totalTools += state.prune.tools ? Object.keys(state.prune.tools).length : 0;
2864
2994
  result.totalMessages += state.prune.messages?.byMessageId ? Object.keys(state.prune.messages.byMessageId).length : 0;
2995
+ result.compressionUsage = addCompressionUsageTotals(
2996
+ result.compressionUsage,
2997
+ usage
2998
+ );
2865
2999
  result.sessionCount++;
2866
3000
  }
2867
3001
  } catch {
@@ -3302,7 +3436,8 @@ function createSessionState(idFormat = "xml") {
3302
3436
  },
3303
3437
  stats: {
3304
3438
  pruneTokenCounter: 0,
3305
- totalPruneTokens: 0
3439
+ totalPruneTokens: 0,
3440
+ compressionUsage: emptyCompressionUsage()
3306
3441
  },
3307
3442
  compressionTiming: {
3308
3443
  startsByCallId: /* @__PURE__ */ new Map(),
@@ -3339,7 +3474,8 @@ function resetSessionState(state) {
3339
3474
  };
3340
3475
  state.stats = {
3341
3476
  pruneTokenCounter: 0,
3342
- totalPruneTokens: 0
3477
+ totalPruneTokens: 0,
3478
+ compressionUsage: emptyCompressionUsage()
3343
3479
  };
3344
3480
  state.toolParameters.clear();
3345
3481
  state.subAgentResultCache.clear();
@@ -3385,7 +3521,8 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
3385
3521
  );
3386
3522
  state.stats = {
3387
3523
  pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
3388
- totalPruneTokens: persisted.stats?.totalPruneTokens || 0
3524
+ totalPruneTokens: persisted.stats?.totalPruneTokens || 0,
3525
+ compressionUsage: normalizeCompressionUsageTotals(persisted.stats?.compressionUsage)
3389
3526
  };
3390
3527
  const applied = applyPendingCompressionDurations(state);
3391
3528
  if (applied > 0) {
@@ -4562,6 +4699,22 @@ ${output2}`);
4562
4699
 
4563
4700
  // lib/compress/summary.ts
4564
4701
  var RC_SUMMARY_MARKER = "[[DCP-RC-SUMMARY]]";
4702
+ var RC_SUMMARY_MARKER_PREFIX = "[[DCP-RC-SUMMARY";
4703
+ function sanitizeCallId(callId) {
4704
+ return callId.replace(/[^A-Za-z0-9_-]/g, "-");
4705
+ }
4706
+ function buildSummaryMarker(callId) {
4707
+ if (!callId) {
4708
+ return RC_SUMMARY_MARKER;
4709
+ }
4710
+ return `${RC_SUMMARY_MARKER_PREFIX}:${sanitizeCallId(callId)}]]`;
4711
+ }
4712
+ function hasSummaryMarker(text) {
4713
+ return text.includes(RC_SUMMARY_MARKER_PREFIX);
4714
+ }
4715
+ function extractSummaryCallId(text) {
4716
+ return text.match(/\[\[DCP-RC-SUMMARY:([A-Za-z0-9_-]+)\]\]/)?.[1];
4717
+ }
4565
4718
  function stringify(value) {
4566
4719
  if (typeof value === "string") {
4567
4720
  return value;
@@ -4641,7 +4794,7 @@ var SUMMARY_RULES = `RULES
4641
4794
  - Preserve every high-signal detail: user intent (quote short user messages exactly), decisions, constraints, file paths, function and symbol names, commands run, key findings, errors, verification results, and any subagent task IDs with their scope and completion state.
4642
4795
  - Drop noise: failed attempts that led nowhere, repeated tool output, exploration that produced no durable result.
4643
4796
  - Be dense and precise. Prefer terse technical wording over narrative.`;
4644
- function buildSummaryPrompt(selectors, idFormat = "xml") {
4797
+ function buildSummaryPrompt(selectors, idFormat = "xml", callId) {
4645
4798
  const selectorList = selectors.map((segment) => segment.selector);
4646
4799
  const example = selectorList[0] ?? (idFormat === "compact" ? "" : "m0004");
4647
4800
  const body = selectors.map((segment) => {
@@ -4652,7 +4805,7 @@ ${prior.body.trim()}`
4652
4805
  ).join("\n\n");
4653
4806
  return [messageBody, priorBody].filter((part) => part.length > 0).join("\n\n");
4654
4807
  }).join("\n\n");
4655
- return `${RC_SUMMARY_MARKER}
4808
+ return `${buildSummaryMarker(callId)}
4656
4809
 
4657
4810
  You generate compressed-context entries for a coding session. Summarize ONLY the conversation segments listed below. Do not summarize anything else and do not ask questions.
4658
4811
 
@@ -4759,6 +4912,11 @@ function parseSummarySections(text, expected, idFormat = "xml") {
4759
4912
  }
4760
4913
 
4761
4914
  // lib/compress/rc.ts
4915
+ var summaryCallCounter = 0;
4916
+ function nextSummaryCallId() {
4917
+ summaryCallCounter += 1;
4918
+ return `c${summaryCallCounter.toString(36)}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
4919
+ }
4762
4920
  function validateArgs(args) {
4763
4921
  if (!args || typeof args !== "object") {
4764
4922
  throw new Error("compress requires an object argument with an `ids` array");
@@ -4905,7 +5063,12 @@ function createRcCompressTool(ctx) {
4905
5063
  messages: plan.messages,
4906
5064
  priorSummaries: plan.priorSummaries
4907
5065
  }));
4908
- const text = await generateSummaries(ctx, sessionID, segments);
5066
+ const { text, usage } = await generateSummaries(ctx, sessionID, segments);
5067
+ ctx.state.stats.compressionUsage = addCompressionUsageTotals(
5068
+ ctx.state.stats.compressionUsage,
5069
+ usage
5070
+ );
5071
+ await saveSessionState(ctx.state, ctx.logger);
4909
5072
  const accepted = await resolveAcceptedSections(
4910
5073
  ctx,
4911
5074
  plans.map((plan) => plan.selector.selector),
@@ -4944,8 +5107,19 @@ function createRcCompressTool(ctx) {
4944
5107
  });
4945
5108
  }
4946
5109
  async function generateSummaries(ctx, sessionID, segments) {
4947
- const prompt = buildSummaryPrompt(segments, ctx.state.idFormat);
4948
- let text = await ctx.generate({ sessionID, prompt });
5110
+ let usage = emptyCompressionUsage();
5111
+ const summarize = async (promptSegments) => {
5112
+ const callId = nextSummaryCallId();
5113
+ const prompt = buildSummaryPrompt(promptSegments, ctx.state.idFormat, callId);
5114
+ const result = await ctx.generate({ sessionID, prompt, callId });
5115
+ const text2 = result.text ?? "";
5116
+ usage = addCompressionUsage(
5117
+ usage,
5118
+ result.usage ?? estimatedCompressionUsage(countTokens2(prompt), countTokens2(text2))
5119
+ );
5120
+ return text2;
5121
+ };
5122
+ let text = await summarize(segments);
4949
5123
  for (let retry = 0; retry < 2; retry++) {
4950
5124
  const parsed = parseSummarySections(
4951
5125
  text,
@@ -4962,14 +5136,13 @@ async function generateSummaries(ctx, sessionID, segments) {
4962
5136
  if (retrySegments.length === 0) {
4963
5137
  break;
4964
5138
  }
4965
- const retryPrompt = buildSummaryPrompt(retrySegments, ctx.state.idFormat);
4966
5139
  try {
4967
- text += "\n\n" + await ctx.generate({ sessionID, prompt: retryPrompt });
5140
+ text += "\n\n" + await summarize(retrySegments);
4968
5141
  } catch {
4969
5142
  break;
4970
5143
  }
4971
5144
  }
4972
- return text;
5145
+ return { text, usage };
4973
5146
  }
4974
5147
  async function resolveAcceptedSections(ctx, selectors, text) {
4975
5148
  const parsed = parseSummarySections(text, selectors, ctx.state.idFormat);
@@ -5081,6 +5254,1140 @@ async function resetManualMode(ctx, sessionID) {
5081
5254
  await saveSessionState(ctx.state, ctx.logger);
5082
5255
  }
5083
5256
 
5257
+ // lib/v2/scrub.ts
5258
+ function outputScrubEnabled(config) {
5259
+ return config.modelOutput || config.messageIds;
5260
+ }
5261
+ var DCP_PAIRED_TAG_REGEX = /<dcp[^>]*>[\s\S]*?<\/dcp[^>]*>/gi;
5262
+ var DCP_UNPAIRED_TAG_REGEX = /<\/?dcp[^>]*>/gi;
5263
+ var ID_LINE_REGEX = /^[ \t]*@(?:[1-9]\d*|b[1-9]\d*|blocked)@(?:[ \t]+\[(?:low|medium|high)\])?[ \t]*$/;
5264
+ function isIdLine(line) {
5265
+ const value = line.endsWith("\r") ? line.slice(0, -1) : line;
5266
+ return ID_LINE_REGEX.test(value);
5267
+ }
5268
+ function isIdLinePrefix(text) {
5269
+ let index = 0;
5270
+ while (index < text.length && (text[index] === " " || text[index] === " ")) {
5271
+ index += 1;
5272
+ }
5273
+ if (index === text.length) {
5274
+ return true;
5275
+ }
5276
+ if (text[index] !== "@") {
5277
+ return false;
5278
+ }
5279
+ index += 1;
5280
+ if (index === text.length) {
5281
+ return true;
5282
+ }
5283
+ if (text.startsWith("blocked", index)) {
5284
+ index += 7;
5285
+ } else if (text[index] === "b") {
5286
+ if (index + 1 === text.length) {
5287
+ return true;
5288
+ }
5289
+ if (!/\d/.test(text[index + 1])) {
5290
+ return false;
5291
+ }
5292
+ index += 2;
5293
+ while (index < text.length && /\d/.test(text[index])) {
5294
+ index += 1;
5295
+ }
5296
+ } else if (/[1-9]/.test(text[index])) {
5297
+ index += 1;
5298
+ while (index < text.length && /\d/.test(text[index])) {
5299
+ index += 1;
5300
+ }
5301
+ } else {
5302
+ return false;
5303
+ }
5304
+ if (index === text.length) {
5305
+ return true;
5306
+ }
5307
+ if (text[index] !== "@") {
5308
+ return false;
5309
+ }
5310
+ return isIdPriorityPrefix(text.slice(index + 1));
5311
+ }
5312
+ function isIdPriorityPrefix(text) {
5313
+ if (text.length === 0 || /^[ \t]+$/.test(text)) {
5314
+ return true;
5315
+ }
5316
+ return /^[ \t]*\[(?:l(?:o(?:w)?)?|m(?:e(?:d(?:i(?:u(?:m)?)?)?)?)?|h(?:i(?:g(?:h)?)?)?)?\]?[ \t]*$/.test(
5317
+ text
5318
+ );
5319
+ }
5320
+ function stripDcpArtifacts(text, config) {
5321
+ let out = text;
5322
+ if (config.modelOutput) {
5323
+ out = out.replace(DCP_PAIRED_TAG_REGEX, "").replace(DCP_UNPAIRED_TAG_REGEX, "");
5324
+ }
5325
+ if (config.messageIds && out.includes("\n")) {
5326
+ out = out.split("\n").filter((line) => !isIdLine(line)).join("\n");
5327
+ } else if (config.messageIds && isIdLine(out)) {
5328
+ out = "";
5329
+ }
5330
+ return out;
5331
+ }
5332
+ var TAG_OPEN = "<dcp";
5333
+ var TAG_CLOSE = "</dcp";
5334
+ var MAX_TAG_LENGTH = 512;
5335
+ var MAX_BLOCK_LENGTH = 8192;
5336
+ function heldTagSuffix(text) {
5337
+ const max = Math.min(4, text.length);
5338
+ for (let length = max; length >= 1; length -= 1) {
5339
+ const suffix = text.slice(text.length - length).toLowerCase();
5340
+ if (TAG_OPEN.startsWith(suffix) || TAG_CLOSE.startsWith(suffix)) {
5341
+ return text.slice(text.length - length);
5342
+ }
5343
+ }
5344
+ return "";
5345
+ }
5346
+ var DcpTagStripper = class {
5347
+ mode = "text";
5348
+ carry = "";
5349
+ push(chunk) {
5350
+ this.carry += chunk;
5351
+ let out = "";
5352
+ for (; ; ) {
5353
+ if (this.mode === "text") {
5354
+ const lower = this.carry.toLowerCase();
5355
+ const open = lower.indexOf(TAG_OPEN);
5356
+ const close2 = lower.indexOf(TAG_CLOSE);
5357
+ let index = -1;
5358
+ let next = "open";
5359
+ if (open === -1 && close2 === -1) {
5360
+ index = -1;
5361
+ } else if (open === -1) {
5362
+ index = close2;
5363
+ next = "close";
5364
+ } else if (close2 === -1) {
5365
+ index = open;
5366
+ } else if (open <= close2) {
5367
+ index = open;
5368
+ } else {
5369
+ index = close2;
5370
+ next = "close";
5371
+ }
5372
+ if (index === -1) {
5373
+ const held = heldTagSuffix(this.carry);
5374
+ out += held ? this.carry.slice(0, -held.length) : this.carry;
5375
+ this.carry = held;
5376
+ break;
5377
+ }
5378
+ out += this.carry.slice(0, index);
5379
+ this.carry = this.carry.slice(index);
5380
+ this.mode = next;
5381
+ continue;
5382
+ }
5383
+ if (this.mode === "open" || this.mode === "close") {
5384
+ const end = this.carry.indexOf(">");
5385
+ if (end === -1) {
5386
+ if (this.carry.length > MAX_TAG_LENGTH) {
5387
+ out += this.carry;
5388
+ this.carry = "";
5389
+ this.mode = "text";
5390
+ }
5391
+ break;
5392
+ }
5393
+ const wasOpen = this.mode === "open";
5394
+ this.carry = this.carry.slice(end + 1);
5395
+ this.mode = wasOpen ? "block" : "text";
5396
+ continue;
5397
+ }
5398
+ const close = this.carry.toLowerCase().indexOf(TAG_CLOSE);
5399
+ if (close !== -1) {
5400
+ const end = this.carry.indexOf(">", close);
5401
+ if (end !== -1) {
5402
+ this.carry = this.carry.slice(end + 1);
5403
+ this.mode = "text";
5404
+ continue;
5405
+ }
5406
+ }
5407
+ if (this.carry.length > MAX_BLOCK_LENGTH) {
5408
+ out += this.carry;
5409
+ this.carry = "";
5410
+ this.mode = "text";
5411
+ }
5412
+ break;
5413
+ }
5414
+ return out;
5415
+ }
5416
+ flush() {
5417
+ const out = this.mode === "block" ? this.carry : "";
5418
+ this.carry = "";
5419
+ this.mode = "text";
5420
+ return out;
5421
+ }
5422
+ reset() {
5423
+ this.carry = "";
5424
+ this.mode = "text";
5425
+ }
5426
+ };
5427
+ var IdLineStripper = class {
5428
+ carry = "";
5429
+ push(chunk) {
5430
+ this.carry += chunk;
5431
+ let out = "";
5432
+ for (; ; ) {
5433
+ const newline = this.carry.indexOf("\n");
5434
+ if (newline === -1) {
5435
+ if (isIdLinePrefix(this.carry)) {
5436
+ break;
5437
+ }
5438
+ out += this.carry;
5439
+ this.carry = "";
5440
+ break;
5441
+ }
5442
+ const line = this.carry.slice(0, newline);
5443
+ this.carry = this.carry.slice(newline + 1);
5444
+ if (!isIdLine(line)) {
5445
+ out += line + "\n";
5446
+ }
5447
+ }
5448
+ return out;
5449
+ }
5450
+ flush() {
5451
+ const held = this.carry;
5452
+ this.carry = "";
5453
+ return isIdLine(held) ? "" : held;
5454
+ }
5455
+ reset() {
5456
+ this.carry = "";
5457
+ }
5458
+ };
5459
+ var DcpStreamScrubber = class {
5460
+ constructor(config) {
5461
+ this.config = config;
5462
+ }
5463
+ config;
5464
+ tags = new DcpTagStripper();
5465
+ ids = new IdLineStripper();
5466
+ push(chunk) {
5467
+ let out = chunk;
5468
+ if (this.config.modelOutput) {
5469
+ out = this.tags.push(out);
5470
+ }
5471
+ if (this.config.messageIds) {
5472
+ out = this.ids.push(out);
5473
+ }
5474
+ return out;
5475
+ }
5476
+ flush() {
5477
+ let out = this.config.modelOutput ? this.tags.flush() : "";
5478
+ if (out && this.config.messageIds) {
5479
+ out = this.ids.push(out);
5480
+ }
5481
+ if (this.config.messageIds) {
5482
+ out += this.ids.flush();
5483
+ }
5484
+ return out;
5485
+ }
5486
+ reset() {
5487
+ this.tags.reset();
5488
+ this.ids.reset();
5489
+ }
5490
+ };
5491
+ function assignText(target, key, value) {
5492
+ if (target[key] === value) {
5493
+ return false;
5494
+ }
5495
+ target[key] = value;
5496
+ return true;
5497
+ }
5498
+ var TEXT_PART_TYPES = /* @__PURE__ */ new Set(["text", "output_text", "reasoning_text", "summary_text"]);
5499
+ var ProviderPayloadScrubber = class {
5500
+ constructor(config) {
5501
+ this.config = config;
5502
+ }
5503
+ config;
5504
+ streams = /* @__PURE__ */ new Map();
5505
+ /** Scrub one streamed provider event. Returns true when it was modified. */
5506
+ scrub(payload) {
5507
+ try {
5508
+ if (!payload || typeof payload !== "object") {
5509
+ return false;
5510
+ }
5511
+ const record = payload;
5512
+ const type = typeof record.type === "string" ? record.type : "";
5513
+ if (Array.isArray(record.choices)) {
5514
+ return this.scrubChat(record, false);
5515
+ }
5516
+ if (Array.isArray(record.candidates)) {
5517
+ return this.scrubGoogle(record, false);
5518
+ }
5519
+ if (type.startsWith("response.")) {
5520
+ return this.scrubResponses(record);
5521
+ }
5522
+ if (type.startsWith("content_block") || type === "message" || type === "message_delta") {
5523
+ return this.scrubAnthropic(record);
5524
+ }
5525
+ return false;
5526
+ } catch {
5527
+ return false;
5528
+ }
5529
+ }
5530
+ /** Scrub a complete (non-streamed) JSON payload. */
5531
+ scrubComplete(payload) {
5532
+ try {
5533
+ if (Array.isArray(payload)) {
5534
+ let changed = false;
5535
+ for (const item of payload) {
5536
+ changed = this.scrubComplete(item) || changed;
5537
+ }
5538
+ return changed;
5539
+ }
5540
+ if (!payload || typeof payload !== "object") {
5541
+ return false;
5542
+ }
5543
+ const record = payload;
5544
+ if (Array.isArray(record.choices)) {
5545
+ return this.scrubChat(record, true);
5546
+ }
5547
+ if (Array.isArray(record.candidates)) {
5548
+ return this.scrubGoogle(record, true);
5549
+ }
5550
+ if (record.type === "message" && Array.isArray(record.content)) {
5551
+ return this.scrubAnthropic(record);
5552
+ }
5553
+ if (Array.isArray(record.output)) {
5554
+ return this.scrubItems(record.output);
5555
+ }
5556
+ if (typeof record.output_text === "string") {
5557
+ return assignText(record, "output_text", this.complete(record.output_text));
5558
+ }
5559
+ return false;
5560
+ } catch {
5561
+ return false;
5562
+ }
5563
+ }
5564
+ complete(text) {
5565
+ return stripDcpArtifacts(text, this.config);
5566
+ }
5567
+ stream(key) {
5568
+ let scrubber = this.streams.get(key);
5569
+ if (!scrubber) {
5570
+ scrubber = new DcpStreamScrubber(this.config);
5571
+ this.streams.set(key, scrubber);
5572
+ }
5573
+ return scrubber;
5574
+ }
5575
+ textFor(value, key, complete) {
5576
+ if (complete || !key) {
5577
+ return this.complete(value);
5578
+ }
5579
+ return this.stream(key).push(value);
5580
+ }
5581
+ scrubField(target, key, streamKey, complete = false) {
5582
+ if (!target || typeof target !== "object" || typeof target[key] !== "string") {
5583
+ return false;
5584
+ }
5585
+ return assignText(target, key, this.textFor(target[key], streamKey, complete));
5586
+ }
5587
+ scrubParts(parts, key, complete = false) {
5588
+ if (!Array.isArray(parts)) {
5589
+ return false;
5590
+ }
5591
+ let changed = false;
5592
+ parts.forEach((part, index) => {
5593
+ if (!part || typeof part !== "object" || typeof part.text !== "string") {
5594
+ return;
5595
+ }
5596
+ if (part.type && !TEXT_PART_TYPES.has(part.type)) {
5597
+ return;
5598
+ }
5599
+ changed = assignText(
5600
+ part,
5601
+ "text",
5602
+ this.textFor(part.text, key ? `${key}:${index}` : void 0, complete)
5603
+ ) || changed;
5604
+ });
5605
+ return changed;
5606
+ }
5607
+ scrubChat(record, complete) {
5608
+ let changed = false;
5609
+ const choices = Array.isArray(record.choices) ? record.choices : [];
5610
+ choices.forEach((choice, index) => {
5611
+ const delta = choice?.delta;
5612
+ if (delta && typeof delta === "object") {
5613
+ changed = this.scrubField(delta, "content", `chat:content:${index}`) || changed;
5614
+ changed = this.scrubField(delta, "reasoning_content", `chat:reasoning:${index}`) || changed;
5615
+ changed = this.scrubField(delta, "reasoning", `chat:reasoning:${index}`) || changed;
5616
+ changed = this.scrubParts(delta.content, `chat:parts:${index}`) || changed;
5617
+ if (choice?.finish_reason) {
5618
+ this.streams.delete(`chat:content:${index}`);
5619
+ this.streams.delete(`chat:reasoning:${index}`);
5620
+ this.streams.delete(`chat:parts:${index}`);
5621
+ }
5622
+ }
5623
+ const message = choice?.message;
5624
+ if (message && typeof message === "object") {
5625
+ changed = this.scrubField(message, "content", void 0, true) || changed;
5626
+ changed = this.scrubField(message, "reasoning_content", void 0, true) || changed;
5627
+ changed = this.scrubField(message, "reasoning", void 0, true) || changed;
5628
+ changed = this.scrubParts(message.content, void 0, true) || changed;
5629
+ }
5630
+ });
5631
+ return changed;
5632
+ }
5633
+ scrubResponses(record) {
5634
+ const type = record.type;
5635
+ const itemId = record.item_id ?? "";
5636
+ const outputIndex = record.output_index ?? "";
5637
+ const contentIndex = record.content_index ?? "";
5638
+ const key = (family) => `resp:${family}:${itemId}:${outputIndex}:${contentIndex}`;
5639
+ switch (type) {
5640
+ case "response.output_text.delta":
5641
+ return this.scrubField(record, "delta", key("text"));
5642
+ case "response.reasoning_text.delta":
5643
+ return this.scrubField(record, "delta", key("reasoning"));
5644
+ case "response.reasoning_summary_text.delta":
5645
+ return this.scrubField(record, "delta", key("summary"));
5646
+ case "response.output_text.done":
5647
+ case "response.reasoning_text.done":
5648
+ case "response.reasoning_summary_text.done":
5649
+ return this.scrubField(record, "text", void 0, true);
5650
+ case "response.content_part.added":
5651
+ case "response.content_part.done":
5652
+ case "response.reasoning_summary_part.added":
5653
+ case "response.reasoning_summary_part.done":
5654
+ return this.scrubParts(record.part ? [record.part] : void 0, void 0, true);
5655
+ case "response.output_item.added":
5656
+ case "response.output_item.done":
5657
+ return this.scrubItems(record.item ? [record.item] : void 0);
5658
+ case "response.completed": {
5659
+ const changed = this.scrubResponseObject(record.response);
5660
+ this.streams.clear();
5661
+ return changed;
5662
+ }
5663
+ default:
5664
+ return false;
5665
+ }
5666
+ }
5667
+ scrubResponseObject(response) {
5668
+ if (!response || typeof response !== "object") {
5669
+ return false;
5670
+ }
5671
+ let changed = this.scrubItems(response.output);
5672
+ if (typeof response.output_text === "string") {
5673
+ changed = assignText(response, "output_text", this.complete(response.output_text)) || changed;
5674
+ }
5675
+ return changed;
5676
+ }
5677
+ scrubItems(items) {
5678
+ if (!Array.isArray(items)) {
5679
+ return false;
5680
+ }
5681
+ let changed = false;
5682
+ items.forEach((entry) => {
5683
+ if (!entry || typeof entry !== "object") {
5684
+ return;
5685
+ }
5686
+ changed = this.scrubParts(entry.content, void 0, true) || changed;
5687
+ changed = this.scrubParts(entry.summary, void 0, true) || changed;
5688
+ });
5689
+ return changed;
5690
+ }
5691
+ scrubAnthropic(record) {
5692
+ const type = record.type;
5693
+ if (type === "content_block_start") {
5694
+ const block = record.content_block;
5695
+ if (!block || typeof block !== "object") {
5696
+ return false;
5697
+ }
5698
+ if (block.type === "text") {
5699
+ return this.scrubField(block, "text", `anthropic:text:${record.index}`);
5700
+ }
5701
+ if (block.type === "thinking") {
5702
+ return this.scrubField(block, "thinking", `anthropic:thinking:${record.index}`);
5703
+ }
5704
+ return false;
5705
+ }
5706
+ if (type === "content_block_delta") {
5707
+ const delta = record.delta;
5708
+ if (!delta || typeof delta !== "object") {
5709
+ return false;
5710
+ }
5711
+ if (delta.type === "text_delta") {
5712
+ return this.scrubField(delta, "text", `anthropic:text:${record.index}`);
5713
+ }
5714
+ if (delta.type === "thinking_delta") {
5715
+ return this.scrubField(delta, "thinking", `anthropic:thinking:${record.index}`);
5716
+ }
5717
+ return false;
5718
+ }
5719
+ if (type === "content_block_stop") {
5720
+ this.streams.delete(`anthropic:text:${record.index}`);
5721
+ this.streams.delete(`anthropic:thinking:${record.index}`);
5722
+ return false;
5723
+ }
5724
+ if (type === "message" && Array.isArray(record.content)) {
5725
+ let changed = false;
5726
+ record.content.forEach((block) => {
5727
+ if (block?.type === "text") {
5728
+ changed = this.scrubField(block, "text", void 0, true) || changed;
5729
+ } else if (block?.type === "thinking") {
5730
+ changed = this.scrubField(block, "thinking", void 0, true) || changed;
5731
+ }
5732
+ });
5733
+ return changed;
5734
+ }
5735
+ return false;
5736
+ }
5737
+ scrubGoogle(record, complete) {
5738
+ const candidates = Array.isArray(record.candidates) ? record.candidates : [];
5739
+ let changed = false;
5740
+ candidates.forEach((candidate, candidateIndex) => {
5741
+ const parts = candidate?.content?.parts;
5742
+ if (Array.isArray(parts)) {
5743
+ parts.forEach((part, partIndex) => {
5744
+ if (!part || typeof part !== "object" || typeof part.text !== "string") {
5745
+ return;
5746
+ }
5747
+ changed = assignText(
5748
+ part,
5749
+ "text",
5750
+ this.textFor(
5751
+ part.text,
5752
+ complete ? void 0 : `google:${candidateIndex}:${partIndex}`,
5753
+ complete
5754
+ )
5755
+ ) || changed;
5756
+ });
5757
+ }
5758
+ if (candidate?.finishReason) {
5759
+ this.streams.clear();
5760
+ }
5761
+ });
5762
+ return changed;
5763
+ }
5764
+ };
5765
+ function findBoundary(buffer) {
5766
+ const lf = buffer.indexOf("\n\n");
5767
+ const crlf = buffer.indexOf("\r\n\r\n");
5768
+ if (lf === -1 && crlf === -1) {
5769
+ return null;
5770
+ }
5771
+ if (crlf !== -1 && (lf === -1 || crlf <= lf)) {
5772
+ return { start: crlf, end: crlf + 4, separator: "\r\n\r\n" };
5773
+ }
5774
+ return { start: lf, end: lf + 2, separator: "\n\n" };
5775
+ }
5776
+ function scrubJsonBody(body, config, scrubber) {
5777
+ const trimmed = body.trim();
5778
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
5779
+ return body;
5780
+ }
5781
+ try {
5782
+ const payload = JSON.parse(trimmed);
5783
+ const worker = scrubber ?? new ProviderPayloadScrubber(config);
5784
+ if (worker.scrubComplete(payload)) {
5785
+ return JSON.stringify(payload);
5786
+ }
5787
+ } catch {
5788
+ }
5789
+ return body;
5790
+ }
5791
+ function createSseScrubTransform(config) {
5792
+ const TransformStreamImpl = globalThis.TransformStream;
5793
+ const decoder = new TextDecoder();
5794
+ const encoder = new TextEncoder();
5795
+ const scrubber = new ProviderPayloadScrubber(config);
5796
+ let buffer = "";
5797
+ const processBlock = (block) => {
5798
+ try {
5799
+ const lines = block.split("\n");
5800
+ const dataLines = [];
5801
+ lines.forEach((raw, index) => {
5802
+ const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
5803
+ const match = /^data:( ?)(.*)$/.exec(line);
5804
+ if (match) {
5805
+ dataLines.push({ index, space: match[1] ?? "", value: match[2] ?? "" });
5806
+ }
5807
+ });
5808
+ if (dataLines.length === 0) {
5809
+ return block;
5810
+ }
5811
+ const data = dataLines.map((entry) => entry.value).join("\n");
5812
+ const payloadText = data.trim();
5813
+ if (!payloadText || payloadText === "[DONE]") {
5814
+ return block;
5815
+ }
5816
+ let payload;
5817
+ try {
5818
+ payload = JSON.parse(data);
5819
+ } catch {
5820
+ return block;
5821
+ }
5822
+ if (!scrubber.scrub(payload)) {
5823
+ return block;
5824
+ }
5825
+ const first = dataLines[0];
5826
+ const drop = new Set(dataLines.slice(1).map((entry) => entry.index));
5827
+ const rebuilt = [];
5828
+ lines.forEach((raw, index) => {
5829
+ if (drop.has(index)) {
5830
+ return;
5831
+ }
5832
+ if (index === first.index) {
5833
+ rebuilt.push(`data:${first.space}${JSON.stringify(payload)}`);
5834
+ return;
5835
+ }
5836
+ rebuilt.push(raw);
5837
+ });
5838
+ return rebuilt.join("\n");
5839
+ } catch {
5840
+ return block;
5841
+ }
5842
+ };
5843
+ return new TransformStreamImpl({
5844
+ transform(chunk, controller) {
5845
+ let enqueued = false;
5846
+ try {
5847
+ buffer += decoder.decode(chunk, { stream: true });
5848
+ let boundary = findBoundary(buffer);
5849
+ while (boundary) {
5850
+ const block = buffer.slice(0, boundary.start);
5851
+ buffer = buffer.slice(boundary.end);
5852
+ controller.enqueue(encoder.encode(processBlock(block) + boundary.separator));
5853
+ enqueued = true;
5854
+ boundary = findBoundary(buffer);
5855
+ }
5856
+ } catch {
5857
+ if (!enqueued) {
5858
+ try {
5859
+ controller.enqueue(chunk);
5860
+ } catch {
5861
+ }
5862
+ }
5863
+ }
5864
+ },
5865
+ flush(controller) {
5866
+ try {
5867
+ buffer += decoder.decode();
5868
+ if (buffer) {
5869
+ controller.enqueue(encoder.encode(processBlock(buffer)));
5870
+ }
5871
+ buffer = "";
5872
+ } catch {
5873
+ }
5874
+ }
5875
+ });
5876
+ }
5877
+ function createJsonScrubTransform(config) {
5878
+ const TransformStreamImpl = globalThis.TransformStream;
5879
+ const decoder = new TextDecoder();
5880
+ const encoder = new TextEncoder();
5881
+ const scrubber = new ProviderPayloadScrubber(config);
5882
+ let buffer = "";
5883
+ return new TransformStreamImpl({
5884
+ transform(chunk, _controller) {
5885
+ try {
5886
+ buffer += decoder.decode(chunk, { stream: true });
5887
+ } catch {
5888
+ }
5889
+ },
5890
+ flush(controller) {
5891
+ try {
5892
+ buffer += decoder.decode();
5893
+ controller.enqueue(encoder.encode(scrubJsonBody(buffer, config, scrubber)));
5894
+ buffer = "";
5895
+ } catch {
5896
+ try {
5897
+ controller.enqueue(encoder.encode(buffer));
5898
+ buffer = "";
5899
+ } catch {
5900
+ }
5901
+ }
5902
+ }
5903
+ });
5904
+ }
5905
+ function wrapScrubResponse(response, config) {
5906
+ try {
5907
+ if (!response || typeof response !== "object" || !response.body) {
5908
+ return response;
5909
+ }
5910
+ const headers = new Headers(response.headers);
5911
+ const contentType = headers.get("content-type") ?? "";
5912
+ let body;
5913
+ if (/text\/event-stream/i.test(contentType)) {
5914
+ body = response.body.pipeThrough(createSseScrubTransform(config));
5915
+ } else if (/json/i.test(contentType)) {
5916
+ body = response.body.pipeThrough(createJsonScrubTransform(config));
5917
+ } else {
5918
+ return response;
5919
+ }
5920
+ headers.delete("content-length");
5921
+ return new Response(body, {
5922
+ status: response.status,
5923
+ statusText: response.statusText,
5924
+ headers
5925
+ });
5926
+ } catch {
5927
+ return response;
5928
+ }
5929
+ }
5930
+ async function installResponseScrubHook(ctx, config, logger) {
5931
+ if (!outputScrubEnabled(config)) {
5932
+ return;
5933
+ }
5934
+ const session2 = ctx?.session;
5935
+ if (!session2 || typeof session2.hook !== "function") {
5936
+ return;
5937
+ }
5938
+ try {
5939
+ await session2.hook("http.response", async (event) => {
5940
+ try {
5941
+ const response = event?.response;
5942
+ if (!response || typeof response !== "object") {
5943
+ return;
5944
+ }
5945
+ const wrapped = wrapScrubResponse(response, config);
5946
+ if (wrapped !== response) {
5947
+ event.response = wrapped;
5948
+ }
5949
+ } catch (error) {
5950
+ logger.debug("DCP output scrub failed", { error: error?.message });
5951
+ }
5952
+ });
5953
+ logger.debug("DCP output scrub hook installed");
5954
+ } catch (error) {
5955
+ logger.warn("DCP output scrub hook registration failed", { error: error?.message });
5956
+ }
5957
+ }
5958
+ function isTextDelta(part) {
5959
+ if (part?.type === "text-delta") {
5960
+ return { family: "text-delta", field: deltaField(part) };
5961
+ }
5962
+ if (part?.type === "reasoning-delta" || part?.type === "reasoning") {
5963
+ return { family: "reasoning-delta", field: deltaField(part) };
5964
+ }
5965
+ return void 0;
5966
+ }
5967
+ function deltaField(part) {
5968
+ if (typeof part.text === "string") {
5969
+ return "text";
5970
+ }
5971
+ if (typeof part.delta === "string") {
5972
+ return "delta";
5973
+ }
5974
+ return "textDelta";
5975
+ }
5976
+ var AisdkPartScrubber = class {
5977
+ constructor(config) {
5978
+ this.config = config;
5979
+ }
5980
+ config;
5981
+ streams = /* @__PURE__ */ new Map();
5982
+ process(part) {
5983
+ try {
5984
+ if (!part || typeof part !== "object") {
5985
+ return [part];
5986
+ }
5987
+ const delta = isTextDelta(part);
5988
+ if (delta) {
5989
+ const key = `${delta.family}:${part.id ?? ""}`;
5990
+ return [{ ...part, [delta.field]: this.stream(key).push(part[delta.field]) }];
5991
+ }
5992
+ if (part.type === "text-start" || part.type === "reasoning-start") {
5993
+ const family = part.type === "text-start" ? "text-delta" : "reasoning-delta";
5994
+ this.streams.delete(`${family}:${part.id ?? ""}`);
5995
+ return [part];
5996
+ }
5997
+ if (part.type === "text-end" || part.type === "reasoning-end") {
5998
+ const family = part.type === "text-end" ? "text-delta" : "reasoning-delta";
5999
+ const out = [];
6000
+ const leftover = this.take(`${family}:${part.id ?? ""}`);
6001
+ if (leftover) {
6002
+ out.push({ type: family, id: part.id, text: leftover });
6003
+ }
6004
+ const field = deltaField(part);
6005
+ if (typeof part[field] === "string") {
6006
+ out.push({ ...part, [field]: stripDcpArtifacts(part[field], this.config) });
6007
+ } else {
6008
+ out.push(part);
6009
+ }
6010
+ return out;
6011
+ }
6012
+ if (part.type === "finish") {
6013
+ return [...this.flushStreams(), part];
6014
+ }
6015
+ return [part];
6016
+ } catch {
6017
+ return [part];
6018
+ }
6019
+ }
6020
+ stream(key) {
6021
+ let scrubber = this.streams.get(key);
6022
+ if (!scrubber) {
6023
+ scrubber = new DcpStreamScrubber(this.config);
6024
+ this.streams.set(key, scrubber);
6025
+ }
6026
+ return scrubber;
6027
+ }
6028
+ take(key) {
6029
+ const scrubber = this.streams.get(key);
6030
+ if (!scrubber) {
6031
+ return "";
6032
+ }
6033
+ this.streams.delete(key);
6034
+ return scrubber.flush();
6035
+ }
6036
+ flushStreams() {
6037
+ const out = [];
6038
+ for (const [key, scrubber] of this.streams) {
6039
+ const leftover = scrubber.flush();
6040
+ if (!leftover) {
6041
+ continue;
6042
+ }
6043
+ const separator = key.indexOf(":");
6044
+ out.push({
6045
+ type: separator === -1 ? "text-delta" : key.slice(0, separator),
6046
+ id: separator === -1 ? "" : key.slice(separator + 1),
6047
+ text: leftover
6048
+ });
6049
+ }
6050
+ this.streams.clear();
6051
+ return out;
6052
+ }
6053
+ };
6054
+ function stripAisdkResultContent(result, config) {
6055
+ if (!result || !Array.isArray(result.content)) {
6056
+ return;
6057
+ }
6058
+ for (const part of result.content) {
6059
+ if (!part || typeof part !== "object") {
6060
+ continue;
6061
+ }
6062
+ if ((part.type === "text" || part.type === "reasoning") && typeof part.text === "string") {
6063
+ part.text = stripDcpArtifacts(part.text, config);
6064
+ }
6065
+ }
6066
+ }
6067
+
6068
+ // lib/v2/usage.ts
6069
+ function createCompressionUsageTracker() {
6070
+ const pending = /* @__PURE__ */ new Map();
6071
+ const entry = (callId) => {
6072
+ let current = pending.get(callId);
6073
+ if (!current) {
6074
+ current = {};
6075
+ pending.set(callId, current);
6076
+ }
6077
+ return current;
6078
+ };
6079
+ return {
6080
+ recordProvider(callId, usage) {
6081
+ entry(callId).provider = usage;
6082
+ },
6083
+ recordEstimatedInput(callId, inputTokens) {
6084
+ if (Number.isFinite(inputTokens) && inputTokens > 0) {
6085
+ entry(callId).estimatedInput = Math.round(inputTokens);
6086
+ }
6087
+ },
6088
+ resolve(callId, outputText) {
6089
+ const current = pending.get(callId);
6090
+ if (!current) {
6091
+ return void 0;
6092
+ }
6093
+ if (current.provider) {
6094
+ return current.provider;
6095
+ }
6096
+ if (current.estimatedInput === void 0) {
6097
+ return void 0;
6098
+ }
6099
+ return estimatedCompressionUsage(current.estimatedInput, countTokens2(outputText));
6100
+ },
6101
+ discard(callId) {
6102
+ pending.delete(callId);
6103
+ }
6104
+ };
6105
+ }
6106
+ function findSummaryCallId(options) {
6107
+ const texts = [];
6108
+ collectTexts(options?.prompt, texts);
6109
+ for (const text of texts) {
6110
+ const callId = extractSummaryCallId(text);
6111
+ if (callId) {
6112
+ return callId;
6113
+ }
6114
+ }
6115
+ return void 0;
6116
+ }
6117
+ function collectTexts(value, out, depth = 0) {
6118
+ if (depth > 8 || out.length > 200) {
6119
+ return;
6120
+ }
6121
+ if (typeof value === "string") {
6122
+ out.push(value);
6123
+ return;
6124
+ }
6125
+ if (Array.isArray(value)) {
6126
+ for (const item of value) {
6127
+ collectTexts(item, out, depth + 1);
6128
+ }
6129
+ return;
6130
+ }
6131
+ if (value && typeof value === "object") {
6132
+ const record = value;
6133
+ if (typeof record.text === "string") {
6134
+ out.push(record.text);
6135
+ }
6136
+ if (typeof record.content === "string") {
6137
+ out.push(record.content);
6138
+ } else if (Array.isArray(record.content)) {
6139
+ collectTexts(record.content, out, depth + 1);
6140
+ }
6141
+ }
6142
+ }
6143
+ function wrapLanguageModel(model, tracker, logger, scrub) {
6144
+ return new Proxy(model, {
6145
+ get(target, property, receiver) {
6146
+ if (property === "doStream") {
6147
+ return (options) => trackedStream(target, options, tracker, logger, scrub);
6148
+ }
6149
+ if (property === "doGenerate") {
6150
+ return (options) => trackedGenerate(target, options, tracker, logger, scrub);
6151
+ }
6152
+ return Reflect.get(target, property, receiver);
6153
+ }
6154
+ });
6155
+ }
6156
+ async function trackedStream(target, options, tracker, logger, scrub) {
6157
+ const result = await target.doStream(options);
6158
+ const callId = findSummaryCallId(options);
6159
+ const scrubEnabled = !!scrub && outputScrubEnabled(scrub);
6160
+ logger.debug("Compression usage stream call", { callId: callId ?? null });
6161
+ const TransformStreamImpl = globalThis.TransformStream;
6162
+ if (!callId && !scrubEnabled || typeof result?.stream?.pipeThrough !== "function") {
6163
+ return result;
6164
+ }
6165
+ if (typeof TransformStreamImpl !== "function") {
6166
+ return result;
6167
+ }
6168
+ const partScrubber = scrubEnabled ? new AisdkPartScrubber(scrub) : void 0;
6169
+ let recorded = false;
6170
+ const transform = new TransformStreamImpl({
6171
+ transform(part, controller) {
6172
+ try {
6173
+ if (callId && !recorded && part?.type === "finish" && part.usage) {
6174
+ const usage = normalizeProviderUsage(part.usage);
6175
+ if (usage) {
6176
+ recorded = true;
6177
+ tracker.recordProvider(callId, usage);
6178
+ }
6179
+ }
6180
+ } catch {
6181
+ }
6182
+ const parts = partScrubber ? partScrubber.process(part) : [part];
6183
+ for (const next of parts) {
6184
+ controller.enqueue(next);
6185
+ }
6186
+ }
6187
+ });
6188
+ return { ...result, stream: result.stream.pipeThrough(transform) };
6189
+ }
6190
+ async function trackedGenerate(target, options, tracker, logger, scrub) {
6191
+ const result = await target.doGenerate(options);
6192
+ const callId = findSummaryCallId(options);
6193
+ logger.debug("Compression usage generate call", { callId: callId ?? null });
6194
+ if (scrub && outputScrubEnabled(scrub)) {
6195
+ try {
6196
+ stripAisdkResultContent(result, scrub);
6197
+ } catch {
6198
+ }
6199
+ }
6200
+ if (!callId) {
6201
+ return result;
6202
+ }
6203
+ const usage = normalizeProviderUsage(result?.usage);
6204
+ if (usage) {
6205
+ tracker.recordProvider(callId, usage);
6206
+ }
6207
+ return result;
6208
+ }
6209
+ async function installCompressionUsageHook(ctx, tracker, logger, scrub) {
6210
+ const aisdk = ctx?.aisdk;
6211
+ if (!aisdk || typeof aisdk.hook !== "function") {
6212
+ logger.debug("aisdk hook unavailable; compression usage will be estimated");
6213
+ return;
6214
+ }
6215
+ try {
6216
+ await aisdk.hook("language", (event) => {
6217
+ try {
6218
+ const isModel = (candidate) => !!candidate && typeof candidate.doStream === "function";
6219
+ logger.debug("Compression usage hook event", {
6220
+ modelID: event?.model?.modelID ?? event?.model?.id ?? null,
6221
+ hasLanguage: isModel(event?.language),
6222
+ hasFactory: typeof event?.sdk?.languageModel === "function"
6223
+ });
6224
+ let model = isModel(event?.language) ? event.language : void 0;
6225
+ if (!model && typeof event?.sdk?.languageModel === "function") {
6226
+ model = event.sdk.languageModel(
6227
+ event?.model?.modelID ?? event?.model?.id
6228
+ );
6229
+ }
6230
+ if (!isModel(model)) {
6231
+ return;
6232
+ }
6233
+ event.language = wrapLanguageModel(model, tracker, logger, scrub);
6234
+ } catch (error) {
6235
+ logger.debug("Compression usage hook could not wrap language model", {
6236
+ error: error?.message
6237
+ });
6238
+ }
6239
+ });
6240
+ logger.debug("Compression usage hook installed");
6241
+ } catch (error) {
6242
+ logger.warn("Compression usage hook registration failed", {
6243
+ error: error?.message
6244
+ });
6245
+ }
6246
+ }
6247
+ function emptyUsageAccumulator() {
6248
+ return {
6249
+ input: 0,
6250
+ output: 0,
6251
+ cacheRead: 0,
6252
+ cacheWrite: 0,
6253
+ reasoning: 0,
6254
+ exclusiveInput: false
6255
+ };
6256
+ }
6257
+ function usageField(key) {
6258
+ switch (key) {
6259
+ case "input_tokens":
6260
+ case "prompt_tokens":
6261
+ case "promptTokenCount":
6262
+ return "input";
6263
+ case "output_tokens":
6264
+ case "completion_tokens":
6265
+ case "candidatesTokenCount":
6266
+ return "output";
6267
+ case "cache_read_input_tokens":
6268
+ case "cache_creation_input_tokens":
6269
+ case "cached_tokens":
6270
+ case "cachedContentTokenCount":
6271
+ return key === "cache_creation_input_tokens" ? "cacheWrite" : "cacheRead";
6272
+ case "reasoning_tokens":
6273
+ case "thoughtsTokenCount":
6274
+ return "reasoning";
6275
+ default:
6276
+ return void 0;
6277
+ }
6278
+ }
6279
+ function scanUsage(value, acc, depth) {
6280
+ if (depth > 8 || !value || typeof value !== "object") {
6281
+ return;
6282
+ }
6283
+ if (Array.isArray(value)) {
6284
+ for (const item of value) {
6285
+ scanUsage(item, acc, depth + 1);
6286
+ }
6287
+ return;
6288
+ }
6289
+ for (const [key, raw] of Object.entries(value)) {
6290
+ const field = usageField(key);
6291
+ if (field && typeof raw === "number" && Number.isFinite(raw)) {
6292
+ acc[field] = Math.max(acc[field], raw);
6293
+ if (key === "cache_read_input_tokens" || key === "cache_creation_input_tokens") {
6294
+ acc.exclusiveInput = true;
6295
+ }
6296
+ continue;
6297
+ }
6298
+ if (raw && typeof raw === "object") {
6299
+ scanUsage(raw, acc, depth + 1);
6300
+ }
6301
+ }
6302
+ }
6303
+ function usageFromAccumulator(acc) {
6304
+ const input = acc.exclusiveInput ? acc.input + acc.cacheRead + acc.cacheWrite : acc.input;
6305
+ const usage = {
6306
+ inputTokens: Math.max(0, Math.round(input)),
6307
+ outputTokens: Math.max(0, Math.round(acc.output)),
6308
+ cacheReadTokens: Math.max(0, Math.round(acc.cacheRead)),
6309
+ cacheWriteTokens: Math.max(0, Math.round(acc.cacheWrite)),
6310
+ reasoningTokens: Math.max(0, Math.round(acc.reasoning)),
6311
+ source: "provider"
6312
+ };
6313
+ const total = usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens + usage.reasoningTokens;
6314
+ return total > 0 ? usage : void 0;
6315
+ }
6316
+ function parseProviderUsageFromBody(body) {
6317
+ const acc = emptyUsageAccumulator();
6318
+ const trimmed = body.trim();
6319
+ let parsedJson = false;
6320
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
6321
+ try {
6322
+ scanUsage(JSON.parse(trimmed), acc, 0);
6323
+ parsedJson = true;
6324
+ } catch {
6325
+ }
6326
+ }
6327
+ if (!parsedJson) {
6328
+ for (const line of body.split(/\r?\n/)) {
6329
+ const match = /^\s*data:\s*(.*)$/.exec(line);
6330
+ if (!match) {
6331
+ continue;
6332
+ }
6333
+ const payload = match[1].trim();
6334
+ if (!payload || payload === "[DONE]") {
6335
+ continue;
6336
+ }
6337
+ try {
6338
+ scanUsage(JSON.parse(payload), acc, 0);
6339
+ } catch {
6340
+ }
6341
+ }
6342
+ }
6343
+ return usageFromAccumulator(acc);
6344
+ }
6345
+ async function installHttpUsageHook(ctx, tracker, logger) {
6346
+ const session2 = ctx?.session;
6347
+ if (!session2 || typeof session2.hook !== "function") {
6348
+ return;
6349
+ }
6350
+ try {
6351
+ await session2.hook("http.response", async (event) => {
6352
+ if (event?.kind !== "generate") {
6353
+ return;
6354
+ }
6355
+ try {
6356
+ const request = event.request;
6357
+ const response = event.response;
6358
+ if (!request || typeof request.clone !== "function") {
6359
+ return;
6360
+ }
6361
+ const callId = extractSummaryCallId(await request.clone().text());
6362
+ if (!callId) {
6363
+ return;
6364
+ }
6365
+ if (!response || typeof response.clone !== "function") {
6366
+ return;
6367
+ }
6368
+ const usage = parseProviderUsageFromBody(await response.clone().text());
6369
+ if (usage) {
6370
+ tracker.recordProvider(callId, usage);
6371
+ logger.debug("Compression usage captured from http response", {
6372
+ callId,
6373
+ inputTokens: usage.inputTokens,
6374
+ outputTokens: usage.outputTokens
6375
+ });
6376
+ }
6377
+ } catch (error) {
6378
+ logger.debug("Compression usage http capture failed", {
6379
+ error: error?.message
6380
+ });
6381
+ }
6382
+ });
6383
+ logger.debug("Compression usage http hook installed");
6384
+ } catch (error) {
6385
+ logger.warn("Compression usage http hook registration failed", {
6386
+ error: error?.message
6387
+ });
6388
+ }
6389
+ }
6390
+
5084
6391
  // lib/prompts/index.ts
5085
6392
  function renderSystemPrompt(prompts, protectedToolsExtension, manual, subagent) {
5086
6393
  const extensions = [];
@@ -5285,8 +6592,8 @@ async function handleContextCommand(ctx) {
5285
6592
  // lib/messages/utils.ts
5286
6593
  import { createHash } from "crypto";
5287
6594
  var SUMMARY_ID_HASH_LENGTH = 16;
5288
- var DCP_PAIRED_TAG_REGEX = /<dcp[^>]*>[\s\S]*?<\/dcp[^>]*>/gi;
5289
- var DCP_UNPAIRED_TAG_REGEX = /<\/?dcp[^>]*>/gi;
6595
+ var DCP_PAIRED_TAG_REGEX2 = /<dcp[^>]*>[\s\S]*?<\/dcp[^>]*>/gi;
6596
+ var DCP_UNPAIRED_TAG_REGEX2 = /<\/?dcp[^>]*>/gi;
5290
6597
  var INJECTED_MESSAGE_ID_SUFFIX_REGEX = /(?<=\n)<dcp-message-id[^>]*>m\d+<\/dcp-message-id>\s*$/;
5291
6598
  var HALLUCINATED_PARAMETER_SUFFIX_REGEX = /(?<=\n)m\d+<\/parameter>\s*$/;
5292
6599
  var COMPACT_TAG_REGEX = /@(?:[1-9]\d*|b[1-9]\d*|blocked)@(?:[ \t]+\[(?:low|medium|high)\])?/gi;
@@ -5410,7 +6717,7 @@ function buildToolIdList(state, messages) {
5410
6717
  var stripHallucinationsFromString = (text, format2 = "xml") => {
5411
6718
  if (format2 === "compact") text = text.replace(COMPACT_TAG_REGEX, "");
5412
6719
  const withoutKnownSuffixes = text.replace(INJECTED_MESSAGE_ID_SUFFIX_REGEX, "").replace(HALLUCINATED_PARAMETER_SUFFIX_REGEX, "");
5413
- return withoutKnownSuffixes.replace(DCP_PAIRED_TAG_REGEX, "").replace(DCP_UNPAIRED_TAG_REGEX, "");
6720
+ return withoutKnownSuffixes.replace(DCP_PAIRED_TAG_REGEX2, "").replace(DCP_UNPAIRED_TAG_REGEX2, "");
5414
6721
  };
5415
6722
  var stripHallucinations = (messages, format2 = "xml") => {
5416
6723
  for (const message of messages) {
@@ -6592,7 +7899,7 @@ async function handleRecompressCommand(ctx) {
6592
7899
  }
6593
7900
 
6594
7901
  // lib/commands/stats.ts
6595
- function formatStatsMessage(sessionTokens, sessionSummaryTokens, sessionTools, sessionMessages, sessionDurationMs, allTime) {
7902
+ function formatStatsMessage(sessionTokens, sessionSummaryTokens, sessionTools, sessionMessages, sessionDurationMs, sessionUsage, allTime) {
6596
7903
  const lines = [];
6597
7904
  lines.push("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E");
6598
7905
  lines.push("\u2502 DCP Statistics \u2502");
@@ -6607,6 +7914,16 @@ function formatStatsMessage(sessionTokens, sessionSummaryTokens, sessionTools, s
6607
7914
  lines.push(` Time: ${formatCompressionTime(sessionDurationMs)}`);
6608
7915
  lines.push(` Messages: ${sessionMessages}`);
6609
7916
  lines.push(` Tools: ${sessionTools}`);
7917
+ lines.push(
7918
+ ` Summary requests: ${sessionUsage.calls} (${sessionUsage.providerCalls} provider, ${sessionUsage.estimatedCalls} estimated)`
7919
+ );
7920
+ lines.push(
7921
+ ` Request in|out: ~${formatTokenCount(sessionUsage.inputTokens)} | ~${formatTokenCount(sessionUsage.outputTokens)}`
7922
+ );
7923
+ lines.push(
7924
+ ` Request cache: ~${formatTokenCount(sessionUsage.cacheReadTokens)} read | ~${formatTokenCount(sessionUsage.cacheWriteTokens)} write`
7925
+ );
7926
+ lines.push(` Request reason: ~${formatTokenCount(sessionUsage.reasoningTokens)}`);
6610
7927
  lines.push("");
6611
7928
  lines.push("All-time:");
6612
7929
  lines.push("\u2500".repeat(60));
@@ -6614,6 +7931,9 @@ function formatStatsMessage(sessionTokens, sessionSummaryTokens, sessionTools, s
6614
7931
  lines.push(` Tools pruned: ${allTime.totalTools}`);
6615
7932
  lines.push(` Messages pruned: ${allTime.totalMessages}`);
6616
7933
  lines.push(` Sessions: ${allTime.sessionCount}`);
7934
+ lines.push(
7935
+ ` Summary requests: ${allTime.compressionUsage.calls} (~${formatTokenCount(allTime.compressionUsage.inputTokens)} in | ~${formatTokenCount(allTime.compressionUsage.outputTokens)} out)`
7936
+ );
6617
7937
  return lines.join("\n");
6618
7938
  }
6619
7939
  function formatCompressionRatio(inputTokens, outputTokens) {
@@ -6653,6 +7973,7 @@ async function handleStatsCommand(ctx) {
6653
7973
  report2.sessionTools,
6654
7974
  report2.sessionMessages,
6655
7975
  report2.sessionDurationMs,
7976
+ report2.sessionCompressionUsage,
6656
7977
  report2.allTime
6657
7978
  );
6658
7979
  const params = getCurrentParams(state, messages, logger);
@@ -6663,9 +7984,11 @@ async function handleStatsCommand(ctx) {
6663
7984
  sessionTools: report2.sessionTools,
6664
7985
  sessionMessages: report2.sessionMessages,
6665
7986
  sessionDurationMs: report2.sessionDurationMs,
7987
+ sessionCompressionUsage: report2.sessionCompressionUsage,
6666
7988
  allTimeTokens: report2.allTime.totalTokens,
6667
7989
  allTimeTools: report2.allTime.totalTools,
6668
- allTimeMessages: report2.allTime.totalMessages
7990
+ allTimeMessages: report2.allTime.totalMessages,
7991
+ allTimeCompressionUsage: report2.allTime.compressionUsage
6669
7992
  });
6670
7993
  }
6671
7994
  async function buildStatsReport(state, logger) {
@@ -6687,6 +8010,7 @@ async function buildStatsReport(state, logger) {
6687
8010
  }
6688
8011
  }
6689
8012
  const sessionTools = prunedToolIds.size;
8013
+ const sessionCompressionUsage = normalizeCompressionUsageTotals(state.stats.compressionUsage);
6690
8014
  let sessionMessages = 0;
6691
8015
  for (const entry of state.prune.messages.byMessageId.values()) {
6692
8016
  if (entry.activeBlockIds.length > 0) {
@@ -6700,6 +8024,7 @@ async function buildStatsReport(state, logger) {
6700
8024
  sessionTools,
6701
8025
  sessionMessages,
6702
8026
  sessionDurationMs,
8027
+ sessionCompressionUsage,
6703
8028
  allTime
6704
8029
  };
6705
8030
  }
@@ -7235,17 +8560,29 @@ function project(native, entries, session2) {
7235
8560
  import { tool as tool2 } from "@opencode-ai/plugin";
7236
8561
  var z = tool2.schema;
7237
8562
  var session = z.object({ sessionID: z.string() });
8563
+ var compressionUsage = z.object({
8564
+ calls: z.number(),
8565
+ providerCalls: z.number(),
8566
+ estimatedCalls: z.number(),
8567
+ inputTokens: z.number(),
8568
+ outputTokens: z.number(),
8569
+ cacheReadTokens: z.number(),
8570
+ cacheWriteTokens: z.number(),
8571
+ reasoningTokens: z.number()
8572
+ });
7238
8573
  var stats = z.object({
7239
8574
  sessionTokens: z.number(),
7240
8575
  sessionSummaryTokens: z.number(),
7241
8576
  sessionDurationMs: z.number(),
7242
8577
  sessionTools: z.number(),
7243
8578
  sessionMessages: z.number(),
8579
+ sessionCompressionUsage: compressionUsage,
7244
8580
  allTime: z.object({
7245
8581
  totalTokens: z.number(),
7246
8582
  totalTools: z.number(),
7247
8583
  totalMessages: z.number(),
7248
- sessionCount: z.number()
8584
+ sessionCount: z.number(),
8585
+ compressionUsage
7249
8586
  })
7250
8587
  });
7251
8588
  var context = z.object({
@@ -7300,6 +8637,14 @@ async function setup(ctx) {
7300
8637
  return;
7301
8638
  }
7302
8639
  const logger = new Logger(config.debug);
8640
+ const usageTracker = createCompressionUsageTracker();
8641
+ const scrub = {
8642
+ modelOutput: config.compress.scrubModelOutput,
8643
+ messageIds: config.compress.scrubMessageIds
8644
+ };
8645
+ await installCompressionUsageHook(ctx, usageTracker, logger, scrub);
8646
+ await installHttpUsageHook(ctx, usageTracker, logger);
8647
+ await installResponseScrubHook(ctx, scrub, logger);
7303
8648
  const prompts = new PromptStore(
7304
8649
  logger,
7305
8650
  ctx.location.directory,
@@ -7465,7 +8810,7 @@ async function setup(ctx) {
7465
8810
  await ctx.session.hook("generate", (event) => {
7466
8811
  const own = event.messages.filter(
7467
8812
  (message) => Array.isArray(message.content) && message.content.some(
7468
- (part) => part?.type === "text" && typeof part.text === "string" && part.text.includes(RC_SUMMARY_MARKER)
8813
+ (part) => part?.type === "text" && typeof part.text === "string" && hasSummaryMarker(part.text)
7469
8814
  )
7470
8815
  );
7471
8816
  if (own.length === 0) {
@@ -7475,6 +8820,25 @@ async function setup(ctx) {
7475
8820
  for (const name of Object.keys(event.tools ?? {})) {
7476
8821
  delete event.tools[name];
7477
8822
  }
8823
+ const callId = own.flatMap(
8824
+ (message) => Array.isArray(message.content) ? message.content : []
8825
+ ).filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => extractSummaryCallId(part.text)).find((id) => !!id);
8826
+ if (callId) {
8827
+ const texts = [];
8828
+ for (const part of event.system ?? []) {
8829
+ if (typeof part?.text === "string") {
8830
+ texts.push(part.text);
8831
+ }
8832
+ }
8833
+ for (const message of own) {
8834
+ for (const part of message.content ?? []) {
8835
+ if (part?.type === "text" && typeof part.text === "string") {
8836
+ texts.push(part.text);
8837
+ }
8838
+ }
8839
+ }
8840
+ usageTracker.recordEstimatedInput(callId, countTokens2(texts.join("\n")));
8841
+ }
7478
8842
  });
7479
8843
  if (config.compress.permission !== "deny") {
7480
8844
  const define = (state) => {
@@ -7485,8 +8849,19 @@ async function setup(ctx) {
7485
8849
  config,
7486
8850
  prompts,
7487
8851
  generate: async (input) => {
7488
- const result = await ctx.session.generate(input);
7489
- return result?.text ?? "";
8852
+ try {
8853
+ const result = await ctx.session.generate({
8854
+ sessionID: input.sessionID,
8855
+ prompt: input.prompt
8856
+ });
8857
+ const text = result?.text ?? "";
8858
+ const usage = input.callId ? usageTracker.resolve(input.callId, text) : void 0;
8859
+ return { text, usage };
8860
+ } finally {
8861
+ if (input.callId) {
8862
+ usageTracker.discard(input.callId);
8863
+ }
8864
+ }
7490
8865
  }
7491
8866
  };
7492
8867
  return createRcCompressTool(context2);