@royalcat/opencode-dcp-rc 4.0.0 → 4.0.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.
package/dist/index.js CHANGED
@@ -2250,6 +2250,102 @@ import { existsSync as existsSync4 } from "fs";
2250
2250
  import { homedir as homedir4 } from "os";
2251
2251
  import { join as join4 } from "path";
2252
2252
 
2253
+ // lib/compress/usage.ts
2254
+ function emptyCompressionUsage() {
2255
+ return {
2256
+ calls: 0,
2257
+ providerCalls: 0,
2258
+ estimatedCalls: 0,
2259
+ inputTokens: 0,
2260
+ outputTokens: 0,
2261
+ cacheReadTokens: 0,
2262
+ cacheWriteTokens: 0,
2263
+ reasoningTokens: 0
2264
+ };
2265
+ }
2266
+ function toCount(value, fallback = 0) {
2267
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
2268
+ return fallback;
2269
+ }
2270
+ return Math.round(value);
2271
+ }
2272
+ function normalizeProviderUsage(raw) {
2273
+ if (!raw || typeof raw !== "object") {
2274
+ return void 0;
2275
+ }
2276
+ const value = raw;
2277
+ const input = value.inputTokens && typeof value.inputTokens === "object" ? value.inputTokens : {};
2278
+ const output2 = value.outputTokens && typeof value.outputTokens === "object" ? value.outputTokens : {};
2279
+ const cacheRead = toCount(input.cacheRead);
2280
+ const cacheWrite = toCount(input.cacheWrite);
2281
+ const uncached = toCount(input.noCache);
2282
+ const inputFromParts = uncached + cacheRead + cacheWrite;
2283
+ const inputTotal = Math.max(toCount(input.total, inputFromParts), inputFromParts);
2284
+ const reasoning = toCount(output2.reasoning);
2285
+ const outputTotal = Math.max(toCount(output2.total, toCount(output2.text) + reasoning), reasoning);
2286
+ if (inputTotal === 0 && outputTotal === 0 && cacheRead === 0 && cacheWrite === 0 && reasoning === 0) {
2287
+ return void 0;
2288
+ }
2289
+ return {
2290
+ inputTokens: inputTotal,
2291
+ outputTokens: outputTotal,
2292
+ cacheReadTokens: cacheRead,
2293
+ cacheWriteTokens: cacheWrite,
2294
+ reasoningTokens: reasoning,
2295
+ source: "provider"
2296
+ };
2297
+ }
2298
+ function estimatedCompressionUsage(inputTokens, outputTokens) {
2299
+ return {
2300
+ inputTokens: toCount(inputTokens),
2301
+ outputTokens: toCount(outputTokens),
2302
+ cacheReadTokens: 0,
2303
+ cacheWriteTokens: 0,
2304
+ reasoningTokens: 0,
2305
+ source: "estimated"
2306
+ };
2307
+ }
2308
+ function addCompressionUsage(totals, usage) {
2309
+ return {
2310
+ calls: totals.calls + 1,
2311
+ providerCalls: totals.providerCalls + (usage.source === "provider" ? 1 : 0),
2312
+ estimatedCalls: totals.estimatedCalls + (usage.source === "estimated" ? 1 : 0),
2313
+ inputTokens: totals.inputTokens + usage.inputTokens,
2314
+ outputTokens: totals.outputTokens + usage.outputTokens,
2315
+ cacheReadTokens: totals.cacheReadTokens + usage.cacheReadTokens,
2316
+ cacheWriteTokens: totals.cacheWriteTokens + usage.cacheWriteTokens,
2317
+ reasoningTokens: totals.reasoningTokens + usage.reasoningTokens
2318
+ };
2319
+ }
2320
+ function addCompressionUsageTotals(totals, other) {
2321
+ return {
2322
+ calls: totals.calls + other.calls,
2323
+ providerCalls: totals.providerCalls + other.providerCalls,
2324
+ estimatedCalls: totals.estimatedCalls + other.estimatedCalls,
2325
+ inputTokens: totals.inputTokens + other.inputTokens,
2326
+ outputTokens: totals.outputTokens + other.outputTokens,
2327
+ cacheReadTokens: totals.cacheReadTokens + other.cacheReadTokens,
2328
+ cacheWriteTokens: totals.cacheWriteTokens + other.cacheWriteTokens,
2329
+ reasoningTokens: totals.reasoningTokens + other.reasoningTokens
2330
+ };
2331
+ }
2332
+ function normalizeCompressionUsageTotals(value) {
2333
+ if (!value || typeof value !== "object") {
2334
+ return emptyCompressionUsage();
2335
+ }
2336
+ const raw = value;
2337
+ return {
2338
+ calls: toCount(raw.calls),
2339
+ providerCalls: toCount(raw.providerCalls),
2340
+ estimatedCalls: toCount(raw.estimatedCalls),
2341
+ inputTokens: toCount(raw.inputTokens),
2342
+ outputTokens: toCount(raw.outputTokens),
2343
+ cacheReadTokens: toCount(raw.cacheReadTokens),
2344
+ cacheWriteTokens: toCount(raw.cacheWriteTokens),
2345
+ reasoningTokens: toCount(raw.reasoningTokens)
2346
+ };
2347
+ }
2348
+
2253
2349
  // lib/messages/shape.ts
