@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/README.md +57 -1
- package/dist/index.d.ts +553 -1
- package/dist/index.js +959 -38
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
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(
|
|
538
|
+
function parseTeleportEvents(lines2) {
|
|
537
539
|
const events = [];
|
|
538
|
-
for (const line of
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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
|
|
674
|
-
latest.
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
|
950
|
+
const sum2 = (list, pick) => list.reduce((total, episode) => total + pick(episode), 0);
|
|
946
951
|
return {
|
|
947
952
|
episodes: episodes.length,
|
|
948
|
-
requests:
|
|
949
|
-
costUsd:
|
|
953
|
+
requests: sum2(episodes, (episode) => episode.requests),
|
|
954
|
+
costUsd: sum2(episodes, (episode) => episode.costUsd),
|
|
950
955
|
noActionEpisodes: noAction.length,
|
|
951
|
-
noActionCostUsd:
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
1287
|
-
const requests = buckets2.reduce((
|
|
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
|
|
1381
|
-
return [title, ...
|
|
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
|
|
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 =
|
|
1465
|
+
const netSavingUsd = sum2((effect) => effect.repriceSavingUsd - effect.rebuildCostUsd);
|
|
1461
1466
|
return {
|
|
1462
1467
|
sessions,
|
|
1463
1468
|
requests: effects.length,
|
|
1464
|
-
write1hTokens:
|
|
1465
|
-
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:
|
|
1468
|
-
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
|
|
1518
|
+
const span2 = window ? `, ${window.since ?? "beginning"} to ${window.until ?? "now"}` : "";
|
|
1514
1519
|
const header2 = [
|
|
1515
|
-
`Cache TTL what-if, 5m against 1h${
|
|
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(
|
|
1638
|
+
function parseDenials(lines2, seat) {
|
|
1634
1639
|
const calls = /* @__PURE__ */ new Map();
|
|
1635
1640
|
const denials = [];
|
|
1636
|
-
for (const line of
|
|
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
|
-
|
|
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((
|
|
1785
|
-
denials: denialSection(input.denials.filter((d) => inScope(d.seat, d.at))),
|
|
1786
|
-
idleSlots: { cites: ["journals"], holdMaxMin: TICK_HOLD_MAX_MIN, total: idle.reduce((
|
|
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
|