@titan-design/session-analytics 0.5.0 → 0.8.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.
package/dist/index.js CHANGED
@@ -515,6 +515,8 @@ var reviewerCostSchema = z.object({
515
515
  requestsPerPr: z.number(),
516
516
  /** The reviewer model whose sessions give `requestsPerPr`: the row's own, else the newest reviewer cohort; `pooled` with none. */
517
517
  requestsFrom: z.string(),
518
+ /** Sessions in the `requestsFrom` cohort; 1 means the per-PR figure rests on one session. */
519
+ requestsFromSessions: count,
518
520
  costUsd: z.number()
519
521
  });
520
522
  var reviewerSchema = z.object({ prs: count, fresh: z.array(reviewerCostSchema), standing: z.array(reviewerCostSchema) });
@@ -533,9 +535,9 @@ var handoffThresholdSchema = z.object({
533
535
  function isBootAction(calls) {
534
536
  return calls.some((call) => BOOT_TOOL.test(call.tool) || (call.writePaths?.length ?? 0) > 0);
535
537
  }
536
- function parseTeleportEvents(lines) {
538
+ function parseTeleportEvents(lines2) {
537
539
  const events = [];
538
- for (const line of lines) {
540
+ for (const line of lines2) {
539
541
  const entry = parseJson(line);
540
542
  if (entry?.event !== TELEPORT_EVENT || typeof entry.from !== "string" || typeof entry.ts !== "string") continue;
541
543
  events.push({ ts: entry.ts, name: typeof entry.name === "string" ? entry.name : null, fromAgentId: entry.from });
@@ -593,7 +595,7 @@ function fitSession(unsorted, prices, sweep) {
593
595
  const price = findPrice(first.model, first.ts, prices);
594
596
  if (bootIndex < 0 || price === null) return null;
595
597
  const params = {
596
- bootCostUsd: requests.slice(0, bootIndex + 1).reduce((sum, row) => sum + priceRequest(row.tokens, row.model, row.ts, prices).costUsd, 0),
598
+ bootCostUsd: requests.slice(0, bootIndex + 1).reduce((sum2, row) => sum2 + priceRequest(row.tokens, row.model, row.ts, prices).costUsd, 0),
597
599
  bootFill: requests[bootIndex].fill,
598
600
  growthPerRequest: meanGrowth(requests.slice(bootIndex).map((row) => row.fill)),
599
601
  readPricePerMTok: price.cacheRead
@@ -622,7 +624,7 @@ function cohortsOf(sessions, sweep, configuredK) {
622
624
  }).sort((a, b) => a.role.localeCompare(b.role) || a.model.localeCompare(b.model));
623
625
  }
624
626
  function meanParams(sessions) {
625
- const mean = (pick) => sessions.reduce((sum, s) => sum + pick(s), 0) / sessions.length;
627
+ const mean = (pick) => sessions.reduce((sum2, s) => sum2 + pick(s), 0) / sessions.length;
626
628
  return {
627
629
  bootCostUsd: mean((s) => s.bootCostUsd),
628
630
  bootFill: mean((s) => s.bootFill),
@@ -632,7 +634,7 @@ function meanParams(sessions) {
632
634
  }
633
635
  function meanGrowthOf(sessions) {
634
636
  const grown = sessions.filter((s) => s.growthPerRequest > 0);
635
- return grown.length === 0 ? 0 : grown.reduce((sum, s) => sum + s.growthPerRequest, 0) / grown.length;
637
+ return grown.length === 0 ? 0 : grown.reduce((sum2, s) => sum2 + s.growthPerRequest, 0) / grown.length;
636
638
  }
637
639
  function teleportExit(event, agentSessions2, bySession, cohorts, configuredK) {
638
640
  const candidates = (agentSessions2.get(event.fromAgentId) ?? []).flatMap((id) => bySession.get(id) ?? []);
@@ -661,7 +663,8 @@ function reviewerComparison(sessions, reviewerRole, standingRole, prs) {
661
663
  const priced = (role, costOf2) => cohorts(role).map(([model, members]) => {
662
664
  const from = perPr.has(model) ? model : newest;
663
665
  const requestsPerPr = from === null ? 0 : perPr.get(from);
664
- return { role, model, sessions: members.length, requestsPerPr, requestsFrom: from ?? POOLED_REVIEWERS, costUsd: costOf2(meanParams(members), requestsPerPr) };
666
+ const requestsFromSessions = from === null ? 0 : reviewers.find(([name]) => name === from)?.[1].length ?? 0;
667
+ return { role, model, sessions: members.length, requestsPerPr, requestsFrom: from ?? POOLED_REVIEWERS, requestsFromSessions, costUsd: costOf2(meanParams(members), requestsPerPr) };
665
668
  });
666
669
  return {
667
670
  prs,
@@ -670,12 +673,13 @@ function reviewerComparison(sessions, reviewerRole, standingRole, prs) {
670
673
  };
671
674
  }
672
675
  function newestCohort(reviewers) {
673
- const latest = reviewers.map(([model, members]) => ({ model, ts: members.reduce((max, s) => s.lastTs > max ? s.lastTs : max, "") }));
674
- latest.sort((a, b) => b.ts.localeCompare(a.ts) || b.model.localeCompare(a.model));
676
+ const epoch = (ts) => Number.isNaN(Date.parse(ts)) ? Number.NEGATIVE_INFINITY : Date.parse(ts);
677
+ const latest = reviewers.map(([model, members]) => ({ model, at: Math.max(...members.map((s) => epoch(s.lastTs))) }));
678
+ latest.sort((a, b) => a.at === b.at ? b.model.localeCompare(a.model) : b.at - a.at);
675
679
  return latest[0]?.model ?? null;
676
680
  }
677
681
  function meanRequestsAfterBoot(sessions) {
678
- return sessions.length === 0 ? 0 : sessions.reduce((sum, s) => sum + s.requests - s.bootRequests, 0) / sessions.length;
682
+ return sessions.length === 0 ? 0 : sessions.reduce((sum2, s) => sum2 + s.requests - s.bootRequests, 0) / sessions.length;
679
683
  }
680
684
  function groupBy(rows, keyOf) {
681
685
  const groups = /* @__PURE__ */ new Map();
@@ -765,6 +769,7 @@ var PROFILE_ROLES = {
765
769
  "fable-architect": "planner",
766
770
  planner: "planner",
767
771
  "bd-planner": "planner",
772
+ decider: "planner",
768
773
  "fable-coordinator": "coordinator",
769
774
  "opus-coordinator": "coordinator",
770
775
  peer: "standing_peer"
@@ -919,7 +924,7 @@ function buildWakeEpisodes(events, requests, names, noActionClasses) {
919
924
  from: pairSender(fromKind, event.fromName, names.seats),
920
925
  to: names.receivers.get(event.sessionId) ?? UNNAMED,
921
926
  requests: members.length,
922
- costUsd: members.reduce((sum, row) => sum + row.costUsd, 0),
927
+ costUsd: members.reduce((sum2, row) => sum2 + row.costUsd, 0),
923
928
  noAction: members.every((row) => noActionClasses.includes(row.action))
924
929
  };
925
930
  });
@@ -935,20 +940,20 @@ function summarizeWakeEpisodes(episodes, requests, roles, noActionClasses) {
935
940
  roles: [...roles],
936
941
  noActionClasses: [...noActionClasses],
937
942
  ...costOf(episodes),
938
- unattributed: { requests: unattributed.length, costUsd: unattributed.reduce((sum, row) => sum + row.costUsd, 0) },
943
+ unattributed: { requests: unattributed.length, costUsd: unattributed.reduce((sum2, row) => sum2 + row.costUsd, 0) },
939
944
  byCause: causeBuckets(episodes),
940
945
  pairs: pairBuckets(episodes)
941
946
  };
942
947
  }
943
948
  function costOf(episodes) {
944
949
  const noAction = episodes.filter((episode) => episode.noAction);
945
- const sum = (list, pick) => list.reduce((total, episode) => total + pick(episode), 0);
950
+ const sum2 = (list, pick) => list.reduce((total, episode) => total + pick(episode), 0);
946
951
  return {
947
952
  episodes: episodes.length,
948
- requests: sum(episodes, (episode) => episode.requests),
949
- costUsd: sum(episodes, (episode) => episode.costUsd),
953
+ requests: sum2(episodes, (episode) => episode.requests),
954
+ costUsd: sum2(episodes, (episode) => episode.costUsd),
950
955
  noActionEpisodes: noAction.length,
951
- noActionCostUsd: sum(noAction, (episode) => episode.costUsd)
956
+ noActionCostUsd: sum2(noAction, (episode) => episode.costUsd)
952
957
  };
953
958
  }
954
959
  function causeBuckets(episodes) {
@@ -969,7 +974,7 @@ function pairBuckets(episodes) {
969
974
  return [...groupBy2(sent, (episode) => JSON.stringify([episode.from, episode.fromKind, episode.to]))].map(([key, members]) => {
970
975
  const [from, fromKind, to] = JSON.parse(key);
971
976
  return { from, fromKind, to, ...costOf(members) };
972
- }).sort((a, b) => b.costUsd - a.costUsd || a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
977
+ }).sort((a, b) => b.costUsd - a.costUsd || a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || a.fromKind.localeCompare(b.fromKind));
973
978
  }
974
979
  function groupBy2(rows, keyOf) {
975
980
  const groups = /* @__PURE__ */ new Map();
@@ -1096,7 +1101,7 @@ function episodeCountKey(episodes) {
1096
1101
  return episodes >= EPISODE_COUNT_CAP ? `${EPISODE_COUNT_CAP}+` : String(episodes);
1097
1102
  }
1098
1103
  function sumOf(rows) {
1099
- return { requests: rows.length, costUsd: rows.reduce((sum, row) => sum + row.costUsd, 0) };
1104
+ return { requests: rows.length, costUsd: rows.reduce((sum2, row) => sum2 + row.costUsd, 0) };
1100
1105
  }
1101
1106
  function tokenClasses(rows) {
1102
1107
  const pick = {
@@ -1108,7 +1113,7 @@ function tokenClasses(rows) {
1108
1113
  };
1109
1114
  return TOKEN_CLASSES.map((tokenClass) => {
1110
1115
  const pairs = rows.map(pick[tokenClass]);
1111
- return { tokenClass, tokens: pairs.reduce((sum, [tokens]) => sum + tokens, 0), costUsd: pairs.reduce((sum, [, cost]) => sum + cost, 0) };
1116
+ return { tokenClass, tokens: pairs.reduce((sum2, [tokens]) => sum2 + tokens, 0), costUsd: pairs.reduce((sum2, [, cost]) => sum2 + cost, 0) };
1112
1117
  });
1113
1118
  }
1114
1119
  function dimensionBuckets(rows) {
@@ -1208,7 +1213,7 @@ function compactionCounts(rows) {
1208
1213
  manual: rows.filter((row) => row.trigger === "manual").length,
1209
1214
  auto: rows.filter((row) => row.trigger === "auto").length,
1210
1215
  midLoop: rows.filter((row) => row.midLoop).length,
1211
- droppedTokens: rows.reduce((sum, row) => sum + (row.droppedTokens ?? 0), 0)
1216
+ droppedTokens: rows.reduce((sum2, row) => sum2 + (row.droppedTokens ?? 0), 0)
1212
1217
  };
1213
1218
  }
1214
1219
  function topSessions(rows, top) {
@@ -1219,7 +1224,7 @@ function topSessions(rows, top) {
1219
1224
  }
1220
1225
  function unpricedModels(rows) {
1221
1226
  const unpriced = rows.filter((row) => !row.priced);
1222
- return [...groupRows(unpriced, (row) => row.model)].map(([model, members]) => ({ model, requests: members.length, tokens: members.reduce((sum, row) => sum + tokensOf(row), 0) })).sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model));
1227
+ return [...groupRows(unpriced, (row) => row.model)].map(([model, members]) => ({ model, requests: members.length, tokens: members.reduce((sum2, row) => sum2 + tokensOf(row), 0) })).sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model));
1223
1228
  }
1224
1229
  function tokensOf(row) {
1225
1230
  return row.inputTokens + row.cacheReadTokens + row.cacheWrite5mTokens + row.cacheWrite1hTokens + row.outputTokens;
@@ -1283,8 +1288,8 @@ function bucketTable(title, buckets2, total) {
1283
1288
  }
1284
1289
  function actionTable(roles) {
1285
1290
  const rows = roles.flatMap(({ role, buckets: buckets2 }) => {
1286
- const total = buckets2.reduce((sum, b) => sum + b.costUsd, 0);
1287
- const requests = buckets2.reduce((sum, b) => sum + b.requests, 0);
1291
+ const total = buckets2.reduce((sum2, b) => sum2 + b.costUsd, 0);
1292
+ const requests = buckets2.reduce((sum2, b) => sum2 + b.requests, 0);
1288
1293
  return [[role, requests, usd(total), share(total, total)], ...buckets2.map((b) => [` ${b.key}`, b.requests, usd(b.costUsd), share(b.costUsd, total)])];
1289
1294
  });
1290
1295
  return table("By action per role", ["role / action", "requests", "cost", "share"], rows);
@@ -1377,8 +1382,8 @@ function table(title, head, rows) {
1377
1382
  const widths = head.map((_, i) => Math.max(...text.map((row) => row[i].length)));
1378
1383
  const rightAligned = head.map((_, i) => rows.every((row) => isNumeric(row[i])));
1379
1384
  const pad = (cell, i) => rightAligned[i] ? cell.padStart(widths[i]) : cell.padEnd(widths[i]);
1380
- const lines = text.map((row) => row.map(pad).join(" ").trimEnd());
1381
- return [title, ...lines].join("\n");
1385
+ const lines2 = text.map((row) => row.map(pad).join(" ").trimEnd());
1386
+ return [title, ...lines2].join("\n");
1382
1387
  }
1383
1388
  function isNumeric(cell) {
1384
1389
  return typeof cell === "number" || /^\$|%$/.test(cell);
@@ -1455,17 +1460,17 @@ function requestEffect(row, price) {
1455
1460
  };
1456
1461
  }
1457
1462
  function sumEffects(effects) {
1458
- const sum = (pick) => effects.reduce((total, effect) => total + pick(effect), 0);
1463
+ const sum2 = (pick) => effects.reduce((total, effect) => total + pick(effect), 0);
1459
1464
  const sessions = new Set(effects.map((effect) => effect.sessionId)).size;
1460
- const netSavingUsd = sum((effect) => effect.repriceSavingUsd - effect.rebuildCostUsd);
1465
+ const netSavingUsd = sum2((effect) => effect.repriceSavingUsd - effect.rebuildCostUsd);
1461
1466
  return {
1462
1467
  sessions,
1463
1468
  requests: effects.length,
1464
- write1hTokens: sum((effect) => effect.write1hTokens),
1465
- repriceSavingUsd: sum((effect) => effect.repriceSavingUsd),
1469
+ write1hTokens: sum2((effect) => effect.write1hTokens),
1470
+ repriceSavingUsd: sum2((effect) => effect.repriceSavingUsd),
1466
1471
  rebuilds: effects.filter((effect) => effect.rebuildTokens > 0).length,
1467
- rebuildTokens: sum((effect) => effect.rebuildTokens),
1468
- rebuildCostUsd: sum((effect) => effect.rebuildCostUsd),
1472
+ rebuildTokens: sum2((effect) => effect.rebuildTokens),
1473
+ rebuildCostUsd: sum2((effect) => effect.rebuildCostUsd),
1469
1474
  netSavingUsd,
1470
1475
  netPerSessionUsd: sessions > 0 ? netSavingUsd / sessions : 0
1471
1476
  };
@@ -1510,9 +1515,9 @@ function readTtlRows(db, window) {
1510
1515
  }
1511
1516
  function renderCacheTtlText(whatIf) {
1512
1517
  const { totals, window } = whatIf;
1513
- const span = window ? `, ${window.since ?? "beginning"} to ${window.until ?? "now"}` : "";
1518
+ const span2 = window ? `, ${window.since ?? "beginning"} to ${window.until ?? "now"}` : "";
1514
1519
  const header2 = [
1515
- `Cache TTL what-if, 5m against 1h${span}`,
1520
+ `Cache TTL what-if, 5m against 1h${span2}`,
1516
1521
  `Net ${usd(totals.netSavingUsd)} over ${totals.sessions} sessions: 1h writes repriced save ${usd(totals.repriceSavingUsd)}, ${totals.rebuilds} rebuilds after gaps in ${whatIf.rebuildGapBands.join(", ")} cost ${usd(totals.rebuildCostUsd)}`,
1517
1522
  `Roles where 5m loses: ${whatIf.lossRoles.join(", ") || "none"}`
1518
1523
  ].join("\n");
@@ -1630,10 +1635,10 @@ function classifyDeniedAction(tool, input) {
1630
1635
  function bashAction(command) {
1631
1636
  return BASH_ACTIONS.find(([, pattern]) => pattern.test(command))?.[0] ?? "bash-other";
1632
1637
  }
1633
- function parseDenials(lines, seat) {
1638
+ function parseDenials(lines2, seat) {
1634
1639
  const calls = /* @__PURE__ */ new Map();
1635
1640
  const denials = [];
1636
- for (const line of lines) {
1641
+ for (const line of lines2) {
1637
1642
  if (!line.includes('"tool_use"') && !line.includes("auto mode classifier")) continue;
1638
1643
  const entry = parseLine(line);
1639
1644
  for (const block of blocksOf(entry)) {
@@ -1641,11 +1646,22 @@ function parseDenials(lines, seat) {
1641
1646
  const reason = block.type === "tool_result" ? DENIAL.exec(resultText(block.content))?.[1] : void 0;
1642
1647
  if (reason === void 0) continue;
1643
1648
  const call = calls.get(block.tool_use_id ?? "") ?? { tool: "unknown", input: null };
1644
- denials.push({ seat, at: entry?.timestamp ?? "", reason, action: classifyDeniedAction(call.tool, call.input), tool: call.tool });
1649
+ const toolUseId = block.tool_use_id ? { toolUseId: block.tool_use_id } : {};
1650
+ denials.push({ seat, at: entry?.timestamp ?? "", reason, action: classifyDeniedAction(call.tool, call.input), tool: call.tool, ...toolUseId });
1645
1651
  }
1646
1652
  }
1647
1653
  return denials;
1648
1654
  }
1655
+ function dedupeDenials(denials) {
1656
+ const seen = /* @__PURE__ */ new Set();
1657
+ return denials.filter((d) => {
1658
+ if (d.toolUseId === void 0) return true;
1659
+ const key = `${d.seat}\0${d.toolUseId}`;
1660
+ if (seen.has(key)) return false;
1661
+ seen.add(key);
1662
+ return true;
1663
+ });
1664
+ }
1649
1665
  function parseLine(line) {
1650
1666
  try {
1651
1667
  return JSON.parse(line);
@@ -1781,9 +1797,9 @@ function blockedFlowReport(input) {
1781
1797
  ...input.splitAt ? { splitAt: input.splitAt } : {},
1782
1798
  sources: BLOCKED_FLOW_SOURCES,
1783
1799
  verdictToMerge: { cites: ["verdicts", "pulls"], rows: latencyRows(outcomes, input.splitAt) },
1784
- openHoldingMerge: { cites: ["verdicts", "pulls"], prMinutes: round(open.reduce((sum, r) => sum + r.ageMin, 0)), rows: open },
1785
- denials: denialSection(input.denials.filter((d) => inScope(d.seat, d.at))),
1786
- idleSlots: { cites: ["journals"], holdMaxMin: TICK_HOLD_MAX_MIN, total: idle.reduce((sum, r) => sum + r.slotMinutes, 0), rows: idle }
1800
+ openHoldingMerge: { cites: ["verdicts", "pulls"], prMinutes: round(open.reduce((sum2, r) => sum2 + r.ageMin, 0)), rows: open },
1801
+ denials: denialSection(dedupeDenials(input.denials).filter((d) => inScope(d.seat, d.at))),
1802
+ idleSlots: { cites: ["journals"], holdMaxMin: TICK_HOLD_MAX_MIN, total: idle.reduce((sum2, r) => sum2 + r.slotMinutes, 0), rows: idle }
1787
1803
  };
1788
1804
  }
1789
1805
  function scopeTest(seats, window) {
@@ -1846,6 +1862,888 @@ function idleTable(report) {
1846
1862
  function sourceLines(report) {
1847
1863
  return ["Sources", ...Object.entries(report.sources).map(([name, s]) => `${name}: ${s.command} (${s.field})`)].join("\n");
1848
1864
  }
1865
+
1866
+ // src/liveness-broker.ts
1867
+ function parseBrokerLog(lines2) {
1868
+ const entries = [];
1869
+ let line = 0;
1870
+ for (const text of lines2) {
1871
+ line += 1;
1872
+ const fields = text.startsWith("{") ? parseObject(text) : null;
1873
+ if (fields && typeof fields.ts === "string" && typeof fields.event === "string") entries.push({ line, ts: fields.ts, event: fields.event, fields });
1874
+ }
1875
+ return entries;
1876
+ }
1877
+ function parseObject(text) {
1878
+ try {
1879
+ const value = JSON.parse(text);
1880
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1881
+ } catch {
1882
+ return null;
1883
+ }
1884
+ }
1885
+ function stringField(entry, key) {
1886
+ const value = entry.fields[key];
1887
+ return typeof value === "string" ? value : null;
1888
+ }
1889
+ function splitNames(to) {
1890
+ return (to ?? "").split(",").map((name) => name.trim()).filter((name) => name.length > 0);
1891
+ }
1892
+ function minutesBetween2(from, to) {
1893
+ return Math.round((Date.parse(to) - Date.parse(from)) / 6e3) / 10;
1894
+ }
1895
+
1896
+ // src/liveness-routes.ts
1897
+ var INBOX_KINDS = /* @__PURE__ */ new Set(["answer", "decided"]);
1898
+ function routeMisses(entries) {
1899
+ return entries.filter((e) => e.event === "route").flatMap((entry) => {
1900
+ const base = { line: entry.line, at: entry.ts, from: stringField(entry, "from") ?? "", kind: stringField(entry, "kind") ?? "" };
1901
+ return missedRecipients(entry).map(({ recipient, miss }) => ({ ...base, recipient, miss }));
1902
+ });
1903
+ }
1904
+ function missedRecipients(entry) {
1905
+ const to = stringField(entry, "to");
1906
+ const kind = stringField(entry, "kind") ?? "";
1907
+ if (to === null || to.startsWith("tag ") || kind === "broadcast") return [];
1908
+ const named = splitNames(to);
1909
+ if (entry.fields.delivered === false) {
1910
+ const miss = entry.fields.held !== void 0 || INBOX_KINDS.has(kind) ? "queued" : "failed";
1911
+ return named.map((recipient) => ({ recipient, miss }));
1912
+ }
1913
+ const reached = entry.fields.recipients;
1914
+ if (entry.fields.delivered !== true || !Array.isArray(reached)) return [];
1915
+ return named.filter((name) => !reached.includes(name)).map((recipient) => ({ recipient, miss: "partial" }));
1916
+ }
1917
+ function routeFailureRows(misses) {
1918
+ const rows = /* @__PURE__ */ new Map();
1919
+ for (const m of misses) {
1920
+ const row = rows.get(m.recipient) ?? { recipient: m.recipient, failed: 0, partial: 0, queued: 0, first: m.at, last: m.at, lines: [] };
1921
+ rows.set(m.recipient, { ...row, [m.miss]: row[m.miss] + 1, last: m.at, lines: [...row.lines, m.line] });
1922
+ }
1923
+ const total = (r) => r.failed + r.partial + r.queued;
1924
+ return [...rows.values()].sort((a, b) => total(b) - total(a) || a.recipient.localeCompare(b.recipient));
1925
+ }
1926
+ function countMisses(misses) {
1927
+ return { failed: misses.filter((m) => m.miss === "failed").length, partial: misses.filter((m) => m.miss === "partial").length, queued: misses.filter((m) => m.miss === "queued").length };
1928
+ }
1929
+
1930
+ // src/liveness-dark.ts
1931
+ var DARK_MIN = 5;
1932
+ var TELEPORT_EVENTS = /* @__PURE__ */ new Set(["teleport_started", "teleport_completed", "teleport_failed", "teleport_aborted"]);
1933
+ function darkGaps(entries, misses, asOf) {
1934
+ return gapsByLifecycle(entries, asOf).filter((gap) => minutesBetween2(gap.from, gap.to ?? asOf) > DARK_MIN).map((gap) => toDarkGap(gap, misses, asOf)).filter((gap) => gap.to !== null || gap.failedRoutes + gap.partialRoutes + gap.queuedRoutes > 0).sort((a, b) => a.from.localeCompare(b.from) || a.seat.localeCompare(b.seat));
1935
+ }
1936
+ function gapsByLifecycle(entries, asOf) {
1937
+ const states = /* @__PURE__ */ new Map();
1938
+ const gaps = [];
1939
+ const keep = (state, gap) => {
1940
+ if (!state.exited || gap.teleportLines.length > 0) gaps.push(gap);
1941
+ };
1942
+ for (const entry of entries) {
1943
+ const seat = stringField(entry, "name");
1944
+ if (seat === null || entry.ts >= asOf) continue;
1945
+ const state = states.get(seat) ?? { open: null, teleportLines: [], exited: false };
1946
+ states.set(seat, state);
1947
+ step(state, entry, (gap) => keep(state, { ...gap, seat, to: entry.ts, closeLine: entry.line }));
1948
+ }
1949
+ for (const [seat, state] of states) if (state.open) keep(state, { ...state.open, seat, to: null });
1950
+ return gaps;
1951
+ }
1952
+ function step(state, entry, close) {
1953
+ if (entry.event === "agent_exited") {
1954
+ state.exited ||= exitedCleanly(entry);
1955
+ } else if (TELEPORT_EVENTS.has(entry.event)) {
1956
+ state.teleportLines.push(entry.line);
1957
+ state.open?.teleportLines.push(entry.line);
1958
+ } else if (entry.event === "deregistered" && !state.open) {
1959
+ state.open = { from: entry.ts, lines: [entry.line], teleportLines: [...state.teleportLines] };
1960
+ } else if (entry.event === "registered") {
1961
+ if (state.open) close(state.open);
1962
+ state.open = null;
1963
+ state.teleportLines = [];
1964
+ state.exited = false;
1965
+ }
1966
+ }
1967
+ function exitedCleanly(entry) {
1968
+ return entry.fields.code === 0 && entry.fields.inferred !== true;
1969
+ }
1970
+ function toDarkGap(gap, misses, asOf) {
1971
+ const end = gap.to ?? asOf;
1972
+ const missed = misses.filter((m) => m.recipient === gap.seat && m.at >= gap.from && m.at < end);
1973
+ const counts = countMisses(missed);
1974
+ return {
1975
+ seat: gap.seat,
1976
+ from: gap.from,
1977
+ to: gap.to,
1978
+ minutes: minutesBetween2(gap.from, end),
1979
+ teleport: gap.teleportLines.length > 0,
1980
+ failedRoutes: counts.failed,
1981
+ partialRoutes: counts.partial,
1982
+ queuedRoutes: counts.queued,
1983
+ lines: [...gap.lines, ...gap.closeLine === void 0 ? [] : [gap.closeLine], ...gap.teleportLines],
1984
+ routeLines: missed.map((m) => m.line)
1985
+ };
1986
+ }
1987
+
1988
+ // src/liveness-exits.ts
1989
+ var UNKNOWN_PROFILE = "unknown";
1990
+ function unreportedExitRows(entries, spawns) {
1991
+ const byAgent = new Map(spawns.map((s) => [s.agentId, s]));
1992
+ const rows = /* @__PURE__ */ new Map();
1993
+ for (const entry of entries.filter((e) => e.event === "unreported-exit")) {
1994
+ const agentId = stringField(entry, "agentId") ?? "";
1995
+ const spawn = byAgent.get(agentId);
1996
+ const profile = spawn?.profile ?? UNKNOWN_PROFILE;
1997
+ const exit = {
1998
+ at: entry.ts,
1999
+ line: entry.line,
2000
+ agentId,
2001
+ name: stringField(entry, "name") ?? spawn?.name ?? "",
2002
+ spawner: stringField(entry, "spawner") ?? "",
2003
+ lastAction: stringField(entry, "lastAction") ?? "",
2004
+ spawnEventId: spawn?.eventId ?? null
2005
+ };
2006
+ rows.set(profile, [...rows.get(profile) ?? [], exit]);
2007
+ }
2008
+ return [...rows.entries()].map(([profile, exits]) => ({ profile, count: exits.length, exits })).sort((a, b) => b.count - a.count || a.profile.localeCompare(b.profile));
2009
+ }
2010
+
2011
+ // src/liveness-prompts.ts
2012
+ var PROMPT_STALE_MIN = 10;
2013
+ function stalePromptRows(lastEvents, broker, asOf) {
2014
+ const gone = goneSinceRestart(broker, asOf);
2015
+ return lastEvents.filter((e) => e.kind === "approval_request" && e.at < asOf && e.endEventId === null && !gone(e)).map((e) => ({ agent: e.actor, at: e.at, ageMin: minutesBetween2(e.at, asOf), tool: e.tool, eventId: e.eventId, resolutionEventId: e.resolutionEventId })).filter((row) => row.ageMin > PROMPT_STALE_MIN).sort((a, b) => b.ageMin - a.ageMin || a.agent.localeCompare(b.agent));
2016
+ }
2017
+ function goneSinceRestart(broker, asOf) {
2018
+ const before = broker.filter((e) => e.ts < asOf);
2019
+ const restart = before.filter((e) => e.event === "broker_started").at(-1)?.ts;
2020
+ if (restart === void 0) return () => false;
2021
+ const live = new Set(before.filter((e) => e.event === "registered" && e.ts >= restart).map((e) => stringField(e, "name")));
2022
+ return (e) => e.at < restart && !live.has(e.actor);
2023
+ }
2024
+
2025
+ // src/liveness.ts
2026
+ import { z as z7 } from "zod";
2027
+ var LIVENESS_SOURCES = {
2028
+ registrations: {
2029
+ command: `grep -nE '"event":"(registered|deregistered|agent_exited|teleport_started|teleport_completed|teleport_failed|teleport_aborted)"' <broker.log>`,
2030
+ field: "ts, event, name; cited as broker.log line numbers; a gap with a clean agent_exited (code 0, not inferred) and no teleport is a resume"
2031
+ },
2032
+ routes: { command: `grep -n '"event":"route"' <broker.log>`, field: "to, delivered, recipients" },
2033
+ exits: { command: `grep -n '"event":"unreported-exit"' <broker.log>`, field: "agentId, name, spawner, lastAction" },
2034
+ spawns: {
2035
+ command: `sqlite3 -readonly <events.db> "SELECT id, msg_id, target, json_extract(meta,'$.profile') FROM events WHERE kind='agent_spawned'"`,
2036
+ field: "events.msg_id (agent id), events.meta.profile"
2037
+ },
2038
+ prompts: {
2039
+ command: `sqlite3 -readonly <events.db> "SELECT e.* FROM events e JOIN (SELECT actor, max(id) id FROM events WHERE ts < <asOf> AND kind != 'resolution' GROUP BY actor) l ON e.id = l.id WHERE e.kind='approval_request'"`,
2040
+ field: "events.ts, events.actor, events.meta.tool_name; a resolution row's events.ref; skipped after the actor's agent_exited or agent_retired, or a broker_started it never re-registered after"
2041
+ }
2042
+ };
2043
+ var source2 = z7.enum(["registrations", "routes", "exits", "spawns", "prompts"]);
2044
+ var count6 = z7.number().int().nonnegative();
2045
+ var lines = z7.array(count6);
2046
+ var darkRow = z7.object({
2047
+ seat: z7.string(),
2048
+ from: z7.string(),
2049
+ to: z7.string().nullable(),
2050
+ minutes: z7.number(),
2051
+ teleport: z7.boolean(),
2052
+ failedRoutes: count6,
2053
+ partialRoutes: count6,
2054
+ queuedRoutes: count6,
2055
+ lines,
2056
+ routeLines: lines
2057
+ });
2058
+ var exitRow = z7.object({ at: z7.string(), line: count6, agentId: z7.string(), name: z7.string(), spawner: z7.string(), lastAction: z7.string(), spawnEventId: count6.nullable() });
2059
+ var promptRow = z7.object({ agent: z7.string(), at: z7.string(), ageMin: z7.number(), tool: z7.string().nullable(), eventId: count6, resolutionEventId: count6.nullable() });
2060
+ var livenessSchema = z7.object({
2061
+ asOf: z7.string(),
2062
+ window: z7.object({ since: z7.string().optional(), until: z7.string().optional() }),
2063
+ thresholds: z7.object({ darkMin: z7.number(), promptStaleMin: z7.number() }),
2064
+ sources: z7.record(source2, z7.object({ command: z7.string(), field: z7.string() })),
2065
+ darkSeats: z7.object({ cites: z7.array(source2), withTeleport: count6, withoutTeleport: count6, rows: z7.array(darkRow) }),
2066
+ routeFailures: z7.object({
2067
+ cites: z7.array(source2),
2068
+ failed: count6,
2069
+ partial: count6,
2070
+ queued: count6,
2071
+ rows: z7.array(z7.object({ recipient: z7.string(), failed: count6, partial: count6, queued: count6, first: z7.string(), last: z7.string(), lines }))
2072
+ }),
2073
+ unreportedExits: z7.object({ cites: z7.array(source2), total: count6, rows: z7.array(z7.object({ profile: z7.string(), count: count6, exits: z7.array(exitRow) })) }),
2074
+ stalePrompts: z7.object({ cites: z7.array(source2), rows: z7.array(promptRow), resolvedRows: z7.array(promptRow) })
2075
+ });
2076
+ function livenessReport(input) {
2077
+ const window = input.window ?? {};
2078
+ const keep = scopeTest2(input.seats, window);
2079
+ const broker = input.broker.filter((e) => e.ts < input.asOf);
2080
+ const misses = routeMisses(broker);
2081
+ const dark = darkGaps(broker, misses, input.asOf).filter((g) => keep([g.seat], g.from));
2082
+ const routeMissesInScope = misses.filter((m) => keep([m.recipient], m.at));
2083
+ const routes = routeFailureRows(routeMissesInScope);
2084
+ const prompts = stalePromptRows(input.lastEvents, broker, input.asOf).filter((p) => !input.seats || input.seats.includes(p.agent));
2085
+ const exits = unreportedExitRows(broker.filter((e) => keep([stringField(e, "name") ?? "", stringField(e, "spawner") ?? ""], e.ts)), input.spawns);
2086
+ return {
2087
+ asOf: input.asOf,
2088
+ window,
2089
+ thresholds: { darkMin: DARK_MIN, promptStaleMin: PROMPT_STALE_MIN },
2090
+ sources: LIVENESS_SOURCES,
2091
+ darkSeats: { cites: ["registrations", "routes"], withTeleport: dark.filter((g) => g.teleport).length, withoutTeleport: dark.filter((g) => !g.teleport).length, rows: dark },
2092
+ routeFailures: { cites: ["routes"], ...countMisses(routeMissesInScope), rows: routes },
2093
+ unreportedExits: { cites: ["exits", "spawns"], total: sum(exits.map((r) => r.count)), rows: exits },
2094
+ stalePrompts: { cites: ["prompts"], rows: prompts.filter((p) => p.resolutionEventId === null), resolvedRows: prompts.filter((p) => p.resolutionEventId !== null) }
2095
+ };
2096
+ }
2097
+ function scopeTest2(seats, window) {
2098
+ const named = seats ? new Set(seats) : null;
2099
+ return (names, at) => (!named || names.some((n) => named.has(n))) && (!window.since || at >= window.since) && (!window.until || at < window.until);
2100
+ }
2101
+ function sum(values) {
2102
+ return values.reduce((total, v) => total + v, 0);
2103
+ }
2104
+ var CITED_LINES = 4;
2105
+ function cite(prefix, ids) {
2106
+ if (ids.length === 0) return "-";
2107
+ const shown = ids.slice(0, CITED_LINES).map((id) => `${prefix}${id}`).join(",");
2108
+ return ids.length > CITED_LINES ? `${shown} +${ids.length - CITED_LINES}` : shown;
2109
+ }
2110
+ function renderLivenessText(report) {
2111
+ const scope = `Liveness as of ${report.asOf}${report.window.since ? `, since ${report.window.since}` : ""}${report.window.until ? `, until ${report.window.until}` : ""}`;
2112
+ const sources = ["Sources", ...Object.entries(report.sources).map(([name, s]) => `${name}: ${s.command} (${s.field})`)].join("\n");
2113
+ return [scope, darkTable(report), routeTable(report), exitTable(report), promptTable(report), sources, LIST_PRICE_CAVEAT].join("\n\n") + "\n";
2114
+ }
2115
+ function darkTable(report) {
2116
+ const { rows, withTeleport, withoutTeleport } = report.darkSeats;
2117
+ const cells = rows.map((r) => [r.seat, r.from, r.to ?? "still dark", r.minutes, r.teleport ? "yes" : "no", r.failedRoutes, r.partialRoutes, r.queuedRoutes, cite("broker.log:", [...r.lines, ...r.routeLines])]);
2118
+ const title = `Seats dark over ${report.thresholds.darkMin} min, ${withTeleport} with a teleport and ${withoutTeleport} without [darkSeats.rows[]]`;
2119
+ return table(title, ["seat", "from", "to", "min", "teleport", "failed routes", "partial routes", "queued routes", "cites"], cells);
2120
+ }
2121
+ function routeTable(report) {
2122
+ const { rows, failed, partial, queued } = report.routeFailures;
2123
+ const cells = rows.map((r) => [r.recipient, r.failed, r.partial, r.queued, r.first, r.last, cite("broker.log:", r.lines)]);
2124
+ const title = `Routes that missed a recipient, ${failed} dropped, ${partial} partial and ${queued} held or queued for later [routeFailures.rows[]]`;
2125
+ return table(title, ["recipient", "failed", "partial", "queued", "first", "last", "cites"], cells);
2126
+ }
2127
+ function exitTable(report) {
2128
+ const cells = report.unreportedExits.rows.flatMap((r) => r.exits.map((e) => [r.profile, e.name, e.spawner, e.lastAction, e.at, cite("broker.log:", [e.line]), e.spawnEventId === null ? "-" : `events#${e.spawnEventId}`]));
2129
+ const title = `Unreported exits by profile, ${report.unreportedExits.total} in all [unreportedExits.rows[].exits[]]`;
2130
+ return table(title, ["profile", "agent", "spawner", "last action", "at", "exit", "spawn"], cells);
2131
+ }
2132
+ function promptTable(report) {
2133
+ const cells = (rows) => rows.map((r) => [r.agent, r.tool ?? "-", r.at, r.ageMin, `events#${r.eventId}`, r.resolutionEventId === null ? "-" : `events#${r.resolutionEventId}`]);
2134
+ const head = ["agent", "tool", "at", "age min", "prompt", "resolution"];
2135
+ const over = `over ${report.thresholds.promptStaleMin} min old`;
2136
+ return [
2137
+ table(`Agents whose last event is an unresolved permission prompt ${over} [stalePrompts.rows[]]`, head, cells(report.stalePrompts.rows)),
2138
+ table(`Agents silent since a resolved permission prompt ${over} [stalePrompts.resolvedRows[]]`, head, cells(report.stalePrompts.resolvedRows))
2139
+ ].join("\n\n");
2140
+ }
2141
+
2142
+ // src/timeline-types.ts
2143
+ var SESSION_TIMELINE_VERSION = 1;
2144
+ var TIMELINE_GAP_MIN_MS = 10 * 6e4;
2145
+ var TIMELINE_TEXT_CAP = 4e3;
2146
+
2147
+ // src/timeline-breakdowns.ts
2148
+ var WRITE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
2149
+ function ascending(times) {
2150
+ return times.filter((time) => time !== null).sort((a, b) => a - b);
2151
+ }
2152
+ function toolBreakdown(calls) {
2153
+ const byName = /* @__PURE__ */ new Map();
2154
+ const byFamily = /* @__PURE__ */ new Map();
2155
+ for (const call of calls) {
2156
+ const failed = call.outcome === "error" ? 1 : 0;
2157
+ const name = byName.get(call.name) ?? { name: call.name, family: call.family, calls: 0, errors: 0, durationMs: 0 };
2158
+ name.calls++;
2159
+ name.errors += failed;
2160
+ name.durationMs += call.durationMs ?? 0;
2161
+ byName.set(call.name, name);
2162
+ const family = byFamily.get(call.family) ?? { family: call.family, calls: 0, errors: 0, atMs: [] };
2163
+ family.calls++;
2164
+ family.errors += failed;
2165
+ if (call.atMs !== null) family.atMs.push(call.atMs);
2166
+ byFamily.set(call.family, family);
2167
+ }
2168
+ for (const family of byFamily.values()) family.atMs.sort((a, b) => a - b);
2169
+ return {
2170
+ byName: [...byName.values()].sort((a, b) => b.calls - a.calls || a.name.localeCompare(b.name)),
2171
+ byFamily: [...byFamily.values()].sort((a, b) => b.calls - a.calls || a.family.localeCompare(b.family)),
2172
+ atMs: ascending(calls.map((call) => call.atMs))
2173
+ };
2174
+ }
2175
+ function fileBreakdown(calls) {
2176
+ const touches = /* @__PURE__ */ new Map();
2177
+ for (const call of calls) {
2178
+ if (!call.filePath) continue;
2179
+ const access = WRITE_TOOLS.has(call.name) ? "write" : "read";
2180
+ const key = `${access}:${call.filePath}`;
2181
+ const touch2 = touches.get(key) ?? { path: call.filePath, access, atMs: call.atMs, calls: 0 };
2182
+ touch2.calls++;
2183
+ touches.set(key, touch2);
2184
+ }
2185
+ const rows = [...touches.values()].sort((a, b) => (a.atMs ?? Infinity) - (b.atMs ?? Infinity));
2186
+ return {
2187
+ touches: rows,
2188
+ readCount: rows.filter((row) => row.access === "read").length,
2189
+ writeCount: rows.filter((row) => row.access === "write").length
2190
+ };
2191
+ }
2192
+ function errorBreakdown(calls) {
2193
+ const items = calls.filter((call) => call.outcome === "error").map((call) => ({
2194
+ callId: call.id,
2195
+ toolName: call.name,
2196
+ turnIndex: call.turnIndex,
2197
+ atMs: call.endMs ?? call.atMs,
2198
+ message: call.errorMessage ?? "",
2199
+ sidechain: call.sidechain
2200
+ }));
2201
+ return { rate: calls.length > 0 ? items.length / calls.length : 0, items, atMs: ascending(items.map((item) => item.atMs)) };
2202
+ }
2203
+ function agentSpans(calls) {
2204
+ return calls.filter((call) => call.family === "subagent").map((call) => ({ callId: call.id, label: call.inputSummary || call.name, startMs: call.atMs, endMs: call.endMs, outcome: call.outcome }));
2205
+ }
2206
+
2207
+ // src/timeline-buckets.ts
2208
+ var MINUTE_MS3 = 6e4;
2209
+ function activityTimes(input) {
2210
+ const times = input.points.map((point) => point.atMs);
2211
+ for (const turn of input.turns) {
2212
+ times.push(turn.user?.atMs ?? null, ...turn.assistant.map((message) => message.atMs));
2213
+ }
2214
+ for (const call of input.calls) times.push(call.atMs, call.endMs);
2215
+ return times.filter((time) => time !== null).sort((a, b) => a - b);
2216
+ }
2217
+ function findGaps(sortedMs, gapMinMs, calls) {
2218
+ const gaps = [];
2219
+ for (let i = 1; i < sortedMs.length; i++) {
2220
+ const startMs = sortedMs[i - 1];
2221
+ const endMs = sortedMs[i];
2222
+ if (endMs - startMs < gapMinMs || calls.some((call) => inFlightAcross(call, startMs, endMs))) continue;
2223
+ gaps.push({ startMs, endMs, durationMs: endMs - startMs });
2224
+ }
2225
+ return gaps;
2226
+ }
2227
+ function inFlightAcross(call, startMs, endMs) {
2228
+ return call.atMs !== null && call.endMs !== null && call.atMs <= startMs && call.endMs >= endMs;
2229
+ }
2230
+ function minuteBuckets(input, gaps) {
2231
+ const buckets2 = /* @__PURE__ */ new Map();
2232
+ const at = (ms) => bucketAt(buckets2, ms);
2233
+ for (const turn of input.turns) {
2234
+ for (const message of [turn.user, ...turn.assistant]) {
2235
+ if (message?.atMs != null) countEvent(at(message.atMs), "messages");
2236
+ }
2237
+ }
2238
+ for (const call of input.calls) countCall(call, at);
2239
+ for (const point of input.points) {
2240
+ const bucket = at(point.atMs);
2241
+ bucket.outputTokens += point.outputTokens;
2242
+ bucket.costUsd += point.costUsd;
2243
+ }
2244
+ for (const gap of gaps) at(gap.endMs).gapBeforeMs = gap.durationMs;
2245
+ return [...buckets2.values()].sort((a, b) => a.minuteMs - b.minuteMs);
2246
+ }
2247
+ function markTurnGaps(turns, gapMinMs) {
2248
+ let previousEndMs = null;
2249
+ for (const turn of turns) {
2250
+ const idleMs = previousEndMs !== null && turn.startMs !== null ? turn.startMs - previousEndMs : 0;
2251
+ turn.gapBeforeMs = idleMs >= gapMinMs ? idleMs : null;
2252
+ previousEndMs = turn.endMs ?? previousEndMs;
2253
+ }
2254
+ }
2255
+ function bucketAt(buckets2, ms) {
2256
+ const minuteMs = Math.floor(ms / MINUTE_MS3) * MINUTE_MS3;
2257
+ let bucket = buckets2.get(minuteMs);
2258
+ if (!bucket) {
2259
+ bucket = { minuteMs, events: 0, messages: 0, toolCalls: 0, errors: 0, outputTokens: 0, costUsd: 0, gapBeforeMs: null };
2260
+ buckets2.set(minuteMs, bucket);
2261
+ }
2262
+ return bucket;
2263
+ }
2264
+ function countEvent(bucket, field) {
2265
+ bucket.events++;
2266
+ if (field) bucket[field]++;
2267
+ }
2268
+ function countCall(call, at) {
2269
+ if (call.atMs !== null) countEvent(at(call.atMs), "toolCalls");
2270
+ if (call.endMs !== null) countEvent(at(call.endMs), null);
2271
+ const errorMs = call.endMs ?? call.atMs;
2272
+ if (call.outcome === "error" && errorMs !== null) at(errorMs).errors++;
2273
+ }
2274
+
2275
+ // src/timeline-tokens.ts
2276
+ import { foldUsage } from "@titan-design/agent-protocol";
2277
+
2278
+ // src/timeline-turns.ts
2279
+ import { findInjectedMarker, toolFamily } from "@titan-design/session-read";
2280
+ var ERROR_TEXT_CAP = 500;
2281
+ var SUMMARY_CAP = 160;
2282
+ var SUMMARY_KEYS = ["command", "pattern", "description", "query", "url", "prompt"];
2283
+ var PATH_KEYS = ["file_path", "notebook_path", "path"];
2284
+ function capText(text, cap) {
2285
+ return text.length <= cap ? { text, truncated: false } : { text: text.slice(0, cap), truncated: true };
2286
+ }
2287
+ function isSidechain(observation) {
2288
+ return observation.nativeExtensions?.some((entry) => entry.name === "isSidechain" && entry.value === true) ?? false;
2289
+ }
2290
+ var TurnFold = class {
2291
+ constructor(maxTextChars) {
2292
+ this.maxTextChars = maxTextChars;
2293
+ }
2294
+ maxTextChars;
2295
+ turns = [];
2296
+ calls = [];
2297
+ callsById = /* @__PURE__ */ new Map();
2298
+ seenMessages = /* @__PURE__ */ new Set();
2299
+ seq = 0;
2300
+ currentIndex() {
2301
+ return this.turns.length > 0 ? this.turns.length - 1 : null;
2302
+ }
2303
+ message(observation, atMs) {
2304
+ if (isSidechain(observation) || this.isRepeat(observation)) return;
2305
+ const raw = observation.content.map((part) => part.text).join("\n");
2306
+ const message = {
2307
+ role: observation.role,
2308
+ seq: this.seq++,
2309
+ atMs,
2310
+ ...capText(raw, this.maxTextChars),
2311
+ byteOffset: observation.evidence.line.byteOffset
2312
+ };
2313
+ if (observation.role === "user") this.open(message, raw);
2314
+ else touch(this.current(atMs), atMs).assistant.push(message);
2315
+ }
2316
+ toolCall(observation, atMs) {
2317
+ const turn = touch(this.current(atMs), atMs);
2318
+ const call = {
2319
+ id: observation.call.nativeId,
2320
+ seq: this.seq++,
2321
+ turnIndex: turn.index,
2322
+ name: observation.name,
2323
+ family: toolFamily(observation.name).family,
2324
+ atMs,
2325
+ endMs: null,
2326
+ durationMs: null,
2327
+ outcome: "pending",
2328
+ errorMessage: null,
2329
+ inputSummary: inputSummary(observation.input),
2330
+ filePath: firstString(observation.input, PATH_KEYS),
2331
+ sidechain: isSidechain(observation),
2332
+ byteOffset: observation.evidence.line.byteOffset
2333
+ };
2334
+ turn.toolCalls.push(call);
2335
+ this.calls.push(call);
2336
+ this.callsById.set(call.id, call);
2337
+ }
2338
+ /** A result whose call was never seen, as in a resumed slice, is dropped. */
2339
+ toolResult(observation, atMs) {
2340
+ const call = this.callsById.get(observation.call.nativeId);
2341
+ const turn = call ? this.turns[call.turnIndex] : void 0;
2342
+ if (!call || !turn) return;
2343
+ call.endMs = atMs;
2344
+ call.durationMs = span(call.atMs, atMs);
2345
+ call.outcome = outcomeOf(observation.isError);
2346
+ touch(turn, atMs);
2347
+ if (call.outcome !== "error") return;
2348
+ call.errorMessage = capText(outputText(observation.output), ERROR_TEXT_CAP).text;
2349
+ turn.errorCount++;
2350
+ }
2351
+ isRepeat(observation) {
2352
+ const key = observation.item?.nativeId;
2353
+ if (!key) return false;
2354
+ if (this.seenMessages.has(key)) return true;
2355
+ this.seenMessages.add(key);
2356
+ return false;
2357
+ }
2358
+ open(user, raw) {
2359
+ const marker = findInjectedMarker(raw);
2360
+ this.turns.push({ ...emptyTurn(this.turns.length, originOf(marker), user.atMs), injectedMarker: marker, user });
2361
+ }
2362
+ current(atMs) {
2363
+ const last = this.turns[this.turns.length - 1];
2364
+ if (last) return last;
2365
+ const turn = emptyTurn(0, "none", atMs);
2366
+ this.turns.push(turn);
2367
+ return turn;
2368
+ }
2369
+ };
2370
+ function emptyTurn(index, origin, atMs) {
2371
+ return {
2372
+ index,
2373
+ origin,
2374
+ injectedMarker: null,
2375
+ startMs: atMs,
2376
+ endMs: atMs,
2377
+ gapBeforeMs: null,
2378
+ user: null,
2379
+ assistant: [],
2380
+ toolCalls: [],
2381
+ errorCount: 0,
2382
+ tokens: { input: 0, cacheRead: 0, cacheWrite: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 0 },
2383
+ costUsd: 0
2384
+ };
2385
+ }
2386
+ function originOf(marker) {
2387
+ if (marker === null) return "prompt";
2388
+ return marker === "compaction_summary" ? "compaction" : "injected";
2389
+ }
2390
+ function touch(turn, atMs) {
2391
+ if (atMs === null) return turn;
2392
+ if (turn.startMs === null || atMs < turn.startMs) turn.startMs = atMs;
2393
+ if (turn.endMs === null || atMs > turn.endMs) turn.endMs = atMs;
2394
+ return turn;
2395
+ }
2396
+ function span(startMs, endMs) {
2397
+ if (startMs === null || endMs === null || endMs < startMs) return null;
2398
+ return endMs - startMs;
2399
+ }
2400
+ function outcomeOf(isError) {
2401
+ if (isError === null) return "unknown";
2402
+ return isError ? "error" : "success";
2403
+ }
2404
+ function firstString(input, keys) {
2405
+ if (typeof input !== "object" || input === null) return null;
2406
+ const record = input;
2407
+ for (const key of keys) {
2408
+ const value = record[key];
2409
+ if (typeof value === "string" && value.length > 0) return value;
2410
+ }
2411
+ return null;
2412
+ }
2413
+ function inputSummary(input) {
2414
+ const text = firstString(input, PATH_KEYS) ?? firstString(input, SUMMARY_KEYS) ?? "";
2415
+ const firstLine = text.split("\n", 1)[0] ?? "";
2416
+ return capText(firstLine, SUMMARY_CAP).text;
2417
+ }
2418
+ function outputText(output) {
2419
+ if (typeof output === "string") return output;
2420
+ if (!Array.isArray(output)) return "";
2421
+ for (const block of output) {
2422
+ const text = firstString(block, ["text"]);
2423
+ if (text) return text;
2424
+ }
2425
+ return "";
2426
+ }
2427
+
2428
+ // src/timeline-tokens.ts
2429
+ function emptyTokens() {
2430
+ return { input: 0, cacheRead: 0, cacheWrite: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 0 };
2431
+ }
2432
+ function addTokens(into, from) {
2433
+ into.input += from.input;
2434
+ into.cacheRead += from.cacheRead;
2435
+ into.cacheWrite += from.cacheWrite;
2436
+ into.cacheWrite5m += from.cacheWrite5m;
2437
+ into.cacheWrite1h += from.cacheWrite1h;
2438
+ into.output += from.output;
2439
+ }
2440
+ var TokenFold = class {
2441
+ constructor(maxTextChars, prices) {
2442
+ this.maxTextChars = maxTextChars;
2443
+ this.prices = prices;
2444
+ }
2445
+ maxTextChars;
2446
+ prices;
2447
+ compactions = [];
2448
+ requests = /* @__PURE__ */ new Map();
2449
+ snapshots = [];
2450
+ compactedSinceRequest = false;
2451
+ usage(observation, atMs, turnIndex) {
2452
+ const { measurement } = observation;
2453
+ if (measurement.kind === "snapshot") {
2454
+ this.snapshots.push(measurement);
2455
+ return;
2456
+ }
2457
+ const held = this.requests.get(measurement.responseId);
2458
+ const tokens = disjointTokens(measurement.tokens, "cacheWriteSplit" in observation ? observation.cacheWriteSplit : void 0);
2459
+ const priced = priceRequest(priceInput(tokens), measurement.model ?? "", isoOf(held ? held.atMs : atMs), this.prices);
2460
+ this.requests.set(measurement.responseId, {
2461
+ atMs: held ? held.atMs : atMs,
2462
+ turnIndex: held ? held.turnIndex : turnIndex,
2463
+ model: measurement.model,
2464
+ contextTokens: measurement.tokens.input,
2465
+ tokens,
2466
+ costUsd: priced.costUsd,
2467
+ priced: priced.priced,
2468
+ afterCompaction: held ? held.afterCompaction : this.takeCompactionFlag()
2469
+ });
2470
+ }
2471
+ /**
2472
+ * Claude Code writes one compaction as a boundary line and then a summary line, and the
2473
+ * decoder reports both. The second joins the first: the mark keeps the boundary's time and gains the summary.
2474
+ */
2475
+ compaction(observation, atMs, turnIndex) {
2476
+ const summary = observation.nativeExtensions?.find((entry) => entry.name === "compactionSummary")?.value;
2477
+ const capped = typeof summary === "string" ? capText(summary, this.maxTextChars) : null;
2478
+ const open = this.compactedSinceRequest ? this.compactions.at(-1) : void 0;
2479
+ if (open && open.summary === null) {
2480
+ open.summary = capped?.text ?? null;
2481
+ open.summaryTruncated = capped?.truncated ?? false;
2482
+ return;
2483
+ }
2484
+ this.compactions.push({
2485
+ atMs,
2486
+ turnIndex,
2487
+ byteOffset: observation.evidence.line.byteOffset,
2488
+ summary: capped?.text ?? null,
2489
+ summaryTruncated: capped?.truncated ?? false
2490
+ });
2491
+ this.compactedSinceRequest = true;
2492
+ }
2493
+ result() {
2494
+ const requests = [...this.requests.values()];
2495
+ const basis = requests.length > 0 ? "delta" : this.snapshots.length > 0 ? "snapshot" : "unreported";
2496
+ const timeline2 = { basis, points: cumulativePoints(requests), compactions: this.compactions, models: modelCounts(requests) };
2497
+ if (basis === "snapshot") return { timeline: timeline2, requests, requestCount: null, tokens: snapshotTokens(this.snapshots) };
2498
+ const tokens = emptyTokens();
2499
+ for (const request of requests) addTokens(tokens, request.tokens);
2500
+ return { timeline: timeline2, requests, requestCount: requests.length, tokens };
2501
+ }
2502
+ takeCompactionFlag() {
2503
+ const flag = this.compactedSinceRequest;
2504
+ this.compactedSinceRequest = false;
2505
+ return flag;
2506
+ }
2507
+ };
2508
+ function disjointTokens(counts, split) {
2509
+ const cacheRead = counts.cachedInput ?? 0;
2510
+ const cacheWrite = counts.cacheWriteInput ?? 0;
2511
+ const input = Math.max(0, (counts.input ?? 0) - cacheRead - cacheWrite);
2512
+ return { input, cacheRead, cacheWrite, ...cacheWriteByTtl(cacheWrite, split), output: counts.output ?? 0 };
2513
+ }
2514
+ function cacheWriteByTtl(total, split) {
2515
+ if (split && split.ttl5m + split.ttl1h > 0) return { cacheWrite5m: split.ttl5m, cacheWrite1h: split.ttl1h };
2516
+ return { cacheWrite5m: total, cacheWrite1h: 0 };
2517
+ }
2518
+ function priceInput(tokens) {
2519
+ return {
2520
+ inputTokens: tokens.input,
2521
+ cacheReadTokens: tokens.cacheRead,
2522
+ cacheCreation5mTokens: tokens.cacheWrite5m,
2523
+ cacheCreation1hTokens: tokens.cacheWrite1h,
2524
+ outputTokens: tokens.output
2525
+ };
2526
+ }
2527
+ function isoOf(atMs) {
2528
+ return atMs === null ? "" : new Date(atMs).toISOString();
2529
+ }
2530
+ function cumulativePoints(requests) {
2531
+ const points = [];
2532
+ let cumulativeOutputTokens = 0;
2533
+ let cumulativeCostUsd = 0;
2534
+ for (const request of requests) {
2535
+ cumulativeOutputTokens += request.tokens.output;
2536
+ cumulativeCostUsd += request.costUsd;
2537
+ if (request.atMs === null) continue;
2538
+ const { atMs, turnIndex, model, contextTokens, costUsd, priced, afterCompaction } = request;
2539
+ points.push({ atMs, turnIndex, model, contextTokens, outputTokens: request.tokens.output, cumulativeOutputTokens, costUsd, cumulativeCostUsd, priced, afterCompaction });
2540
+ }
2541
+ return points;
2542
+ }
2543
+ function modelCounts(requests) {
2544
+ const counts = /* @__PURE__ */ new Map();
2545
+ for (const request of requests) {
2546
+ if (request.model) counts.set(request.model, (counts.get(request.model) ?? 0) + 1);
2547
+ }
2548
+ return [...counts].map(([model, count8]) => ({ model, requests: count8 })).sort((a, b) => b.requests - a.requests || a.model.localeCompare(b.model));
2549
+ }
2550
+ function snapshotTokens(snapshots) {
2551
+ const tokens = emptyTokens();
2552
+ for (const measurement of foldUsage(snapshots).measurements) addTokens(tokens, disjointTokens(measurement.tokens));
2553
+ return tokens;
2554
+ }
2555
+
2556
+ // src/timeline.ts
2557
+ var SessionTimelineAccumulator = class {
2558
+ turns;
2559
+ tokens;
2560
+ gapMinMs;
2561
+ conversation = null;
2562
+ startMs = null;
2563
+ endMs = null;
2564
+ constructor(options = {}) {
2565
+ const maxTextChars = options.maxTextChars ?? TIMELINE_TEXT_CAP;
2566
+ this.gapMinMs = options.gapMinMs ?? TIMELINE_GAP_MIN_MS;
2567
+ this.turns = new TurnFold(maxTextChars);
2568
+ this.tokens = new TokenFold(maxTextChars, options.prices);
2569
+ }
2570
+ add(observation) {
2571
+ this.claim(observation);
2572
+ if (observation.historyOrigin) return;
2573
+ const atMs = this.clock(observation.timestamp);
2574
+ if (observation.kind === "message") this.turns.message(observation, atMs);
2575
+ if (observation.kind === "tool_call") this.turns.toolCall(observation, atMs);
2576
+ if (observation.kind === "tool_result") this.turns.toolResult(observation, atMs);
2577
+ if (observation.kind === "usage") this.tokens.usage(observation, atMs, this.turns.currentIndex());
2578
+ if (observation.kind === "compaction") this.tokens.compaction(observation, atMs, this.turns.currentIndex());
2579
+ }
2580
+ result() {
2581
+ const { turns, calls } = this.turns;
2582
+ const tokens = this.tokens.result();
2583
+ attributeRequests(turns, tokens);
2584
+ markTurnGaps(turns, this.gapMinMs);
2585
+ const bucketInput = { turns, calls, points: tokens.timeline.points };
2586
+ const gaps = findGaps(activityTimes(bucketInput), this.gapMinMs, calls);
2587
+ const errors = errorBreakdown(calls);
2588
+ return structuredClone({
2589
+ version: SESSION_TIMELINE_VERSION,
2590
+ sessionId: this.conversation?.nativeId ?? null,
2591
+ harness: this.conversation?.harness ?? null,
2592
+ startMs: this.startMs,
2593
+ endMs: this.endMs,
2594
+ durationMs: this.startMs !== null && this.endMs !== null ? this.endMs - this.startMs : 0,
2595
+ totals: totalsOf(turns, calls.length, errors.items.length, tokens),
2596
+ turns,
2597
+ buckets: minuteBuckets(bucketInput, gaps),
2598
+ gaps,
2599
+ tokens: tokens.timeline,
2600
+ tools: toolBreakdown(calls),
2601
+ files: fileBreakdown(calls),
2602
+ errors,
2603
+ agents: agentSpans(calls)
2604
+ });
2605
+ }
2606
+ claim(observation) {
2607
+ const { harness, namespace, nativeId } = observation.conversation;
2608
+ this.conversation ??= observation.conversation;
2609
+ const held = this.conversation;
2610
+ if (held.harness !== harness || held.namespace !== namespace || held.nativeId !== nativeId) {
2611
+ throw new TypeError("cannot build one timeline from observations of different conversations");
2612
+ }
2613
+ }
2614
+ /** A record with no usable timestamp takes the latest one seen, so it still lands in order. */
2615
+ clock(timestamp) {
2616
+ const parsed = timestamp === null ? Number.NaN : Date.parse(timestamp);
2617
+ if (!Number.isFinite(parsed)) return this.endMs;
2618
+ if (this.startMs === null || parsed < this.startMs) this.startMs = parsed;
2619
+ if (this.endMs === null || parsed > this.endMs) this.endMs = parsed;
2620
+ return parsed;
2621
+ }
2622
+ };
2623
+ function buildSessionTimeline(observations, options = {}) {
2624
+ const accumulator = new SessionTimelineAccumulator(options);
2625
+ for (const observation of observations) accumulator.add(observation);
2626
+ return accumulator.result();
2627
+ }
2628
+ function attributeRequests(turns, tokens) {
2629
+ for (const turn of turns) {
2630
+ turn.tokens = emptyTokens();
2631
+ turn.costUsd = 0;
2632
+ }
2633
+ for (const request of tokens.requests) {
2634
+ const turn = request.turnIndex === null ? void 0 : turns[request.turnIndex];
2635
+ if (!turn) continue;
2636
+ addTokens(turn.tokens, request.tokens);
2637
+ turn.costUsd += request.costUsd;
2638
+ }
2639
+ }
2640
+ function totalsOf(turns, toolCalls, errors, tokens) {
2641
+ return {
2642
+ turns: turns.length,
2643
+ userMessages: turns.filter((turn) => turn.user !== null).length,
2644
+ assistantMessages: turns.reduce((sum2, turn) => sum2 + turn.assistant.length, 0),
2645
+ toolCalls,
2646
+ errors,
2647
+ compactions: tokens.timeline.compactions.length,
2648
+ requests: tokens.requestCount,
2649
+ unpricedRequests: tokens.requests.filter((request) => !request.priced).length,
2650
+ tokens: tokens.tokens,
2651
+ costUsd: tokens.requests.reduce((sum2, request) => sum2 + request.costUsd, 0)
2652
+ };
2653
+ }
2654
+
2655
+ // src/count-at-or-before.ts
2656
+ function countAtOrBefore(sortedMs, targetMs) {
2657
+ let lo = 0;
2658
+ let hi = sortedMs.length;
2659
+ while (lo < hi) {
2660
+ const mid = lo + hi >>> 1;
2661
+ if (sortedMs[mid] <= targetMs) lo = mid + 1;
2662
+ else hi = mid;
2663
+ }
2664
+ return lo;
2665
+ }
2666
+
2667
+ // src/review-fill.ts
2668
+ import { z as z8 } from "zod";
2669
+ var APPROVE = "approve";
2670
+ var CHANGES_REQUESTED = "changes_requested";
2671
+ var CHAT_KEY = /^chat:([^:]+):/;
2672
+ var VERDICTS = `
2673
+ SELECT source_key AS sourceKey, verdict, ts, pr_ref AS prRef, transcript_id AS transcriptId FROM pr_review
2674
+ WHERE (@since IS NULL OR ts >= @since) AND (@until IS NULL OR ts < @until)
2675
+ `;
2676
+ var CHANGES = `SELECT pr_ref AS prRef, ts FROM pr_review WHERE verdict = '${CHANGES_REQUESTED}' AND pr_ref IS NOT NULL`;
2677
+ var ISSUER = `
2678
+ SELECT r.model, r.context_tokens AS tokens FROM tool_call c
2679
+ JOIN request r ON r.transcript_id = c.transcript_id AND r.byte_offset <= c.byte_offset
2680
+ WHERE c.tool_use_id = @toolUseId AND c.transcript_id = @transcriptId
2681
+ ORDER BY r.byte_offset DESC LIMIT 1
2682
+ `;
2683
+ var count7 = z8.number().int().nonnegative();
2684
+ var rate = z8.number().min(0).max(1).nullable();
2685
+ var reviewFillSchema = z8.object({
2686
+ window: z8.object({ since: z8.string().nullable(), until: z8.string().nullable() }),
2687
+ /** Verdicts with no fill: GitHub-surface reviews, and chat verdicts whose issuing request is not in the graph. */
2688
+ unfilled: count7,
2689
+ /** Verdicts whose `pr_ref` is not resolved; they count in their band but are never matched to another review. */
2690
+ unresolved: count7,
2691
+ rows: z8.array(
2692
+ z8.object({
2693
+ band: z8.string(),
2694
+ model: z8.string(),
2695
+ verdicts: count7,
2696
+ changesRequested: count7,
2697
+ changesRate: rate,
2698
+ verdictErrors: count7,
2699
+ /** Verdict errors over all verdicts in the cell, not over approvals. */
2700
+ errorRate: rate
2701
+ })
2702
+ )
2703
+ });
2704
+ function reviewFillReport(db, window) {
2705
+ const changesByPr = readChangesByPr(db);
2706
+ const cells = /* @__PURE__ */ new Map();
2707
+ let unfilled = 0;
2708
+ let unresolved = 0;
2709
+ for (const row of db.prepare(VERDICTS).all(window)) {
2710
+ if (!row.prRef) unresolved += 1;
2711
+ const issuer = issuerOf(db, row);
2712
+ const band = issuer ? contextBand(issuer.tokens) : null;
2713
+ if (!issuer || !band) {
2714
+ unfilled += 1;
2715
+ continue;
2716
+ }
2717
+ const key = `${band}|${issuer.model}`;
2718
+ cells.set(key, tally(cells.get(key) ?? emptyCell(band, issuer.model), row, changesByPr));
2719
+ }
2720
+ return { window, unfilled, unresolved, rows: [...cells.values()].map(withRates) };
2721
+ }
2722
+ function issuerOf(db, row) {
2723
+ const toolUseId = CHAT_KEY.exec(row.sourceKey)?.[1];
2724
+ if (!toolUseId || row.transcriptId === null) return null;
2725
+ return db.prepare(ISSUER).get({ toolUseId, transcriptId: row.transcriptId }) ?? null;
2726
+ }
2727
+ function readChangesByPr(db) {
2728
+ const byPr = /* @__PURE__ */ new Map();
2729
+ for (const { prRef, ts } of db.prepare(CHANGES).all()) byPr.set(prRef, [...byPr.get(prRef) ?? [], ts]);
2730
+ return byPr;
2731
+ }
2732
+ function tally(cell, row, changesByPr) {
2733
+ const contradicted = row.verdict === APPROVE && row.prRef !== null && (changesByPr.get(row.prRef) ?? []).some((ts) => ts > row.ts);
2734
+ return {
2735
+ ...cell,
2736
+ verdicts: cell.verdicts + 1,
2737
+ changesRequested: cell.changesRequested + (row.verdict === CHANGES_REQUESTED ? 1 : 0),
2738
+ verdictErrors: cell.verdictErrors + (contradicted ? 1 : 0)
2739
+ };
2740
+ }
2741
+ function emptyCell(band, model) {
2742
+ return { band, model, verdicts: 0, changesRequested: 0, changesRate: null, verdictErrors: 0, errorRate: null };
2743
+ }
2744
+ function withRates(cell) {
2745
+ return { ...cell, changesRate: cell.changesRequested / cell.verdicts, errorRate: cell.verdictErrors / cell.verdicts };
2746
+ }
1849
2747
  export {
1850
2748
  ACTION_CLASSES,
1851
2749
  AGENT_LIFECYCLE,
@@ -1859,6 +2757,7 @@ export {
1859
2757
  COORDINATOR_MIN_EPISODE_REQUESTS,
1860
2758
  COORDINATOR_WAVE_QUIET_REQUESTS,
1861
2759
  COST_REPORT_SECTIONS,
2760
+ DARK_MIN,
1862
2761
  DEFAULT_ACTION_RULES,
1863
2762
  DEFAULT_CONFIGURED_K,
1864
2763
  DEFAULT_EPISODE_ROLES,
@@ -1872,16 +2771,23 @@ export {
1872
2771
  HEURISTIC_VERSIONS,
1873
2772
  IDLE_GAP_MS,
1874
2773
  LIST_PRICE_CAVEAT,
2774
+ LIVENESS_SOURCES,
1875
2775
  POOLED_REVIEWERS,
1876
2776
  PRICE_TABLE,
1877
2777
  PRICE_TABLE_VERSION,
1878
2778
  PROFILE_ROLES,
2779
+ PROMPT_STALE_MIN,
1879
2780
  REBUILD_GAP_BANDS,
2781
+ SESSION_TIMELINE_VERSION,
1880
2782
  STANDING_PEER_MIN_ASSIGNMENTS,
1881
2783
  STANDING_PEER_MIN_HOURS,
2784
+ SessionTimelineAccumulator,
1882
2785
  TELEPORT_EVENT,
1883
2786
  TICK_HOLD_MAX_MIN,
2787
+ TIMELINE_GAP_MIN_MS,
2788
+ TIMELINE_TEXT_CAP,
1884
2789
  TOKEN_CLASSES,
2790
+ UNKNOWN_PROFILE,
1885
2791
  UNSTATED_REASON,
1886
2792
  WAKE_FROM_KINDS,
1887
2793
  assignmentCount,
@@ -1889,6 +2795,7 @@ export {
1889
2795
  blockedFlowReport,
1890
2796
  blockedFlowSchema,
1891
2797
  buildEpisodes,
2798
+ buildSessionTimeline,
1892
2799
  buildWakeEpisodes,
1893
2800
  cacheTtlReport,
1894
2801
  cacheTtlWhatIf,
@@ -1900,6 +2807,10 @@ export {
1900
2807
  costPerRequest,
1901
2808
  costReport,
1902
2809
  costReportSchema,
2810
+ countAtOrBefore,
2811
+ countMisses,
2812
+ darkGaps,
2813
+ dedupeDenials,
1903
2814
  episodeNames,
1904
2815
  findPrice,
1905
2816
  gapBand,
@@ -1911,7 +2822,10 @@ export {
1911
2822
  isBootAction,
1912
2823
  latencyStats,
1913
2824
  latencyStatsSchema,
2825
+ livenessReport,
2826
+ livenessSchema,
1914
2827
  mergeOutcomes,
2828
+ parseBrokerLog,
1915
2829
  parseDenials,
1916
2830
  parseSeatJournal,
1917
2831
  parseTeleportEvents,
@@ -1925,12 +2839,19 @@ export {
1925
2839
  renderCacheTtlText,
1926
2840
  renderCostReportSections,
1927
2841
  renderCostReportText,
2842
+ renderLivenessText,
2843
+ reviewFillReport,
2844
+ reviewFillSchema,
1928
2845
  roleFromProfile,
2846
+ routeFailureRows,
2847
+ routeMisses,
1929
2848
  scopeFilter,
1930
2849
  sessionInitiative,
1931
2850
  sessionRole,
2851
+ stalePromptRows,
1932
2852
  summarizeWakeEpisodes,
1933
2853
  sweepK,
2854
+ unreportedExitRows,
1934
2855
  wakeEpisodesSchema,
1935
2856
  workerRole,
1936
2857
  writeEpisodes