@titan-design/session-analytics 0.5.0 → 0.6.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
  };
@@ -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,282 @@ 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
+ }
1849
2141
  export {
1850
2142
  ACTION_CLASSES,
1851
2143
  AGENT_LIFECYCLE,
@@ -1859,6 +2151,7 @@ export {
1859
2151
  COORDINATOR_MIN_EPISODE_REQUESTS,
1860
2152
  COORDINATOR_WAVE_QUIET_REQUESTS,
1861
2153
  COST_REPORT_SECTIONS,
2154
+ DARK_MIN,
1862
2155
  DEFAULT_ACTION_RULES,
1863
2156
  DEFAULT_CONFIGURED_K,
1864
2157
  DEFAULT_EPISODE_ROLES,
@@ -1872,16 +2165,19 @@ export {
1872
2165
  HEURISTIC_VERSIONS,
1873
2166
  IDLE_GAP_MS,
1874
2167
  LIST_PRICE_CAVEAT,
2168
+ LIVENESS_SOURCES,
1875
2169
  POOLED_REVIEWERS,
1876
2170
  PRICE_TABLE,
1877
2171
  PRICE_TABLE_VERSION,
1878
2172
  PROFILE_ROLES,
2173
+ PROMPT_STALE_MIN,
1879
2174
  REBUILD_GAP_BANDS,
1880
2175
  STANDING_PEER_MIN_ASSIGNMENTS,
1881
2176
  STANDING_PEER_MIN_HOURS,
1882
2177
  TELEPORT_EVENT,
1883
2178
  TICK_HOLD_MAX_MIN,
1884
2179
  TOKEN_CLASSES,
2180
+ UNKNOWN_PROFILE,
1885
2181
  UNSTATED_REASON,
1886
2182
  WAKE_FROM_KINDS,
1887
2183
  assignmentCount,
@@ -1900,6 +2196,9 @@ export {
1900
2196
  costPerRequest,
1901
2197
  costReport,
1902
2198
  costReportSchema,
2199
+ countMisses,
2200
+ darkGaps,
2201
+ dedupeDenials,
1903
2202
  episodeNames,
1904
2203
  findPrice,
1905
2204
  gapBand,
@@ -1911,7 +2210,10 @@ export {
1911
2210
  isBootAction,
1912
2211
  latencyStats,
1913
2212
  latencyStatsSchema,
2213
+ livenessReport,
2214
+ livenessSchema,
1914
2215
  mergeOutcomes,
2216
+ parseBrokerLog,
1915
2217
  parseDenials,
1916
2218
  parseSeatJournal,
1917
2219
  parseTeleportEvents,
@@ -1925,12 +2227,17 @@ export {
1925
2227
  renderCacheTtlText,
1926
2228
  renderCostReportSections,
1927
2229
  renderCostReportText,
2230
+ renderLivenessText,
1928
2231
  roleFromProfile,
2232
+ routeFailureRows,
2233
+ routeMisses,
1929
2234
  scopeFilter,
1930
2235
  sessionInitiative,
1931
2236
  sessionRole,
2237
+ stalePromptRows,
1932
2238
  summarizeWakeEpisodes,
1933
2239
  sweepK,
2240
+ unreportedExitRows,
1934
2241
  wakeEpisodesSchema,
1935
2242
  workerRole,
1936
2243
  writeEpisodes