sentinelayer-cli 0.34.5 → 0.34.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.34.5",
3
+ "version": "0.34.7",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -109,6 +109,7 @@ import {
109
109
  } from "../session/rotating-log.js";
110
110
  import { buildSessionRecap } from "../session/recap.js";
111
111
  import { computeTranscriptStats } from "../session/transcript.js";
112
+ import { buildSessionUsageLedger } from "../session/pricing-ledger.js";
112
113
  import {
113
114
  buildSessionUsageReport,
114
115
  buildSessionUsageReportFromLedgerPayload,
@@ -150,6 +151,22 @@ function normalizeString(value) {
150
151
  return String(value || "").trim();
151
152
  }
152
153
 
154
+ // A wake hook hands remote-influenced event fields to a local host command
155
+ // (as SL_WAKE_* env vars). Bound + strip each value so a crafted event cannot
156
+ // inject control chars / newlines into the child environment or blow it up.
157
+ const WAKE_VALUE_MAX_LENGTH = 512;
158
+ function sanitizeWakeValue(value) {
159
+ // Strip control chars (incl. NUL / newline) by codepoint so a crafted remote
160
+ // event cannot inject into the child env; a codepoint check avoids putting a
161
+ // control-char regex literal in source.
162
+ let out = "";
163
+ for (const ch of normalizeString(value)) {
164
+ const code = ch.codePointAt(0);
165
+ out += code < 0x20 || code === 0x7f ? " " : ch;
166
+ }
167
+ return out.slice(0, WAKE_VALUE_MAX_LENGTH);
168
+ }
169
+
153
170
  function messagePartsHaveContent(messageParts) {
154
171
  const parts = Array.isArray(messageParts) ? messageParts : [messageParts];
155
172
  return parts.some((part) => normalizeString(part));
@@ -2160,14 +2177,28 @@ export function createSessionWakeRunner({
2160
2177
  return;
2161
2178
  }
2162
2179
  busy = true;
2180
+ const rawSequence = event?.sequenceId ?? event?.sequence_id ?? "";
2181
+ const sanitizedSequence = sanitizeWakeValue(rawSequence);
2163
2182
  const env = {
2164
2183
  ...process.env,
2165
- SL_WAKE_SESSION_ID: normalizeString(sessionId),
2166
- SL_WAKE_AGENT_ID: normalizeString(agentId),
2167
- SL_WAKE_EVENT_TYPE: normalizeString(event?.event),
2168
- SL_WAKE_EVENT_CURSOR: normalizeString(event?.cursor),
2169
- SL_WAKE_EVENT_SEQUENCE: String(event?.sequenceId ?? event?.sequence_id ?? ""),
2170
- SL_WAKE_ACTOR_ID: normalizeString(event?.agent?.id || event?.agentId),
2184
+ SL_WAKE_SESSION_ID: sanitizeWakeValue(sessionId),
2185
+ SL_WAKE_AGENT_ID: sanitizeWakeValue(agentId),
2186
+ SL_WAKE_EVENT_TYPE: sanitizeWakeValue(event?.event),
2187
+ SL_WAKE_EVENT_CURSOR: sanitizeWakeValue(event?.cursor),
2188
+ SL_WAKE_EVENT_SEQUENCE: sanitizedSequence,
2189
+ SL_WAKE_ACTOR_ID: sanitizeWakeValue(event?.agent?.id || event?.agentId),
2190
+ };
2191
+ // Metadata-only wake envelope for the host command's stdin. Deliberately
2192
+ // EXCLUDES the event body/content so session message text / PII never
2193
+ // reaches an external process — the woken agent re-reads the room over its
2194
+ // own authenticated channel using this cursor/sequence.
2195
+ const wakePayload = {
2196
+ event: env.SL_WAKE_EVENT_TYPE,
2197
+ sessionId: env.SL_WAKE_SESSION_ID,
2198
+ agentId: env.SL_WAKE_AGENT_ID,
2199
+ cursor: env.SL_WAKE_EVENT_CURSOR,
2200
+ sequenceId: typeof rawSequence === "number" && Number.isFinite(rawSequence) ? rawSequence : sanitizedSequence || null,
2201
+ actorId: env.SL_WAKE_ACTOR_ID,
2171
2202
  };
2172
2203
  let child;
2173
2204
  try {
@@ -2185,7 +2216,7 @@ export function createSessionWakeRunner({
2185
2216
  });
2186
2217
  try {
2187
2218
  if (child && child.stdin) {
2188
- child.stdin.write(JSON.stringify(event ?? {}));
2219
+ child.stdin.write(JSON.stringify(wakePayload));
2189
2220
  child.stdin.end();
2190
2221
  }
2191
2222
  } catch {
@@ -4509,7 +4540,7 @@ export function registerSessionCommand(program) {
4509
4540
  .option("--max-polls <n>", "Stop after N poll cycles (useful for tests and smoke checks)")
4510
4541
  .option(
4511
4542
  "--wake <command>",
4512
- "Wake hook: run this shell command on each matched event (notify->resume bridge). Event JSON is piped to stdin; SL_WAKE_* env vars are set.",
4543
+ "Wake hook: run this shell command on each matched event (notify->resume bridge). Metadata JSON is piped to stdin; sanitized SL_WAKE_* env vars are set.",
4513
4544
  )
4514
4545
  .option(
4515
4546
  "--wake-host <name>",
@@ -6267,9 +6298,14 @@ export function registerSessionCommand(program) {
6267
6298
  const hiddenControlEventCount =
6268
6299
  dedupedEvents.length - transcriptBaseEvents.length + rawActionEvents.length - actionEvents.length;
6269
6300
  const exportEvents = mergeSessionActionEvents(transcriptBaseEvents, actionEvents);
6301
+ const usageLedger = buildSessionUsageLedger(exportEvents, {
6302
+ sessionId: normalizedSessionId,
6303
+ includeEstimatedMessages: true,
6304
+ });
6270
6305
  const stats = computeTranscriptStats({
6271
6306
  sessionMeta: sessionPayload,
6272
6307
  events: exportEvents,
6308
+ usageLedger,
6273
6309
  });
6274
6310
  const participants = buildSessionParticipants({
6275
6311
  statsAgents: stats.agents,
@@ -227,16 +227,21 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
227
227
  with: {
228
228
  github_token: "${{ github.token }}",
229
229
  openai_api_key: "${{ secrets.OPENAI_API_KEY }}",
230
- google_api_key: "${{ secrets.GOOGLE_API_KEY }}",
231
- llm_provider: "${{ secrets.GOOGLE_API_KEY != '' && 'google' || 'openai' }}",
230
+ google_api_key:
231
+ "${{ secrets.GOOGLE_GEMINI_API_KEY != '' && secrets.GOOGLE_GEMINI_API_KEY || secrets.GOOGLE_API_KEY }}",
232
+ llm_provider:
233
+ "${{ secrets.OPENAI_API_KEY != '' && 'openai' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'google' || 'openai') }}",
232
234
  sentinelayer_token: `\${{ secrets.${normalizedSecret} }}`,
233
- sentinelayer_managed_llm: `\${{ secrets.GOOGLE_API_KEY == '' && secrets.OPENAI_API_KEY == '' && secrets.${normalizedSecret} != '' }}`,
235
+ sentinelayer_managed_llm: `\${{ secrets.GOOGLE_GEMINI_API_KEY == '' && secrets.GOOGLE_API_KEY == '' && secrets.OPENAI_API_KEY == '' && secrets.${normalizedSecret} != '' }}`,
234
236
  scan_mode: profile.scanMode || "deep",
235
237
  severity_gate: profile.severityGate || "P1",
236
- model: "${{ secrets.GOOGLE_API_KEY != '' && 'gemini-2.5-pro' || 'gpt-5.3-codex' }}",
238
+ model:
239
+ "${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-5.3-codex') }}",
237
240
  codex_model: "gpt-5.3-codex",
238
- model_fallback: "${{ secrets.GOOGLE_API_KEY != '' && 'gemini-2.5-flash' || 'gpt-4.1-mini' }}",
239
- use_codex: "${{ secrets.GOOGLE_API_KEY == '' }}",
241
+ model_fallback:
242
+ "${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-4.1-mini' }}",
243
+ use_codex:
244
+ "${{ secrets.OPENAI_API_KEY != '' || (secrets.GOOGLE_GEMINI_API_KEY == '' && secrets.GOOGLE_API_KEY == '') }}",
240
245
  codex_only: "false",
241
246
  llm_failure_policy: "block",
242
247
  max_daily_scans: "${{ vars.OMAR_MAX_DAILY_SCANS || '200' }}",
@@ -295,11 +300,15 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
295
300
  OMAR_SEVERITY_GATE: "${{ github.event_name == 'workflow_dispatch' && inputs.severity_gate || 'P1' }}",
296
301
  OMAR_P2_MAX_ALLOWED: "${{ github.event_name == 'workflow_dispatch' && inputs.p2_max_allowed || '5' }}",
297
302
  OMAR_OPENAI_KEY_PRESENT: "${{ secrets.OPENAI_API_KEY != '' }}",
298
- OMAR_GOOGLE_KEY_PRESENT: "${{ secrets.GOOGLE_API_KEY != '' }}",
299
- OMAR_MANAGED_LLM: `\${{ secrets.GOOGLE_API_KEY == '' && secrets.OPENAI_API_KEY == '' && secrets.${normalizedSecret} != '' }}`,
300
- OMAR_LLM_PROVIDER: "${{ secrets.GOOGLE_API_KEY != '' && 'google' || 'openai' }}",
301
- OMAR_MODEL: "${{ secrets.GOOGLE_API_KEY != '' && 'gemini-2.5-pro' || 'gpt-5.3-codex' }}",
302
- OMAR_MODEL_FALLBACK: "${{ secrets.GOOGLE_API_KEY != '' && 'gemini-2.5-flash' || 'gpt-4.1-mini' }}",
303
+ OMAR_GOOGLE_KEY_PRESENT:
304
+ "${{ secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '' }}",
305
+ OMAR_MANAGED_LLM: `\${{ secrets.GOOGLE_GEMINI_API_KEY == '' && secrets.GOOGLE_API_KEY == '' && secrets.OPENAI_API_KEY == '' && secrets.${normalizedSecret} != '' }}`,
306
+ OMAR_LLM_PROVIDER:
307
+ "${{ secrets.OPENAI_API_KEY != '' && 'openai' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'google' || 'openai') }}",
308
+ OMAR_MODEL:
309
+ "${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-5.3-codex') }}",
310
+ OMAR_MODEL_FALLBACK:
311
+ "${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-4.1-mini' }}",
303
312
  },
304
313
  run: [
305
314
  "set -euo pipefail",
@@ -344,7 +353,7 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
344
353
  " 'llm_provider': env('OMAR_LLM_PROVIDER', 'openai'),",
345
354
  " 'model': env('OMAR_MODEL', 'gpt-5.3-codex'),",
346
355
  " 'model_fallback': env('OMAR_MODEL_FALLBACK', 'gpt-4.1-mini'),",
347
- " 'llm_route': 'google_api_key' if bool_env('OMAR_GOOGLE_KEY_PRESENT') else ('openai_api_key' if bool_env('OMAR_OPENAI_KEY_PRESENT') else 'sentinelayer_managed'),",
356
+ " 'llm_route': 'openai_api_key' if bool_env('OMAR_OPENAI_KEY_PRESENT') else ('google_api_key' if bool_env('OMAR_GOOGLE_KEY_PRESENT') else 'sentinelayer_managed'),",
348
357
  " 'google_key_present': bool_env('OMAR_GOOGLE_KEY_PRESENT'),",
349
358
  " 'openai_key_present': bool_env('OMAR_OPENAI_KEY_PRESENT'),",
350
359
  " 'managed_llm': bool_env('OMAR_MANAGED_LLM'),",
@@ -4,10 +4,12 @@ import {
4
4
  DEFAULT_PRICE_BOOK_VERSION,
5
5
  estimateModelCost,
6
6
  } from "../cost/tracker.js";
7
+ import { estimateTokens } from "../cost/tokenizer.js";
7
8
 
8
9
  const SESSION_USAGE_EVENT = "session_usage";
9
10
  export const BILLING_SESSION_USAGE_SCHEMA = "billing/v1";
10
11
  export const LOCAL_SESSION_USAGE_SCHEMA = "session_usage/local-v1";
12
+ export const ESTIMATED_MESSAGE_USAGE_SCHEMA = "session_usage/estimated-message-v1";
11
13
  const LEGACY_SESSION_USAGE_SCHEMAS = new Set(["", "session_usage/v0"]);
12
14
  const SUPPORTED_SESSION_USAGE_SCHEMAS = new Set([
13
15
  BILLING_SESSION_USAGE_SCHEMA,
@@ -38,6 +40,19 @@ const USAGE_HINT_KEYS = [
38
40
  "cost_usd",
39
41
  "cost",
40
42
  ];
43
+ const ESTIMATED_MESSAGE_EVENT_KINDS = new Set([
44
+ "agent_response",
45
+ "session_message",
46
+ "session_say",
47
+ ]);
48
+ const HUMAN_AGENT_IDS = new Set([
49
+ "cli-user",
50
+ "human",
51
+ "user",
52
+ "you",
53
+ ]);
54
+ const DEFAULT_ESTIMATED_MESSAGE_USAGE_DEDUPE_WINDOW_MS = 5 * 60 * 1000;
55
+ const DEFAULT_ESTIMATED_MESSAGE_USAGE_DEDUPE_SEQUENCE_WINDOW = 3;
41
56
 
42
57
  function n(value) {
43
58
  return String(value == null ? "" : value).trim();
@@ -94,6 +109,12 @@ function roundUsd(value) {
94
109
  return Math.round(Number(value || 0) * 1_000_000) / 1_000_000;
95
110
  }
96
111
 
112
+ function positiveInteger(value, fallbackValue) {
113
+ const parsed = Number(value);
114
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallbackValue;
115
+ return Math.floor(parsed);
116
+ }
117
+
97
118
  export function createSessionUsageLedgerId({
98
119
  sessionId = "",
99
120
  agentId = "",
@@ -116,6 +137,52 @@ function fallbackIdempotencyKey({ sessionId, event, agentId, action, model, tota
116
137
  return `event:${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
117
138
  }
118
139
 
140
+ function messageEstimateIdempotencyKey({ sessionId, event, agentId, model, text }) {
141
+ const payload = object(event?.payload);
142
+ const explicit =
143
+ n(payload.clientMessageId || payload.client_message_id) ||
144
+ n(event?.eventId || event?.event_id) ||
145
+ n(event?.idempotencyToken || event?.idempotency_token) ||
146
+ n(event?.cursor);
147
+ if (explicit) return `estimated-message:${explicit}`;
148
+ const sequence = n(event?.sequenceId ?? event?.sequence_id);
149
+ if (sequence) return `estimated-message:seq:${sequence}`;
150
+ const timestamp = n(event?.ts || event?.timestamp);
151
+ const source = [sessionId, timestamp, agentId, model, text].join("\x1f");
152
+ return `estimated-message:${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
153
+ }
154
+
155
+ function isLikelyHumanAgent({ agentId = "", role = "", model = "" } = {}) {
156
+ const id = n(agentId).toLowerCase();
157
+ const normalizedRole = n(role).toLowerCase();
158
+ const normalizedModel = n(model).toLowerCase();
159
+ if (!id && !normalizedModel) return true;
160
+ if (HUMAN_AGENT_IDS.has(id)) return true;
161
+ if (id.startsWith("human-") || id.startsWith("user-")) return true;
162
+ if (normalizedModel === "human") return true;
163
+ const agentLikeRole = ["coder", "reviewer", "tester", "observer", "orchestrator"].includes(normalizedRole);
164
+ const agentLikeId = /(codex|claude|gpt|gemini|grok|senti|kai-chen|agent|bot|warden|architect|builder)/i.test(id);
165
+ const agentLikeModel = /(gpt|claude|gemini|grok|codex|sonnet|opus|haiku)/i.test(normalizedModel);
166
+ if (normalizedRole === "human") return true;
167
+ if (normalizedRole === "participant" && !agentLikeId && !agentLikeModel) return true;
168
+ if (!agentLikeRole && !agentLikeId && !agentLikeModel && (!normalizedModel || normalizedModel === "unknown")) {
169
+ return true;
170
+ }
171
+ return false;
172
+ }
173
+
174
+ function responseText(payload = {}) {
175
+ if (payload.response && typeof payload.response === "object" && !Array.isArray(payload.response)) {
176
+ return n(payload.response.text);
177
+ }
178
+ return n(payload.response);
179
+ }
180
+
181
+ function messageText(event = {}) {
182
+ const payload = object(event?.payload);
183
+ return n(payload.message) || responseText(payload) || n(payload.text);
184
+ }
185
+
119
186
  function providerCostFromPriceBook({ model, inputTokens, outputTokens, explicitProviderCost }) {
120
187
  if (explicitProviderCost != null) {
121
188
  return { providerCostUsd: explicitProviderCost, unpriced: false };
@@ -217,9 +284,139 @@ export function buildUsageLedgerEntry(
217
284
  unpriced,
218
285
  timestamp: n(event?.ts || event?.timestamp),
219
286
  sequenceId: nonNegativeInt(event?.sequenceId ?? event?.sequence_id),
287
+ estimated: false,
288
+ };
289
+ }
290
+
291
+ export function buildEstimatedMessageLedgerEntry(
292
+ event,
293
+ {
294
+ sessionId = "",
295
+ priceBookVersion = DEFAULT_PRICE_BOOK_VERSION,
296
+ billingTier = "estimated",
297
+ includeHumanMessages = false,
298
+ action = "estimated_agent_message",
299
+ } = {},
300
+ ) {
301
+ const kind = n(event?.event || event?.type);
302
+ if (!ESTIMATED_MESSAGE_EVENT_KINDS.has(kind)) return null;
303
+
304
+ const payload = object(event?.payload);
305
+ if (hasUsageHints(payload) || hasUsageHints(payload.usage)) return null;
306
+
307
+ const text = messageText(event);
308
+ if (!text) return null;
309
+
310
+ const agent = object(event?.agent);
311
+ const agentId = n(agent.id || event?.agentId) || "unknown";
312
+ const model = n(agent.model || event?.agentModel || payload.model) || "unknown";
313
+ const role = n(agent.role || payload.role);
314
+ const lowerAgentId = agentId.toLowerCase();
315
+ if (lowerAgentId === "senti" || lowerAgentId === "kai-chen") {
316
+ return null;
317
+ }
318
+ if (!includeHumanMessages && isLikelyHumanAgent({ agentId, role, model })) {
319
+ return null;
320
+ }
321
+
322
+ const outputTokens = estimateTokens(text, {
323
+ model,
324
+ provider: n(agent.provider || payload.provider),
325
+ });
326
+ if (outputTokens <= 0) return null;
327
+
328
+ const actionName = n(action) || "estimated_agent_message";
329
+ const idempotencyKey = messageEstimateIdempotencyKey({
330
+ sessionId,
331
+ event,
332
+ agentId,
333
+ model,
334
+ text,
335
+ });
336
+ const ledgerEntryId = createSessionUsageLedgerId({
337
+ sessionId,
338
+ agentId,
339
+ action: actionName,
340
+ idempotencyKey,
341
+ });
342
+ const { providerCostUsd, unpriced } = providerCostFromPriceBook({
343
+ model,
344
+ inputTokens: 0,
345
+ outputTokens,
346
+ explicitProviderCost: null,
347
+ });
348
+
349
+ return {
350
+ ledgerEntryId,
351
+ idempotencyKey,
352
+ schema: ESTIMATED_MESSAGE_USAGE_SCHEMA,
353
+ sessionId: n(sessionId),
354
+ agentId,
355
+ action: actionName,
356
+ model,
357
+ priceBookVersion: n(priceBookVersion) || DEFAULT_PRICE_BOOK_VERSION,
358
+ billingTier: n(billingTier) || "estimated",
359
+ provider: n(agent.provider || payload.provider),
360
+ inputTokens: 0,
361
+ outputTokens,
362
+ totalTokens: outputTokens,
363
+ providerCostUsd: roundUsd(providerCostUsd),
364
+ customerCostUsd: null,
365
+ unpriced,
366
+ timestamp: n(event?.ts || event?.timestamp),
367
+ sequenceId: nonNegativeInt(event?.sequenceId ?? event?.sequence_id),
368
+ estimated: true,
220
369
  };
221
370
  }
222
371
 
372
+ function collectRealUsageAnchors(events = [], options = {}) {
373
+ const anchors = new Map();
374
+ for (const event of events) {
375
+ const entry = buildUsageLedgerEntry(event, options);
376
+ if (!entry || entry.estimated) continue;
377
+ if (!anchors.has(entry.agentId)) anchors.set(entry.agentId, []);
378
+ anchors.get(entry.agentId).push({
379
+ timestampMs: Date.parse(n(entry.timestamp)),
380
+ sequenceId: Number.isFinite(entry.sequenceId) ? entry.sequenceId : null,
381
+ });
382
+ }
383
+ return anchors;
384
+ }
385
+
386
+ function hasNearbyRealUsageAnchor(entry, anchors, options = {}) {
387
+ if (!entry || !entry.estimated || !anchors || anchors.size === 0) return false;
388
+ const agentAnchors = anchors.get(entry.agentId);
389
+ if (!agentAnchors || agentAnchors.length === 0) return false;
390
+ const windowMs = positiveInteger(
391
+ options.estimatedMessageUsageDedupeWindowMs,
392
+ DEFAULT_ESTIMATED_MESSAGE_USAGE_DEDUPE_WINDOW_MS,
393
+ );
394
+ const sequenceWindow = positiveInteger(
395
+ options.estimatedMessageUsageDedupeSequenceWindow,
396
+ DEFAULT_ESTIMATED_MESSAGE_USAGE_DEDUPE_SEQUENCE_WINDOW,
397
+ );
398
+ const entryTimestampMs = Date.parse(n(entry.timestamp));
399
+ const hasEntryTimestamp = Number.isFinite(entryTimestampMs);
400
+ const entrySequenceId = Number.isFinite(entry.sequenceId) ? entry.sequenceId : null;
401
+ return agentAnchors.some((anchor) => {
402
+ if (
403
+ entrySequenceId !== null &&
404
+ Number.isFinite(anchor.sequenceId) &&
405
+ Math.abs(entrySequenceId - anchor.sequenceId) <= sequenceWindow
406
+ ) {
407
+ return true;
408
+ }
409
+ if (
410
+ hasEntryTimestamp &&
411
+ Number.isFinite(anchor.timestampMs) &&
412
+ Math.abs(entryTimestampMs - anchor.timestampMs) <= windowMs
413
+ ) {
414
+ return true;
415
+ }
416
+ return false;
417
+ });
418
+ }
419
+
223
420
  function newRollup(label) {
224
421
  return {
225
422
  label,
@@ -231,6 +428,7 @@ function newRollup(label) {
231
428
  customerCostUsd: 0,
232
429
  hasCustomerCost: false,
233
430
  unpriced: 0,
431
+ estimatedEntries: 0,
234
432
  };
235
433
  }
236
434
 
@@ -245,6 +443,7 @@ function addToRollup(rollup, entry) {
245
443
  rollup.hasCustomerCost = true;
246
444
  }
247
445
  if (entry.unpriced) rollup.unpriced += 1;
446
+ if (entry.estimated) rollup.estimatedEntries += 1;
248
447
  }
249
448
 
250
449
  function finalizeRollup(rollup) {
@@ -257,6 +456,9 @@ export function buildSessionUsageLedger(events = [], options = {}) {
257
456
  if (!Array.isArray(events)) {
258
457
  throw new Error("events must be an array.");
259
458
  }
459
+ const realUsageAnchors = options.includeEstimatedMessages
460
+ ? collectRealUsageAnchors(events, options)
461
+ : new Map();
260
462
  const entries = [];
261
463
  const totals = newRollup("session");
262
464
  const perAgent = new Map();
@@ -266,8 +468,13 @@ export function buildSessionUsageLedger(events = [], options = {}) {
266
468
  let duplicatesSkipped = 0;
267
469
 
268
470
  for (const event of events) {
269
- const entry = buildUsageLedgerEntry(event, options);
471
+ const entry =
472
+ buildUsageLedgerEntry(event, options) ||
473
+ (options.includeEstimatedMessages
474
+ ? buildEstimatedMessageLedgerEntry(event, options)
475
+ : null);
270
476
  if (!entry) continue;
477
+ if (hasNearbyRealUsageAnchor(entry, realUsageAnchors, options)) continue;
271
478
  const dedupeKeys = [
272
479
  entry.idempotencyKey ? `idem:${entry.idempotencyKey}` : "",
273
480
  entry.ledgerEntryId ? `ledger:${entry.ledgerEntryId}` : "",
@@ -22,6 +22,23 @@ const DEFAULT_TASK_SUMMARY_LIMIT = 3;
22
22
  const DEFAULT_WORK_PLAN_SUMMARY_LIMIT = 5;
23
23
  const MAX_WORK_PLAN_BYTES = 128_000;
24
24
  const WORK_PLAN_RELATIVE_PATH = "tasks/todo.md";
25
+ const HISTORICAL_WORK_PLAN_MIN_COMPLETED = 25;
26
+ const HISTORICAL_WORK_PLAN_MIN_TOTAL = 50;
27
+ const HISTORICAL_WORK_PLAN_COMPLETED_RATIO = 0.75;
28
+ const GENERIC_WORK_PLAN_SECTIONS = new Set([
29
+ "plan",
30
+ "plans",
31
+ "todo",
32
+ "todos",
33
+ "to do",
34
+ "tasks",
35
+ "work items",
36
+ "workitems",
37
+ "workstream",
38
+ "workstreams",
39
+ "backlog",
40
+ "next",
41
+ ]);
25
42
  const RECAP_SOURCE_IGNORED_EVENTS = new Set([
26
43
  "agent_heartbeat",
27
44
  "agent_join",
@@ -458,6 +475,41 @@ function shortWorkPlanText(value) {
458
475
  return `${text.slice(0, 97)}...`;
459
476
  }
460
477
 
478
+ function normalizedWorkPlanSection(value = "") {
479
+ return normalizeString(value)
480
+ .toLowerCase()
481
+ .replace(/[^a-z0-9\s-]/g, "")
482
+ .replace(/\s+/g, " ")
483
+ .trim();
484
+ }
485
+
486
+ function isGenericWorkPlanSection(value = "") {
487
+ const normalized = normalizedWorkPlanSection(value);
488
+ if (!normalized) {
489
+ return false;
490
+ }
491
+ if (GENERIC_WORK_PLAN_SECTIONS.has(normalized)) {
492
+ return true;
493
+ }
494
+ return /^(active\s+)?(plan|todo|tasks|backlog)(\s+\d+)?$/.test(normalized);
495
+ }
496
+
497
+ function shouldSuppressHistoricalWorkPlanDetails(summary = emptyWorkPlanSummary()) {
498
+ const total = Number(summary.total || 0);
499
+ const open = Number(summary.open || 0);
500
+ const completed = Number(summary.completed || 0);
501
+ if (summary.truncated || open <= 0 || completed < HISTORICAL_WORK_PLAN_MIN_COMPLETED) {
502
+ return false;
503
+ }
504
+ if (total < HISTORICAL_WORK_PLAN_MIN_TOTAL) {
505
+ return false;
506
+ }
507
+ if (!isGenericWorkPlanSection(summary.currentSection)) {
508
+ return false;
509
+ }
510
+ return completed / Math.max(1, total) >= HISTORICAL_WORK_PLAN_COMPLETED_RATIO;
511
+ }
512
+
461
513
  function summarizeWorkPlanMarkdown(
462
514
  raw = "",
463
515
  { limit = DEFAULT_WORK_PLAN_SUMMARY_LIMIT, truncated = false, sourceContext = {} } = {}
@@ -507,6 +559,10 @@ function summarizeWorkPlanMarkdown(
507
559
  .filter((record) => record.status === "open")
508
560
  .slice(-normalizedLimit);
509
561
  summary.recent = records.slice(-normalizedLimit);
562
+ if (!summary.detailSuppressed && shouldSuppressHistoricalWorkPlanDetails(summary)) {
563
+ summary.detailSuppressed = true;
564
+ summary.detailSuppressionReason = "historical_generic_plan_section";
565
+ }
510
566
  if (summary.detailSuppressed) {
511
567
  summary.currentSection = "";
512
568
  summary.recentOpen = [];
@@ -785,7 +841,15 @@ function buildWorkPlanText(workPlan = emptyWorkPlanSummary()) {
785
841
  const sourceReason = normalizeString(workPlan.sourceReason);
786
842
  const sourceReasonText = sourceReason ? ` (${sourceReason})` : "";
787
843
  if (workPlan.detailSuppressed || workPlan.truncated) {
788
- return `Plan: ${open} open / ${completed} done in recent ${sourceText} window${sourceReasonText}. Current/next items suppressed because the plan file is large.`;
844
+ const suppressionReason = normalizeString(workPlan.detailSuppressionReason);
845
+ const sourceWindowText = workPlan.truncated
846
+ ? `recent ${sourceText} window`
847
+ : sourceText;
848
+ const suppressionText =
849
+ suppressionReason === "historical_generic_plan_section"
850
+ ? "Current/next items suppressed because this generic plan is mostly historical completed work."
851
+ : "Current/next items suppressed because the plan file is large.";
852
+ return `Plan: ${open} open / ${completed} done in ${sourceWindowText}${sourceReasonText}. ${suppressionText}`;
789
853
  }
790
854
  const currentSection = normalizeString(workPlan.currentSection);
791
855
  const currentText = currentSection ? ` Current: ${currentSection}.` : "";
@@ -814,7 +878,7 @@ function roundCurrency(value) {
814
878
  }
815
879
 
816
880
  function normalizeUsageSummary(events = []) {
817
- const aggregate = aggregateSessionUsage(events);
881
+ const aggregate = aggregateSessionUsage(events, { includeEstimatedMessages: true });
818
882
  const totals = {
819
883
  totalTokens: Number(aggregate.totals.totalTokens || 0),
820
884
  inputTokens: Number(aggregate.totals.inputTokens || 0),
@@ -349,6 +349,65 @@ function recordCircuitSuccess(circuit) {
349
349
  persistCircuitState();
350
350
  }
351
351
 
352
+ function responseHeader(response, name) {
353
+ const headers = response?.headers;
354
+ if (!headers || !name) return "";
355
+ if (typeof headers.get === "function") {
356
+ return normalizeString(headers.get(name));
357
+ }
358
+ const lower = String(name).toLowerCase();
359
+ if (headers instanceof Map) {
360
+ for (const [key, value] of headers.entries()) {
361
+ if (String(key).toLowerCase() === lower) return normalizeString(value);
362
+ }
363
+ return "";
364
+ }
365
+ if (typeof headers === "object") {
366
+ for (const [key, value] of Object.entries(headers)) {
367
+ if (String(key).toLowerCase() === lower) return normalizeString(value);
368
+ }
369
+ }
370
+ return "";
371
+ }
372
+
373
+ function retryAfterMsFromResponse(response, payload, nowMs = Date.now()) {
374
+ const retryAfterMs = [
375
+ payload?.retryAfterMs,
376
+ payload?.error?.retryAfterMs,
377
+ payload?.detail?.retryAfterMs,
378
+ ].map(Number).find((value) => Number.isFinite(value) && value > 0);
379
+ if (Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
380
+ return Math.ceil(retryAfterMs);
381
+ }
382
+
383
+ const retryAfterSeconds = [
384
+ payload?.retryAfterSeconds,
385
+ payload?.error?.retryAfterSeconds,
386
+ payload?.detail?.retryAfterSeconds,
387
+ ].map(Number).find((value) => Number.isFinite(value) && value > 0);
388
+ if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
389
+ return Math.ceil(retryAfterSeconds * 1000);
390
+ }
391
+
392
+ const header = responseHeader(response, "Retry-After");
393
+ if (!header) return null;
394
+ const seconds = Number(header);
395
+ if (Number.isFinite(seconds) && seconds >= 0) {
396
+ return Math.ceil(seconds * 1000);
397
+ }
398
+ const retryAtMs = Date.parse(header);
399
+ if (Number.isFinite(retryAtMs)) {
400
+ return Math.max(0, Math.ceil(retryAtMs - nowMs));
401
+ }
402
+ return null;
403
+ }
404
+
405
+ function isRateLimitResponse(response, payload) {
406
+ if (response?.status === 429) return true;
407
+ const code = normalizeString(payload?.error?.code || payload?.code || payload?.detail?.code).toUpperCase();
408
+ return code === "RATE_LIMITED";
409
+ }
410
+
352
411
  // Test-only helper. Resets both circuits in memory AND on disk so unit
353
412
  // tests that exercise hydration don't leak state across runs.
354
413
  export function __resetCircuitStateForTests(homeDir) {
@@ -1357,8 +1416,19 @@ export async function pollSessionEvents(
1357
1416
  },
1358
1417
  normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
1359
1418
  fetchImpl,
1419
+ { readErrorBody: true },
1360
1420
  );
1361
1421
  if (!response || !response.ok) {
1422
+ if (isRateLimitResponse(response, payload)) {
1423
+ return {
1424
+ ok: false,
1425
+ reason: "rate_limited",
1426
+ status: response?.status || 429,
1427
+ retryAfterMs: retryAfterMsFromResponse(response, payload, normalizedNowMs),
1428
+ events: [],
1429
+ cursor: normalizedSince || null,
1430
+ };
1431
+ }
1362
1432
  recordCircuitFailure(inboundCircuit, normalizedNowMs);
1363
1433
  return {
1364
1434
  ok: false,
@@ -1677,8 +1747,20 @@ export async function pollSessionEventsBefore(
1677
1747
  },
1678
1748
  normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
1679
1749
  fetchImpl,
1750
+ { readErrorBody: true },
1680
1751
  );
1681
1752
  if (!response || !response.ok) {
1753
+ if (isRateLimitResponse(response, payload)) {
1754
+ return {
1755
+ ok: false,
1756
+ reason: "rate_limited",
1757
+ status: response?.status || 429,
1758
+ retryAfterMs: retryAfterMsFromResponse(response, payload, normalizedNowMs),
1759
+ events: [],
1760
+ cursor: null,
1761
+ beforeSequence: Number.isFinite(normalizedBeforeSequence) ? normalizedBeforeSequence : null,
1762
+ };
1763
+ }
1682
1764
  recordCircuitFailure(inboundCircuit, normalizedNowMs);
1683
1765
  return {
1684
1766
  ok: false,
@@ -425,6 +425,7 @@ export function computeTranscriptStats({
425
425
  usageEntries: resolvedUsageLedger.entries.length,
426
426
  duplicatesSkipped: resolvedUsageLedger.duplicatesSkipped,
427
427
  unpriced: resolvedUsageLedger.totals.unpriced,
428
+ estimatedEntries: resolvedUsageLedger.totals.estimatedEntries,
428
429
  priceBookVersions: resolvedUsageLedger.priceBookVersions,
429
430
  },
430
431
  sentiActions,
@@ -504,6 +505,12 @@ function rollupsByCostAndTokens(map) {
504
505
  ));
505
506
  }
506
507
 
508
+ function estimateNote(count) {
509
+ const normalized = Math.max(0, Math.floor(Number(count || 0)));
510
+ if (normalized <= 0) return "";
511
+ return `${normalized.toLocaleString("en-US")} estimated`;
512
+ }
513
+
507
514
  function recentUsageEntries(entries, limit = 10) {
508
515
  return entries
509
516
  .map((entry, index) => ({ entry, index }))
@@ -561,7 +568,15 @@ function appendUsageLedgerSection(lines, usageLedger) {
561
568
  );
562
569
  lines.push(`Provider cost: ${usdCell(totals.providerCostUsd)} · ${customerCostText}`);
563
570
  lines.push(`Price books: ${usageLedger.priceBookVersions.map(tableText).filter(Boolean).join(", ") || "—"}`);
564
- lines.push(`Duplicates skipped: ${intCell(usageLedger.duplicatesSkipped)} · Unpriced entries: ${intCell(totals.unpriced)}`);
571
+ const estimateText = totals.estimatedEntries > 0
572
+ ? ` · Estimated entries: ${intCell(totals.estimatedEntries)}`
573
+ : "";
574
+ lines.push(
575
+ `Duplicates skipped: ${intCell(usageLedger.duplicatesSkipped)} · Unpriced entries: ${intCell(totals.unpriced)}${estimateText}`,
576
+ );
577
+ if (totals.estimatedEntries > 0) {
578
+ lines.push("_Estimated entries are output-text estimates from non-human session messages, not billing-grade provider usage._");
579
+ }
565
580
  lines.push("");
566
581
 
567
582
  appendUsageRollupTable(lines, {
@@ -580,8 +595,11 @@ function appendUsageLedgerSection(lines, usageLedger) {
580
595
  lines.push("| Time | Agent | Action | Model | Tokens | Provider cost | Customer cost | Idempotency key |");
581
596
  lines.push("|---|---|---|---|---:|---:|---:|---|");
582
597
  for (const entry of recentUsageEntries(usageLedger.entries)) {
598
+ const action = entry.estimated
599
+ ? `${entry.action} (${estimateNote(1)})`
600
+ : entry.action;
583
601
  lines.push(
584
- `| ${tableText(timestampOnly(entry.timestamp)) || "—"} | ${codeCell(entry.agentId)} | ${codeCell(entry.action)} | ${codeCell(entry.model)} | ${intCell(entry.totalTokens)} | ${usdCell(entry.providerCostUsd)} | ${optionalUsdCell(entry.customerCostUsd, entry.customerCostUsd != null)} | ${codeCell(shortenIdempotencyKey(entry.idempotencyKey))} |`,
602
+ `| ${tableText(timestampOnly(entry.timestamp)) || "—"} | ${codeCell(entry.agentId)} | ${codeCell(action)} | ${codeCell(entry.model)} | ${intCell(entry.totalTokens)} | ${usdCell(entry.providerCostUsd)} | ${optionalUsdCell(entry.customerCostUsd, entry.customerCostUsd != null)} | ${codeCell(shortenIdempotencyKey(entry.idempotencyKey))} |`,
585
603
  );
586
604
  }
587
605
  lines.push("");
@@ -608,8 +626,10 @@ export function buildTranscriptMarkdown({
608
626
  speakerProfiles = new Map(),
609
627
  options = {},
610
628
  } = {}) {
629
+ const estimateMessageUsage = options.estimateMessageUsage !== false;
611
630
  const usageLedger = buildSessionUsageLedger(events, {
612
631
  sessionId: normalize(sessionMeta.sessionId),
632
+ includeEstimatedMessages: estimateMessageUsage,
613
633
  });
614
634
  const includeSystemEvents = options.includeSystemEvents !== false;
615
635
  const stats = computeTranscriptStats({ sessionMeta, events, speakerProfiles, usageLedger });
@@ -628,8 +648,12 @@ export function buildTranscriptMarkdown({
628
648
  stats.totals.customerCostTotalUsd == null
629
649
  ? ""
630
650
  : ` · Billable: $${stats.totals.customerCostTotalUsd.toFixed(4)}`;
651
+ const estimatedText =
652
+ stats.totals.estimatedEntries > 0
653
+ ? ` · Estimated entries: ${stats.totals.estimatedEntries.toLocaleString("en-US")}`
654
+ : "";
631
655
  lines.push(
632
- `Tokens: ${stats.totals.tokenTotal.toLocaleString("en-US")} · Cost: $${stats.totals.costTotalUsd.toFixed(4)}${billableText}`,
656
+ `Tokens: ${stats.totals.tokenTotal.toLocaleString("en-US")} · Cost: $${stats.totals.costTotalUsd.toFixed(4)}${billableText}${estimatedText}`,
633
657
  );
634
658
  }
635
659
  lines.push("");
@@ -116,6 +116,7 @@ function rollupToJson(rollup) {
116
116
  providerCostUsd: rollup.providerCostUsd,
117
117
  customerCostUsd: rollup.hasCustomerCost ? rollup.customerCostUsd : null,
118
118
  unpriced: rollup.unpriced,
119
+ estimatedEntries: rollup.estimatedEntries || 0,
119
120
  };
120
121
  }
121
122
 
@@ -133,6 +134,7 @@ function hostedRollupToJson(rollup, fallbackLabel = "") {
133
134
  ? money(bag.customerCostUsd ?? bag.customer_cost_usd ?? bag.billableCostUsd ?? bag.billable_cost_usd) ?? 0
134
135
  : null,
135
136
  unpriced: nonNegativeInt(bag.unpriced),
137
+ estimatedEntries: nonNegativeInt(bag.estimatedEntries ?? bag.estimated_entries),
136
138
  };
137
139
  }
138
140
 
@@ -155,6 +157,7 @@ function entryToJson(entry) {
155
157
  providerCostUsd: entry.providerCostUsd,
156
158
  customerCostUsd: entry.customerCostUsd,
157
159
  unpriced: entry.unpriced,
160
+ estimated: Boolean(entry.estimated),
158
161
  };
159
162
  }
160
163
 
@@ -181,6 +184,7 @@ function hostedEntryToJson(entry) {
181
184
  providerCostUsd: money(bag.providerCostUsd ?? bag.provider_cost_usd ?? bag.costUsd ?? bag.cost_usd ?? bag.cost) ?? 0,
182
185
  customerCostUsd: money(bag.customerCostUsd ?? bag.customer_cost_usd ?? bag.billableCostUsd ?? bag.billable_cost_usd),
183
186
  unpriced: Boolean(bag.unpriced),
187
+ estimated: Boolean(bag.estimated),
184
188
  };
185
189
  }
186
190
 
@@ -214,8 +218,12 @@ export function buildSessionUsageReport({
214
218
  sessionId = "",
215
219
  events = [],
216
220
  recentLimit = DEFAULT_RECENT_LIMIT,
221
+ estimateMessageUsage = true,
217
222
  } = {}) {
218
- const ledger = buildSessionUsageLedger(events, { sessionId: normalize(sessionId) });
223
+ const ledger = buildSessionUsageLedger(events, {
224
+ sessionId: normalize(sessionId),
225
+ includeEstimatedMessages: estimateMessageUsage,
226
+ });
219
227
  const limit = clampRecentLimit(recentLimit);
220
228
  return {
221
229
  schema: SESSION_USAGE_REPORT_SCHEMA,
@@ -230,6 +238,7 @@ export function buildSessionUsageReport({
230
238
  providerCostUsd: ledger.totals.providerCostUsd,
231
239
  customerCostUsd: ledger.totals.hasCustomerCost ? ledger.totals.customerCostUsd : null,
232
240
  unpriced: ledger.totals.unpriced,
241
+ estimatedEntries: ledger.totals.estimatedEntries || 0,
233
242
  priceBookVersions: ledger.priceBookVersions,
234
243
  },
235
244
  perAgent: sortRollups(ledger.perAgent).map(rollupToJson),
@@ -281,6 +290,7 @@ export function buildSessionUsageReportFromLedgerPayload({
281
290
  providerCostUsd: totals.providerCostUsd,
282
291
  customerCostUsd: totals.customerCostUsd,
283
292
  unpriced: totals.unpriced,
293
+ estimatedEntries: nonNegativeInt(source.estimatedEntries ?? source.estimated_entries ?? sourceTotals.estimatedEntries ?? sourceTotals.estimated_entries),
284
294
  priceBookVersions: resolvedPriceBookVersions,
285
295
  },
286
296
  perAgent: sourceArray(
@@ -343,7 +353,12 @@ export function renderSessionUsageMarkdown(report = {}) {
343
353
  `Provider cost: ${usdCell(totals.providerCostUsd)} - Customer cost: ${optionalUsdCell(totals.customerCostUsd, totals.customerCostUsd != null)}`,
344
354
  );
345
355
  lines.push(`Price books: ${(totals.priceBookVersions || []).map(tableText).filter(Boolean).join(", ") || "-"}`);
346
- lines.push(`Duplicates skipped: ${intCell(totals.duplicatesSkipped)} - Unpriced entries: ${intCell(totals.unpriced)}`);
356
+ lines.push(
357
+ `Duplicates skipped: ${intCell(totals.duplicatesSkipped)} - Unpriced entries: ${intCell(totals.unpriced)} - Estimated entries: ${intCell(totals.estimatedEntries)}`,
358
+ );
359
+ if (totals.estimatedEntries > 0) {
360
+ lines.push("_Estimated entries are output-text estimates from non-human session messages, not billing-grade provider usage._");
361
+ }
347
362
  lines.push("");
348
363
 
349
364
  appendRollupTable(lines, {
@@ -366,8 +381,9 @@ export function renderSessionUsageMarkdown(report = {}) {
366
381
  lines.push(`| - | - | - | - | 0 | ${usdCell(0)} | - | - | - |`);
367
382
  } else {
368
383
  for (const entry of entries) {
384
+ const action = entry.estimated ? `${entry.action} (estimated)` : entry.action;
369
385
  lines.push(
370
- `| ${tableText(timestampOnly(entry.timestamp)) || "-"} | ${codeCell(entry.agentId)} | ${codeCell(entry.action)} | ${codeCell(entry.model)} | ${intCell(entry.totalTokens)} | ${usdCell(entry.providerCostUsd)} | ${optionalUsdCell(entry.customerCostUsd, entry.customerCostUsd != null)} | ${codeCell(entry.ledgerEntryId)} | ${codeCell(entry.idempotencyKeyHash)} |`,
386
+ `| ${tableText(timestampOnly(entry.timestamp)) || "-"} | ${codeCell(entry.agentId)} | ${codeCell(action)} | ${codeCell(entry.model)} | ${intCell(entry.totalTokens)} | ${usdCell(entry.providerCostUsd)} | ${optionalUsdCell(entry.customerCostUsd, entry.customerCostUsd != null)} | ${codeCell(entry.ledgerEntryId)} | ${codeCell(entry.idempotencyKeyHash)} |`,
371
387
  );
372
388
  }
373
389
  }
@@ -387,6 +403,9 @@ export function renderSessionUsageSummary(report = {}) {
387
403
  lines.push(
388
404
  `priceBooks=${(totals.priceBookVersions || []).map(tableText).filter(Boolean).join(",") || "-"} duplicatesSkipped=${intCell(totals.duplicatesSkipped)} unpriced=${intCell(totals.unpriced)}`,
389
405
  );
406
+ if (totals.estimatedEntries > 0) {
407
+ lines.push(`estimatedEntries=${intCell(totals.estimatedEntries)} (output-text estimates; not billing-grade)`);
408
+ }
390
409
  const agents = Array.isArray(report.perAgent) ? report.perAgent.slice(0, 8) : [];
391
410
  if (agents.length) {
392
411
  lines.push("");
@@ -403,7 +422,7 @@ export function renderSessionUsageSummary(report = {}) {
403
422
  lines.push("Recent:");
404
423
  for (const entry of entries) {
405
424
  lines.push(
406
- `- ${timestampOnly(entry.timestamp) || "-"} ${entry.agentId} ${entry.action} ${intCell(entry.totalTokens)} tokens ${usdCell(entry.providerCostUsd)} ledger=${entry.ledgerEntryId}`,
425
+ `- ${timestampOnly(entry.timestamp) || "-"} ${entry.agentId} ${entry.action}${entry.estimated ? " (estimated)" : ""} ${intCell(entry.totalTokens)} tokens ${usdCell(entry.providerCostUsd)} ledger=${entry.ledgerEntryId}`,
407
426
  );
408
427
  }
409
428
  }
@@ -241,8 +241,8 @@ export async function emitLLMInteraction(
241
241
  * totals: { totalTokens, inputTokens, outputTokens, costUsd, interactions },
242
242
  * }}
243
243
  */
244
- export function aggregateSessionUsage(events = []) {
245
- const ledger = buildSessionUsageLedger(events);
244
+ export function aggregateSessionUsage(events = [], options = {}) {
245
+ const ledger = buildSessionUsageLedger(events, options);
246
246
  const perAgent = new Map();
247
247
  const totals = {
248
248
  totalTokens: 0,