@titan-design/session-analytics 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1515,9 +1515,9 @@ function readTtlRows(db, window) {
1515
1515
  }
1516
1516
  function renderCacheTtlText(whatIf) {
1517
1517
  const { totals, window } = whatIf;
1518
- const span = window ? `, ${window.since ?? "beginning"} to ${window.until ?? "now"}` : "";
1518
+ const span2 = window ? `, ${window.since ?? "beginning"} to ${window.until ?? "now"}` : "";
1519
1519
  const header2 = [
1520
- `Cache TTL what-if, 5m against 1h${span}`,
1520
+ `Cache TTL what-if, 5m against 1h${span2}`,
1521
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)}`,
1522
1522
  `Roles where 5m loses: ${whatIf.lossRoles.join(", ") || "none"}`
1523
1523
  ].join("\n");
@@ -2138,6 +2138,612 @@ function promptTable(report) {
2138
2138
  table(`Agents silent since a resolved permission prompt ${over} [stalePrompts.resolvedRows[]]`, head, cells(report.stalePrompts.resolvedRows))
2139
2139
  ].join("\n\n");
2140
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
+ }
2141
2747
  export {
2142
2748
  ACTION_CLASSES,
2143
2749
  AGENT_LIFECYCLE,
@@ -2172,10 +2778,14 @@ export {
2172
2778
  PROFILE_ROLES,
2173
2779
  PROMPT_STALE_MIN,
2174
2780
  REBUILD_GAP_BANDS,
2781
+ SESSION_TIMELINE_VERSION,
2175
2782
  STANDING_PEER_MIN_ASSIGNMENTS,
2176
2783
  STANDING_PEER_MIN_HOURS,
2784
+ SessionTimelineAccumulator,
2177
2785
  TELEPORT_EVENT,
2178
2786
  TICK_HOLD_MAX_MIN,
2787
+ TIMELINE_GAP_MIN_MS,
2788
+ TIMELINE_TEXT_CAP,
2179
2789
  TOKEN_CLASSES,
2180
2790
  UNKNOWN_PROFILE,
2181
2791
  UNSTATED_REASON,
@@ -2185,6 +2795,7 @@ export {
2185
2795
  blockedFlowReport,
2186
2796
  blockedFlowSchema,
2187
2797
  buildEpisodes,
2798
+ buildSessionTimeline,
2188
2799
  buildWakeEpisodes,
2189
2800
  cacheTtlReport,
2190
2801
  cacheTtlWhatIf,
@@ -2196,6 +2807,7 @@ export {
2196
2807
  costPerRequest,
2197
2808
  costReport,
2198
2809
  costReportSchema,
2810
+ countAtOrBefore,
2199
2811
  countMisses,
2200
2812
  darkGaps,
2201
2813
  dedupeDenials,
@@ -2228,6 +2840,8 @@ export {
2228
2840
  renderCostReportSections,
2229
2841
  renderCostReportText,
2230
2842
  renderLivenessText,
2843
+ reviewFillReport,
2844
+ reviewFillSchema,
2231
2845
  roleFromProfile,
2232
2846
  routeFailureRows,
2233
2847
  routeMisses,