2254
2350
  function isMessageWithInfo(message) {
2255
2351
  if (!message || typeof message !== "object") {
@@ -2824,7 +2920,8 @@ function emptyPersistedState(manualMode) {
2824
2920
  },
2825
2921
  stats: {
2826
2922
  pruneTokenCounter: 0,
2827
- totalPruneTokens: 0
2923
+ totalPruneTokens: 0,
2924
+ compressionUsage: emptyCompressionUsage()
2828
2925
  },
2829
2926
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
2830
2927
  };
@@ -2845,7 +2942,8 @@ async function loadAllSessionStats(logger) {
2845
2942
  totalTokens: 0,
2846
2943
  totalTools: 0,
2847
2944
  totalMessages: 0,
2848
- sessionCount: 0
2945
+ sessionCount: 0,
2946
+ compressionUsage: emptyCompressionUsage()
2849
2947
  };
2850
2948
  try {
2851
2949
  if (!existsSync4(STORAGE_DIR)) {
@@ -2858,10 +2956,16 @@ async function loadAllSessionStats(logger) {
2858
2956
  const filePath = join4(STORAGE_DIR, file);
2859
2957
  const content = await fs.readFile(filePath, "utf-8");
2860
2958
  const state = JSON.parse(content);
2861
- if (state?.stats?.totalPruneTokens && state?.prune) {
2862
- result.totalTokens += state.stats.totalPruneTokens;
2959
+ const usage = normalizeCompressionUsageTotals(state?.stats?.compressionUsage);
2960
+ const hasUsage = usage.calls > 0;
2961
+ if (state?.prune && (state?.stats?.totalPruneTokens || hasUsage)) {
2962
+ result.totalTokens += state.stats?.totalPruneTokens || 0;
2863
2963
  result.totalTools += state.prune.tools ? Object.keys(state.prune.tools).length : 0;
2864
2964
  result.totalMessages += state.prune.messages?.byMessageId ? Object.keys(state.prune.messages.byMessageId).length : 0;
2965
+ result.compressionUsage = addCompressionUsageTotals(
2966
+ result.compressionUsage,
2967
+ usage
2968
+ );
2865
2969
  result.sessionCount++;
2866
2970
  }
2867
2971
  } catch {
@@ -3302,7 +3406,8 @@ function createSessionState(idFormat = "xml") {
3302
3406
  },
3303
3407
  stats: {
3304
3408
  pruneTokenCounter: 0,
3305
- totalPruneTokens: 0
3409
+ totalPruneTokens: 0,
3410
+ compressionUsage: emptyCompressionUsage()
3306
3411
  },
3307
3412
  compressionTiming: {
3308
3413
  startsByCallId: /* @__PURE__ */ new Map(),
@@ -3339,7 +3444,8 @@ function resetSessionState(state) {
3339
3444
  };
3340
3445
  state.stats = {
3341
3446
  pruneTokenCounter: 0,
3342
- totalPruneTokens: 0
3447
+ totalPruneTokens: 0,
3448
+ compressionUsage: emptyCompressionUsage()
3343
3449
  };
3344
3450
  state.toolParameters.clear();
3345
3451
  state.subAgentResultCache.clear();
@@ -3385,7 +3491,8 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
3385
3491
  );
3386
3492
  state.stats = {
3387
3493
  pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
3388
- totalPruneTokens: persisted.stats?.totalPruneTokens || 0
3494
+ totalPruneTokens: persisted.stats?.totalPruneTokens || 0,
3495
+ compressionUsage: normalizeCompressionUsageTotals(persisted.stats?.compressionUsage)
3389
3496
  };
3390
3497
  const applied = applyPendingCompressionDurations(state);
3391
3498
  if (applied > 0) {
@@ -4562,6 +4669,22 @@ ${output2}`);
4562
4669
 
4563
4670
  // lib/compress/summary.ts
4564
4671
  var RC_SUMMARY_MARKER = "[[DCP-RC-SUMMARY]]";
4672
+ var RC_SUMMARY_MARKER_PREFIX = "[[DCP-RC-SUMMARY";
4673
+ function sanitizeCallId(callId) {
4674
+ return callId.replace(/[^A-Za-z0-9_-]/g, "-");
4675
+ }
4676
+ function buildSummaryMarker(callId) {
4677
+ if (!callId) {
4678
+ return RC_SUMMARY_MARKER;
4679
+ }
4680
+ return `${RC_SUMMARY_MARKER_PREFIX}:${sanitizeCallId(callId)}]]`;
4681
+ }
4682
+ function hasSummaryMarker(text) {
4683
+ return text.includes(RC_SUMMARY_MARKER_PREFIX);
4684
+ }
4685
+ function extractSummaryCallId(text) {
4686
+ return text.match(/\[\[DCP-RC-SUMMARY:([A-Za-z0-9_-]+)\]\]/)?.[1];
4687
+ }
4565
4688
  function stringify(value) {
4566
4689
  if (typeof value === "string") {
4567
4690
  return value;
@@ -4641,7 +4764,7 @@ var SUMMARY_RULES = `RULES
4641
4764
  - 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
4765
  - Drop noise: failed attempts that led nowhere, repeated tool output, exploration that produced no durable result.
4643
4766
  - Be dense and precise. Prefer terse technical wording over narrative.`;
4644
- function buildSummaryPrompt(selectors, idFormat = "xml") {
4767
+ function buildSummaryPrompt(selectors, idFormat = "xml", callId) {
4645
4768
  const selectorList = selectors.map((segment) => segment.selector);
4646
4769
  const example = selectorList[0] ?? (idFormat === "compact" ? "" : "m0004");
4647
4770
  const body = selectors.map((segment) => {
@@ -4652,7 +4775,7 @@ ${prior.body.trim()}`
4652
4775
  ).join("\n\n");
4653
4776
  return [messageBody, priorBody].filter((part) => part.length > 0).join("\n\n");
4654
4777
  }).join("\n\n");
4655
- return `${RC_SUMMARY_MARKER}
4778
+ return `${buildSummaryMarker(callId)}
4656
4779
 
4657
4780
  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
4781
 
@@ -4759,6 +4882,11 @@ function parseSummarySections(text, expected, idFormat = "xml") {
4759
4882
  }
4760
4883
 
4761
4884
  // lib/compress/rc.ts
4885
+ var summaryCallCounter = 0;
4886
+ function nextSummaryCallId() {
4887
+ summaryCallCounter += 1;
4888
+ return `c${summaryCallCounter.toString(36)}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
4889
+ }
4762
4890
  function validateArgs(args) {
4763
4891
  if (!args || typeof args !== "object") {
4764
4892
  throw new Error("compress requires an object argument with an `ids` array");
@@ -4905,7 +5033,12 @@ function createRcCompressTool(ctx) {
4905
5033
  messages: plan.messages,
4906
5034
  priorSummaries: plan.priorSummaries
4907
5035
  }));
4908
- const text = await generateSummaries(ctx, sessionID, segments);
5036
+ const { text, usage } = await generateSummaries(ctx, sessionID, segments);
5037
+ ctx.state.stats.compressionUsage = addCompressionUsageTotals(
5038
+ ctx.state.stats.compressionUsage,
5039
+ usage
5040
+ );
5041
+ await saveSessionState(ctx.state, ctx.logger);
4909
5042
  const accepted = await resolveAcceptedSections(
4910
5043
  ctx,
4911
5044
  plans.map((plan) => plan.selector.selector),
@@ -4944,8 +5077,19 @@ function createRcCompressTool(ctx) {
4944
5077
  });
4945
5078
  }
4946
5079
  async function generateSummaries(ctx, sessionID, segments) {
4947
- const prompt = buildSummaryPrompt(segments, ctx.state.idFormat);
4948
- let text = await ctx.generate({ sessionID, prompt });
5080
+ let usage = emptyCompressionUsage();
5081
+ const summarize = async (promptSegments) => {
5082
+ const callId = nextSummaryCallId();
5083
+ const prompt = buildSummaryPrompt(promptSegments, ctx.state.idFormat, callId);
5084
+ const result = await ctx.generate({ sessionID, prompt, callId });
5085
+ const text2 = result.text ?? "";
5086
+ usage = addCompressionUsage(
5087
+ usage,
5088
+ result.usage ?? estimatedCompressionUsage(countTokens2(prompt), countTokens2(text2))
5089
+ );
5090
+ return text2;
5091
+ };
5092
+ let text = await summarize(segments);
4949
5093
  for (let retry = 0; retry < 2; retry++) {
4950
5094
  const parsed = parseSummarySections(
4951
5095
  text,
@@ -4962,14 +5106,13 @@ async function generateSummaries(ctx, sessionID, segments) {
4962
5106
  if (retrySegments.length === 0) {
4963
5107
  break;
4964
5108
  }
4965
- const retryPrompt = buildSummaryPrompt(retrySegments, ctx.state.idFormat);
4966
5109
  try {
4967
- text += "\n\n" + await ctx.generate({ sessionID, prompt: retryPrompt });
5110
+ text += "\n\n" + await summarize(retrySegments);
4968
5111
  } catch {
4969
5112
  break;
4970
5113
  }
4971
5114
  }
4972
- return text;
5115
+ return { text, usage };
4973
5116
  }
4974
5117
  async function resolveAcceptedSections(ctx, selectors, text) {
4975
5118
  const parsed = parseSummarySections(text, selectors, ctx.state.idFormat);
@@ -5081,6 +5224,318 @@ async function resetManualMode(ctx, sessionID) {
5081
5224
  await saveSessionState(ctx.state, ctx.logger);
5082
5225
  }
5083
5226
 
5227
+ // lib/v2/usage.ts
5228
+ function createCompressionUsageTracker() {
5229
+ const pending = /* @__PURE__ */ new Map();
5230
+ const entry = (callId) => {
5231
+ let current = pending.get(callId);
5232
+ if (!current) {
5233
+ current = {};
5234
+ pending.set(callId, current);
5235
+ }
5236
+ return current;
5237
+ };
5238
+ return {
5239
+ recordProvider(callId, usage) {
5240
+ entry(callId).provider = usage;
5241
+ },
5242
+ recordEstimatedInput(callId, inputTokens) {
5243
+ if (Number.isFinite(inputTokens) && inputTokens > 0) {
5244
+ entry(callId).estimatedInput = Math.round(inputTokens);
5245
+ }
5246
+ },
5247
+ resolve(callId, outputText) {
5248
+ const current = pending.get(callId);
5249
+ if (!current) {
5250
+ return void 0;
5251
+ }
5252
+ if (current.provider) {
5253
+ return current.provider;
5254
+ }
5255
+ if (current.estimatedInput === void 0) {
5256
+ return void 0;
5257
+ }
5258
+ return estimatedCompressionUsage(current.estimatedInput, countTokens2(outputText));
5259
+ },
5260
+ discard(callId) {
5261
+ pending.delete(callId);
5262
+ }
5263
+ };
5264
+ }
5265
+ function findSummaryCallId(options) {
5266
+ const texts = [];
5267
+ collectTexts(options?.prompt, texts);
5268
+ for (const text of texts) {
5269
+ const callId = extractSummaryCallId(text);
5270
+ if (callId) {
5271
+ return callId;
5272
+ }
5273
+ }
5274
+ return void 0;
5275
+ }
5276
+ function collectTexts(value, out, depth = 0) {
5277
+ if (depth > 8 || out.length > 200) {
5278
+ return;
5279
+ }
5280
+ if (typeof value === "string") {
5281
+ out.push(value);
5282
+ return;
5283
+ }
5284
+ if (Array.isArray(value)) {
5285
+ for (const item of value) {
5286
+ collectTexts(item, out, depth + 1);
5287
+ }
5288
+ return;
5289
+ }
5290
+ if (value && typeof value === "object") {
5291
+ const record = value;
5292
+ if (typeof record.text === "string") {
5293
+ out.push(record.text);
5294
+ }
5295
+ if (typeof record.content === "string") {
5296
+ out.push(record.content);
5297
+ } else if (Array.isArray(record.content)) {
5298
+ collectTexts(record.content, out, depth + 1);
5299
+ }
5300
+ }
5301
+ }
5302
+ function wrapLanguageModel(model, tracker, logger) {
5303
+ return new Proxy(model, {
5304
+ get(target, property, receiver) {
5305
+ if (property === "doStream") {
5306
+ return (options) => trackedStream(target, options, tracker, logger);
5307
+ }
5308
+ if (property === "doGenerate") {
5309
+ return (options) => trackedGenerate(target, options, tracker, logger);
5310
+ }
5311
+ return Reflect.get(target, property, receiver);
5312
+ }
5313
+ });
5314
+ }
5315
+ async function trackedStream(target, options, tracker, logger) {
5316
+ const result = await target.doStream(options);
5317
+ const callId = findSummaryCallId(options);
5318
+ logger.debug("Compression usage stream call", { callId: callId ?? null });
5319
+ const TransformStreamImpl = globalThis.TransformStream;
5320
+ if (!callId || typeof result?.stream?.pipeThrough !== "function") {
5321
+ return result;
5322
+ }
5323
+ if (typeof TransformStreamImpl !== "function") {
5324
+ return result;
5325
+ }
5326
+ let recorded = false;
5327
+ const transform = new TransformStreamImpl({
5328
+ transform(part, controller) {
5329
+ try {
5330
+ if (!recorded && part?.type === "finish" && part.usage) {
5331
+ const usage = normalizeProviderUsage(part.usage);
5332
+ if (usage) {
5333
+ recorded = true;
5334
+ tracker.recordProvider(callId, usage);
5335
+ }
5336
+ }
5337
+ } catch {
5338
+ }
5339
+ controller.enqueue(part);
5340
+ }
5341
+ });
5342
+ return { ...result, stream: result.stream.pipeThrough(transform) };
5343
+ }
5344
+ async function trackedGenerate(target, options, tracker, logger) {
5345
+ const result = await target.doGenerate(options);
5346
+ const callId = findSummaryCallId(options);
5347
+ logger.debug("Compression usage generate call", { callId: callId ?? null });
5348
+ if (!callId) {
5349
+ return result;
5350
+ }
5351
+ const usage = normalizeProviderUsage(result?.usage);
5352
+ if (usage) {
5353
+ tracker.recordProvider(callId, usage);
5354
+ }
5355
+ return result;
5356
+ }
5357
+ async function installCompressionUsageHook(ctx, tracker, logger) {
5358
+ const aisdk = ctx?.aisdk;
5359
+ if (!aisdk || typeof aisdk.hook !== "function") {
5360
+ logger.debug("aisdk hook unavailable; compression usage will be estimated");
5361
+ return;
5362
+ }
5363
+ try {
5364
+ await aisdk.hook("language", (event) => {
5365
+ try {
5366
+ const isModel = (candidate) => !!candidate && typeof candidate.doStream === "function";
5367
+ logger.debug("Compression usage hook event", {
5368
+ modelID: event?.model?.modelID ?? event?.model?.id ?? null,
5369
+ hasLanguage: isModel(event?.language),
5370
+ hasFactory: typeof event?.sdk?.languageModel === "function"
5371
+ });
5372
+ let model = isModel(event?.language) ? event.language : void 0;
5373
+ if (!model && typeof event?.sdk?.languageModel === "function") {
5374
+ model = event.sdk.languageModel(
5375
+ event?.model?.modelID ?? event?.model?.id
5376
+ );
5377
+ }
5378
+ if (!isModel(model)) {
5379
+ return;
5380
+ }
5381
+ event.language = wrapLanguageModel(model, tracker, logger);
5382
+ } catch (error) {
5383
+ logger.debug("Compression usage hook could not wrap language model", {
5384
+ error: error?.message
5385
+ });
5386
+ }
5387
+ });
5388
+ logger.debug("Compression usage hook installed");
5389
+ } catch (error) {
5390
+ logger.warn("Compression usage hook registration failed", {
5391
+ error: error?.message
5392
+ });
5393
+ }
5394
+ }
5395
+ function emptyUsageAccumulator() {
5396
+ return {
5397
+ input: 0,
5398
+ output: 0,
5399
+ cacheRead: 0,
5400
+ cacheWrite: 0,
5401
+ reasoning: 0,
5402
+ exclusiveInput: false
5403
+ };
5404
+ }
5405
+ function usageField(key) {
5406
+ switch (key) {
5407
+ case "input_tokens":
5408
+ case "prompt_tokens":
5409
+ case "promptTokenCount":
5410
+ return "input";
5411
+ case "output_tokens":
5412
+ case "completion_tokens":
5413
+ case "candidatesTokenCount":
5414
+ return "output";
5415
+ case "cache_read_input_tokens":
5416
+ case "cache_creation_input_tokens":
5417
+ case "cached_tokens":
5418
+ case "cachedContentTokenCount":
5419
+ return key === "cache_creation_input_tokens" ? "cacheWrite" : "cacheRead";
5420
+ case "reasoning_tokens":
5421
+ case "thoughtsTokenCount":
5422
+ return "reasoning";
5423
+ default:
5424
+ return void 0;
5425
+ }
5426
+ }
5427
+ function scanUsage(value, acc, depth) {
5428
+ if (depth > 8 || !value || typeof value !== "object") {
5429
+ return;
5430
+ }
5431
+ if (Array.isArray(value)) {
5432
+ for (const item of value) {
5433
+ scanUsage(item, acc, depth + 1);
5434
+ }
5435
+ return;
5436
+ }
5437
+ for (const [key, raw] of Object.entries(value)) {
5438
+ const field = usageField(key);
5439
+ if (field && typeof raw === "number" && Number.isFinite(raw)) {
5440
+ acc[field] = Math.max(acc[field], raw);
5441
+ if (key === "cache_read_input_tokens" || key === "cache_creation_input_tokens") {
5442
+ acc.exclusiveInput = true;
5443
+ }
5444
+ continue;
5445
+ }
5446
+ if (raw && typeof raw === "object") {
5447
+ scanUsage(raw, acc, depth + 1);
5448
+ }
5449
+ }
5450
+ }
5451
+ function usageFromAccumulator(acc) {
5452
+ const input = acc.exclusiveInput ? acc.input + acc.cacheRead + acc.cacheWrite : acc.input;
5453
+ const usage = {
5454
+ inputTokens: Math.max(0, Math.round(input)),
5455
+ outputTokens: Math.max(0, Math.round(acc.output)),
5456
+ cacheReadTokens: Math.max(0, Math.round(acc.cacheRead)),
5457
+ cacheWriteTokens: Math.max(0, Math.round(acc.cacheWrite)),
5458
+ reasoningTokens: Math.max(0, Math.round(acc.reasoning)),
5459
+ source: "provider"
5460
+ };
5461
+ const total = usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens + usage.reasoningTokens;
5462
+ return total > 0 ? usage : void 0;
5463
+ }
5464
+ function parseProviderUsageFromBody(body) {
5465
+ const acc = emptyUsageAccumulator();
5466
+ const trimmed = body.trim();
5467
+ let parsedJson = false;
5468
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
5469
+ try {
5470
+ scanUsage(JSON.parse(trimmed), acc, 0);
5471
+ parsedJson = true;
5472
+ } catch {
5473
+ }
5474
+ }
5475
+ if (!parsedJson) {
5476
+ for (const line of body.split(/\r?\n/)) {
5477
+ const match = /^\s*data:\s*(.*)$/.exec(line);
5478
+ if (!match) {
5479
+ continue;
5480
+ }
5481
+ const payload = match[1].trim();
5482
+ if (!payload || payload === "[DONE]") {
5483
+ continue;
5484
+ }
5485
+ try {
5486
+ scanUsage(JSON.parse(payload), acc, 0);
5487
+ } catch {
5488
+ }
5489
+ }
5490
+ }
5491
+ return usageFromAccumulator(acc);
5492
+ }
5493
+ async function installHttpUsageHook(ctx, tracker, logger) {
5494
+ const session2 = ctx?.session;
5495
+ if (!session2 || typeof session2.hook !== "function") {
5496
+ return;
5497
+ }
5498
+ try {
5499
+ await session2.hook("http.response", async (event) => {
5500
+ if (event?.kind !== "generate") {
5501
+ return;
5502
+ }
5503
+ try {
5504
+ const request = event.request;
5505
+ const response = event.response;
5506
+ if (!request || typeof request.clone !== "function") {
5507
+ return;
5508
+ }
5509
+ const callId = extractSummaryCallId(await request.clone().text());
5510
+ if (!callId) {
5511
+ return;
5512
+ }
5513
+ if (!response || typeof response.clone !== "function") {
5514
+ return;
5515
+ }
5516
+ const usage = parseProviderUsageFromBody(await response.clone().text());
5517
+ if (usage) {
5518
+ tracker.recordProvider(callId, usage);
5519
+ logger.debug("Compression usage captured from http response", {
5520
+ callId,
5521
+ inputTokens: usage.inputTokens,
5522
+ outputTokens: usage.outputTokens
5523
+ });
5524
+ }
5525
+ } catch (error) {
5526
+ logger.debug("Compression usage http capture failed", {
5527
+ error: error?.message
5528
+ });
5529
+ }
5530
+ });
5531
+ logger.debug("Compression usage http hook installed");
5532
+ } catch (error) {
5533
+ logger.warn("Compression usage http hook registration failed", {
5534
+ error: error?.message
5535
+ });
5536
+ }
5537
+ }
5538
+
5084
5539
  // lib/prompts/index.ts
5085
5540
  function renderSystemPrompt(prompts, protectedToolsExtension, manual, subagent) {
5086
5541
  const extensions = [];
@@ -6592,7 +7047,7 @@ async function handleRecompressCommand(ctx) {
6592
7047
  }
6593
7048
 
6594
7049
  // lib/commands/stats.ts
6595
- function formatStatsMessage(sessionTokens, sessionSummaryTokens, sessionTools, sessionMessages, sessionDurationMs, allTime) {
7050
+ function formatStatsMessage(sessionTokens, sessionSummaryTokens, sessionTools, sessionMessages, sessionDurationMs, sessionUsage, allTime) {
6596
7051
  const lines = [];
6597
7052
  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
7053
  lines.push("\u2502 DCP Statistics \u2502");
@@ -6607,6 +7062,16 @@ function formatStatsMessage(sessionTokens, sessionSummaryTokens, sessionTools, s
6607
7062
  lines.push(` Time: ${formatCompressionTime(sessionDurationMs)}`);
6608
7063
  lines.push(` Messages: ${sessionMessages}`);
6609
7064
  lines.push(` Tools: ${sessionTools}`);
7065
+ lines.push(
7066
+ ` Summary requests: ${sessionUsage.calls} (${sessionUsage.providerCalls} provider, ${sessionUsage.estimatedCalls} estimated)`
7067
+ );
7068
+ lines.push(
7069
+ ` Request in|out: ~${formatTokenCount(sessionUsage.inputTokens)} | ~${formatTokenCount(sessionUsage.outputTokens)}`
7070
+ );
7071
+ lines.push(
7072
+ ` Request cache: ~${formatTokenCount(sessionUsage.cacheReadTokens)} read | ~${formatTokenCount(sessionUsage.cacheWriteTokens)} write`
7073
+ );
7074
+ lines.push(` Request reason: ~${formatTokenCount(sessionUsage.reasoningTokens)}`);
6610
7075
  lines.push("");
6611
7076
  lines.push("All-time:");
6612
7077
  lines.push("\u2500".repeat(60));
@@ -6614,6 +7079,9 @@ function formatStatsMessage(sessionTokens, sessionSummaryTokens, sessionTools, s
6614
7079
  lines.push(` Tools pruned: ${allTime.totalTools}`);
6615
7080
  lines.push(` Messages pruned: ${allTime.totalMessages}`);
6616
7081
  lines.push(` Sessions: ${allTime.sessionCount}`);
7082
+ lines.push(
7083
+ ` Summary requests: ${allTime.compressionUsage.calls} (~${formatTokenCount(allTime.compressionUsage.inputTokens)} in | ~${formatTokenCount(allTime.compressionUsage.outputTokens)} out)`
7084
+ );
6617
7085
  return lines.join("\n");
6618
7086
  }
6619
7087
  function formatCompressionRatio(inputTokens, outputTokens) {
@@ -6653,6 +7121,7 @@ async function handleStatsCommand(ctx) {
6653
7121
  report2.sessionTools,
6654
7122
  report2.sessionMessages,
6655
7123
  report2.sessionDurationMs,
7124
+ report2.sessionCompressionUsage,
6656
7125
  report2.allTime
6657
7126
  );
6658
7127
  const params = getCurrentParams(state, messages, logger);
@@ -6663,9 +7132,11 @@ async function handleStatsCommand(ctx) {
6663
7132
  sessionTools: report2.sessionTools,
6664
7133
  sessionMessages: report2.sessionMessages,
6665
7134
  sessionDurationMs: report2.sessionDurationMs,
7135
+ sessionCompressionUsage: report2.sessionCompressionUsage,
6666
7136
  allTimeTokens: report2.allTime.totalTokens,
6667
7137
  allTimeTools: report2.allTime.totalTools,
6668
- allTimeMessages: report2.allTime.totalMessages
7138
+ allTimeMessages: report2.allTime.totalMessages,
7139
+ allTimeCompressionUsage: report2.allTime.compressionUsage
6669
7140
  });
6670
7141
  }
6671
7142
  async function buildStatsReport(state, logger) {
@@ -6687,6 +7158,7 @@ async function buildStatsReport(state, logger) {
6687
7158
  }
6688
7159
  }
6689
7160
  const sessionTools = prunedToolIds.size;
7161
+ const sessionCompressionUsage = normalizeCompressionUsageTotals(state.stats.compressionUsage);
6690
7162
  let sessionMessages = 0;
6691
7163
  for (const entry of state.prune.messages.byMessageId.values()) {
6692
7164
  if (entry.activeBlockIds.length > 0) {
@@ -6700,6 +7172,7 @@ async function buildStatsReport(state, logger) {
6700
7172
  sessionTools,
6701
7173
  sessionMessages,
6702
7174
  sessionDurationMs,
7175
+ sessionCompressionUsage,
6703
7176
  allTime
6704
7177
  };
6705
7178
  }
@@ -7235,17 +7708,29 @@ function project(native, entries, session2) {
7235
7708
  import { tool as tool2 } from "@opencode-ai/plugin";
7236
7709
  var z = tool2.schema;
7237
7710
  var session = z.object({ sessionID: z.string() });
7711
+ var compressionUsage = z.object({
7712
+ calls: z.number(),
7713
+ providerCalls: z.number(),
7714
+ estimatedCalls: z.number(),
7715
+ inputTokens: z.number(),
7716
+ outputTokens: z.number(),
7717
+ cacheReadTokens: z.number(),
7718
+ cacheWriteTokens: z.number(),
7719
+ reasoningTokens: z.number()
7720
+ });
7238
7721
  var stats = z.object({
7239
7722
  sessionTokens: z.number(),
7240
7723
  sessionSummaryTokens: z.number(),
7241
7724
  sessionDurationMs: z.number(),
7242
7725
  sessionTools: z.number(),
7243
7726
  sessionMessages: z.number(),
7727
+ sessionCompressionUsage: compressionUsage,
7244
7728
  allTime: z.object({
7245
7729
  totalTokens: z.number(),
7246
7730
  totalTools: z.number(),
7247
7731
  totalMessages: z.number(),
7248
- sessionCount: z.number()
7732
+ sessionCount: z.number(),
7733
+ compressionUsage
7249
7734
  })
7250
7735
  });
7251
7736
  var context = z.object({
@@ -7300,6 +7785,9 @@ async function setup(ctx) {
7300
7785
  return;
7301
7786
  }
7302
7787
  const logger = new Logger(config.debug);
7788
+ const usageTracker = createCompressionUsageTracker();
7789
+ await installCompressionUsageHook(ctx, usageTracker, logger);
7790
+ await installHttpUsageHook(ctx, usageTracker, logger);
7303
7791
  const prompts = new PromptStore(
7304
7792
  logger,
7305
7793
  ctx.location.directory,
@@ -7465,7 +7953,7 @@ async function setup(ctx) {
7465
7953
  await ctx.session.hook("generate", (event) => {
7466
7954
  const own = event.messages.filter(
7467
7955
  (message) => Array.isArray(message.content) && message.content.some(
7468
- (part) => part?.type === "text" && typeof part.text === "string" && part.text.includes(RC_SUMMARY_MARKER)
7956
+ (part) => part?.type === "text" && typeof part.text === "string" && hasSummaryMarker(part.text)
7469
7957
  )
7470
7958
  );
7471
7959
  if (own.length === 0) {
@@ -7475,6 +7963,25 @@ async function setup(ctx) {
7475
7963
  for (const name of Object.keys(event.tools ?? {})) {
7476
7964
  delete event.tools[name];
7477
7965
  }
7966
+ const callId = own.flatMap(
7967
+ (message) => Array.isArray(message.content) ? message.content : []
7968
+ ).filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => extractSummaryCallId(part.text)).find((id) => !!id);
7969
+ if (callId) {
7970
+ const texts = [];
7971
+ for (const part of event.system ?? []) {
7972
+ if (typeof part?.text === "string") {
7973
+ texts.push(part.text);
7974
+ }
7975
+ }
7976
+ for (const message of own) {
7977
+ for (const part of message.content ?? []) {
7978
+ if (part?.type === "text" && typeof part.text === "string") {
7979
+ texts.push(part.text);
7980
+ }
7981
+ }
7982
+ }
7983
+ usageTracker.recordEstimatedInput(callId, countTokens2(texts.join("\n")));
7984
+ }
7478
7985
  });
7479
7986
  if (config.compress.permission !== "deny") {
7480
7987
  const define = (state) => {
@@ -7485,8 +7992,19 @@ async function setup(ctx) {
7485
7992
  config,
7486
7993
  prompts,
7487
7994
  generate: async (input) => {
7488
- const result = await ctx.session.generate(input);
7489
- return result?.text ?? "";
7995
+ try {
7996
+ const result = await ctx.session.generate({
7997
+ sessionID: input.sessionID,
7998
+ prompt: input.prompt
7999
+ });
8000
+ const text = result?.text ?? "";
8001
+ const usage = input.callId ? usageTracker.resolve(input.callId, text) : void 0;
8002
+ return { text, usage };
8003
+ } finally {
8004
+ if (input.callId) {
8005
+ usageTracker.discard(input.callId);
8006
+ }
8007
+ }
7490
8008
  }
7491
8009
  };
7492
8010
  return createRcCompressTool(context2);