@titan-design/session-analytics 0.4.0 → 0.5.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
@@ -1,14 +1,19 @@
1
1
  // src/prices.ts
2
- var PRICE_TABLE_VERSION = 1;
2
+ var PRICE_TABLE_VERSION = 2;
3
3
  var OPUS = { input: 5, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 };
4
+ var OPUS_5_5 = { input: 4, cacheRead: 0.2, cacheWrite5m: 5, cacheWrite1h: 8, output: 20 };
4
5
  var SONNET = { input: 2, cacheRead: 0.2, cacheWrite5m: 2.5, cacheWrite1h: 4, output: 10 };
5
6
  var FABLE = { input: 10, cacheRead: 0.25, cacheWrite5m: 12.5, cacheWrite1h: 20, output: 50 };
7
+ var SONNET_5_5 = { input: 2, cacheRead: 0.2, cacheWrite5m: 2.5, cacheWrite1h: 4, output: 10 };
6
8
  var HAIKU = { input: 1, cacheRead: 0.1, cacheWrite5m: 1.25, cacheWrite1h: 2, output: 5 };
7
9
  var FREE = { input: 0, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 0 };
8
10
  var GENESIS = "2026-01-01";
9
11
  var PRICE_TABLE = [
12
+ // 5.5 rows: claude-api skill shared/models.md and prompt-caching.md, bundled copy cached 2026-09-25.
13
+ { modelPrefix: "claude-opus-5-5", effectiveFrom: GENESIS, ...OPUS_5_5 },
10
14
  { modelPrefix: "claude-opus-5", effectiveFrom: GENESIS, ...OPUS },
11
15
  { modelPrefix: "claude-opus-4-8", effectiveFrom: GENESIS, ...OPUS },
16
+ { modelPrefix: "claude-sonnet-5-5", effectiveFrom: GENESIS, ...SONNET_5_5 },
12
17
  { modelPrefix: "claude-sonnet-5", effectiveFrom: GENESIS, ...SONNET },
13
18
  { modelPrefix: "claude-fable-5-1", effectiveFrom: GENESIS, ...FABLE },
14
19
  { modelPrefix: "claude-fable-5", effectiveFrom: GENESIS, ...FABLE },
@@ -18,7 +23,7 @@ var PRICE_TABLE = [
18
23
  function findPrice(model, ts, prices = PRICE_TABLE) {
19
24
  let best = null;
20
25
  for (const row of prices) {
21
- if (!model.startsWith(row.modelPrefix)) continue;
26
+ if (!matchesAtBoundary(model, row.modelPrefix)) continue;
22
27
  if (row.effectiveFrom > ts) continue;
23
28
  if (best && !isBetterMatch(row, best)) continue;
24
29
  best = row;
@@ -29,6 +34,11 @@ function isBetterMatch(row, best) {
29
34
  if (row.modelPrefix.length !== best.modelPrefix.length) return row.modelPrefix.length > best.modelPrefix.length;
30
35
  return row.effectiveFrom > best.effectiveFrom;
31
36
  }
37
+ function matchesAtBoundary(model, prefix) {
38
+ if (!model.startsWith(prefix)) return false;
39
+ const rest = model.slice(prefix.length);
40
+ return rest === "" || rest.startsWith("[") || /^-\d{8}(\[.*\])?$/.test(rest);
41
+ }
32
42
 
33
43
  // src/price-request.ts
34
44
  var UNPRICED = {
@@ -121,14 +131,14 @@ function gapBand(millis) {
121
131
  }
122
132
 
123
133
  // src/cost-report.ts
124
- import { z } from "zod";
134
+ import { z as z3 } from "zod";
125
135
 
126
136
  // src/cost-report-queries.ts
127
137
  import { AUDIT_FACET, EPISODE_TABLE, FACET_TABLE, KIT } from "@titan-design/session-graph";
128
138
  var IN_WINDOW = "(@since IS NULL OR ts >= @since) AND (@until IS NULL OR ts < @until)";
129
139
  var IN_SESSIONS = "session_id IN (SELECT value FROM json_each(@ids))";
130
140
  var COST_ROWS = `
131
- SELECT session_id AS sessionId, model,
141
+ SELECT session_id AS sessionId, request_id AS requestId, ts, model, COALESCE(context_tokens, 0) AS contextTokens, is_sidechain AS isSidechain,
132
142
  input_tokens AS inputTokens, cache_read_tokens AS cacheReadTokens,
133
143
  CASE WHEN cache_creation_5m + cache_creation_1h = 0 THEN cache_creation_tokens ELSE cache_creation_5m END AS cacheWrite5mTokens,
134
144
  cache_creation_1h AS cacheWrite1hTokens, output_tokens AS outputTokens,
@@ -140,7 +150,7 @@ var COST_ROWS = `
140
150
  `;
141
151
  function readCostRows(db, window) {
142
152
  const rows = db.prepare(COST_ROWS).all(window);
143
- return rows.map((row) => ({ ...row, priced: row.priced === 1, isCold: row.isCold === 1 }));
153
+ return rows.map((row) => ({ ...row, priced: row.priced === 1, isCold: row.isCold === 1, isSidechain: row.isSidechain === 1 }));
144
154
  }
145
155
  function readCompactions(db, window) {
146
156
  const sql = `SELECT trigger, mid_loop AS midLoop, dropped_tokens AS droppedTokens FROM compaction WHERE ${IN_WINDOW}`;
@@ -159,6 +169,46 @@ function readCoverage(db, facetVersion) {
159
169
  ).get({ facet: AUDIT_FACET, version: facetVersion });
160
170
  return { transcriptsIndexed: indexed.n, facetBacklog: backlog.n };
161
171
  }
172
+ var EPISODE_DELIVERIES = "('turn_start', 'mid_loop')";
173
+ var WAKE_EVENTS = `
174
+ WITH event AS (
175
+ SELECT transcript_id, byte_offset, MAX(block_index) AS block_index FROM inbound
176
+ WHERE ${IN_SESSIONS} AND ${IN_WINDOW} AND delivery IN ${EPISODE_DELIVERIES}
177
+ GROUP BY transcript_id, byte_offset
178
+ ), fanout AS (
179
+ SELECT msg_id, COUNT(DISTINCT session_id) AS receivers FROM inbound
180
+ WHERE msg_id IN (SELECT i.msg_id FROM event JOIN inbound i USING (transcript_id, byte_offset, block_index))
181
+ GROUP BY msg_id
182
+ )
183
+ SELECT i.transcript_id || ':' || i.byte_offset AS key, i.session_id AS sessionId, i.cause, i.delivery,
184
+ i.from_name AS fromName, COALESCE(f.receivers, 1) > 1 AS broadcast
185
+ FROM event JOIN inbound i USING (transcript_id, byte_offset, block_index)
186
+ LEFT JOIN fanout f ON f.msg_id = i.msg_id
187
+ ORDER BY i.transcript_id, i.byte_offset`;
188
+ var REQUEST_EVENTS = `
189
+ SELECT d.request_id AS requestId, d.transcript_id || ':' || (
190
+ SELECT MAX(i.byte_offset) FROM inbound i
191
+ WHERE i.transcript_id = d.transcript_id AND i.session_id = d.session_id AND i.byte_offset < d.byte_offset
192
+ AND i.delivery IN ${EPISODE_DELIVERIES}) AS eventKey
193
+ FROM request_dedup d WHERE d.${IN_SESSIONS} AND (@since IS NULL OR d.ts >= @since) AND (@until IS NULL OR d.ts < @until)`;
194
+ function readWakeEvents(db, window, sessionIds) {
195
+ const rows = db.prepare(WAKE_EVENTS).all({ ...window, ids: JSON.stringify(sessionIds) });
196
+ return rows.map((row) => ({ ...row, broadcast: row.broadcast === 1 }));
197
+ }
198
+ function readRequestEvents(db, window, sessionIds) {
199
+ const rows = db.prepare(REQUEST_EVENTS).all({ ...window, ids: JSON.stringify(sessionIds) });
200
+ return new Map(rows.map((row) => [row.requestId, row.eventKey]));
201
+ }
202
+ function readAgentNames(db) {
203
+ const sql = `SELECT session_id AS sessionId, agent_name AS agentName, origin_kind AS originKind, profile FROM session_origin WHERE agent_name IS NOT NULL`;
204
+ return db.prepare(sql).all();
205
+ }
206
+ function readAgentSessions(db) {
207
+ const sql = `SELECT agent_id AS agentId, session_id AS sessionId FROM session_origin WHERE agent_id IS NOT NULL`;
208
+ const sessions = /* @__PURE__ */ new Map();
209
+ for (const row of db.prepare(sql).all()) sessions.set(row.agentId, [...sessions.get(row.agentId) ?? [], row.sessionId]);
210
+ return sessions;
211
+ }
162
212
  function readSessionContexts(db, sessionIds) {
163
213
  const ids = JSON.stringify(sessionIds);
164
214
  const contexts = new Map(sessionIds.map((id) => [id, emptyContext()]));
@@ -412,6 +462,232 @@ function writeEpisodes(graph, sessionIds) {
412
462
  return written;
413
463
  }
414
464
 
465
+ // src/handoff-threshold.ts
466
+ import { z } from "zod";
467
+ var BOOT_TOOL = /(agent_(spawn|resume)|chat_(send|broadcast))$/;
468
+ var DEFAULT_CONFIGURED_K = [17e4, 25e4];
469
+ var DEFAULT_K_SWEEP = { fromK: 2e4, toK: 1e6, stepK: 5e3 };
470
+ var DEFAULT_REVIEWER_ROLE = "worker:reviewer";
471
+ var DEFAULT_STANDING_ROLE = "worker:standing_peer";
472
+ var DEFAULT_REVIEWER_PRS = 10;
473
+ var TELEPORT_EVENT = "teleport_started";
474
+ var MTOK = 1e6;
475
+ var count = z.number().int().nonnegative();
476
+ var kCostSchema = z.object({ k: count, costPerRequest: z.number().nullable(), deltaPerRequest: z.number().nullable() });
477
+ var sweepResultSchema = z.object({ bestK: count.nullable(), bestCostPerRequest: z.number().nullable(), atConfigured: z.array(kCostSchema) });
478
+ var paramsSchema = z.object({ bootCostUsd: z.number(), bootFill: z.number(), growthPerRequest: z.number(), readPricePerMTok: z.number() });
479
+ var sessionSchema = paramsSchema.extend({
480
+ sessionId: z.string(),
481
+ role: z.string(),
482
+ model: z.string(),
483
+ requests: count,
484
+ bootRequests: count,
485
+ exitFill: count,
486
+ /** Timestamp of the session's last request; orders reviewer cohorts by recency. */
487
+ lastTs: z.string(),
488
+ bestK: count.nullable()
489
+ });
490
+ var cohortSchema = paramsSchema.extend({
491
+ role: z.string(),
492
+ model: z.string(),
493
+ sessions: count,
494
+ ...sweepResultSchema.shape,
495
+ /** The same sweep when only half the boot is overhead the next session pays again. */
496
+ halfBoot: sweepResultSchema
497
+ });
498
+ var teleportSchema = z.object({
499
+ ts: z.string(),
500
+ name: z.string().nullable(),
501
+ sessionId: z.string(),
502
+ role: z.string(),
503
+ model: z.string(),
504
+ exitFill: count,
505
+ bestK: count.nullable(),
506
+ /** Exit fill minus each configured K; positive means the seat left late. */
507
+ overConfigured: z.array(z.object({ k: count, over: z.number() }))
508
+ });
509
+ var POOLED_REVIEWERS = "pooled";
510
+ var reviewerCostSchema = z.object({
511
+ role: z.string(),
512
+ model: z.string(),
513
+ sessions: count,
514
+ /** Mean requests after boot of a per-PR reviewer: the work each PR adds. */
515
+ requestsPerPr: z.number(),
516
+ /** The reviewer model whose sessions give `requestsPerPr`: the row's own, else the newest reviewer cohort; `pooled` with none. */
517
+ requestsFrom: z.string(),
518
+ costUsd: z.number()
519
+ });
520
+ var reviewerSchema = z.object({ prs: count, fresh: z.array(reviewerCostSchema), standing: z.array(reviewerCostSchema) });
521
+ var handoffThresholdSchema = z.object({
522
+ configuredK: z.array(count),
523
+ sweep: z.object({ fromK: count, toK: count, stepK: count }),
524
+ cohorts: z.array(cohortSchema),
525
+ sessions: z.array(sessionSchema),
526
+ /** Sessions with no dispatch, send or write, or none priced: they have no boot to measure. */
527
+ unbootedSessions: count,
528
+ teleports: z.array(teleportSchema),
529
+ unmatchedTeleports: count,
530
+ /** Boot and cache reads for `prs` reviews: a fresh reviewer per PR against one standing reviewer. */
531
+ reviewers: reviewerSchema
532
+ });
533
+ function isBootAction(calls) {
534
+ return calls.some((call) => BOOT_TOOL.test(call.tool) || (call.writePaths?.length ?? 0) > 0);
535
+ }
536
+ function parseTeleportEvents(lines) {
537
+ const events = [];
538
+ for (const line of lines) {
539
+ const entry = parseJson(line);
540
+ if (entry?.event !== TELEPORT_EVENT || typeof entry.from !== "string" || typeof entry.ts !== "string") continue;
541
+ events.push({ ts: entry.ts, name: typeof entry.name === "string" ? entry.name : null, fromAgentId: entry.from });
542
+ }
543
+ return events;
544
+ }
545
+ function parseJson(line) {
546
+ try {
547
+ const value = JSON.parse(line);
548
+ return typeof value === "object" && value !== null ? value : null;
549
+ } catch {
550
+ return null;
551
+ }
552
+ }
553
+ function costPerRequest(params, k) {
554
+ const requests = (k - params.bootFill) / params.growthPerRequest;
555
+ if (!(params.growthPerRequest > 0) || requests < 1) return null;
556
+ return params.bootCostUsd / requests + params.readPricePerMTok / MTOK * ((params.bootFill + k) / 2);
557
+ }
558
+ function sweepK(params, sweep, configuredK) {
559
+ let best = null;
560
+ for (let k = sweep.fromK; k <= sweep.toK; k += sweep.stepK) {
561
+ const cost = costPerRequest(params, k);
562
+ if (cost !== null && (best === null || cost < best.cost)) best = { k, cost };
563
+ }
564
+ const atConfigured = configuredK.map((k) => {
565
+ const cost = costPerRequest(params, k);
566
+ return { k, costPerRequest: cost, deltaPerRequest: cost === null || best === null ? null : cost - best.cost };
567
+ });
568
+ return { bestK: best?.k ?? null, bestCostPerRequest: best?.cost ?? null, atConfigured };
569
+ }
570
+ function handoffThreshold(rows, teleports, agentSessions2, options = {}) {
571
+ const configuredK = [...options.configuredK ?? DEFAULT_CONFIGURED_K];
572
+ const sweep = { ...options.sweep ?? DEFAULT_K_SWEEP };
573
+ const bySession = groupBy(rows, (row) => row.sessionId);
574
+ const fits = [...bySession.values()].map((requests) => fitSession(requests, options.prices ?? PRICE_TABLE, sweep));
575
+ const sessions = fits.filter((fit) => fit !== null).sort((a, b) => a.sessionId.localeCompare(b.sessionId));
576
+ const cohorts = cohortsOf(sessions, sweep, configuredK);
577
+ const matched = teleports.map((event) => teleportExit(event, agentSessions2, bySession, cohorts, configuredK)).filter((t) => t !== null);
578
+ return {
579
+ configuredK,
580
+ sweep,
581
+ cohorts,
582
+ sessions,
583
+ unbootedSessions: fits.length - sessions.length,
584
+ teleports: matched.sort((a, b) => a.ts.localeCompare(b.ts)),
585
+ unmatchedTeleports: teleports.length - matched.length,
586
+ reviewers: reviewerComparison(sessions, options.reviewerRole ?? DEFAULT_REVIEWER_ROLE, options.standingRole ?? DEFAULT_STANDING_ROLE, options.reviewerPrs ?? DEFAULT_REVIEWER_PRS)
587
+ };
588
+ }
589
+ function fitSession(unsorted, prices, sweep) {
590
+ const requests = [...unsorted].sort((a, b) => a.ts.localeCompare(b.ts));
591
+ const first = requests[0];
592
+ const bootIndex = requests.findIndex((row) => row.bootAction);
593
+ const price = findPrice(first.model, first.ts, prices);
594
+ if (bootIndex < 0 || price === null) return null;
595
+ const params = {
596
+ bootCostUsd: requests.slice(0, bootIndex + 1).reduce((sum, row) => sum + priceRequest(row.tokens, row.model, row.ts, prices).costUsd, 0),
597
+ bootFill: requests[bootIndex].fill,
598
+ growthPerRequest: meanGrowth(requests.slice(bootIndex).map((row) => row.fill)),
599
+ readPricePerMTok: price.cacheRead
600
+ };
601
+ const { sessionId, role, model } = first;
602
+ const last = requests[requests.length - 1];
603
+ return { sessionId, role, model, requests: requests.length, bootRequests: bootIndex + 1, exitFill: last.fill, lastTs: last.ts, ...params, bestK: sweepK(params, sweep, []).bestK };
604
+ }
605
+ function meanGrowth(fills) {
606
+ if (fills.length < 2) return 0;
607
+ let rise = 0;
608
+ for (let i = 1; i < fills.length; i += 1) rise += Math.max(0, fills[i] - fills[i - 1]);
609
+ return rise / (fills.length - 1);
610
+ }
611
+ function cohortsOf(sessions, sweep, configuredK) {
612
+ return [...groupBy(sessions, (s) => JSON.stringify([s.role, s.model]))].map(([, members]) => {
613
+ const params = meanParams(members);
614
+ return {
615
+ role: members[0].role,
616
+ model: members[0].model,
617
+ sessions: members.length,
618
+ ...params,
619
+ ...sweepK(params, sweep, configuredK),
620
+ halfBoot: sweepK({ ...params, bootCostUsd: params.bootCostUsd / 2 }, sweep, configuredK)
621
+ };
622
+ }).sort((a, b) => a.role.localeCompare(b.role) || a.model.localeCompare(b.model));
623
+ }
624
+ function meanParams(sessions) {
625
+ const mean = (pick) => sessions.reduce((sum, s) => sum + pick(s), 0) / sessions.length;
626
+ return {
627
+ bootCostUsd: mean((s) => s.bootCostUsd),
628
+ bootFill: mean((s) => s.bootFill),
629
+ growthPerRequest: meanGrowthOf(sessions),
630
+ readPricePerMTok: mean((s) => s.readPricePerMTok)
631
+ };
632
+ }
633
+ function meanGrowthOf(sessions) {
634
+ const grown = sessions.filter((s) => s.growthPerRequest > 0);
635
+ return grown.length === 0 ? 0 : grown.reduce((sum, s) => sum + s.growthPerRequest, 0) / grown.length;
636
+ }
637
+ function teleportExit(event, agentSessions2, bySession, cohorts, configuredK) {
638
+ const candidates = (agentSessions2.get(event.fromAgentId) ?? []).flatMap((id) => bySession.get(id) ?? []);
639
+ const exit = candidates.filter((row) => row.ts <= event.ts).sort((a, b) => b.ts.localeCompare(a.ts))[0];
640
+ if (!exit) return null;
641
+ const cohort = cohorts.find((c) => c.role === exit.role && c.model === exit.model);
642
+ return {
643
+ ts: event.ts,
644
+ name: event.name,
645
+ sessionId: exit.sessionId,
646
+ role: exit.role,
647
+ model: exit.model,
648
+ exitFill: exit.fill,
649
+ bestK: cohort?.bestK ?? null,
650
+ overConfigured: configuredK.map((k) => ({ k, over: exit.fill - k }))
651
+ };
652
+ }
653
+ function readCost(params, requests) {
654
+ return (requests * params.bootFill + params.growthPerRequest * requests * (requests + 1) / 2) * params.readPricePerMTok / MTOK;
655
+ }
656
+ function reviewerComparison(sessions, reviewerRole, standingRole, prs) {
657
+ const cohorts = (role) => [...groupBy(sessions.filter((s) => s.role === role), (s) => s.model)].sort(([a], [b]) => a.localeCompare(b));
658
+ const reviewers = cohorts(reviewerRole);
659
+ const perPr = new Map(reviewers.map(([model, members]) => [model, meanRequestsAfterBoot(members)]));
660
+ const newest = newestCohort(reviewers);
661
+ const priced = (role, costOf2) => cohorts(role).map(([model, members]) => {
662
+ const from = perPr.has(model) ? model : newest;
663
+ const requestsPerPr = from === null ? 0 : perPr.get(from);
664
+ return { role, model, sessions: members.length, requestsPerPr, requestsFrom: from ?? POOLED_REVIEWERS, costUsd: costOf2(meanParams(members), requestsPerPr) };
665
+ });
666
+ return {
667
+ prs,
668
+ fresh: priced(reviewerRole, (params, requests) => prs * (params.bootCostUsd + readCost(params, requests))),
669
+ standing: priced(standingRole, (params, requests) => params.bootCostUsd + readCost(params, prs * requests))
670
+ };
671
+ }
672
+ function newestCohort(reviewers) {
673
+ const latest = reviewers.map(([model, members]) => ({ model, ts: members.reduce((max, s) => s.lastTs > max ? s.lastTs : max, "") }));
674
+ latest.sort((a, b) => b.ts.localeCompare(a.ts) || b.model.localeCompare(a.model));
675
+ return latest[0]?.model ?? null;
676
+ }
677
+ function meanRequestsAfterBoot(sessions) {
678
+ return sessions.length === 0 ? 0 : sessions.reduce((sum, s) => sum + s.requests - s.bootRequests, 0) / sessions.length;
679
+ }
680
+ function groupBy(rows, keyOf) {
681
+ const groups = /* @__PURE__ */ new Map();
682
+ for (const row of rows) {
683
+ const key = keyOf(row);
684
+ const members = groups.get(key);
685
+ if (members) members.push(row);
686
+ else groups.set(key, [row]);
687
+ }
688
+ return groups;
689
+ }
690
+
415
691
  // src/initiative.ts
416
692
  function sessionInitiative(tasks, cwd) {
417
693
  const best = [...tasks].sort((a, b) => b.edges - a.edges || a.initiative.localeCompare(b.initiative))[0];
@@ -428,6 +704,50 @@ function initiativeFromCwd(cwd) {
428
704
  return `other:${trimmed.slice(trimmed.lastIndexOf("/") + 1)}`;
429
705
  }
430
706
 
707
+ // src/request-owner.ts
708
+ var CALL_SIGNALS = { command_heads: "heads", file_read: "readPaths", file_write: "writePaths" };
709
+ var OWNED_CALLS = `
710
+ WITH scope AS (
711
+ SELECT DISTINCT transcript_id FROM request_dedup
712
+ WHERE (@since IS NULL OR ts >= @since) AND (@until IS NULL OR ts < @until)
713
+ ), stream AS (
714
+ SELECT transcript_id, byte_offset, -1 AS lane, 0 AS block_index, request_id, NULL AS tool_use_id, NULL AS name
715
+ FROM request WHERE transcript_id IN (SELECT transcript_id FROM scope)
716
+ UNION ALL
717
+ SELECT transcript_id, byte_offset, 0, block_index, NULL, tool_use_id, name
718
+ FROM tool_call WHERE transcript_id IN (SELECT transcript_id FROM scope)
719
+ ), ordered AS (
720
+ SELECT *, COUNT(request_id) OVER (PARTITION BY transcript_id ORDER BY byte_offset, lane, block_index ROWS UNBOUNDED PRECEDING) AS requests_seen
721
+ FROM stream
722
+ ), owned AS (
723
+ SELECT c.transcript_id, c.byte_offset, c.block_index, c.tool_use_id, c.name, r.request_id
724
+ FROM ordered c JOIN ordered r ON r.transcript_id = c.transcript_id AND r.lane = -1 AND r.requests_seen = c.requests_seen
725
+ WHERE c.lane = 0
726
+ )
727
+ SELECT o.request_id AS requestId, o.transcript_id || ':' || o.byte_offset || ':' || o.block_index AS callKey, o.name AS tool, s.signal, s.detail
728
+ FROM owned o
729
+ JOIN request_dedup d ON d.transcript_id = o.transcript_id AND d.request_id = o.request_id
730
+ LEFT JOIN session_signal s ON s.transcript_id = o.transcript_id AND s.tool_use_id = o.tool_use_id
731
+ AND s.signal IN ('command_heads', 'file_read', 'file_write')
732
+ WHERE (@since IS NULL OR d.ts >= @since) AND (@until IS NULL OR d.ts < @until)
733
+ ORDER BY o.transcript_id, o.byte_offset, o.block_index
734
+ `;
735
+ function readRequestToolCalls(db, window) {
736
+ const byRequest = /* @__PURE__ */ new Map();
737
+ for (const row of db.prepare(OWNED_CALLS).all(window)) {
738
+ const calls = byRequest.get(row.requestId) ?? /* @__PURE__ */ new Map();
739
+ byRequest.set(row.requestId, calls);
740
+ const call = calls.get(row.callKey) ?? { tool: row.tool };
741
+ calls.set(row.callKey, row.signal ? withSignal(call, row.signal, row.detail ?? "") : call);
742
+ }
743
+ return new Map([...byRequest].map(([requestId, calls]) => [requestId, [...calls.values()]]));
744
+ }
745
+ function withSignal(call, signal, detail) {
746
+ const field = CALL_SIGNALS[signal];
747
+ const values = signal === "command_heads" ? detail.split(";").filter(Boolean) : [detail];
748
+ return { ...call, [field]: [...call[field] ?? [], ...values] };
749
+ }
750
+
431
751
  // src/roles.ts
432
752
  var PROFILE_ROLES = {
433
753
  implementer: "implementer",
@@ -435,13 +755,18 @@ var PROFILE_ROLES = {
435
755
  "relay-implementer": "implementer",
436
756
  "beat-builder": "implementer",
437
757
  "fable-optimizer": "implementer",
758
+ "bd-implementer": "implementer",
759
+ "bd-implementer-lite": "implementer",
438
760
  reviewer: "reviewer",
439
761
  "relay-reviewer": "reviewer",
762
+ "bd-reviewer": "reviewer",
440
763
  researcher: "researcher",
441
764
  explorer: "researcher",
442
765
  "fable-architect": "planner",
443
766
  planner: "planner",
767
+ "bd-planner": "planner",
444
768
  "fable-coordinator": "coordinator",
769
+ "opus-coordinator": "coordinator",
445
770
  peer: "standing_peer"
446
771
  };
447
772
  var STANDING_PEER_MIN_HOURS = 12;
@@ -460,42 +785,252 @@ function sessionRole(classification, worker) {
460
785
  return classification.sessionClass;
461
786
  }
462
787
 
788
+ // src/scope.ts
789
+ function scopeFilter(db, scope = {}) {
790
+ const ids = scope.sessionIds ? new Set(scope.sessionIds) : null;
791
+ const named = scope.agentPrefix === void 0 ? null : agentSessions(db, scope.agentPrefix);
792
+ const roles = scope.roles ? new Set(scope.roles) : null;
793
+ return (row) => (!ids || ids.has(row.sessionId)) && (!named || named.has(row.sessionId)) && (!roles || roles.has(row.role));
794
+ }
795
+ function agentSessions(db, prefix) {
796
+ return new Set(readAgentNames(db).filter((row) => row.agentName.startsWith(prefix)).map((row) => row.sessionId));
797
+ }
798
+
799
+ // src/turn-action.ts
800
+ var ACTION_CLASSES = [
801
+ "journal-write",
802
+ "pr-ci-check",
803
+ "merge",
804
+ "dispatch",
805
+ "retire",
806
+ "task-state",
807
+ "budget-status",
808
+ "scorer",
809
+ "message",
810
+ "read-investigate",
811
+ "text-only",
812
+ "other"
813
+ ];
814
+ var JOURNAL_BASENAME = String.raw`(?:(?:[^/]*[-_.])?(?:log|journal|events?)(?:[-_.][^/]*)?\.(?:md|jsonl)|[^/]*journal[^/]*)$`;
815
+ var INTERPRETER = String.raw`(?:(?:python3?|node|tsx|bash|sh) )?`;
816
+ var DEFAULT_ACTION_RULES = [
817
+ { cls: "journal-write", writePath: new RegExp(String.raw`(?:^|/)${JOURNAL_BASENAME}`, "i") },
818
+ { cls: "journal-write", head: new RegExp(`^>(?:[^/]*/)?${JOURNAL_BASENAME}`, "i") },
819
+ { cls: "pr-ci-check", head: /^gh pr (checks|view)\b/ },
820
+ { cls: "pr-ci-check", head: /^gh run\b/ },
821
+ { cls: "pr-ci-check", head: /^git merge-tree\b/ },
822
+ { cls: "pr-ci-check", head: /^gh api GET commits\/check-runs$/ },
823
+ { cls: "pr-ci-check", head: /^gh api GET commits\/status$/ },
824
+ { cls: "merge", head: /^gh pr merge\b/ },
825
+ { cls: "merge", head: /^gh api PUT pulls\/merge$/ },
826
+ { cls: "dispatch", tool: /agent_(spawn|resume)$/ },
827
+ { cls: "retire", head: /^(agent-chat )?agent retire\b/ },
828
+ { cls: "task-state", head: /^active-work task\b/ },
829
+ { cls: "budget-status", tool: /(session_budget|chat_status)$/ },
830
+ { cls: "budget-status", head: /^(agent-chat )?agent list\b/ },
831
+ { cls: "budget-status", head: /^(agent-chat )?agent (ls|budget|worktrees)\b/ },
832
+ { cls: "budget-status", head: /^git worktree list\b/ },
833
+ { cls: "budget-status", tool: /(agent_list|chat_list|ListAgents)$/ },
834
+ { cls: "scorer", tool: /(^|__)scor/i },
835
+ { cls: "scorer", head: new RegExp(`^${INTERPRETER}[\\w.-]*scor`, "i") },
836
+ { cls: "message", tool: /chat_send$/ },
837
+ { cls: "message", tool: /(chat_ask|chat_inbox|chat_claim|chat_release)$/ },
838
+ { cls: "read-investigate", tool: /^(Read|Grep|Glob)$/ },
839
+ { cls: "read-investigate", head: /^(cat|ls|grep|rg|head|tail|find|wc|git (log|diff|show|status))\b/ }
840
+ ];
841
+ var DEFAULT_MECHANICAL_CLASSES = [
842
+ "journal-write",
843
+ "pr-ci-check",
844
+ "task-state",
845
+ "budget-status",
846
+ "retire",
847
+ "merge",
848
+ "scorer"
849
+ ];
850
+ function callMatches(call, rule) {
851
+ if (rule.tool && !rule.tool.test(call.tool)) return false;
852
+ if (rule.head && !(call.heads ?? []).some((h) => rule.head?.test(h))) return false;
853
+ if (rule.readPath && !(call.readPaths ?? []).some((p) => rule.readPath?.test(p))) return false;
854
+ if (rule.writePath && !(call.writePaths ?? []).some((p) => rule.writePath?.test(p))) return false;
855
+ return true;
856
+ }
857
+ function classifyRequest(calls, rules = DEFAULT_ACTION_RULES) {
858
+ if (calls.length === 0) return "text-only";
859
+ const hit = rules.find((rule) => calls.some((call) => callMatches(call, rule)));
860
+ return hit?.cls ?? "other";
861
+ }
862
+
863
+ // src/wake-episodes.ts
864
+ import { z as z2 } from "zod";
865
+ var WAKE_FROM_KINDS = ["seat", "agent", "broadcast", "broker", "none"];
866
+ var DEFAULT_EPISODE_ROLES = ["coordinator", "worker:coordinator"];
867
+ var DEFAULT_NO_ACTION_CLASSES = ["read-investigate", "text-only", "other"];
868
+ var BROKER_SENDER = "agent-chat";
869
+ var AGENT_LIFECYCLE = "agent_lifecycle";
870
+ var UNNAMED = "unnamed";
871
+ var count2 = z2.number().int().nonnegative();
872
+ var costFields = { episodes: count2, requests: count2, costUsd: z2.number(), noActionEpisodes: count2, noActionCostUsd: z2.number() };
873
+ var fromSchema = z2.object({ key: z2.enum(WAKE_FROM_KINDS), ...costFields });
874
+ var causeSchema = z2.object({
875
+ key: z2.string(),
876
+ ...costFields,
877
+ midLoopEpisodes: count2,
878
+ requestsPerEpisode: z2.number(),
879
+ costPerEpisode: z2.number(),
880
+ byFrom: z2.array(fromSchema)
881
+ });
882
+ var pairSchema = z2.object({ from: z2.string(), fromKind: z2.enum(WAKE_FROM_KINDS), to: z2.string(), ...costFields });
883
+ var wakeEpisodesSchema = z2.object({
884
+ roles: z2.array(z2.string()),
885
+ noActionClasses: z2.array(z2.enum(ACTION_CLASSES)),
886
+ ...costFields,
887
+ /** Requests in the window that follow no event in it: the work an earlier arrival started. */
888
+ unattributed: z2.object({ requests: count2, costUsd: z2.number() }),
889
+ byCause: z2.array(causeSchema),
890
+ /** Sender by receiver; a seat is named, every other sender is its kind. */
891
+ pairs: z2.array(pairSchema)
892
+ });
893
+ function episodeNames(rows) {
894
+ const isSeat = (row) => row.originKind !== "spawned" || roleFromProfile(row.profile) === "coordinator";
895
+ return {
896
+ seats: new Set(rows.filter(isSeat).map((row) => row.agentName)),
897
+ receivers: new Map(rows.map((row) => [row.sessionId, row.agentName]))
898
+ };
899
+ }
900
+ function episodeCause(cause) {
901
+ return cause === "channel_system" ? AGENT_LIFECYCLE : cause;
902
+ }
903
+ function fromKindOf(event, seats) {
904
+ if (!event.cause.startsWith("channel_") || event.fromName === null) return "none";
905
+ if (event.fromName === BROKER_SENDER) return "broker";
906
+ if (event.broadcast) return "broadcast";
907
+ return seats.has(event.fromName) ? "seat" : "agent";
908
+ }
909
+ function buildWakeEpisodes(events, requests, names, noActionClasses) {
910
+ const byEvent = groupBy2(requests, (request) => request.eventKey ?? "");
911
+ return events.map((event) => {
912
+ const members = byEvent.get(event.key) ?? [];
913
+ const fromKind = fromKindOf(event, names.seats);
914
+ return {
915
+ key: event.key,
916
+ cause: episodeCause(event.cause),
917
+ midLoop: event.delivery === "mid_loop",
918
+ fromKind,
919
+ from: pairSender(fromKind, event.fromName, names.seats),
920
+ to: names.receivers.get(event.sessionId) ?? UNNAMED,
921
+ requests: members.length,
922
+ costUsd: members.reduce((sum, row) => sum + row.costUsd, 0),
923
+ noAction: members.every((row) => noActionClasses.includes(row.action))
924
+ };
925
+ });
926
+ }
927
+ function pairSender(kind, name, seats) {
928
+ if (kind === "seat" || kind === "broadcast" && name !== null && seats.has(name)) return name;
929
+ return kind === "broadcast" ? "agent" : kind;
930
+ }
931
+ function summarizeWakeEpisodes(episodes, requests, roles, noActionClasses) {
932
+ const keys = new Set(episodes.map((episode) => episode.key));
933
+ const unattributed = requests.filter((row) => row.eventKey === null || !keys.has(row.eventKey));
934
+ return {
935
+ roles: [...roles],
936
+ noActionClasses: [...noActionClasses],
937
+ ...costOf(episodes),
938
+ unattributed: { requests: unattributed.length, costUsd: unattributed.reduce((sum, row) => sum + row.costUsd, 0) },
939
+ byCause: causeBuckets(episodes),
940
+ pairs: pairBuckets(episodes)
941
+ };
942
+ }
943
+ function costOf(episodes) {
944
+ const noAction = episodes.filter((episode) => episode.noAction);
945
+ const sum = (list, pick) => list.reduce((total, episode) => total + pick(episode), 0);
946
+ return {
947
+ episodes: episodes.length,
948
+ requests: sum(episodes, (episode) => episode.requests),
949
+ costUsd: sum(episodes, (episode) => episode.costUsd),
950
+ noActionEpisodes: noAction.length,
951
+ noActionCostUsd: sum(noAction, (episode) => episode.costUsd)
952
+ };
953
+ }
954
+ function causeBuckets(episodes) {
955
+ return [...groupBy2(episodes, (episode) => episode.cause)].map(([key, members]) => {
956
+ const cost = costOf(members);
957
+ return {
958
+ key,
959
+ ...cost,
960
+ midLoopEpisodes: members.filter((episode) => episode.midLoop).length,
961
+ requestsPerEpisode: cost.requests / cost.episodes,
962
+ costPerEpisode: cost.costUsd / cost.episodes,
963
+ byFrom: [...groupBy2(members, (episode) => episode.fromKind)].map(([kind, part]) => ({ key: kind, ...costOf(part) })).sort(byCostThenKey)
964
+ };
965
+ }).sort(byCostThenKey);
966
+ }
967
+ function pairBuckets(episodes) {
968
+ const sent = episodes.filter((episode) => episode.fromKind !== "none");
969
+ return [...groupBy2(sent, (episode) => JSON.stringify([episode.from, episode.fromKind, episode.to]))].map(([key, members]) => {
970
+ const [from, fromKind, to] = JSON.parse(key);
971
+ return { from, fromKind, to, ...costOf(members) };
972
+ }).sort((a, b) => b.costUsd - a.costUsd || a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
973
+ }
974
+ function groupBy2(rows, keyOf) {
975
+ const groups = /* @__PURE__ */ new Map();
976
+ for (const row of rows) {
977
+ const key = keyOf(row);
978
+ const members = groups.get(key);
979
+ if (members) members.push(row);
980
+ else groups.set(key, [row]);
981
+ }
982
+ return groups;
983
+ }
984
+ function byCostThenKey(a, b) {
985
+ return b.costUsd - a.costUsd || a.key.localeCompare(b.key);
986
+ }
987
+
463
988
  // src/cost-report.ts
464
- var count = z.number().int().nonnegative();
465
- var bucketSchema = z.object({ key: z.string(), requests: count, sessions: count, costUsd: z.number() });
466
- var wakePartSchema = bucketSchema.extend({ midLoopRequests: count, midLoopCostUsd: z.number() });
467
- var cellSchema = z.object({ wakeCause: z.string(), gapBand: z.string(), requests: count, costUsd: z.number() });
989
+ var count3 = z3.number().int().nonnegative();
990
+ var bucketSchema = z3.object({ key: z3.string(), requests: count3, sessions: count3, costUsd: z3.number() });
991
+ var wakePartSchema = bucketSchema.extend({ midLoopRequests: count3, midLoopCostUsd: z3.number() });
992
+ var cellSchema = z3.object({ wakeCause: z3.string(), gapBand: z3.string(), requests: count3, costUsd: z3.number() });
993
+ var fraction = z3.number().min(0).max(1);
994
+ var roleShareSchema = z3.object({ role: z3.string(), costUsd: z3.number(), share: fraction });
468
995
  var TOKEN_CLASSES = ["input", "cache_read", "cache_write_5m", "cache_write_1h", "output"];
469
- var costReportSchema = z.object({
470
- window: z.object({ since: z.string().nullable(), until: z.string().nullable() }),
471
- priceTableVersion: z.number().int().nullable(),
472
- totals: z.object({ requests: count, sessions: count, costUsd: z.number(), unpricedRequests: count }),
473
- byTokenClass: z.array(z.object({ tokenClass: z.enum(TOKEN_CLASSES), tokens: count, costUsd: z.number() })),
474
- byAccount: z.array(bucketSchema),
475
- byModel: z.array(bucketSchema),
476
- byClass: z.array(bucketSchema),
477
- byRole: z.array(bucketSchema),
996
+ var costReportSchema = z3.object({
997
+ window: z3.object({ since: z3.string().nullable(), until: z3.string().nullable() }),
998
+ priceTableVersion: z3.number().int().nullable(),
999
+ totals: z3.object({ requests: count3, sessions: count3, costUsd: z3.number(), unpricedRequests: count3 }),
1000
+ byTokenClass: z3.array(z3.object({ tokenClass: z3.enum(TOKEN_CLASSES), tokens: count3, costUsd: z3.number() })),
1001
+ byAccount: z3.array(bucketSchema),
1002
+ byModel: z3.array(bucketSchema),
1003
+ byClass: z3.array(bucketSchema),
1004
+ byRole: z3.array(bucketSchema),
1005
+ /** Per role, cost by the action class of each request, from the tool calls it issued. */
1006
+ byAction: z3.array(z3.object({ role: z3.string(), buckets: z3.array(bucketSchema) })),
1007
+ /** Cost of the mechanical action classes over the window total; each role's share is over that role's cost. */
1008
+ mechanicalShare: z3.object({ classes: z3.array(z3.enum(ACTION_CLASSES)), costUsd: z3.number(), share: fraction, byRole: z3.array(roleShareSchema) }),
478
1009
  /** Sessions by how many episodes their class's heuristic cut them into; `none` when not yet segmented. */
479
- byEpisodeCount: z.array(bucketSchema),
480
- byInitiative: z.array(bucketSchema),
481
- byContextBand: z.array(bucketSchema),
482
- byWakeCause: z.array(wakePartSchema.extend({ parts: z.array(wakePartSchema) })),
483
- wakeCauseByGapBand: z.array(cellSchema),
484
- coldRebuild: z.object({ requests: count, costUsd: z.number(), byGapBandAndCause: z.array(cellSchema) }),
485
- compactions: z.object({ total: count, manual: count, auto: count, midLoop: count, droppedTokens: count }),
486
- topSessions: z.array(
487
- z.object({
488
- sessionId: z.string(),
489
- sessionClass: z.string(),
490
- role: z.string(),
491
- initiative: z.string(),
492
- account: z.string(),
493
- requests: count,
494
- costUsd: z.number()
1010
+ byEpisodeCount: z3.array(bucketSchema),
1011
+ byInitiative: z3.array(bucketSchema),
1012
+ byContextBand: z3.array(bucketSchema),
1013
+ byWakeCause: z3.array(wakePartSchema.extend({ parts: z3.array(wakePartSchema) })),
1014
+ /** Per wake episode of the episode roles: an arrival and the requests up to the next one, mid-loop deliveries included. */
1015
+ wakeEpisodes: wakeEpisodesSchema,
1016
+ /** Per role and model, the handoff threshold K that minimises boot plus cache-read cost per request. */
1017
+ handoffThreshold: handoffThresholdSchema,
1018
+ wakeCauseByGapBand: z3.array(cellSchema),
1019
+ coldRebuild: z3.object({ requests: count3, costUsd: z3.number(), byGapBandAndCause: z3.array(cellSchema) }),
1020
+ compactions: z3.object({ total: count3, manual: count3, auto: count3, midLoop: count3, droppedTokens: count3 }),
1021
+ topSessions: z3.array(
1022
+ z3.object({
1023
+ sessionId: z3.string(),
1024
+ sessionClass: z3.string(),
1025
+ role: z3.string(),
1026
+ initiative: z3.string(),
1027
+ account: z3.string(),
1028
+ requests: count3,
1029
+ costUsd: z3.number()
495
1030
  })
496
1031
  ),
497
- unpricedModels: z.array(z.object({ model: z.string(), requests: count, tokens: count })),
498
- coverage: z.object({ transcriptsIndexed: count, transcriptsDiscovered: count.nullable(), facetBacklog: count })
1032
+ unpricedModels: z3.array(z3.object({ model: z3.string(), requests: count3, tokens: count3 })),
1033
+ coverage: z3.object({ transcriptsIndexed: count3, transcriptsDiscovered: count3.nullable(), facetBacklog: count3 })
499
1034
  });
500
1035
  var HUMAN_CAUSES = ["human_typed", "ask_user_answer"];
501
1036
  var NONE = "none";
@@ -504,17 +1039,18 @@ var EPISODE_COUNT_CAP = 5;
504
1039
  var DAY_MS = 864e5;
505
1040
  function costReport(db, options = {}) {
506
1041
  const window = resolveWindow(options);
507
- const costRows = readCostRows(db, window);
508
- const sessionIds = [...new Set(costRows.map((row) => row.sessionId))];
509
- const tags = tagSessions(readSessionContexts(db, sessionIds));
510
- const rows = costRows.map((row) => ({ ...row, ...tags.get(row.sessionId) }));
1042
+ const rows = readTaggedRows(db, window, options.actionRules ?? DEFAULT_ACTION_RULES).filter(scopeFilter(db, options.scope));
511
1043
  return {
512
1044
  window,
513
1045
  priceTableVersion: readPriceTableVersion(db),
514
- totals: { ...sumOf(rows), sessions: sessionIds.length, unpricedRequests: rows.filter((row) => !row.priced).length },
1046
+ totals: { ...sumOf(rows), sessions: new Set(rows.map((row) => row.sessionId)).size, unpricedRequests: rows.filter((row) => !row.priced).length },
515
1047
  byTokenClass: tokenClasses(rows),
516
1048
  ...dimensionBuckets(rows),
1049
+ byAction: roleActions(rows),
1050
+ mechanicalShare: mechanicalShare(rows, options.mechanicalClasses ?? DEFAULT_MECHANICAL_CLASSES),
517
1051
  byWakeCause: wakeCauses(rows),
1052
+ wakeEpisodes: wakeEpisodes(db, window, rows, options),
1053
+ handoffThreshold: handoff(db, window, rows, options),
518
1054
  wakeCauseByGapBand: gapCells(rows),
519
1055
  coldRebuild: coldRebuild(rows),
520
1056
  compactions: compactionCounts(readCompactions(db, window)),
@@ -523,6 +1059,15 @@ function costReport(db, options = {}) {
523
1059
  coverage: { ...readCoverage(db, options.facetVersion ?? null), transcriptsDiscovered: options.transcriptsDiscovered ?? null }
524
1060
  };
525
1061
  }
1062
+ function readTaggedRows(db, window, rules) {
1063
+ const costRows = readCostRows(db, window);
1064
+ const tags = tagSessions(readSessionContexts(db, [...new Set(costRows.map((row) => row.sessionId))]));
1065
+ const calls = readRequestToolCalls(db, window);
1066
+ return costRows.map((row) => {
1067
+ const requestCalls = calls.get(row.requestId) ?? [];
1068
+ return { ...row, ...tags.get(row.sessionId), action: classifyRequest(requestCalls, rules), bootAction: isBootAction(requestCalls) };
1069
+ });
1070
+ }
526
1071
  function resolveWindow(options) {
527
1072
  const until = options.until ?? null;
528
1073
  if (options.since !== void 0) return { since: options.since, until };
@@ -577,9 +1122,24 @@ function dimensionBuckets(rows) {
577
1122
  byContextBand: bucketsBy(rows, (row) => row.contextBand)
578
1123
  };
579
1124
  }
1125
+ function roleActions(rows) {
1126
+ return [...groupRows(rows, (row) => row.role)].map(([role, members]) => ({ key: role, costUsd: sumOf(members).costUsd, buckets: bucketsBy(members, (row) => row.action) })).sort(byCostThenKey2).map(({ key, buckets: buckets2 }) => ({ role: key, buckets: buckets2 }));
1127
+ }
1128
+ function mechanicalShare(rows, classes) {
1129
+ const isMechanical = (row) => classes.includes(row.action);
1130
+ const shareOf = (members) => {
1131
+ const costUsd = sumOf(members.filter(isMechanical)).costUsd;
1132
+ return { costUsd, share: ratio(costUsd, sumOf(members).costUsd) };
1133
+ };
1134
+ const byRole = [...groupRows(rows, (row) => row.role)].map(([role, members]) => ({ role, ...shareOf(members) })).sort((a, b) => b.costUsd - a.costUsd || a.role.localeCompare(b.role));
1135
+ return { classes: [...classes], ...shareOf(rows), byRole };
1136
+ }
1137
+ function ratio(part, whole) {
1138
+ return whole > 0 ? Math.min(part / whole, 1) : 0;
1139
+ }
580
1140
  function bucketsBy(rows, keyOf) {
581
1141
  const groups = groupRows(rows, keyOf);
582
- return [...groups].map(([key, members]) => ({ key, ...sumOf(members), sessions: new Set(members.map((row) => row.sessionId)).size })).sort(byCostThenKey);
1142
+ return [...groups].map(([key, members]) => ({ key, ...sumOf(members), sessions: new Set(members.map((row) => row.sessionId)).size })).sort(byCostThenKey2);
583
1143
  }
584
1144
  function groupRows(rows, keyOf) {
585
1145
  const groups = /* @__PURE__ */ new Map();
@@ -591,7 +1151,7 @@ function groupRows(rows, keyOf) {
591
1151
  }
592
1152
  return groups;
593
1153
  }
594
- function byCostThenKey(a, b) {
1154
+ function byCostThenKey2(a, b) {
595
1155
  return b.costUsd - a.costUsd || a.key.localeCompare(b.key);
596
1156
  }
597
1157
  function wakeCauses(rows) {
@@ -599,8 +1159,32 @@ function wakeCauses(rows) {
599
1159
  const groupOf = (row) => HUMAN_CAUSES.includes(causeOf(row)) ? "human" : causeOf(row);
600
1160
  return [...groupRows(rows, groupOf)].map(([key, members]) => ({
601
1161
  ...wakePart(key, members),
602
- parts: key === "human" ? [...groupRows(members, causeOf)].map(([cause, part]) => wakePart(cause, part)).sort(byCostThenKey) : []
603
- })).sort(byCostThenKey);
1162
+ parts: key === "human" ? [...groupRows(members, causeOf)].map(([cause, part]) => wakePart(cause, part)).sort(byCostThenKey2) : []
1163
+ })).sort(byCostThenKey2);
1164
+ }
1165
+ function wakeEpisodes(db, window, rows, options) {
1166
+ const roles = options.episodeRoles ?? DEFAULT_EPISODE_ROLES;
1167
+ const noActionClasses = options.noActionClasses ?? DEFAULT_NO_ACTION_CLASSES;
1168
+ const members = rows.filter((row) => roles.includes(row.role));
1169
+ const sessionIds = [...new Set(members.map((row) => row.sessionId))];
1170
+ const eventOf = readRequestEvents(db, window, sessionIds);
1171
+ const requests = members.map((row) => ({ costUsd: row.costUsd, action: row.action, eventKey: eventOf.get(row.requestId) ?? null }));
1172
+ const episodes = buildWakeEpisodes(readWakeEvents(db, window, sessionIds), requests, episodeNames(readAgentNames(db)), noActionClasses);
1173
+ return summarizeWakeEpisodes(episodes, requests, roles, noActionClasses);
1174
+ }
1175
+ function handoff(db, window, rows, options) {
1176
+ const inWindow = (ts) => (window.since === null || ts >= window.since) && (window.until === null || ts < window.until);
1177
+ const teleports = parseTeleportEvents(options.brokerLogLines?.() ?? []).filter((event) => inWindow(event.ts));
1178
+ const requests = rows.filter((row) => !row.isSidechain).map((row) => ({
1179
+ sessionId: row.sessionId,
1180
+ role: row.role,
1181
+ model: row.model,
1182
+ ts: row.ts,
1183
+ fill: row.contextTokens,
1184
+ tokens: { inputTokens: row.inputTokens, cacheReadTokens: row.cacheReadTokens, cacheCreation5mTokens: row.cacheWrite5mTokens, cacheCreation1hTokens: row.cacheWrite1hTokens, outputTokens: row.outputTokens },
1185
+ bootAction: row.bootAction
1186
+ }));
1187
+ return handoffThreshold(requests, teleports, readAgentSessions(db), options.handoff);
604
1188
  }
605
1189
  function wakePart(key, rows) {
606
1190
  const midLoop = rows.filter((row) => row.wakeDelivery === "mid_loop");
@@ -643,6 +1227,7 @@ function tokensOf(row) {
643
1227
 
644
1228
  // src/render-text.ts
645
1229
  var LIST_PRICE_CAVEAT = "List prices, not a bill: the accounts behind these sessions may run on subscription plans. Rankings hold; the absolute figure is not what you paid.";
1230
+ var TOP_WAKE_ROWS = 10;
646
1231
  function renderCostReportText(report) {
647
1232
  const sections = [
648
1233
  header(report),
@@ -651,10 +1236,17 @@ function renderCostReportText(report) {
651
1236
  bucketTable("By model", report.byModel, report.totals.costUsd),
652
1237
  bucketTable("By class", report.byClass, report.totals.costUsd),
653
1238
  bucketTable("By role", report.byRole, report.totals.costUsd),
1239
+ actionTable(report.byAction),
1240
+ mechanicalTable(report.mechanicalShare),
654
1241
  bucketTable("By episode count", report.byEpisodeCount, report.totals.costUsd),
655
1242
  bucketTable("By initiative", report.byInitiative, report.totals.costUsd),
656
1243
  bucketTable("By context band", report.byContextBand, report.totals.costUsd),
657
1244
  wakeCauseTable(report),
1245
+ episodeTable(report.wakeEpisodes),
1246
+ pairTable(report.wakeEpisodes),
1247
+ handoffTable(report.handoffThreshold),
1248
+ teleportTable(report.handoffThreshold),
1249
+ reviewerTable(report.handoffThreshold),
658
1250
  cellTable("Wake cause by gap band", report.wakeCauseByGapBand),
659
1251
  cellTable(`Cold rebuilds: ${report.coldRebuild.requests} requests, ${usd(report.coldRebuild.costUsd)}`, report.coldRebuild.byGapBandAndCause),
660
1252
  compactionTable(report.compactions),
@@ -664,16 +1256,43 @@ function renderCostReportText(report) {
664
1256
  ];
665
1257
  return sections.join("\n\n") + "\n";
666
1258
  }
1259
+ var COST_REPORT_SECTIONS = {
1260
+ byRole: (report) => bucketTable("By role", report.byRole, report.totals.costUsd),
1261
+ byAction: (report) => actionTable(report.byAction),
1262
+ mechanicalShare: (report) => mechanicalTable(report.mechanicalShare),
1263
+ byWakeCause: (report) => wakeCauseTable(report),
1264
+ wakeEpisodes: (report) => episodeTable(report.wakeEpisodes),
1265
+ wakePairs: (report) => pairTable(report.wakeEpisodes),
1266
+ handoff: (report) => handoffTable(report.handoffThreshold),
1267
+ teleports: (report) => teleportTable(report.handoffThreshold),
1268
+ reviewers: (report) => reviewerTable(report.handoffThreshold)
1269
+ };
1270
+ function renderCostReportSections(report, sections) {
1271
+ const body = sections.map((section) => COST_REPORT_SECTIONS[section](report));
1272
+ return [header(report), ...body, [LIST_PRICE_CAVEAT, footer(report)].join("\n")].join("\n\n") + "\n";
1273
+ }
667
1274
  function header(report) {
668
1275
  const { since, until } = report.window;
669
1276
  const { requests, sessions, costUsd } = report.totals;
670
1277
  return `Cost report, ${since ?? "beginning"} to ${until ?? "now"}
671
1278
  Total ${usd(costUsd)} over ${requests} requests in ${sessions} sessions`;
672
1279
  }
673
- function bucketTable(title, buckets, total) {
674
- const rows = buckets.map((b) => [b.key, b.requests, b.sessions, usd(b.costUsd), share(b.costUsd, total)]);
1280
+ function bucketTable(title, buckets2, total) {
1281
+ const rows = buckets2.map((b) => [b.key, b.requests, b.sessions, usd(b.costUsd), share(b.costUsd, total)]);
675
1282
  return table(title, ["key", "requests", "sessions", "cost", "share"], rows);
676
1283
  }
1284
+ function actionTable(roles) {
1285
+ const rows = roles.flatMap(({ role, buckets: buckets2 }) => {
1286
+ const total = buckets2.reduce((sum, b) => sum + b.costUsd, 0);
1287
+ const requests = buckets2.reduce((sum, b) => sum + b.requests, 0);
1288
+ return [[role, requests, usd(total), share(total, total)], ...buckets2.map((b) => [` ${b.key}`, b.requests, usd(b.costUsd), share(b.costUsd, total)])];
1289
+ });
1290
+ return table("By action per role", ["role / action", "requests", "cost", "share"], rows);
1291
+ }
1292
+ function mechanicalTable(m) {
1293
+ const title = `Mechanical share: ${percent(m.share)} (${usd(m.costUsd)}) of total; classes ${m.classes.join(", ") || "none"}`;
1294
+ return table(title, ["role", "mechanical cost", "share of role"], m.byRole.map((r) => [r.role, usd(r.costUsd), percent(r.share)]));
1295
+ }
677
1296
  function wakeCauseTable(report) {
678
1297
  const rows = report.byWakeCause.flatMap((group) => [
679
1298
  [group.key, group.requests, usd(group.costUsd), group.midLoopRequests, usd(group.midLoopCostUsd)],
@@ -681,6 +1300,56 @@ function wakeCauseTable(report) {
681
1300
  ]);
682
1301
  return table("By wake cause", ["cause", "requests", "cost", "mid-loop", "mid-loop cost"], rows);
683
1302
  }
1303
+ function episodeTable(w) {
1304
+ const title = `Wake episodes of ${w.roles.join(", ")}: ${w.episodes} episodes, ${w.requests} requests, ${usd(w.costUsd)}; no-action means only ${w.noActionClasses.join(", ")}`;
1305
+ const causes = [...w.byCause].sort((a, b) => b.costPerEpisode - a.costPerEpisode || a.key.localeCompare(b.key)).slice(0, TOP_WAKE_ROWS);
1306
+ const rows = causes.flatMap((c) => [
1307
+ [c.key, c.episodes, c.midLoopEpisodes, c.requests, c.requestsPerEpisode.toFixed(1), usd(c.costUsd), usd(c.costPerEpisode), noActionCell(c)],
1308
+ ...c.byFrom.map((f) => [` from ${f.key}`, f.episodes, "", f.requests, (f.requests / f.episodes).toFixed(1), usd(f.costUsd), usd(f.costUsd / f.episodes), noActionCell(f)])
1309
+ ]);
1310
+ return table(title, ["cause", "episodes", "mid-loop", "requests", "req/ep", "cost", "cost/ep", "no-action"], rows);
1311
+ }
1312
+ function noActionCell(bucket) {
1313
+ return `${bucket.noActionEpisodes} (${share(bucket.noActionEpisodes, bucket.episodes)})`;
1314
+ }
1315
+ function pairTable(w) {
1316
+ const rows = w.pairs.slice(0, TOP_WAKE_ROWS).map((p) => [p.from, p.fromKind, p.to, p.episodes, p.requests, usd(p.costUsd), noActionCell(p)]);
1317
+ return table("Wake senders by receiver", ["from", "kind", "to", "episodes", "requests", "cost", "no-action"], rows);
1318
+ }
1319
+ function handoffTable(h) {
1320
+ const title = `Handoff threshold per role, priced at package table v${PRICE_TABLE_VERSION}: best K over ${kilo(h.sweep.fromK)}-${kilo(h.sweep.toK)}, half-boot K in brackets`;
1321
+ const head = ["role", "model", "sessions", "boot", "boot fill", "growth/req", "read $/MTok", "best K", "$/req", ...h.configuredK.map((k) => `+$/req @${kilo(k)}`)];
1322
+ const rows = h.cohorts.map((c) => [
1323
+ c.role,
1324
+ c.model,
1325
+ c.sessions,
1326
+ usd(c.bootCostUsd),
1327
+ kilo(c.bootFill),
1328
+ Math.round(c.growthPerRequest),
1329
+ c.readPricePerMTok.toFixed(3),
1330
+ `${kiloOrDash(c.bestK)} (${kiloOrDash(c.halfBoot.bestK)})`,
1331
+ c.bestCostPerRequest === null ? "-" : `$${c.bestCostPerRequest.toFixed(4)}`,
1332
+ ...c.atConfigured.map((at) => at.deltaPerRequest === null ? "-" : `$${at.deltaPerRequest.toFixed(4)}`)
1333
+ ]);
1334
+ return table(title, head, rows);
1335
+ }
1336
+ function teleportTable(h) {
1337
+ const title = `Teleport exit fill: ${h.teleports.length} matched, ${h.unmatchedTeleports} unmatched; the latest ${TOP_WAKE_ROWS}`;
1338
+ const head = ["ts", "name", "role", "exit fill", "best K", ...h.configuredK.map((k) => `over ${kilo(k)}`)];
1339
+ const rows = h.teleports.slice(-TOP_WAKE_ROWS).map((t) => [t.ts, t.name ?? "-", t.role, kilo(t.exitFill), kiloOrDash(t.bestK), ...t.overConfigured.map((o) => kilo(o.over))]);
1340
+ return table(title, head, rows);
1341
+ }
1342
+ function reviewerTable(h) {
1343
+ const row = (kind) => (c) => [kind, c.role, c.model, c.sessions, c.requestsPerPr.toFixed(1), c.requestsFrom, usd(c.costUsd)];
1344
+ const rows = [...h.reviewers.fresh.map(row("fresh per PR")), ...h.reviewers.standing.map(row("standing"))];
1345
+ return table(`Reviewers over ${h.reviewers.prs} PRs, boot and reads: fresh per PR against one standing`, ["reviewer", "role", "model", "sessions", "req/PR", "req/PR from", "cost"], rows);
1346
+ }
1347
+ function kilo(tokens) {
1348
+ return `${Math.round(tokens / 1e3)}k`;
1349
+ }
1350
+ function kiloOrDash(tokens) {
1351
+ return tokens === null ? "-" : kilo(tokens);
1352
+ }
684
1353
  function cellTable(title, cells) {
685
1354
  return table(title, ["cause", "gap", "requests", "cost"], cells.map((c) => [c.wakeCause, c.gapBand, c.requests, usd(c.costUsd)]));
686
1355
  }
@@ -718,42 +1387,551 @@ function usd(value) {
718
1387
  return `$${value.toFixed(2)}`;
719
1388
  }
720
1389
  function share(value, total) {
721
- return total > 0 ? `${(value / total * 100).toFixed(1)}%` : "-";
1390
+ return total > 0 ? percent(value / total) : "-";
1391
+ }
1392
+ function percent(fraction2) {
1393
+ return `${(fraction2 * 100).toFixed(1)}%`;
1394
+ }
1395
+
1396
+ // src/cache-ttl.ts
1397
+ import { z as z4 } from "zod";
1398
+ var FIVE_MINUTES_MS = 5 * 6e4;
1399
+ var MTOK2 = 1e6;
1400
+ var NO_PROFILE = "none";
1401
+ var REBUILD_GAP_BANDS = GAP_BANDS.filter((band) => band.lo >= FIVE_MINUTES_MS).map((band) => band.name);
1402
+ var count4 = z4.number().int().nonnegative();
1403
+ var effectFields = {
1404
+ sessions: count4,
1405
+ requests: count4,
1406
+ write1hTokens: count4,
1407
+ /** 1h writes repriced at the 5m write rate. */
1408
+ repriceSavingUsd: z4.number(),
1409
+ rebuilds: count4,
1410
+ rebuildTokens: count4,
1411
+ /** Each rebuild writes at the 5m rate what the 1h cache still served as a read. */
1412
+ rebuildCostUsd: z4.number(),
1413
+ netSavingUsd: z4.number(),
1414
+ netPerSessionUsd: z4.number()
1415
+ };
1416
+ var bucketSchema2 = z4.object({ key: z4.string(), ...effectFields, fiveMinuteLoses: z4.boolean() });
1417
+ var cacheTtlWhatIfSchema = z4.object({
1418
+ window: z4.object({ since: z4.string().nullable(), until: z4.string().nullable() }).optional(),
1419
+ priceTableVersion: z4.number().int(),
1420
+ rebuildGapBands: z4.array(z4.string()),
1421
+ totals: z4.object(effectFields),
1422
+ unpricedRequests: count4,
1423
+ byRole: z4.array(bucketSchema2),
1424
+ byProfile: z4.array(bucketSchema2),
1425
+ /** Roles whose rebuilds after long gaps outweigh the cheaper writes. */
1426
+ lossRoles: z4.array(z4.string())
1427
+ });
1428
+ function cacheTtlWhatIf(rows, prices = PRICE_TABLE) {
1429
+ const effects = /* @__PURE__ */ new Map();
1430
+ for (const row of rows) {
1431
+ const price = findPrice(row.model, row.ts, prices);
1432
+ if (price) effects.set(row, requestEffect(row, price));
1433
+ }
1434
+ const priced = [...effects.keys()];
1435
+ const byRole = buckets(priced, effects, (row) => row.role);
1436
+ return {
1437
+ priceTableVersion: PRICE_TABLE_VERSION,
1438
+ rebuildGapBands: [...REBUILD_GAP_BANDS],
1439
+ totals: sumEffects([...effects.values()]),
1440
+ unpricedRequests: rows.length - priced.length,
1441
+ byRole,
1442
+ byProfile: buckets(priced, effects, (row) => row.profile),
1443
+ lossRoles: byRole.filter((bucket) => bucket.fiveMinuteLoses).map((bucket) => bucket.key)
1444
+ };
1445
+ }
1446
+ function requestEffect(row, price) {
1447
+ const band = row.gapMs === null ? null : gapBand(row.gapMs);
1448
+ const rebuildTokens = band !== null && REBUILD_GAP_BANDS.includes(band) ? row.cacheReadTokens : 0;
1449
+ return {
1450
+ sessionId: row.sessionId,
1451
+ write1hTokens: row.cacheWrite1hTokens,
1452
+ repriceSavingUsd: row.cacheWrite1hTokens * (price.cacheWrite1h - price.cacheWrite5m) / MTOK2,
1453
+ rebuildTokens,
1454
+ rebuildCostUsd: rebuildTokens * (price.cacheWrite5m - price.cacheRead) / MTOK2
1455
+ };
1456
+ }
1457
+ function sumEffects(effects) {
1458
+ const sum = (pick) => effects.reduce((total, effect) => total + pick(effect), 0);
1459
+ const sessions = new Set(effects.map((effect) => effect.sessionId)).size;
1460
+ const netSavingUsd = sum((effect) => effect.repriceSavingUsd - effect.rebuildCostUsd);
1461
+ return {
1462
+ sessions,
1463
+ requests: effects.length,
1464
+ write1hTokens: sum((effect) => effect.write1hTokens),
1465
+ repriceSavingUsd: sum((effect) => effect.repriceSavingUsd),
1466
+ rebuilds: effects.filter((effect) => effect.rebuildTokens > 0).length,
1467
+ rebuildTokens: sum((effect) => effect.rebuildTokens),
1468
+ rebuildCostUsd: sum((effect) => effect.rebuildCostUsd),
1469
+ netSavingUsd,
1470
+ netPerSessionUsd: sessions > 0 ? netSavingUsd / sessions : 0
1471
+ };
1472
+ }
1473
+ function buckets(rows, effects, keyOf) {
1474
+ const groups = /* @__PURE__ */ new Map();
1475
+ for (const row of rows) {
1476
+ const key = keyOf(row);
1477
+ const members = groups.get(key);
1478
+ if (members) members.push(effects.get(row));
1479
+ else groups.set(key, [effects.get(row)]);
1480
+ }
1481
+ return [...groups].map(([key, members]) => {
1482
+ const totals = sumEffects(members);
1483
+ return { key, ...totals, fiveMinuteLoses: totals.netSavingUsd < 0 };
1484
+ }).sort((a, b) => b.netSavingUsd - a.netSavingUsd || a.key.localeCompare(b.key));
1485
+ }
1486
+ var IN_WINDOW2 = "(@since IS NULL OR ts >= @since) AND (@until IS NULL OR ts < @until)";
1487
+ var TTL_ROWS = `
1488
+ WITH member AS (SELECT DISTINCT session_id FROM request_dedup WHERE ${IN_WINDOW2}),
1489
+ timed AS (
1490
+ SELECT session_id, model, ts, cache_read_tokens, cache_creation_1h, gap_ms,
1491
+ LAG(ts) OVER (PARTITION BY session_id, is_sidechain ORDER BY ts, request_id) AS previous_ts
1492
+ FROM request_dedup WHERE session_id IN member
1493
+ )
1494
+ SELECT session_id AS sessionId, model, ts, cache_read_tokens AS cacheReadTokens, cache_creation_1h AS cacheWrite1hTokens,
1495
+ COALESCE(gap_ms, CAST(ROUND((julianday(ts) - julianday(previous_ts)) * 86400000) AS INTEGER)) AS gapMs
1496
+ FROM timed WHERE ${IN_WINDOW2}`;
1497
+ function cacheTtlReport(db, options = {}, prices = PRICE_TABLE) {
1498
+ const window = resolveWindow(options);
1499
+ return { window, ...cacheTtlWhatIf(readTtlRows(db, window).filter(scopeFilter(db, options.scope)), prices) };
1500
+ }
1501
+ function readTtlRows(db, window) {
1502
+ const rows = db.prepare(TTL_ROWS).all(window);
1503
+ const contexts = readSessionContexts(db, [...new Set(rows.map((row) => row.sessionId))]);
1504
+ const tags = tagSessions(contexts);
1505
+ return rows.map((row) => ({
1506
+ ...row,
1507
+ role: tags.get(row.sessionId).role,
1508
+ profile: contexts.get(row.sessionId).facts.origin?.profile ?? NO_PROFILE
1509
+ }));
1510
+ }
1511
+ function renderCacheTtlText(whatIf) {
1512
+ const { totals, window } = whatIf;
1513
+ const span = window ? `, ${window.since ?? "beginning"} to ${window.until ?? "now"}` : "";
1514
+ const header2 = [
1515
+ `Cache TTL what-if, 5m against 1h${span}`,
1516
+ `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
+ `Roles where 5m loses: ${whatIf.lossRoles.join(", ") || "none"}`
1518
+ ].join("\n");
1519
+ const footer2 = `Price table v${whatIf.priceTableVersion}; ${whatIf.unpricedRequests} unpriced requests left out.`;
1520
+ return [header2, ttlTable("By role", whatIf.byRole), ttlTable("By profile", whatIf.byProfile), [LIST_PRICE_CAVEAT, footer2].join("\n")].join("\n\n") + "\n";
1521
+ }
1522
+ function ttlTable(title, rows) {
1523
+ const head = ["key", "sessions", "requests", "reprice saving", "rebuilds", "rebuild cost", "net", "net per session", "5m"];
1524
+ const cells = rows.map((b) => [b.key, b.sessions, b.requests, usd(b.repriceSavingUsd), b.rebuilds, usd(b.rebuildCostUsd), usd(b.netSavingUsd), usd(b.netPerSessionUsd), b.fiveMinuteLoses ? "loss" : "saves"]);
1525
+ return table(title, head, cells);
1526
+ }
1527
+
1528
+ // src/blocked-flow-merge.ts
1529
+ import { z as z5 } from "zod";
1530
+ var VERDICT_LINE = /^Verdict:\s*([A-Z_]+)/;
1531
+ var PR_LINE = /^PR:\s*(?:https:\/\/github\.com\/)?([\w.-]+\/[\w.-]+)(?:#|\/pull\/)(\d+)/m;
1532
+ var HEAD_LINE = /^Head:\s*`?([0-9a-f]{7,40})\b/m;
1533
+ function parseVerdict(body) {
1534
+ const verdict = VERDICT_LINE.exec(body)?.[1];
1535
+ const pr = PR_LINE.exec(body);
1536
+ const head = HEAD_LINE.exec(body)?.[1];
1537
+ if (!verdict || !pr || !head) return null;
1538
+ return { verdict, repo: pr[1], pr: Number(pr[2]), head };
1539
+ }
1540
+ var MINUTE_MS2 = 6e4;
1541
+ function mergeOutcomes(verdicts, pulls, asOf) {
1542
+ const byPr = /* @__PURE__ */ new Map();
1543
+ for (const v of verdicts) if (v.verdict === "MERGE") byPr.set(prKey(v), [...byPr.get(prKey(v)) ?? [], v]);
1544
+ const pullByKey = new Map(pulls.map((p) => [prKey(p), p]));
1545
+ return [...byPr.values()].map((held) => outcome(held, pullByKey.get(prKey(held[0])), asOf));
1546
+ }
1547
+ function prKey(row) {
1548
+ return `${row.repo}#${row.pr}`;
1549
+ }
1550
+ function outcome(held, pull, asOf) {
1551
+ const { repo, pr } = held[0];
1552
+ if (!pull) return { repo, pr, status: "unknown", verdict: null, minutes: null };
1553
+ const atHead = held.filter((v) => sameSha(v.head, pull.headSha)).sort((a, b) => a.at.localeCompare(b.at))[0];
1554
+ const mergedBy = pull.mergedAt !== null && pull.mergedAt <= asOf;
1555
+ if (!atHead) return { repo, pr, status: pull.mergedAt !== null || pull.state === "open" ? "stale-head" : "closed", verdict: null, minutes: null };
1556
+ if (mergedBy) return { repo, pr, status: "merged", verdict: atHead, minutes: Math.max(0, minutesBetween(atHead.at, pull.mergedAt)) };
1557
+ if (pull.state === "open" || pull.mergedAt !== null) return { repo, pr, status: "open", verdict: atHead, minutes: minutesBetween(atHead.at, asOf) };
1558
+ return { repo, pr, status: "closed", verdict: atHead, minutes: null };
1559
+ }
1560
+ function sameSha(a, b) {
1561
+ return a.startsWith(b) || b.startsWith(a);
1562
+ }
1563
+ function minutesBetween(from, to) {
1564
+ return (Date.parse(to) - Date.parse(from)) / MINUTE_MS2;
1565
+ }
1566
+ var minutes = z5.number().nullable();
1567
+ var latencyStatsSchema = z5.object({
1568
+ merged: z5.number().int().nonnegative(),
1569
+ medianMin: minutes,
1570
+ meanMin: minutes,
1571
+ p90Min: minutes,
1572
+ maxMin: minutes,
1573
+ /** Open PRs holding MERGE at their current head; their ages are lower bounds on the wait. */
1574
+ censored: z5.number().int().nonnegative(),
1575
+ oldestCensoredMin: minutes,
1576
+ censoredPrMinutes: z5.number().nonnegative(),
1577
+ /** Median over merged waits and censored ages together: a lower bound on the true median. */
1578
+ medianWithCensoredMin: minutes
1579
+ });
1580
+ function latencyStats(outcomes) {
1581
+ const merged = sorted(outcomes.filter((o) => o.status === "merged").map((o) => o.minutes));
1582
+ const censored = sorted(outcomes.filter((o) => o.status === "open").map((o) => o.minutes));
1583
+ return {
1584
+ merged: merged.length,
1585
+ medianMin: median(merged),
1586
+ meanMin: merged.length === 0 ? null : round(merged.reduce((a, b) => a + b, 0) / merged.length),
1587
+ p90Min: nearestRank(merged, 0.9),
1588
+ maxMin: merged.at(-1) === void 0 ? null : round(merged.at(-1)),
1589
+ censored: censored.length,
1590
+ oldestCensoredMin: censored.at(-1) === void 0 ? null : round(censored.at(-1)),
1591
+ censoredPrMinutes: round(censored.reduce((a, b) => a + b, 0)),
1592
+ medianWithCensoredMin: median(sorted([...merged, ...censored]))
1593
+ };
1594
+ }
1595
+ function sorted(values) {
1596
+ return values.sort((a, b) => a - b);
1597
+ }
1598
+ function median(values) {
1599
+ if (values.length === 0) return null;
1600
+ const mid = Math.floor(values.length / 2);
1601
+ return round(values.length % 2 === 1 ? values[mid] : (values[mid - 1] + values[mid]) / 2);
1602
+ }
1603
+ function nearestRank(values, p) {
1604
+ return values.length === 0 ? null : round(values[Math.ceil(p * values.length) - 1]);
1605
+ }
1606
+ function round(value) {
1607
+ return Math.round(value * 10) / 10;
1608
+ }
1609
+
1610
+ // src/blocked-flow-denials.ts
1611
+ var DENIAL = /^Permission for this action (?:was|has been) denied by the Claude Code auto mode classifier\. Reason: \[([^\]]+)\]/;
1612
+ var BASH_ACTIONS = [
1613
+ ["merge", /\bgh pr merge\b|\/pulls\/\d+\/merge\b/],
1614
+ ["retire", /\bagent[ _-]retire\b|\bagent\s+retire\b/],
1615
+ ["worktree-remove", /\bgit\b.*\bworktree (?:remove|prune)\b/],
1616
+ ["branch-delete", /\bgit branch -[dD]\b|\bpush\b.*--delete\b|\/git\/refs\/heads\/\S*.*-X DELETE|-X DELETE\b.*\/git\/refs\//],
1617
+ ["push", /\bgit\b.*\bpush\b/],
1618
+ ["github-write", /\bgh (?:pr|issue) (?:create|edit|comment|close)\b|\bgh api\b.*(?:-X|--method)\s*(?:POST|PATCH|PUT|DELETE)\b/],
1619
+ ["service-restart", /\bservice restart\b|\blaunchctl\b/],
1620
+ ["process-kill", /\b(?:pkill|kill)\b/],
1621
+ ["file-write", /\s>>?\s*(?!\/dev\/null|&)\S|\bopen\([^)]*["'][wa]["']\)|\bsed -i\b/]
1622
+ ];
1623
+ var FILE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
1624
+ function classifyDeniedAction(tool, input) {
1625
+ if (tool === "Bash") return bashAction(String(input?.command ?? ""));
1626
+ if (FILE_TOOLS.has(tool)) return "file-edit";
1627
+ if (tool.startsWith("mcp__")) return `mcp:${tool.split("__").at(-1)}`;
1628
+ return `tool:${tool}`;
1629
+ }
1630
+ function bashAction(command) {
1631
+ return BASH_ACTIONS.find(([, pattern]) => pattern.test(command))?.[0] ?? "bash-other";
1632
+ }
1633
+ function parseDenials(lines, seat) {
1634
+ const calls = /* @__PURE__ */ new Map();
1635
+ const denials = [];
1636
+ for (const line of lines) {
1637
+ if (!line.includes('"tool_use"') && !line.includes("auto mode classifier")) continue;
1638
+ const entry = parseLine(line);
1639
+ for (const block of blocksOf(entry)) {
1640
+ if (block.type === "tool_use" && block.id) calls.set(block.id, { tool: block.name ?? "unknown", input: block.input });
1641
+ const reason = block.type === "tool_result" ? DENIAL.exec(resultText(block.content))?.[1] : void 0;
1642
+ if (reason === void 0) continue;
1643
+ const call = calls.get(block.tool_use_id ?? "") ?? { tool: "unknown", input: null };
1644
+ denials.push({ seat, at: entry?.timestamp ?? "", reason, action: classifyDeniedAction(call.tool, call.input), tool: call.tool });
1645
+ }
1646
+ }
1647
+ return denials;
1648
+ }
1649
+ function parseLine(line) {
1650
+ try {
1651
+ return JSON.parse(line);
1652
+ } catch {
1653
+ return null;
1654
+ }
1655
+ }
1656
+ function blocksOf(entry) {
1657
+ const content = entry?.message?.content;
1658
+ return Array.isArray(content) ? content : [];
1659
+ }
1660
+ function resultText(content) {
1661
+ if (typeof content === "string") return content;
1662
+ if (!Array.isArray(content)) return "";
1663
+ const first = content.find((part) => part.type === "text");
1664
+ return first?.text ?? "";
1665
+ }
1666
+
1667
+ // src/blocked-flow-idle.ts
1668
+ var UNSTATED_REASON = "unstated";
1669
+ var TICK_HOLD_MAX_MIN = 60;
1670
+ var STAMP = /^(\d{1,2}):(\d{2})(?::(\d{2}))?\s/;
1671
+ var IMPL = /\bimpl(?:ementers?)?\s+(\d+)\s*\/\s*(\d+)/i;
1672
+ var REASON = /\b(?:no dispatch|dispatch 0)\s*:\s*([^.;(]+)/i;
1673
+ function parseSeatJournal(text, seat, date, utcOffsetMin) {
1674
+ const ticks = [];
1675
+ let lastAt = null;
1676
+ text.split("\n").forEach((raw, index) => {
1677
+ const stamp = STAMP.exec(raw);
1678
+ if (!stamp) return;
1679
+ const at = journalTime(date, stamp, utcOffsetMin);
1680
+ lastAt = at;
1681
+ const impl = IMPL.exec(raw);
1682
+ if (!impl) return;
1683
+ const used = Number(impl[1]);
1684
+ const cap = Number(impl[2]);
1685
+ const reason = used < cap ? REASON.exec(raw)?.[1]?.trim().toLowerCase() ?? UNSTATED_REASON : "";
1686
+ ticks.push({ seat, at, used, cap, reason, line: index + 1 });
1687
+ });
1688
+ return { ticks, lastAt };
1689
+ }
1690
+ function journalTime(date, stamp, utcOffsetMin) {
1691
+ const [year, month, day] = date.split("-").map(Number);
1692
+ const local = Date.UTC(year, month - 1, day, Number(stamp[1]), Number(stamp[2]), Number(stamp[3] ?? 0));
1693
+ return new Date(local - utcOffsetMin * 6e4).toISOString();
1694
+ }
1695
+ function idleSlotMinutes(journals, window) {
1696
+ const rows = /* @__PURE__ */ new Map();
1697
+ for (const journal of journals) {
1698
+ journal.ticks.forEach((tick, i) => {
1699
+ const free = Math.max(0, tick.cap - tick.used);
1700
+ const held = heldMinutes(tick.at, journal.ticks[i + 1]?.at ?? journal.lastAt ?? tick.at, window);
1701
+ if (free === 0 || held <= 0) return;
1702
+ const key = `${tick.seat}\0${tick.reason}`;
1703
+ const row = rows.get(key) ?? { seat: tick.seat, reason: tick.reason, slotMinutes: 0, ticks: 0, lines: [] };
1704
+ rows.set(key, { ...row, slotMinutes: row.slotMinutes + free * held, ticks: row.ticks + 1, lines: [...row.lines, tick.line] });
1705
+ });
1706
+ }
1707
+ return [...rows.values()].map((r) => ({ ...r, slotMinutes: Math.round(r.slotMinutes) })).sort((a, b) => b.slotMinutes - a.slotMinutes);
1708
+ }
1709
+ function heldMinutes(from, to, window) {
1710
+ const start = Math.max(Date.parse(from), window.since ? Date.parse(window.since) : -Infinity);
1711
+ const capped = Math.min(Date.parse(to), Date.parse(from) + TICK_HOLD_MAX_MIN * 6e4);
1712
+ const end = Math.min(capped, window.until ? Date.parse(window.until) : Infinity);
1713
+ return Math.max(0, (end - start) / 6e4);
1714
+ }
1715
+
1716
+ // src/blocked-flow.ts
1717
+ import { z as z6 } from "zod";
1718
+ var BLOCKED_FLOW_SOURCES = {
1719
+ verdicts: {
1720
+ command: `sqlite3 -readonly <events.db> "SELECT id, ts, target, body FROM events WHERE kind='message' AND body LIKE 'Verdict:%'"`,
1721
+ field: "events.ts, events.target, events.body lines Verdict:, PR:, Head:"
1722
+ },
1723
+ pulls: { command: "gh api repos/<owner>/<repo>/pulls/<n>", field: ".state, .merged_at, .head.sha" },
1724
+ denials: {
1725
+ command: "grep 'auto mode classifier. Reason: \\[' <seat transcript>.jsonl",
1726
+ field: "tool_result.content Reason: [<class>], joined to tool_use.input by tool_use_id"
1727
+ },
1728
+ journals: { command: "grep -nE 'impl [0-9]+/[0-9]+' <seat journal>.md", field: "impl <used>/<cap>, No dispatch: <reason>" }
1729
+ };
1730
+ var source = z6.enum(["verdicts", "pulls", "denials", "journals"]);
1731
+ var count5 = z6.number().int().nonnegative();
1732
+ var latencyRowSchema = z6.object({
1733
+ repo: z6.string(),
1734
+ prs: count5,
1735
+ staleHead: count5,
1736
+ closed: count5,
1737
+ unknown: count5,
1738
+ all: latencyStatsSchema,
1739
+ before: latencyStatsSchema.optional(),
1740
+ after: latencyStatsSchema.optional()
1741
+ });
1742
+ var openRowSchema = z6.object({
1743
+ repo: z6.string(),
1744
+ pr: count5,
1745
+ head: z6.string(),
1746
+ seat: z6.string(),
1747
+ verdictAt: z6.string(),
1748
+ ageMin: z6.number(),
1749
+ eventId: count5
1750
+ });
1751
+ var blockedFlowSchema = z6.object({
1752
+ asOf: z6.string(),
1753
+ window: z6.object({ since: z6.string().optional(), until: z6.string().optional() }),
1754
+ splitAt: z6.string().optional(),
1755
+ sources: z6.record(source, z6.object({ command: z6.string(), field: z6.string() })),
1756
+ verdictToMerge: z6.object({ cites: z6.array(source), rows: z6.array(latencyRowSchema) }),
1757
+ openHoldingMerge: z6.object({ cites: z6.array(source), prMinutes: z6.number(), rows: z6.array(openRowSchema) }),
1758
+ denials: z6.object({
1759
+ cites: z6.array(source),
1760
+ total: count5,
1761
+ rows: z6.array(z6.object({ reason: z6.string(), action: z6.string(), seat: z6.string(), count: count5 }))
1762
+ }),
1763
+ idleSlots: z6.object({
1764
+ cites: z6.array(source),
1765
+ holdMaxMin: z6.number(),
1766
+ total: z6.number(),
1767
+ rows: z6.array(z6.object({ seat: z6.string(), reason: z6.string(), slotMinutes: z6.number(), ticks: count5, lines: z6.array(count5) }))
1768
+ })
1769
+ });
1770
+ var ALL_REPOS = "all";
1771
+ function blockedFlowReport(input) {
1772
+ const window = input.window ?? {};
1773
+ const inScope = scopeTest(input.seats, window);
1774
+ const outcomes = mergeOutcomes(input.verdicts.filter((v) => inScope(v.seat, v.at)), input.pulls, input.asOf);
1775
+ const open = openRows(outcomes);
1776
+ const journals = input.journals.map((j) => ({ ...j, ticks: j.ticks.filter((t) => inScope(t.seat)) }));
1777
+ const idle = idleSlotMinutes(journals, window);
1778
+ return {
1779
+ asOf: input.asOf,
1780
+ window,
1781
+ ...input.splitAt ? { splitAt: input.splitAt } : {},
1782
+ sources: BLOCKED_FLOW_SOURCES,
1783
+ verdictToMerge: { cites: ["verdicts", "pulls"], rows: latencyRows(outcomes, input.splitAt) },
1784
+ openHoldingMerge: { cites: ["verdicts", "pulls"], prMinutes: round(open.reduce((sum, r) => sum + r.ageMin, 0)), rows: open },
1785
+ denials: denialSection(input.denials.filter((d) => inScope(d.seat, d.at))),
1786
+ idleSlots: { cites: ["journals"], holdMaxMin: TICK_HOLD_MAX_MIN, total: idle.reduce((sum, r) => sum + r.slotMinutes, 0), rows: idle }
1787
+ };
1788
+ }
1789
+ function scopeTest(seats, window) {
1790
+ const named = seats ? new Set(seats) : null;
1791
+ return (seat, at) => (!named || named.has(seat)) && (at === void 0 || (!window.since || at >= window.since) && (!window.until || at < window.until));
1792
+ }
1793
+ function latencyRows(outcomes, splitAt) {
1794
+ const repos = [...new Set(outcomes.map((o) => o.repo))];
1795
+ const rows = repos.map((repo) => latencyRow(repo, outcomes.filter((o) => o.repo === repo), splitAt));
1796
+ return [...rows.sort((a, b) => b.prs - a.prs || a.repo.localeCompare(b.repo)), latencyRow(ALL_REPOS, outcomes, splitAt)];
1797
+ }
1798
+ function latencyRow(repo, outcomes, splitAt) {
1799
+ const status = (s) => outcomes.filter((o) => o.status === s).length;
1800
+ const row = { repo, prs: outcomes.length, staleHead: status("stale-head"), closed: status("closed"), unknown: status("unknown"), all: latencyStats(outcomes) };
1801
+ if (!splitAt) return row;
1802
+ const timed = outcomes.filter((o) => o.verdict !== null);
1803
+ return { ...row, before: latencyStats(timed.filter((o) => o.verdict.at < splitAt)), after: latencyStats(timed.filter((o) => o.verdict.at >= splitAt)) };
1804
+ }
1805
+ function openRows(outcomes) {
1806
+ return outcomes.filter((o) => o.status === "open").map((o) => ({ repo: o.repo, pr: o.pr, head: o.verdict.head, seat: o.verdict.seat, verdictAt: o.verdict.at, ageMin: round(o.minutes), eventId: o.verdict.eventId })).sort((a, b) => b.ageMin - a.ageMin);
1807
+ }
1808
+ function denialSection(denials) {
1809
+ const counts = /* @__PURE__ */ new Map();
1810
+ for (const d of denials) {
1811
+ const key = [d.reason, d.action, d.seat].join("\0");
1812
+ const row = counts.get(key) ?? { reason: d.reason, action: d.action, seat: d.seat, count: 0 };
1813
+ counts.set(key, { ...row, count: row.count + 1 });
1814
+ }
1815
+ const rows = [...counts.values()].sort((a, b) => b.count - a.count || a.reason.localeCompare(b.reason) || a.action.localeCompare(b.action));
1816
+ return { cites: ["denials"], total: denials.length, rows };
1817
+ }
1818
+ function renderBlockedFlowText(report) {
1819
+ const scope = `Blocked flow as of ${report.asOf}${report.window.since ? `, since ${report.window.since}` : ""}${report.window.until ? `, until ${report.window.until}` : ""}`;
1820
+ const latency = [latencyTable("Verdict to merge, minutes from the first MERGE at the final head [verdictToMerge.rows[].all]", report.verdictToMerge.rows, "all")];
1821
+ if (report.splitAt) latency.push(...["before", "after"].map((part) => latencyTable(`Verdict to merge, verdicts ${part} ${report.splitAt} [verdictToMerge.rows[].${part}]`, report.verdictToMerge.rows, part)));
1822
+ return [scope, ...latency, openTable(report), denialTable(report), idleTable(report), sourceLines(report), LIST_PRICE_CAVEAT].join("\n\n") + "\n";
1823
+ }
1824
+ function latencyTable(title, rows, part) {
1825
+ const cells = rows.map((r) => [r.repo, r.prs, ...statCells(r[part]), r.staleHead]);
1826
+ return table(title, ["repo", "prs", "merged", "median", "mean", "p90", "max", "open", "oldest open", "median incl. open", "stale head"], cells);
1827
+ }
1828
+ function statCells(s) {
1829
+ const n = (v) => v === null ? "-" : v;
1830
+ return [s.merged, n(s.medianMin), n(s.meanMin), n(s.p90Min), n(s.maxMin), s.censored, n(s.oldestCensoredMin), n(s.medianWithCensoredMin)];
1831
+ }
1832
+ function openTable(report) {
1833
+ const rows = report.openHoldingMerge.rows.map((r) => [r.repo, `#${r.pr}`, r.head.slice(0, 7), r.seat, r.verdictAt, r.ageMin, `events#${r.eventId}`]);
1834
+ const title = `Open PRs holding MERGE at their current head, ${report.openHoldingMerge.prMinutes} PR-minutes [openHoldingMerge.rows[].ageMin]`;
1835
+ return table(title, ["repo", "pr", "head", "seat", "verdict at", "age min", "verdict"], rows);
1836
+ }
1837
+ function denialTable(report) {
1838
+ const rows = report.denials.rows.map((r) => [r.reason, r.action, r.seat, r.count]);
1839
+ return table(`Classifier denials, ${report.denials.total} in all [denials.rows[].count]`, ["reason", "action", "seat", "count"], rows);
1840
+ }
1841
+ function idleTable(report) {
1842
+ const rows = report.idleSlots.rows.map((r) => [r.seat, r.reason, r.slotMinutes, r.ticks, r.lines.join(",")]);
1843
+ const title = `Idle implementer slot-minutes by stated reason, ${report.idleSlots.total} in all, each tick held at most ${report.idleSlots.holdMaxMin} min [idleSlots.rows[].slotMinutes]`;
1844
+ return table(title, ["seat", "reason", "slot-min", "ticks", "journal lines"], rows);
1845
+ }
1846
+ function sourceLines(report) {
1847
+ return ["Sources", ...Object.entries(report.sources).map(([name, s]) => `${name}: ${s.command} (${s.field})`)].join("\n");
722
1848
  }
723
1849
  export {
1850
+ ACTION_CLASSES,
1851
+ AGENT_LIFECYCLE,
1852
+ ALL_REPOS,
1853
+ BLOCKED_FLOW_SOURCES,
1854
+ BOOT_TOOL,
724
1855
  CHANNEL_CLUSTER_MS,
725
1856
  CONTEXT_BANDS,
726
1857
  CONTEXT_RESET_TOKENS,
727
1858
  COORDINATOR_MERGE_DEDUPE_REQUESTS,
728
1859
  COORDINATOR_MIN_EPISODE_REQUESTS,
729
1860
  COORDINATOR_WAVE_QUIET_REQUESTS,
1861
+ COST_REPORT_SECTIONS,
1862
+ DEFAULT_ACTION_RULES,
1863
+ DEFAULT_CONFIGURED_K,
1864
+ DEFAULT_EPISODE_ROLES,
1865
+ DEFAULT_K_SWEEP,
1866
+ DEFAULT_MECHANICAL_CLASSES,
1867
+ DEFAULT_NO_ACTION_CLASSES,
1868
+ DEFAULT_REVIEWER_PRS,
1869
+ DEFAULT_REVIEWER_ROLE,
1870
+ DEFAULT_STANDING_ROLE,
730
1871
  GAP_BANDS,
731
1872
  HEURISTIC_VERSIONS,
732
1873
  IDLE_GAP_MS,
733
1874
  LIST_PRICE_CAVEAT,
1875
+ POOLED_REVIEWERS,
734
1876
  PRICE_TABLE,
735
1877
  PRICE_TABLE_VERSION,
736
1878
  PROFILE_ROLES,
1879
+ REBUILD_GAP_BANDS,
737
1880
  STANDING_PEER_MIN_ASSIGNMENTS,
738
1881
  STANDING_PEER_MIN_HOURS,
1882
+ TELEPORT_EVENT,
1883
+ TICK_HOLD_MAX_MIN,
739
1884
  TOKEN_CLASSES,
1885
+ UNSTATED_REASON,
1886
+ WAKE_FROM_KINDS,
740
1887
  assignmentCount,
741
1888
  bandOf,
1889
+ blockedFlowReport,
1890
+ blockedFlowSchema,
742
1891
  buildEpisodes,
1892
+ buildWakeEpisodes,
1893
+ cacheTtlReport,
1894
+ cacheTtlWhatIf,
1895
+ cacheTtlWhatIfSchema,
1896
+ classifyDeniedAction,
1897
+ classifyRequest,
743
1898
  classifySession,
744
1899
  contextBand,
1900
+ costPerRequest,
745
1901
  costReport,
746
1902
  costReportSchema,
1903
+ episodeNames,
747
1904
  findPrice,
748
1905
  gapBand,
1906
+ handoffThreshold,
1907
+ handoffThresholdSchema,
749
1908
  heuristicFor,
1909
+ idleSlotMinutes,
750
1910
  initiativeFromCwd,
1911
+ isBootAction,
1912
+ latencyStats,
1913
+ latencyStatsSchema,
1914
+ mergeOutcomes,
1915
+ parseDenials,
1916
+ parseSeatJournal,
1917
+ parseTeleportEvents,
1918
+ parseVerdict,
1919
+ prKey,
751
1920
  priceRequest,
752
1921
  readEpisodeInput,
1922
+ readRequestToolCalls,
1923
+ readTtlRows,
1924
+ renderBlockedFlowText,
1925
+ renderCacheTtlText,
1926
+ renderCostReportSections,
753
1927
  renderCostReportText,
754
1928
  roleFromProfile,
1929
+ scopeFilter,
755
1930
  sessionInitiative,
756
1931
  sessionRole,
1932
+ summarizeWakeEpisodes,
1933
+ sweepK,
1934
+ wakeEpisodesSchema,
757
1935
  workerRole,
758
1936
  writeEpisodes
759
1937
  };