sentinelayer-cli 0.34.6 → 0.35.0

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.
@@ -1479,6 +1479,7 @@ export async function pollSessionEvents(
1479
1479
  * @param {object} [options]
1480
1480
  * @param {string|null} [options.since] - durable cursor to resume after
1481
1481
  * @param {AbortSignal} [options.signal]
1482
+ * @param {number} [options.idleTimeoutMs] - abort a connected stream after this many silent milliseconds
1482
1483
  * @param {(event: object) => Promise<void>|void} [options.onEvent]
1483
1484
  * @param {(payload: object) => Promise<void>|void} [options.onError]
1484
1485
  * @param {() => Promise<void>|void} [options.onHeartbeat]
@@ -1490,6 +1491,7 @@ export async function streamSessionEvents(
1490
1491
  targetPath = process.cwd(),
1491
1492
  since = null,
1492
1493
  timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
1494
+ idleTimeoutMs = 0,
1493
1495
  signal = undefined,
1494
1496
  resolveAuthSession = resolveActiveAuthSession,
1495
1497
  fetchImpl = fetch,
@@ -1545,10 +1547,26 @@ export async function streamSessionEvents(
1545
1547
  const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/stream${suffix}`;
1546
1548
  const controller = new AbortController();
1547
1549
  const normalizedTimeoutMs = normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS);
1550
+ const normalizedIdleTimeoutMs = normalizePositiveInteger(idleTimeoutMs, 0);
1548
1551
  const timeoutHandle = setTimeout(() => controller.abort(), normalizedTimeoutMs);
1549
1552
  if (typeof timeoutHandle.unref === "function") {
1550
1553
  timeoutHandle.unref();
1551
1554
  }
1555
+ let idleTimeoutHandle = null;
1556
+ let idleTimedOut = false;
1557
+ const clearIdleTimeout = () => {
1558
+ if (!idleTimeoutHandle) return;
1559
+ clearTimeout(idleTimeoutHandle);
1560
+ idleTimeoutHandle = null;
1561
+ };
1562
+ const armIdleTimeout = () => {
1563
+ if (!normalizedIdleTimeoutMs) return;
1564
+ clearIdleTimeout();
1565
+ idleTimeoutHandle = setTimeout(() => {
1566
+ idleTimedOut = true;
1567
+ controller.abort();
1568
+ }, normalizedIdleTimeoutMs);
1569
+ };
1552
1570
  const forwardAbort = () => controller.abort(signal?.reason);
1553
1571
  if (signal) {
1554
1572
  if (signal.aborted) {
@@ -1574,10 +1592,13 @@ export async function streamSessionEvents(
1574
1592
  );
1575
1593
  } catch (error) {
1576
1594
  clearTimeout(timeoutHandle);
1595
+ clearIdleTimeout();
1577
1596
  if (signal) signal.removeEventListener("abort", forwardAbort);
1578
1597
  return {
1579
1598
  ok: false,
1580
- reason: isAbortLike(error) || signal?.aborted ? "aborted" : normalizeString(error?.message) || "stream_failed",
1599
+ reason: idleTimedOut
1600
+ ? "stream_idle_timeout"
1601
+ : isAbortLike(error) || signal?.aborted ? "aborted" : normalizeString(error?.message) || "stream_failed",
1581
1602
  cursor: normalizedSince,
1582
1603
  eventCount: 0,
1583
1604
  errorCount: 0,
@@ -1621,7 +1642,9 @@ export async function streamSessionEvents(
1621
1642
  };
1622
1643
  }
1623
1644
 
1645
+ armIdleTimeout();
1624
1646
  for await (const chunk of readResponseTextChunks(response)) {
1647
+ armIdleTimeout();
1625
1648
  buffer += decoder.decode(chunk, { stream: true });
1626
1649
  buffer = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1627
1650
  const blocks = buffer.split("\n\n");
@@ -1637,13 +1660,16 @@ export async function streamSessionEvents(
1637
1660
  } catch (error) {
1638
1661
  return {
1639
1662
  ok: false,
1640
- reason: isAbortLike(error) || signal?.aborted ? "aborted" : normalizeString(error?.message) || "stream_failed",
1663
+ reason: idleTimedOut
1664
+ ? "stream_idle_timeout"
1665
+ : isAbortLike(error) || signal?.aborted ? "aborted" : normalizeString(error?.message) || "stream_failed",
1641
1666
  cursor,
1642
1667
  eventCount,
1643
1668
  errorCount,
1644
1669
  aborted: Boolean(signal?.aborted || isAbortLike(error)),
1645
1670
  };
1646
1671
  } finally {
1672
+ clearIdleTimeout();
1647
1673
  if (signal) signal.removeEventListener("abort", forwardAbort);
1648
1674
  }
1649
1675
 
@@ -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,