@titan-design/session-analytics 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -3
- package/dist/index.d.ts +1316 -4
- package/dist/index.js +1527 -52
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -131,14 +131,14 @@ function gapBand(millis) {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
// src/cost-report.ts
|
|
134
|
-
import { z } from "zod";
|
|
134
|
+
import { z as z3 } from "zod";
|
|
135
135
|
|
|
136
136
|
// src/cost-report-queries.ts
|
|
137
137
|
import { AUDIT_FACET, EPISODE_TABLE, FACET_TABLE, KIT } from "@titan-design/session-graph";
|
|
138
138
|
var IN_WINDOW = "(@since IS NULL OR ts >= @since) AND (@until IS NULL OR ts < @until)";
|
|
139
139
|
var IN_SESSIONS = "session_id IN (SELECT value FROM json_each(@ids))";
|
|
140
140
|
var COST_ROWS = `
|
|
141
|
-
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,
|
|
142
142
|
input_tokens AS inputTokens, cache_read_tokens AS cacheReadTokens,
|
|
143
143
|
CASE WHEN cache_creation_5m + cache_creation_1h = 0 THEN cache_creation_tokens ELSE cache_creation_5m END AS cacheWrite5mTokens,
|
|
144
144
|
cache_creation_1h AS cacheWrite1hTokens, output_tokens AS outputTokens,
|
|
@@ -150,7 +150,7 @@ var COST_ROWS = `
|
|
|
150
150
|
`;
|
|
151
151
|
function readCostRows(db, window) {
|
|
152
152
|
const rows = db.prepare(COST_ROWS).all(window);
|
|
153
|
-
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 }));
|
|
154
154
|
}
|
|
155
155
|
function readCompactions(db, window) {
|
|
156
156
|
const sql = `SELECT trigger, mid_loop AS midLoop, dropped_tokens AS droppedTokens FROM compaction WHERE ${IN_WINDOW}`;
|
|
@@ -169,6 +169,46 @@ function readCoverage(db, facetVersion) {
|
|
|
169
169
|
).get({ facet: AUDIT_FACET, version: facetVersion });
|
|
170
170
|
return { transcriptsIndexed: indexed.n, facetBacklog: backlog.n };
|
|
171
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
|
+
}
|
|
172
212
|
function readSessionContexts(db, sessionIds) {
|
|
173
213
|
const ids = JSON.stringify(sessionIds);
|
|
174
214
|
const contexts = new Map(sessionIds.map((id) => [id, emptyContext()]));
|
|
@@ -422,6 +462,236 @@ function writeEpisodes(graph, sessionIds) {
|
|
|
422
462
|
return written;
|
|
423
463
|
}
|
|
424
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
|
+
/** Sessions in the `requestsFrom` cohort; 1 means the per-PR figure rests on one session. */
|
|
519
|
+
requestsFromSessions: count,
|
|
520
|
+
costUsd: z.number()
|
|
521
|
+
});
|
|
522
|
+
var reviewerSchema = z.object({ prs: count, fresh: z.array(reviewerCostSchema), standing: z.array(reviewerCostSchema) });
|
|
523
|
+
var handoffThresholdSchema = z.object({
|
|
524
|
+
configuredK: z.array(count),
|
|
525
|
+
sweep: z.object({ fromK: count, toK: count, stepK: count }),
|
|
526
|
+
cohorts: z.array(cohortSchema),
|
|
527
|
+
sessions: z.array(sessionSchema),
|
|
528
|
+
/** Sessions with no dispatch, send or write, or none priced: they have no boot to measure. */
|
|
529
|
+
unbootedSessions: count,
|
|
530
|
+
teleports: z.array(teleportSchema),
|
|
531
|
+
unmatchedTeleports: count,
|
|
532
|
+
/** Boot and cache reads for `prs` reviews: a fresh reviewer per PR against one standing reviewer. */
|
|
533
|
+
reviewers: reviewerSchema
|
|
534
|
+
});
|
|
535
|
+
function isBootAction(calls) {
|
|
536
|
+
return calls.some((call) => BOOT_TOOL.test(call.tool) || (call.writePaths?.length ?? 0) > 0);
|
|
537
|
+
}
|
|
538
|
+
function parseTeleportEvents(lines2) {
|
|
539
|
+
const events = [];
|
|
540
|
+
for (const line of lines2) {
|
|
541
|
+
const entry = parseJson(line);
|
|
542
|
+
if (entry?.event !== TELEPORT_EVENT || typeof entry.from !== "string" || typeof entry.ts !== "string") continue;
|
|
543
|
+
events.push({ ts: entry.ts, name: typeof entry.name === "string" ? entry.name : null, fromAgentId: entry.from });
|
|
544
|
+
}
|
|
545
|
+
return events;
|
|
546
|
+
}
|
|
547
|
+
function parseJson(line) {
|
|
548
|
+
try {
|
|
549
|
+
const value = JSON.parse(line);
|
|
550
|
+
return typeof value === "object" && value !== null ? value : null;
|
|
551
|
+
} catch {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function costPerRequest(params, k) {
|
|
556
|
+
const requests = (k - params.bootFill) / params.growthPerRequest;
|
|
557
|
+
if (!(params.growthPerRequest > 0) || requests < 1) return null;
|
|
558
|
+
return params.bootCostUsd / requests + params.readPricePerMTok / MTOK * ((params.bootFill + k) / 2);
|
|
559
|
+
}
|
|
560
|
+
function sweepK(params, sweep, configuredK) {
|
|
561
|
+
let best = null;
|
|
562
|
+
for (let k = sweep.fromK; k <= sweep.toK; k += sweep.stepK) {
|
|
563
|
+
const cost = costPerRequest(params, k);
|
|
564
|
+
if (cost !== null && (best === null || cost < best.cost)) best = { k, cost };
|
|
565
|
+
}
|
|
566
|
+
const atConfigured = configuredK.map((k) => {
|
|
567
|
+
const cost = costPerRequest(params, k);
|
|
568
|
+
return { k, costPerRequest: cost, deltaPerRequest: cost === null || best === null ? null : cost - best.cost };
|
|
569
|
+
});
|
|
570
|
+
return { bestK: best?.k ?? null, bestCostPerRequest: best?.cost ?? null, atConfigured };
|
|
571
|
+
}
|
|
572
|
+
function handoffThreshold(rows, teleports, agentSessions2, options = {}) {
|
|
573
|
+
const configuredK = [...options.configuredK ?? DEFAULT_CONFIGURED_K];
|
|
574
|
+
const sweep = { ...options.sweep ?? DEFAULT_K_SWEEP };
|
|
575
|
+
const bySession = groupBy(rows, (row) => row.sessionId);
|
|
576
|
+
const fits = [...bySession.values()].map((requests) => fitSession(requests, options.prices ?? PRICE_TABLE, sweep));
|
|
577
|
+
const sessions = fits.filter((fit) => fit !== null).sort((a, b) => a.sessionId.localeCompare(b.sessionId));
|
|
578
|
+
const cohorts = cohortsOf(sessions, sweep, configuredK);
|
|
579
|
+
const matched = teleports.map((event) => teleportExit(event, agentSessions2, bySession, cohorts, configuredK)).filter((t) => t !== null);
|
|
580
|
+
return {
|
|
581
|
+
configuredK,
|
|
582
|
+
sweep,
|
|
583
|
+
cohorts,
|
|
584
|
+
sessions,
|
|
585
|
+
unbootedSessions: fits.length - sessions.length,
|
|
586
|
+
teleports: matched.sort((a, b) => a.ts.localeCompare(b.ts)),
|
|
587
|
+
unmatchedTeleports: teleports.length - matched.length,
|
|
588
|
+
reviewers: reviewerComparison(sessions, options.reviewerRole ?? DEFAULT_REVIEWER_ROLE, options.standingRole ?? DEFAULT_STANDING_ROLE, options.reviewerPrs ?? DEFAULT_REVIEWER_PRS)
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function fitSession(unsorted, prices, sweep) {
|
|
592
|
+
const requests = [...unsorted].sort((a, b) => a.ts.localeCompare(b.ts));
|
|
593
|
+
const first = requests[0];
|
|
594
|
+
const bootIndex = requests.findIndex((row) => row.bootAction);
|
|
595
|
+
const price = findPrice(first.model, first.ts, prices);
|
|
596
|
+
if (bootIndex < 0 || price === null) return null;
|
|
597
|
+
const params = {
|
|
598
|
+
bootCostUsd: requests.slice(0, bootIndex + 1).reduce((sum2, row) => sum2 + priceRequest(row.tokens, row.model, row.ts, prices).costUsd, 0),
|
|
599
|
+
bootFill: requests[bootIndex].fill,
|
|
600
|
+
growthPerRequest: meanGrowth(requests.slice(bootIndex).map((row) => row.fill)),
|
|
601
|
+
readPricePerMTok: price.cacheRead
|
|
602
|
+
};
|
|
603
|
+
const { sessionId, role, model } = first;
|
|
604
|
+
const last = requests[requests.length - 1];
|
|
605
|
+
return { sessionId, role, model, requests: requests.length, bootRequests: bootIndex + 1, exitFill: last.fill, lastTs: last.ts, ...params, bestK: sweepK(params, sweep, []).bestK };
|
|
606
|
+
}
|
|
607
|
+
function meanGrowth(fills) {
|
|
608
|
+
if (fills.length < 2) return 0;
|
|
609
|
+
let rise = 0;
|
|
610
|
+
for (let i = 1; i < fills.length; i += 1) rise += Math.max(0, fills[i] - fills[i - 1]);
|
|
611
|
+
return rise / (fills.length - 1);
|
|
612
|
+
}
|
|
613
|
+
function cohortsOf(sessions, sweep, configuredK) {
|
|
614
|
+
return [...groupBy(sessions, (s) => JSON.stringify([s.role, s.model]))].map(([, members]) => {
|
|
615
|
+
const params = meanParams(members);
|
|
616
|
+
return {
|
|
617
|
+
role: members[0].role,
|
|
618
|
+
model: members[0].model,
|
|
619
|
+
sessions: members.length,
|
|
620
|
+
...params,
|
|
621
|
+
...sweepK(params, sweep, configuredK),
|
|
622
|
+
halfBoot: sweepK({ ...params, bootCostUsd: params.bootCostUsd / 2 }, sweep, configuredK)
|
|
623
|
+
};
|
|
624
|
+
}).sort((a, b) => a.role.localeCompare(b.role) || a.model.localeCompare(b.model));
|
|
625
|
+
}
|
|
626
|
+
function meanParams(sessions) {
|
|
627
|
+
const mean = (pick) => sessions.reduce((sum2, s) => sum2 + pick(s), 0) / sessions.length;
|
|
628
|
+
return {
|
|
629
|
+
bootCostUsd: mean((s) => s.bootCostUsd),
|
|
630
|
+
bootFill: mean((s) => s.bootFill),
|
|
631
|
+
growthPerRequest: meanGrowthOf(sessions),
|
|
632
|
+
readPricePerMTok: mean((s) => s.readPricePerMTok)
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function meanGrowthOf(sessions) {
|
|
636
|
+
const grown = sessions.filter((s) => s.growthPerRequest > 0);
|
|
637
|
+
return grown.length === 0 ? 0 : grown.reduce((sum2, s) => sum2 + s.growthPerRequest, 0) / grown.length;
|
|
638
|
+
}
|
|
639
|
+
function teleportExit(event, agentSessions2, bySession, cohorts, configuredK) {
|
|
640
|
+
const candidates = (agentSessions2.get(event.fromAgentId) ?? []).flatMap((id) => bySession.get(id) ?? []);
|
|
641
|
+
const exit = candidates.filter((row) => row.ts <= event.ts).sort((a, b) => b.ts.localeCompare(a.ts))[0];
|
|
642
|
+
if (!exit) return null;
|
|
643
|
+
const cohort = cohorts.find((c) => c.role === exit.role && c.model === exit.model);
|
|
644
|
+
return {
|
|
645
|
+
ts: event.ts,
|
|
646
|
+
name: event.name,
|
|
647
|
+
sessionId: exit.sessionId,
|
|
648
|
+
role: exit.role,
|
|
649
|
+
model: exit.model,
|
|
650
|
+
exitFill: exit.fill,
|
|
651
|
+
bestK: cohort?.bestK ?? null,
|
|
652
|
+
overConfigured: configuredK.map((k) => ({ k, over: exit.fill - k }))
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
function readCost(params, requests) {
|
|
656
|
+
return (requests * params.bootFill + params.growthPerRequest * requests * (requests + 1) / 2) * params.readPricePerMTok / MTOK;
|
|
657
|
+
}
|
|
658
|
+
function reviewerComparison(sessions, reviewerRole, standingRole, prs) {
|
|
659
|
+
const cohorts = (role) => [...groupBy(sessions.filter((s) => s.role === role), (s) => s.model)].sort(([a], [b]) => a.localeCompare(b));
|
|
660
|
+
const reviewers = cohorts(reviewerRole);
|
|
661
|
+
const perPr = new Map(reviewers.map(([model, members]) => [model, meanRequestsAfterBoot(members)]));
|
|
662
|
+
const newest = newestCohort(reviewers);
|
|
663
|
+
const priced = (role, costOf2) => cohorts(role).map(([model, members]) => {
|
|
664
|
+
const from = perPr.has(model) ? model : newest;
|
|
665
|
+
const requestsPerPr = from === null ? 0 : perPr.get(from);
|
|
666
|
+
const requestsFromSessions = from === null ? 0 : reviewers.find(([name]) => name === from)?.[1].length ?? 0;
|
|
667
|
+
return { role, model, sessions: members.length, requestsPerPr, requestsFrom: from ?? POOLED_REVIEWERS, requestsFromSessions, costUsd: costOf2(meanParams(members), requestsPerPr) };
|
|
668
|
+
});
|
|
669
|
+
return {
|
|
670
|
+
prs,
|
|
671
|
+
fresh: priced(reviewerRole, (params, requests) => prs * (params.bootCostUsd + readCost(params, requests))),
|
|
672
|
+
standing: priced(standingRole, (params, requests) => params.bootCostUsd + readCost(params, prs * requests))
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function newestCohort(reviewers) {
|
|
676
|
+
const epoch = (ts) => Number.isNaN(Date.parse(ts)) ? Number.NEGATIVE_INFINITY : Date.parse(ts);
|
|
677
|
+
const latest = reviewers.map(([model, members]) => ({ model, at: Math.max(...members.map((s) => epoch(s.lastTs))) }));
|
|
678
|
+
latest.sort((a, b) => a.at === b.at ? b.model.localeCompare(a.model) : b.at - a.at);
|
|
679
|
+
return latest[0]?.model ?? null;
|
|
680
|
+
}
|
|
681
|
+
function meanRequestsAfterBoot(sessions) {
|
|
682
|
+
return sessions.length === 0 ? 0 : sessions.reduce((sum2, s) => sum2 + s.requests - s.bootRequests, 0) / sessions.length;
|
|
683
|
+
}
|
|
684
|
+
function groupBy(rows, keyOf) {
|
|
685
|
+
const groups = /* @__PURE__ */ new Map();
|
|
686
|
+
for (const row of rows) {
|
|
687
|
+
const key = keyOf(row);
|
|
688
|
+
const members = groups.get(key);
|
|
689
|
+
if (members) members.push(row);
|
|
690
|
+
else groups.set(key, [row]);
|
|
691
|
+
}
|
|
692
|
+
return groups;
|
|
693
|
+
}
|
|
694
|
+
|
|
425
695
|
// src/initiative.ts
|
|
426
696
|
function sessionInitiative(tasks, cwd) {
|
|
427
697
|
const best = [...tasks].sort((a, b) => b.edges - a.edges || a.initiative.localeCompare(b.initiative))[0];
|
|
@@ -438,6 +708,50 @@ function initiativeFromCwd(cwd) {
|
|
|
438
708
|
return `other:${trimmed.slice(trimmed.lastIndexOf("/") + 1)}`;
|
|
439
709
|
}
|
|
440
710
|
|
|
711
|
+
// src/request-owner.ts
|
|
712
|
+
var CALL_SIGNALS = { command_heads: "heads", file_read: "readPaths", file_write: "writePaths" };
|
|
713
|
+
var OWNED_CALLS = `
|
|
714
|
+
WITH scope AS (
|
|
715
|
+
SELECT DISTINCT transcript_id FROM request_dedup
|
|
716
|
+
WHERE (@since IS NULL OR ts >= @since) AND (@until IS NULL OR ts < @until)
|
|
717
|
+
), stream AS (
|
|
718
|
+
SELECT transcript_id, byte_offset, -1 AS lane, 0 AS block_index, request_id, NULL AS tool_use_id, NULL AS name
|
|
719
|
+
FROM request WHERE transcript_id IN (SELECT transcript_id FROM scope)
|
|
720
|
+
UNION ALL
|
|
721
|
+
SELECT transcript_id, byte_offset, 0, block_index, NULL, tool_use_id, name
|
|
722
|
+
FROM tool_call WHERE transcript_id IN (SELECT transcript_id FROM scope)
|
|
723
|
+
), ordered AS (
|
|
724
|
+
SELECT *, COUNT(request_id) OVER (PARTITION BY transcript_id ORDER BY byte_offset, lane, block_index ROWS UNBOUNDED PRECEDING) AS requests_seen
|
|
725
|
+
FROM stream
|
|
726
|
+
), owned AS (
|
|
727
|
+
SELECT c.transcript_id, c.byte_offset, c.block_index, c.tool_use_id, c.name, r.request_id
|
|
728
|
+
FROM ordered c JOIN ordered r ON r.transcript_id = c.transcript_id AND r.lane = -1 AND r.requests_seen = c.requests_seen
|
|
729
|
+
WHERE c.lane = 0
|
|
730
|
+
)
|
|
731
|
+
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
|
|
732
|
+
FROM owned o
|
|
733
|
+
JOIN request_dedup d ON d.transcript_id = o.transcript_id AND d.request_id = o.request_id
|
|
734
|
+
LEFT JOIN session_signal s ON s.transcript_id = o.transcript_id AND s.tool_use_id = o.tool_use_id
|
|
735
|
+
AND s.signal IN ('command_heads', 'file_read', 'file_write')
|
|
736
|
+
WHERE (@since IS NULL OR d.ts >= @since) AND (@until IS NULL OR d.ts < @until)
|
|
737
|
+
ORDER BY o.transcript_id, o.byte_offset, o.block_index
|
|
738
|
+
`;
|
|
739
|
+
function readRequestToolCalls(db, window) {
|
|
740
|
+
const byRequest = /* @__PURE__ */ new Map();
|
|
741
|
+
for (const row of db.prepare(OWNED_CALLS).all(window)) {
|
|
742
|
+
const calls = byRequest.get(row.requestId) ?? /* @__PURE__ */ new Map();
|
|
743
|
+
byRequest.set(row.requestId, calls);
|
|
744
|
+
const call = calls.get(row.callKey) ?? { tool: row.tool };
|
|
745
|
+
calls.set(row.callKey, row.signal ? withSignal(call, row.signal, row.detail ?? "") : call);
|
|
746
|
+
}
|
|
747
|
+
return new Map([...byRequest].map(([requestId, calls]) => [requestId, [...calls.values()]]));
|
|
748
|
+
}
|
|
749
|
+
function withSignal(call, signal, detail) {
|
|
750
|
+
const field = CALL_SIGNALS[signal];
|
|
751
|
+
const values = signal === "command_heads" ? detail.split(";").filter(Boolean) : [detail];
|
|
752
|
+
return { ...call, [field]: [...call[field] ?? [], ...values] };
|
|
753
|
+
}
|
|
754
|
+
|
|
441
755
|
// src/roles.ts
|
|
442
756
|
var PROFILE_ROLES = {
|
|
443
757
|
implementer: "implementer",
|
|
@@ -445,13 +759,19 @@ var PROFILE_ROLES = {
|
|
|
445
759
|
"relay-implementer": "implementer",
|
|
446
760
|
"beat-builder": "implementer",
|
|
447
761
|
"fable-optimizer": "implementer",
|
|
762
|
+
"bd-implementer": "implementer",
|
|
763
|
+
"bd-implementer-lite": "implementer",
|
|
448
764
|
reviewer: "reviewer",
|
|
449
765
|
"relay-reviewer": "reviewer",
|
|
766
|
+
"bd-reviewer": "reviewer",
|
|
450
767
|
researcher: "researcher",
|
|
451
768
|
explorer: "researcher",
|
|
452
769
|
"fable-architect": "planner",
|
|
453
770
|
planner: "planner",
|
|
771
|
+
"bd-planner": "planner",
|
|
772
|
+
decider: "planner",
|
|
454
773
|
"fable-coordinator": "coordinator",
|
|
774
|
+
"opus-coordinator": "coordinator",
|
|
455
775
|
peer: "standing_peer"
|
|
456
776
|
};
|
|
457
777
|
var STANDING_PEER_MIN_HOURS = 12;
|
|
@@ -470,42 +790,252 @@ function sessionRole(classification, worker) {
|
|
|
470
790
|
return classification.sessionClass;
|
|
471
791
|
}
|
|
472
792
|
|
|
793
|
+
// src/scope.ts
|
|
794
|
+
function scopeFilter(db, scope = {}) {
|
|
795
|
+
const ids = scope.sessionIds ? new Set(scope.sessionIds) : null;
|
|
796
|
+
const named = scope.agentPrefix === void 0 ? null : agentSessions(db, scope.agentPrefix);
|
|
797
|
+
const roles = scope.roles ? new Set(scope.roles) : null;
|
|
798
|
+
return (row) => (!ids || ids.has(row.sessionId)) && (!named || named.has(row.sessionId)) && (!roles || roles.has(row.role));
|
|
799
|
+
}
|
|
800
|
+
function agentSessions(db, prefix) {
|
|
801
|
+
return new Set(readAgentNames(db).filter((row) => row.agentName.startsWith(prefix)).map((row) => row.sessionId));
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/turn-action.ts
|
|
805
|
+
var ACTION_CLASSES = [
|
|
806
|
+
"journal-write",
|
|
807
|
+
"pr-ci-check",
|
|
808
|
+
"merge",
|
|
809
|
+
"dispatch",
|
|
810
|
+
"retire",
|
|
811
|
+
"task-state",
|
|
812
|
+
"budget-status",
|
|
813
|
+
"scorer",
|
|
814
|
+
"message",
|
|
815
|
+
"read-investigate",
|
|
816
|
+
"text-only",
|
|
817
|
+
"other"
|
|
818
|
+
];
|
|
819
|
+
var JOURNAL_BASENAME = String.raw`(?:(?:[^/]*[-_.])?(?:log|journal|events?)(?:[-_.][^/]*)?\.(?:md|jsonl)|[^/]*journal[^/]*)$`;
|
|
820
|
+
var INTERPRETER = String.raw`(?:(?:python3?|node|tsx|bash|sh) )?`;
|
|
821
|
+
var DEFAULT_ACTION_RULES = [
|
|
822
|
+
{ cls: "journal-write", writePath: new RegExp(String.raw`(?:^|/)${JOURNAL_BASENAME}`, "i") },
|
|
823
|
+
{ cls: "journal-write", head: new RegExp(`^>(?:[^/]*/)?${JOURNAL_BASENAME}`, "i") },
|
|
824
|
+
{ cls: "pr-ci-check", head: /^gh pr (checks|view)\b/ },
|
|
825
|
+
{ cls: "pr-ci-check", head: /^gh run\b/ },
|
|
826
|
+
{ cls: "pr-ci-check", head: /^git merge-tree\b/ },
|
|
827
|
+
{ cls: "pr-ci-check", head: /^gh api GET commits\/check-runs$/ },
|
|
828
|
+
{ cls: "pr-ci-check", head: /^gh api GET commits\/status$/ },
|
|
829
|
+
{ cls: "merge", head: /^gh pr merge\b/ },
|
|
830
|
+
{ cls: "merge", head: /^gh api PUT pulls\/merge$/ },
|
|
831
|
+
{ cls: "dispatch", tool: /agent_(spawn|resume)$/ },
|
|
832
|
+
{ cls: "retire", head: /^(agent-chat )?agent retire\b/ },
|
|
833
|
+
{ cls: "task-state", head: /^active-work task\b/ },
|
|
834
|
+
{ cls: "budget-status", tool: /(session_budget|chat_status)$/ },
|
|
835
|
+
{ cls: "budget-status", head: /^(agent-chat )?agent list\b/ },
|
|
836
|
+
{ cls: "budget-status", head: /^(agent-chat )?agent (ls|budget|worktrees)\b/ },
|
|
837
|
+
{ cls: "budget-status", head: /^git worktree list\b/ },
|
|
838
|
+
{ cls: "budget-status", tool: /(agent_list|chat_list|ListAgents)$/ },
|
|
839
|
+
{ cls: "scorer", tool: /(^|__)scor/i },
|
|
840
|
+
{ cls: "scorer", head: new RegExp(`^${INTERPRETER}[\\w.-]*scor`, "i") },
|
|
841
|
+
{ cls: "message", tool: /chat_send$/ },
|
|
842
|
+
{ cls: "message", tool: /(chat_ask|chat_inbox|chat_claim|chat_release)$/ },
|
|
843
|
+
{ cls: "read-investigate", tool: /^(Read|Grep|Glob)$/ },
|
|
844
|
+
{ cls: "read-investigate", head: /^(cat|ls|grep|rg|head|tail|find|wc|git (log|diff|show|status))\b/ }
|
|
845
|
+
];
|
|
846
|
+
var DEFAULT_MECHANICAL_CLASSES = [
|
|
847
|
+
"journal-write",
|
|
848
|
+
"pr-ci-check",
|
|
849
|
+
"task-state",
|
|
850
|
+
"budget-status",
|
|
851
|
+
"retire",
|
|
852
|
+
"merge",
|
|
853
|
+
"scorer"
|
|
854
|
+
];
|
|
855
|
+
function callMatches(call, rule) {
|
|
856
|
+
if (rule.tool && !rule.tool.test(call.tool)) return false;
|
|
857
|
+
if (rule.head && !(call.heads ?? []).some((h) => rule.head?.test(h))) return false;
|
|
858
|
+
if (rule.readPath && !(call.readPaths ?? []).some((p) => rule.readPath?.test(p))) return false;
|
|
859
|
+
if (rule.writePath && !(call.writePaths ?? []).some((p) => rule.writePath?.test(p))) return false;
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
function classifyRequest(calls, rules = DEFAULT_ACTION_RULES) {
|
|
863
|
+
if (calls.length === 0) return "text-only";
|
|
864
|
+
const hit = rules.find((rule) => calls.some((call) => callMatches(call, rule)));
|
|
865
|
+
return hit?.cls ?? "other";
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/wake-episodes.ts
|
|
869
|
+
import { z as z2 } from "zod";
|
|
870
|
+
var WAKE_FROM_KINDS = ["seat", "agent", "broadcast", "broker", "none"];
|
|
871
|
+
var DEFAULT_EPISODE_ROLES = ["coordinator", "worker:coordinator"];
|
|
872
|
+
var DEFAULT_NO_ACTION_CLASSES = ["read-investigate", "text-only", "other"];
|
|
873
|
+
var BROKER_SENDER = "agent-chat";
|
|
874
|
+
var AGENT_LIFECYCLE = "agent_lifecycle";
|
|
875
|
+
var UNNAMED = "unnamed";
|
|
876
|
+
var count2 = z2.number().int().nonnegative();
|
|
877
|
+
var costFields = { episodes: count2, requests: count2, costUsd: z2.number(), noActionEpisodes: count2, noActionCostUsd: z2.number() };
|
|
878
|
+
var fromSchema = z2.object({ key: z2.enum(WAKE_FROM_KINDS), ...costFields });
|
|
879
|
+
var causeSchema = z2.object({
|
|
880
|
+
key: z2.string(),
|
|
881
|
+
...costFields,
|
|
882
|
+
midLoopEpisodes: count2,
|
|
883
|
+
requestsPerEpisode: z2.number(),
|
|
884
|
+
costPerEpisode: z2.number(),
|
|
885
|
+
byFrom: z2.array(fromSchema)
|
|
886
|
+
});
|
|
887
|
+
var pairSchema = z2.object({ from: z2.string(), fromKind: z2.enum(WAKE_FROM_KINDS), to: z2.string(), ...costFields });
|
|
888
|
+
var wakeEpisodesSchema = z2.object({
|
|
889
|
+
roles: z2.array(z2.string()),
|
|
890
|
+
noActionClasses: z2.array(z2.enum(ACTION_CLASSES)),
|
|
891
|
+
...costFields,
|
|
892
|
+
/** Requests in the window that follow no event in it: the work an earlier arrival started. */
|
|
893
|
+
unattributed: z2.object({ requests: count2, costUsd: z2.number() }),
|
|
894
|
+
byCause: z2.array(causeSchema),
|
|
895
|
+
/** Sender by receiver; a seat is named, every other sender is its kind. */
|
|
896
|
+
pairs: z2.array(pairSchema)
|
|
897
|
+
});
|
|
898
|
+
function episodeNames(rows) {
|
|
899
|
+
const isSeat = (row) => row.originKind !== "spawned" || roleFromProfile(row.profile) === "coordinator";
|
|
900
|
+
return {
|
|
901
|
+
seats: new Set(rows.filter(isSeat).map((row) => row.agentName)),
|
|
902
|
+
receivers: new Map(rows.map((row) => [row.sessionId, row.agentName]))
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function episodeCause(cause) {
|
|
906
|
+
return cause === "channel_system" ? AGENT_LIFECYCLE : cause;
|
|
907
|
+
}
|
|
908
|
+
function fromKindOf(event, seats) {
|
|
909
|
+
if (!event.cause.startsWith("channel_") || event.fromName === null) return "none";
|
|
910
|
+
if (event.fromName === BROKER_SENDER) return "broker";
|
|
911
|
+
if (event.broadcast) return "broadcast";
|
|
912
|
+
return seats.has(event.fromName) ? "seat" : "agent";
|
|
913
|
+
}
|
|
914
|
+
function buildWakeEpisodes(events, requests, names, noActionClasses) {
|
|
915
|
+
const byEvent = groupBy2(requests, (request) => request.eventKey ?? "");
|
|
916
|
+
return events.map((event) => {
|
|
917
|
+
const members = byEvent.get(event.key) ?? [];
|
|
918
|
+
const fromKind = fromKindOf(event, names.seats);
|
|
919
|
+
return {
|
|
920
|
+
key: event.key,
|
|
921
|
+
cause: episodeCause(event.cause),
|
|
922
|
+
midLoop: event.delivery === "mid_loop",
|
|
923
|
+
fromKind,
|
|
924
|
+
from: pairSender(fromKind, event.fromName, names.seats),
|
|
925
|
+
to: names.receivers.get(event.sessionId) ?? UNNAMED,
|
|
926
|
+
requests: members.length,
|
|
927
|
+
costUsd: members.reduce((sum2, row) => sum2 + row.costUsd, 0),
|
|
928
|
+
noAction: members.every((row) => noActionClasses.includes(row.action))
|
|
929
|
+
};
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
function pairSender(kind, name, seats) {
|
|
933
|
+
if (kind === "seat" || kind === "broadcast" && name !== null && seats.has(name)) return name;
|
|
934
|
+
return kind === "broadcast" ? "agent" : kind;
|
|
935
|
+
}
|
|
936
|
+
function summarizeWakeEpisodes(episodes, requests, roles, noActionClasses) {
|
|
937
|
+
const keys = new Set(episodes.map((episode) => episode.key));
|
|
938
|
+
const unattributed = requests.filter((row) => row.eventKey === null || !keys.has(row.eventKey));
|
|
939
|
+
return {
|
|
940
|
+
roles: [...roles],
|
|
941
|
+
noActionClasses: [...noActionClasses],
|
|
942
|
+
...costOf(episodes),
|
|
943
|
+
unattributed: { requests: unattributed.length, costUsd: unattributed.reduce((sum2, row) => sum2 + row.costUsd, 0) },
|
|
944
|
+
byCause: causeBuckets(episodes),
|
|
945
|
+
pairs: pairBuckets(episodes)
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
function costOf(episodes) {
|
|
949
|
+
const noAction = episodes.filter((episode) => episode.noAction);
|
|
950
|
+
const sum2 = (list, pick) => list.reduce((total, episode) => total + pick(episode), 0);
|
|
951
|
+
return {
|
|
952
|
+
episodes: episodes.length,
|
|
953
|
+
requests: sum2(episodes, (episode) => episode.requests),
|
|
954
|
+
costUsd: sum2(episodes, (episode) => episode.costUsd),
|
|
955
|
+
noActionEpisodes: noAction.length,
|
|
956
|
+
noActionCostUsd: sum2(noAction, (episode) => episode.costUsd)
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
function causeBuckets(episodes) {
|
|
960
|
+
return [...groupBy2(episodes, (episode) => episode.cause)].map(([key, members]) => {
|
|
961
|
+
const cost = costOf(members);
|
|
962
|
+
return {
|
|
963
|
+
key,
|
|
964
|
+
...cost,
|
|
965
|
+
midLoopEpisodes: members.filter((episode) => episode.midLoop).length,
|
|
966
|
+
requestsPerEpisode: cost.requests / cost.episodes,
|
|
967
|
+
costPerEpisode: cost.costUsd / cost.episodes,
|
|
968
|
+
byFrom: [...groupBy2(members, (episode) => episode.fromKind)].map(([kind, part]) => ({ key: kind, ...costOf(part) })).sort(byCostThenKey)
|
|
969
|
+
};
|
|
970
|
+
}).sort(byCostThenKey);
|
|
971
|
+
}
|
|
972
|
+
function pairBuckets(episodes) {
|
|
973
|
+
const sent = episodes.filter((episode) => episode.fromKind !== "none");
|
|
974
|
+
return [...groupBy2(sent, (episode) => JSON.stringify([episode.from, episode.fromKind, episode.to]))].map(([key, members]) => {
|
|
975
|
+
const [from, fromKind, to] = JSON.parse(key);
|
|
976
|
+
return { from, fromKind, to, ...costOf(members) };
|
|
977
|
+
}).sort((a, b) => b.costUsd - a.costUsd || a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || a.fromKind.localeCompare(b.fromKind));
|
|
978
|
+
}
|
|
979
|
+
function groupBy2(rows, keyOf) {
|
|
980
|
+
const groups = /* @__PURE__ */ new Map();
|
|
981
|
+
for (const row of rows) {
|
|
982
|
+
const key = keyOf(row);
|
|
983
|
+
const members = groups.get(key);
|
|
984
|
+
if (members) members.push(row);
|
|
985
|
+
else groups.set(key, [row]);
|
|
986
|
+
}
|
|
987
|
+
return groups;
|
|
988
|
+
}
|
|
989
|
+
function byCostThenKey(a, b) {
|
|
990
|
+
return b.costUsd - a.costUsd || a.key.localeCompare(b.key);
|
|
991
|
+
}
|
|
992
|
+
|
|
473
993
|
// src/cost-report.ts
|
|
474
|
-
var
|
|
475
|
-
var bucketSchema =
|
|
476
|
-
var wakePartSchema = bucketSchema.extend({ midLoopRequests:
|
|
477
|
-
var cellSchema =
|
|
994
|
+
var count3 = z3.number().int().nonnegative();
|
|
995
|
+
var bucketSchema = z3.object({ key: z3.string(), requests: count3, sessions: count3, costUsd: z3.number() });
|
|
996
|
+
var wakePartSchema = bucketSchema.extend({ midLoopRequests: count3, midLoopCostUsd: z3.number() });
|
|
997
|
+
var cellSchema = z3.object({ wakeCause: z3.string(), gapBand: z3.string(), requests: count3, costUsd: z3.number() });
|
|
998
|
+
var fraction = z3.number().min(0).max(1);
|
|
999
|
+
var roleShareSchema = z3.object({ role: z3.string(), costUsd: z3.number(), share: fraction });
|
|
478
1000
|
var TOKEN_CLASSES = ["input", "cache_read", "cache_write_5m", "cache_write_1h", "output"];
|
|
479
|
-
var costReportSchema =
|
|
480
|
-
window:
|
|
481
|
-
priceTableVersion:
|
|
482
|
-
totals:
|
|
483
|
-
byTokenClass:
|
|
484
|
-
byAccount:
|
|
485
|
-
byModel:
|
|
486
|
-
byClass:
|
|
487
|
-
byRole:
|
|
1001
|
+
var costReportSchema = z3.object({
|
|
1002
|
+
window: z3.object({ since: z3.string().nullable(), until: z3.string().nullable() }),
|
|
1003
|
+
priceTableVersion: z3.number().int().nullable(),
|
|
1004
|
+
totals: z3.object({ requests: count3, sessions: count3, costUsd: z3.number(), unpricedRequests: count3 }),
|
|
1005
|
+
byTokenClass: z3.array(z3.object({ tokenClass: z3.enum(TOKEN_CLASSES), tokens: count3, costUsd: z3.number() })),
|
|
1006
|
+
byAccount: z3.array(bucketSchema),
|
|
1007
|
+
byModel: z3.array(bucketSchema),
|
|
1008
|
+
byClass: z3.array(bucketSchema),
|
|
1009
|
+
byRole: z3.array(bucketSchema),
|
|
1010
|
+
/** Per role, cost by the action class of each request, from the tool calls it issued. */
|
|
1011
|
+
byAction: z3.array(z3.object({ role: z3.string(), buckets: z3.array(bucketSchema) })),
|
|
1012
|
+
/** Cost of the mechanical action classes over the window total; each role's share is over that role's cost. */
|
|
1013
|
+
mechanicalShare: z3.object({ classes: z3.array(z3.enum(ACTION_CLASSES)), costUsd: z3.number(), share: fraction, byRole: z3.array(roleShareSchema) }),
|
|
488
1014
|
/** Sessions by how many episodes their class's heuristic cut them into; `none` when not yet segmented. */
|
|
489
|
-
byEpisodeCount:
|
|
490
|
-
byInitiative:
|
|
491
|
-
byContextBand:
|
|
492
|
-
byWakeCause:
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
1015
|
+
byEpisodeCount: z3.array(bucketSchema),
|
|
1016
|
+
byInitiative: z3.array(bucketSchema),
|
|
1017
|
+
byContextBand: z3.array(bucketSchema),
|
|
1018
|
+
byWakeCause: z3.array(wakePartSchema.extend({ parts: z3.array(wakePartSchema) })),
|
|
1019
|
+
/** Per wake episode of the episode roles: an arrival and the requests up to the next one, mid-loop deliveries included. */
|
|
1020
|
+
wakeEpisodes: wakeEpisodesSchema,
|
|
1021
|
+
/** Per role and model, the handoff threshold K that minimises boot plus cache-read cost per request. */
|
|
1022
|
+
handoffThreshold: handoffThresholdSchema,
|
|
1023
|
+
wakeCauseByGapBand: z3.array(cellSchema),
|
|
1024
|
+
coldRebuild: z3.object({ requests: count3, costUsd: z3.number(), byGapBandAndCause: z3.array(cellSchema) }),
|
|
1025
|
+
compactions: z3.object({ total: count3, manual: count3, auto: count3, midLoop: count3, droppedTokens: count3 }),
|
|
1026
|
+
topSessions: z3.array(
|
|
1027
|
+
z3.object({
|
|
1028
|
+
sessionId: z3.string(),
|
|
1029
|
+
sessionClass: z3.string(),
|
|
1030
|
+
role: z3.string(),
|
|
1031
|
+
initiative: z3.string(),
|
|
1032
|
+
account: z3.string(),
|
|
1033
|
+
requests: count3,
|
|
1034
|
+
costUsd: z3.number()
|
|
505
1035
|
})
|
|
506
1036
|
),
|
|
507
|
-
unpricedModels:
|
|
508
|
-
coverage:
|
|
1037
|
+
unpricedModels: z3.array(z3.object({ model: z3.string(), requests: count3, tokens: count3 })),
|
|
1038
|
+
coverage: z3.object({ transcriptsIndexed: count3, transcriptsDiscovered: count3.nullable(), facetBacklog: count3 })
|
|
509
1039
|
});
|
|
510
1040
|
var HUMAN_CAUSES = ["human_typed", "ask_user_answer"];
|
|
511
1041
|
var NONE = "none";
|
|
@@ -514,17 +1044,18 @@ var EPISODE_COUNT_CAP = 5;
|
|
|
514
1044
|
var DAY_MS = 864e5;
|
|
515
1045
|
function costReport(db, options = {}) {
|
|
516
1046
|
const window = resolveWindow(options);
|
|
517
|
-
const
|
|
518
|
-
const sessionIds = [...new Set(costRows.map((row) => row.sessionId))];
|
|
519
|
-
const tags = tagSessions(readSessionContexts(db, sessionIds));
|
|
520
|
-
const rows = costRows.map((row) => ({ ...row, ...tags.get(row.sessionId) }));
|
|
1047
|
+
const rows = readTaggedRows(db, window, options.actionRules ?? DEFAULT_ACTION_RULES).filter(scopeFilter(db, options.scope));
|
|
521
1048
|
return {
|
|
522
1049
|
window,
|
|
523
1050
|
priceTableVersion: readPriceTableVersion(db),
|
|
524
|
-
totals: { ...sumOf(rows), sessions:
|
|
1051
|
+
totals: { ...sumOf(rows), sessions: new Set(rows.map((row) => row.sessionId)).size, unpricedRequests: rows.filter((row) => !row.priced).length },
|
|
525
1052
|
byTokenClass: tokenClasses(rows),
|
|
526
1053
|
...dimensionBuckets(rows),
|
|
1054
|
+
byAction: roleActions(rows),
|
|
1055
|
+
mechanicalShare: mechanicalShare(rows, options.mechanicalClasses ?? DEFAULT_MECHANICAL_CLASSES),
|
|
527
1056
|
byWakeCause: wakeCauses(rows),
|
|
1057
|
+
wakeEpisodes: wakeEpisodes(db, window, rows, options),
|
|
1058
|
+
handoffThreshold: handoff(db, window, rows, options),
|
|
528
1059
|
wakeCauseByGapBand: gapCells(rows),
|
|
529
1060
|
coldRebuild: coldRebuild(rows),
|
|
530
1061
|
compactions: compactionCounts(readCompactions(db, window)),
|
|
@@ -533,6 +1064,15 @@ function costReport(db, options = {}) {
|
|
|
533
1064
|
coverage: { ...readCoverage(db, options.facetVersion ?? null), transcriptsDiscovered: options.transcriptsDiscovered ?? null }
|
|
534
1065
|
};
|
|
535
1066
|
}
|
|
1067
|
+
function readTaggedRows(db, window, rules) {
|
|
1068
|
+
const costRows = readCostRows(db, window);
|
|
1069
|
+
const tags = tagSessions(readSessionContexts(db, [...new Set(costRows.map((row) => row.sessionId))]));
|
|
1070
|
+
const calls = readRequestToolCalls(db, window);
|
|
1071
|
+
return costRows.map((row) => {
|
|
1072
|
+
const requestCalls = calls.get(row.requestId) ?? [];
|
|
1073
|
+
return { ...row, ...tags.get(row.sessionId), action: classifyRequest(requestCalls, rules), bootAction: isBootAction(requestCalls) };
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
536
1076
|
function resolveWindow(options) {
|
|
537
1077
|
const until = options.until ?? null;
|
|
538
1078
|
if (options.since !== void 0) return { since: options.since, until };
|
|
@@ -561,7 +1101,7 @@ function episodeCountKey(episodes) {
|
|
|
561
1101
|
return episodes >= EPISODE_COUNT_CAP ? `${EPISODE_COUNT_CAP}+` : String(episodes);
|
|
562
1102
|
}
|
|
563
1103
|
function sumOf(rows) {
|
|
564
|
-
return { requests: rows.length, costUsd: rows.reduce((
|
|
1104
|
+
return { requests: rows.length, costUsd: rows.reduce((sum2, row) => sum2 + row.costUsd, 0) };
|
|
565
1105
|
}
|
|
566
1106
|
function tokenClasses(rows) {
|
|
567
1107
|
const pick = {
|
|
@@ -573,7 +1113,7 @@ function tokenClasses(rows) {
|
|
|
573
1113
|
};
|
|
574
1114
|
return TOKEN_CLASSES.map((tokenClass) => {
|
|
575
1115
|
const pairs = rows.map(pick[tokenClass]);
|
|
576
|
-
return { tokenClass, tokens: pairs.reduce((
|
|
1116
|
+
return { tokenClass, tokens: pairs.reduce((sum2, [tokens]) => sum2 + tokens, 0), costUsd: pairs.reduce((sum2, [, cost]) => sum2 + cost, 0) };
|
|
577
1117
|
});
|
|
578
1118
|
}
|
|
579
1119
|
function dimensionBuckets(rows) {
|
|
@@ -587,9 +1127,24 @@ function dimensionBuckets(rows) {
|
|
|
587
1127
|
byContextBand: bucketsBy(rows, (row) => row.contextBand)
|
|
588
1128
|
};
|
|
589
1129
|
}
|
|
1130
|
+
function roleActions(rows) {
|
|
1131
|
+
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 }));
|
|
1132
|
+
}
|
|
1133
|
+
function mechanicalShare(rows, classes) {
|
|
1134
|
+
const isMechanical = (row) => classes.includes(row.action);
|
|
1135
|
+
const shareOf = (members) => {
|
|
1136
|
+
const costUsd = sumOf(members.filter(isMechanical)).costUsd;
|
|
1137
|
+
return { costUsd, share: ratio(costUsd, sumOf(members).costUsd) };
|
|
1138
|
+
};
|
|
1139
|
+
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));
|
|
1140
|
+
return { classes: [...classes], ...shareOf(rows), byRole };
|
|
1141
|
+
}
|
|
1142
|
+
function ratio(part, whole) {
|
|
1143
|
+
return whole > 0 ? Math.min(part / whole, 1) : 0;
|
|
1144
|
+
}
|
|
590
1145
|
function bucketsBy(rows, keyOf) {
|
|
591
1146
|
const groups = groupRows(rows, keyOf);
|
|
592
|
-
return [...groups].map(([key, members]) => ({ key, ...sumOf(members), sessions: new Set(members.map((row) => row.sessionId)).size })).sort(
|
|
1147
|
+
return [...groups].map(([key, members]) => ({ key, ...sumOf(members), sessions: new Set(members.map((row) => row.sessionId)).size })).sort(byCostThenKey2);
|
|
593
1148
|
}
|
|
594
1149
|
function groupRows(rows, keyOf) {
|
|
595
1150
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -601,7 +1156,7 @@ function groupRows(rows, keyOf) {
|
|
|
601
1156
|
}
|
|
602
1157
|
return groups;
|
|
603
1158
|
}
|
|
604
|
-
function
|
|
1159
|
+
function byCostThenKey2(a, b) {
|
|
605
1160
|
return b.costUsd - a.costUsd || a.key.localeCompare(b.key);
|
|
606
1161
|
}
|
|
607
1162
|
function wakeCauses(rows) {
|
|
@@ -609,8 +1164,32 @@ function wakeCauses(rows) {
|
|
|
609
1164
|
const groupOf = (row) => HUMAN_CAUSES.includes(causeOf(row)) ? "human" : causeOf(row);
|
|
610
1165
|
return [...groupRows(rows, groupOf)].map(([key, members]) => ({
|
|
611
1166
|
...wakePart(key, members),
|
|
612
|
-
parts: key === "human" ? [...groupRows(members, causeOf)].map(([cause, part]) => wakePart(cause, part)).sort(
|
|
613
|
-
})).sort(
|
|
1167
|
+
parts: key === "human" ? [...groupRows(members, causeOf)].map(([cause, part]) => wakePart(cause, part)).sort(byCostThenKey2) : []
|
|
1168
|
+
})).sort(byCostThenKey2);
|
|
1169
|
+
}
|
|
1170
|
+
function wakeEpisodes(db, window, rows, options) {
|
|
1171
|
+
const roles = options.episodeRoles ?? DEFAULT_EPISODE_ROLES;
|
|
1172
|
+
const noActionClasses = options.noActionClasses ?? DEFAULT_NO_ACTION_CLASSES;
|
|
1173
|
+
const members = rows.filter((row) => roles.includes(row.role));
|
|
1174
|
+
const sessionIds = [...new Set(members.map((row) => row.sessionId))];
|
|
1175
|
+
const eventOf = readRequestEvents(db, window, sessionIds);
|
|
1176
|
+
const requests = members.map((row) => ({ costUsd: row.costUsd, action: row.action, eventKey: eventOf.get(row.requestId) ?? null }));
|
|
1177
|
+
const episodes = buildWakeEpisodes(readWakeEvents(db, window, sessionIds), requests, episodeNames(readAgentNames(db)), noActionClasses);
|
|
1178
|
+
return summarizeWakeEpisodes(episodes, requests, roles, noActionClasses);
|
|
1179
|
+
}
|
|
1180
|
+
function handoff(db, window, rows, options) {
|
|
1181
|
+
const inWindow = (ts) => (window.since === null || ts >= window.since) && (window.until === null || ts < window.until);
|
|
1182
|
+
const teleports = parseTeleportEvents(options.brokerLogLines?.() ?? []).filter((event) => inWindow(event.ts));
|
|
1183
|
+
const requests = rows.filter((row) => !row.isSidechain).map((row) => ({
|
|
1184
|
+
sessionId: row.sessionId,
|
|
1185
|
+
role: row.role,
|
|
1186
|
+
model: row.model,
|
|
1187
|
+
ts: row.ts,
|
|
1188
|
+
fill: row.contextTokens,
|
|
1189
|
+
tokens: { inputTokens: row.inputTokens, cacheReadTokens: row.cacheReadTokens, cacheCreation5mTokens: row.cacheWrite5mTokens, cacheCreation1hTokens: row.cacheWrite1hTokens, outputTokens: row.outputTokens },
|
|
1190
|
+
bootAction: row.bootAction
|
|
1191
|
+
}));
|
|
1192
|
+
return handoffThreshold(requests, teleports, readAgentSessions(db), options.handoff);
|
|
614
1193
|
}
|
|
615
1194
|
function wakePart(key, rows) {
|
|
616
1195
|
const midLoop = rows.filter((row) => row.wakeDelivery === "mid_loop");
|
|
@@ -634,7 +1213,7 @@ function compactionCounts(rows) {
|
|
|
634
1213
|
manual: rows.filter((row) => row.trigger === "manual").length,
|
|
635
1214
|
auto: rows.filter((row) => row.trigger === "auto").length,
|
|
636
1215
|
midLoop: rows.filter((row) => row.midLoop).length,
|
|
637
|
-
droppedTokens: rows.reduce((
|
|
1216
|
+
droppedTokens: rows.reduce((sum2, row) => sum2 + (row.droppedTokens ?? 0), 0)
|
|
638
1217
|
};
|
|
639
1218
|
}
|
|
640
1219
|
function topSessions(rows, top) {
|
|
@@ -645,7 +1224,7 @@ function topSessions(rows, top) {
|
|
|
645
1224
|
}
|
|
646
1225
|
function unpricedModels(rows) {
|
|
647
1226
|
const unpriced = rows.filter((row) => !row.priced);
|
|
648
|
-
return [...groupRows(unpriced, (row) => row.model)].map(([model, members]) => ({ model, requests: members.length, tokens: members.reduce((
|
|
1227
|
+
return [...groupRows(unpriced, (row) => row.model)].map(([model, members]) => ({ model, requests: members.length, tokens: members.reduce((sum2, row) => sum2 + tokensOf(row), 0) })).sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model));
|
|
649
1228
|
}
|
|
650
1229
|
function tokensOf(row) {
|
|
651
1230
|
return row.inputTokens + row.cacheReadTokens + row.cacheWrite5mTokens + row.cacheWrite1hTokens + row.outputTokens;
|
|
@@ -653,6 +1232,7 @@ function tokensOf(row) {
|
|
|
653
1232
|
|
|
654
1233
|
// src/render-text.ts
|
|
655
1234
|
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.";
|
|
1235
|
+
var TOP_WAKE_ROWS = 10;
|
|
656
1236
|
function renderCostReportText(report) {
|
|
657
1237
|
const sections = [
|
|
658
1238
|
header(report),
|
|
@@ -661,10 +1241,17 @@ function renderCostReportText(report) {
|
|
|
661
1241
|
bucketTable("By model", report.byModel, report.totals.costUsd),
|
|
662
1242
|
bucketTable("By class", report.byClass, report.totals.costUsd),
|
|
663
1243
|
bucketTable("By role", report.byRole, report.totals.costUsd),
|
|
1244
|
+
actionTable(report.byAction),
|
|
1245
|
+
mechanicalTable(report.mechanicalShare),
|
|
664
1246
|
bucketTable("By episode count", report.byEpisodeCount, report.totals.costUsd),
|
|
665
1247
|
bucketTable("By initiative", report.byInitiative, report.totals.costUsd),
|
|
666
1248
|
bucketTable("By context band", report.byContextBand, report.totals.costUsd),
|
|
667
1249
|
wakeCauseTable(report),
|
|
1250
|
+
episodeTable(report.wakeEpisodes),
|
|
1251
|
+
pairTable(report.wakeEpisodes),
|
|
1252
|
+
handoffTable(report.handoffThreshold),
|
|
1253
|
+
teleportTable(report.handoffThreshold),
|
|
1254
|
+
reviewerTable(report.handoffThreshold),
|
|
668
1255
|
cellTable("Wake cause by gap band", report.wakeCauseByGapBand),
|
|
669
1256
|
cellTable(`Cold rebuilds: ${report.coldRebuild.requests} requests, ${usd(report.coldRebuild.costUsd)}`, report.coldRebuild.byGapBandAndCause),
|
|
670
1257
|
compactionTable(report.compactions),
|
|
@@ -674,16 +1261,43 @@ function renderCostReportText(report) {
|
|
|
674
1261
|
];
|
|
675
1262
|
return sections.join("\n\n") + "\n";
|
|
676
1263
|
}
|
|
1264
|
+
var COST_REPORT_SECTIONS = {
|
|
1265
|
+
byRole: (report) => bucketTable("By role", report.byRole, report.totals.costUsd),
|
|
1266
|
+
byAction: (report) => actionTable(report.byAction),
|
|
1267
|
+
mechanicalShare: (report) => mechanicalTable(report.mechanicalShare),
|
|
1268
|
+
byWakeCause: (report) => wakeCauseTable(report),
|
|
1269
|
+
wakeEpisodes: (report) => episodeTable(report.wakeEpisodes),
|
|
1270
|
+
wakePairs: (report) => pairTable(report.wakeEpisodes),
|
|
1271
|
+
handoff: (report) => handoffTable(report.handoffThreshold),
|
|
1272
|
+
teleports: (report) => teleportTable(report.handoffThreshold),
|
|
1273
|
+
reviewers: (report) => reviewerTable(report.handoffThreshold)
|
|
1274
|
+
};
|
|
1275
|
+
function renderCostReportSections(report, sections) {
|
|
1276
|
+
const body = sections.map((section) => COST_REPORT_SECTIONS[section](report));
|
|
1277
|
+
return [header(report), ...body, [LIST_PRICE_CAVEAT, footer(report)].join("\n")].join("\n\n") + "\n";
|
|
1278
|
+
}
|
|
677
1279
|
function header(report) {
|
|
678
1280
|
const { since, until } = report.window;
|
|
679
1281
|
const { requests, sessions, costUsd } = report.totals;
|
|
680
1282
|
return `Cost report, ${since ?? "beginning"} to ${until ?? "now"}
|
|
681
1283
|
Total ${usd(costUsd)} over ${requests} requests in ${sessions} sessions`;
|
|
682
1284
|
}
|
|
683
|
-
function bucketTable(title,
|
|
684
|
-
const rows =
|
|
1285
|
+
function bucketTable(title, buckets2, total) {
|
|
1286
|
+
const rows = buckets2.map((b) => [b.key, b.requests, b.sessions, usd(b.costUsd), share(b.costUsd, total)]);
|
|
685
1287
|
return table(title, ["key", "requests", "sessions", "cost", "share"], rows);
|
|
686
1288
|
}
|
|
1289
|
+
function actionTable(roles) {
|
|
1290
|
+
const rows = roles.flatMap(({ role, buckets: buckets2 }) => {
|
|
1291
|
+
const total = buckets2.reduce((sum2, b) => sum2 + b.costUsd, 0);
|
|
1292
|
+
const requests = buckets2.reduce((sum2, b) => sum2 + b.requests, 0);
|
|
1293
|
+
return [[role, requests, usd(total), share(total, total)], ...buckets2.map((b) => [` ${b.key}`, b.requests, usd(b.costUsd), share(b.costUsd, total)])];
|
|
1294
|
+
});
|
|
1295
|
+
return table("By action per role", ["role / action", "requests", "cost", "share"], rows);
|
|
1296
|
+
}
|
|
1297
|
+
function mechanicalTable(m) {
|
|
1298
|
+
const title = `Mechanical share: ${percent(m.share)} (${usd(m.costUsd)}) of total; classes ${m.classes.join(", ") || "none"}`;
|
|
1299
|
+
return table(title, ["role", "mechanical cost", "share of role"], m.byRole.map((r) => [r.role, usd(r.costUsd), percent(r.share)]));
|
|
1300
|
+
}
|
|
687
1301
|
function wakeCauseTable(report) {
|
|
688
1302
|
const rows = report.byWakeCause.flatMap((group) => [
|
|
689
1303
|
[group.key, group.requests, usd(group.costUsd), group.midLoopRequests, usd(group.midLoopCostUsd)],
|
|
@@ -691,6 +1305,56 @@ function wakeCauseTable(report) {
|
|
|
691
1305
|
]);
|
|
692
1306
|
return table("By wake cause", ["cause", "requests", "cost", "mid-loop", "mid-loop cost"], rows);
|
|
693
1307
|
}
|
|
1308
|
+
function episodeTable(w) {
|
|
1309
|
+
const title = `Wake episodes of ${w.roles.join(", ")}: ${w.episodes} episodes, ${w.requests} requests, ${usd(w.costUsd)}; no-action means only ${w.noActionClasses.join(", ")}`;
|
|
1310
|
+
const causes = [...w.byCause].sort((a, b) => b.costPerEpisode - a.costPerEpisode || a.key.localeCompare(b.key)).slice(0, TOP_WAKE_ROWS);
|
|
1311
|
+
const rows = causes.flatMap((c) => [
|
|
1312
|
+
[c.key, c.episodes, c.midLoopEpisodes, c.requests, c.requestsPerEpisode.toFixed(1), usd(c.costUsd), usd(c.costPerEpisode), noActionCell(c)],
|
|
1313
|
+
...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)])
|
|
1314
|
+
]);
|
|
1315
|
+
return table(title, ["cause", "episodes", "mid-loop", "requests", "req/ep", "cost", "cost/ep", "no-action"], rows);
|
|
1316
|
+
}
|
|
1317
|
+
function noActionCell(bucket) {
|
|
1318
|
+
return `${bucket.noActionEpisodes} (${share(bucket.noActionEpisodes, bucket.episodes)})`;
|
|
1319
|
+
}
|
|
1320
|
+
function pairTable(w) {
|
|
1321
|
+
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)]);
|
|
1322
|
+
return table("Wake senders by receiver", ["from", "kind", "to", "episodes", "requests", "cost", "no-action"], rows);
|
|
1323
|
+
}
|
|
1324
|
+
function handoffTable(h) {
|
|
1325
|
+
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`;
|
|
1326
|
+
const head = ["role", "model", "sessions", "boot", "boot fill", "growth/req", "read $/MTok", "best K", "$/req", ...h.configuredK.map((k) => `+$/req @${kilo(k)}`)];
|
|
1327
|
+
const rows = h.cohorts.map((c) => [
|
|
1328
|
+
c.role,
|
|
1329
|
+
c.model,
|
|
1330
|
+
c.sessions,
|
|
1331
|
+
usd(c.bootCostUsd),
|
|
1332
|
+
kilo(c.bootFill),
|
|
1333
|
+
Math.round(c.growthPerRequest),
|
|
1334
|
+
c.readPricePerMTok.toFixed(3),
|
|
1335
|
+
`${kiloOrDash(c.bestK)} (${kiloOrDash(c.halfBoot.bestK)})`,
|
|
1336
|
+
c.bestCostPerRequest === null ? "-" : `$${c.bestCostPerRequest.toFixed(4)}`,
|
|
1337
|
+
...c.atConfigured.map((at) => at.deltaPerRequest === null ? "-" : `$${at.deltaPerRequest.toFixed(4)}`)
|
|
1338
|
+
]);
|
|
1339
|
+
return table(title, head, rows);
|
|
1340
|
+
}
|
|
1341
|
+
function teleportTable(h) {
|
|
1342
|
+
const title = `Teleport exit fill: ${h.teleports.length} matched, ${h.unmatchedTeleports} unmatched; the latest ${TOP_WAKE_ROWS}`;
|
|
1343
|
+
const head = ["ts", "name", "role", "exit fill", "best K", ...h.configuredK.map((k) => `over ${kilo(k)}`)];
|
|
1344
|
+
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))]);
|
|
1345
|
+
return table(title, head, rows);
|
|
1346
|
+
}
|
|
1347
|
+
function reviewerTable(h) {
|
|
1348
|
+
const row = (kind) => (c) => [kind, c.role, c.model, c.sessions, c.requestsPerPr.toFixed(1), c.requestsFrom, usd(c.costUsd)];
|
|
1349
|
+
const rows = [...h.reviewers.fresh.map(row("fresh per PR")), ...h.reviewers.standing.map(row("standing"))];
|
|
1350
|
+
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);
|
|
1351
|
+
}
|
|
1352
|
+
function kilo(tokens) {
|
|
1353
|
+
return `${Math.round(tokens / 1e3)}k`;
|
|
1354
|
+
}
|
|
1355
|
+
function kiloOrDash(tokens) {
|
|
1356
|
+
return tokens === null ? "-" : kilo(tokens);
|
|
1357
|
+
}
|
|
694
1358
|
function cellTable(title, cells) {
|
|
695
1359
|
return table(title, ["cause", "gap", "requests", "cost"], cells.map((c) => [c.wakeCause, c.gapBand, c.requests, usd(c.costUsd)]));
|
|
696
1360
|
}
|
|
@@ -718,8 +1382,8 @@ function table(title, head, rows) {
|
|
|
718
1382
|
const widths = head.map((_, i) => Math.max(...text.map((row) => row[i].length)));
|
|
719
1383
|
const rightAligned = head.map((_, i) => rows.every((row) => isNumeric(row[i])));
|
|
720
1384
|
const pad = (cell, i) => rightAligned[i] ? cell.padStart(widths[i]) : cell.padEnd(widths[i]);
|
|
721
|
-
const
|
|
722
|
-
return [title, ...
|
|
1385
|
+
const lines2 = text.map((row) => row.map(pad).join(" ").trimEnd());
|
|
1386
|
+
return [title, ...lines2].join("\n");
|
|
723
1387
|
}
|
|
724
1388
|
function isNumeric(cell) {
|
|
725
1389
|
return typeof cell === "number" || /^\$|%$/.test(cell);
|
|
@@ -728,42 +1392,853 @@ function usd(value) {
|
|
|
728
1392
|
return `$${value.toFixed(2)}`;
|
|
729
1393
|
}
|
|
730
1394
|
function share(value, total) {
|
|
731
|
-
return total > 0 ?
|
|
1395
|
+
return total > 0 ? percent(value / total) : "-";
|
|
1396
|
+
}
|
|
1397
|
+
function percent(fraction2) {
|
|
1398
|
+
return `${(fraction2 * 100).toFixed(1)}%`;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/cache-ttl.ts
|
|
1402
|
+
import { z as z4 } from "zod";
|
|
1403
|
+
var FIVE_MINUTES_MS = 5 * 6e4;
|
|
1404
|
+
var MTOK2 = 1e6;
|
|
1405
|
+
var NO_PROFILE = "none";
|
|
1406
|
+
var REBUILD_GAP_BANDS = GAP_BANDS.filter((band) => band.lo >= FIVE_MINUTES_MS).map((band) => band.name);
|
|
1407
|
+
var count4 = z4.number().int().nonnegative();
|
|
1408
|
+
var effectFields = {
|
|
1409
|
+
sessions: count4,
|
|
1410
|
+
requests: count4,
|
|
1411
|
+
write1hTokens: count4,
|
|
1412
|
+
/** 1h writes repriced at the 5m write rate. */
|
|
1413
|
+
repriceSavingUsd: z4.number(),
|
|
1414
|
+
rebuilds: count4,
|
|
1415
|
+
rebuildTokens: count4,
|
|
1416
|
+
/** Each rebuild writes at the 5m rate what the 1h cache still served as a read. */
|
|
1417
|
+
rebuildCostUsd: z4.number(),
|
|
1418
|
+
netSavingUsd: z4.number(),
|
|
1419
|
+
netPerSessionUsd: z4.number()
|
|
1420
|
+
};
|
|
1421
|
+
var bucketSchema2 = z4.object({ key: z4.string(), ...effectFields, fiveMinuteLoses: z4.boolean() });
|
|
1422
|
+
var cacheTtlWhatIfSchema = z4.object({
|
|
1423
|
+
window: z4.object({ since: z4.string().nullable(), until: z4.string().nullable() }).optional(),
|
|
1424
|
+
priceTableVersion: z4.number().int(),
|
|
1425
|
+
rebuildGapBands: z4.array(z4.string()),
|
|
1426
|
+
totals: z4.object(effectFields),
|
|
1427
|
+
unpricedRequests: count4,
|
|
1428
|
+
byRole: z4.array(bucketSchema2),
|
|
1429
|
+
byProfile: z4.array(bucketSchema2),
|
|
1430
|
+
/** Roles whose rebuilds after long gaps outweigh the cheaper writes. */
|
|
1431
|
+
lossRoles: z4.array(z4.string())
|
|
1432
|
+
});
|
|
1433
|
+
function cacheTtlWhatIf(rows, prices = PRICE_TABLE) {
|
|
1434
|
+
const effects = /* @__PURE__ */ new Map();
|
|
1435
|
+
for (const row of rows) {
|
|
1436
|
+
const price = findPrice(row.model, row.ts, prices);
|
|
1437
|
+
if (price) effects.set(row, requestEffect(row, price));
|
|
1438
|
+
}
|
|
1439
|
+
const priced = [...effects.keys()];
|
|
1440
|
+
const byRole = buckets(priced, effects, (row) => row.role);
|
|
1441
|
+
return {
|
|
1442
|
+
priceTableVersion: PRICE_TABLE_VERSION,
|
|
1443
|
+
rebuildGapBands: [...REBUILD_GAP_BANDS],
|
|
1444
|
+
totals: sumEffects([...effects.values()]),
|
|
1445
|
+
unpricedRequests: rows.length - priced.length,
|
|
1446
|
+
byRole,
|
|
1447
|
+
byProfile: buckets(priced, effects, (row) => row.profile),
|
|
1448
|
+
lossRoles: byRole.filter((bucket) => bucket.fiveMinuteLoses).map((bucket) => bucket.key)
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
function requestEffect(row, price) {
|
|
1452
|
+
const band = row.gapMs === null ? null : gapBand(row.gapMs);
|
|
1453
|
+
const rebuildTokens = band !== null && REBUILD_GAP_BANDS.includes(band) ? row.cacheReadTokens : 0;
|
|
1454
|
+
return {
|
|
1455
|
+
sessionId: row.sessionId,
|
|
1456
|
+
write1hTokens: row.cacheWrite1hTokens,
|
|
1457
|
+
repriceSavingUsd: row.cacheWrite1hTokens * (price.cacheWrite1h - price.cacheWrite5m) / MTOK2,
|
|
1458
|
+
rebuildTokens,
|
|
1459
|
+
rebuildCostUsd: rebuildTokens * (price.cacheWrite5m - price.cacheRead) / MTOK2
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
function sumEffects(effects) {
|
|
1463
|
+
const sum2 = (pick) => effects.reduce((total, effect) => total + pick(effect), 0);
|
|
1464
|
+
const sessions = new Set(effects.map((effect) => effect.sessionId)).size;
|
|
1465
|
+
const netSavingUsd = sum2((effect) => effect.repriceSavingUsd - effect.rebuildCostUsd);
|
|
1466
|
+
return {
|
|
1467
|
+
sessions,
|
|
1468
|
+
requests: effects.length,
|
|
1469
|
+
write1hTokens: sum2((effect) => effect.write1hTokens),
|
|
1470
|
+
repriceSavingUsd: sum2((effect) => effect.repriceSavingUsd),
|
|
1471
|
+
rebuilds: effects.filter((effect) => effect.rebuildTokens > 0).length,
|
|
1472
|
+
rebuildTokens: sum2((effect) => effect.rebuildTokens),
|
|
1473
|
+
rebuildCostUsd: sum2((effect) => effect.rebuildCostUsd),
|
|
1474
|
+
netSavingUsd,
|
|
1475
|
+
netPerSessionUsd: sessions > 0 ? netSavingUsd / sessions : 0
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
function buckets(rows, effects, keyOf) {
|
|
1479
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1480
|
+
for (const row of rows) {
|
|
1481
|
+
const key = keyOf(row);
|
|
1482
|
+
const members = groups.get(key);
|
|
1483
|
+
if (members) members.push(effects.get(row));
|
|
1484
|
+
else groups.set(key, [effects.get(row)]);
|
|
1485
|
+
}
|
|
1486
|
+
return [...groups].map(([key, members]) => {
|
|
1487
|
+
const totals = sumEffects(members);
|
|
1488
|
+
return { key, ...totals, fiveMinuteLoses: totals.netSavingUsd < 0 };
|
|
1489
|
+
}).sort((a, b) => b.netSavingUsd - a.netSavingUsd || a.key.localeCompare(b.key));
|
|
1490
|
+
}
|
|
1491
|
+
var IN_WINDOW2 = "(@since IS NULL OR ts >= @since) AND (@until IS NULL OR ts < @until)";
|
|
1492
|
+
var TTL_ROWS = `
|
|
1493
|
+
WITH member AS (SELECT DISTINCT session_id FROM request_dedup WHERE ${IN_WINDOW2}),
|
|
1494
|
+
timed AS (
|
|
1495
|
+
SELECT session_id, model, ts, cache_read_tokens, cache_creation_1h, gap_ms,
|
|
1496
|
+
LAG(ts) OVER (PARTITION BY session_id, is_sidechain ORDER BY ts, request_id) AS previous_ts
|
|
1497
|
+
FROM request_dedup WHERE session_id IN member
|
|
1498
|
+
)
|
|
1499
|
+
SELECT session_id AS sessionId, model, ts, cache_read_tokens AS cacheReadTokens, cache_creation_1h AS cacheWrite1hTokens,
|
|
1500
|
+
COALESCE(gap_ms, CAST(ROUND((julianday(ts) - julianday(previous_ts)) * 86400000) AS INTEGER)) AS gapMs
|
|
1501
|
+
FROM timed WHERE ${IN_WINDOW2}`;
|
|
1502
|
+
function cacheTtlReport(db, options = {}, prices = PRICE_TABLE) {
|
|
1503
|
+
const window = resolveWindow(options);
|
|
1504
|
+
return { window, ...cacheTtlWhatIf(readTtlRows(db, window).filter(scopeFilter(db, options.scope)), prices) };
|
|
1505
|
+
}
|
|
1506
|
+
function readTtlRows(db, window) {
|
|
1507
|
+
const rows = db.prepare(TTL_ROWS).all(window);
|
|
1508
|
+
const contexts = readSessionContexts(db, [...new Set(rows.map((row) => row.sessionId))]);
|
|
1509
|
+
const tags = tagSessions(contexts);
|
|
1510
|
+
return rows.map((row) => ({
|
|
1511
|
+
...row,
|
|
1512
|
+
role: tags.get(row.sessionId).role,
|
|
1513
|
+
profile: contexts.get(row.sessionId).facts.origin?.profile ?? NO_PROFILE
|
|
1514
|
+
}));
|
|
1515
|
+
}
|
|
1516
|
+
function renderCacheTtlText(whatIf) {
|
|
1517
|
+
const { totals, window } = whatIf;
|
|
1518
|
+
const span = window ? `, ${window.since ?? "beginning"} to ${window.until ?? "now"}` : "";
|
|
1519
|
+
const header2 = [
|
|
1520
|
+
`Cache TTL what-if, 5m against 1h${span}`,
|
|
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
|
+
`Roles where 5m loses: ${whatIf.lossRoles.join(", ") || "none"}`
|
|
1523
|
+
].join("\n");
|
|
1524
|
+
const footer2 = `Price table v${whatIf.priceTableVersion}; ${whatIf.unpricedRequests} unpriced requests left out.`;
|
|
1525
|
+
return [header2, ttlTable("By role", whatIf.byRole), ttlTable("By profile", whatIf.byProfile), [LIST_PRICE_CAVEAT, footer2].join("\n")].join("\n\n") + "\n";
|
|
1526
|
+
}
|
|
1527
|
+
function ttlTable(title, rows) {
|
|
1528
|
+
const head = ["key", "sessions", "requests", "reprice saving", "rebuilds", "rebuild cost", "net", "net per session", "5m"];
|
|
1529
|
+
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"]);
|
|
1530
|
+
return table(title, head, cells);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// src/blocked-flow-merge.ts
|
|
1534
|
+
import { z as z5 } from "zod";
|
|
1535
|
+
var VERDICT_LINE = /^Verdict:\s*([A-Z_]+)/;
|
|
1536
|
+
var PR_LINE = /^PR:\s*(?:https:\/\/github\.com\/)?([\w.-]+\/[\w.-]+)(?:#|\/pull\/)(\d+)/m;
|
|
1537
|
+
var HEAD_LINE = /^Head:\s*`?([0-9a-f]{7,40})\b/m;
|
|
1538
|
+
function parseVerdict(body) {
|
|
1539
|
+
const verdict = VERDICT_LINE.exec(body)?.[1];
|
|
1540
|
+
const pr = PR_LINE.exec(body);
|
|
1541
|
+
const head = HEAD_LINE.exec(body)?.[1];
|
|
1542
|
+
if (!verdict || !pr || !head) return null;
|
|
1543
|
+
return { verdict, repo: pr[1], pr: Number(pr[2]), head };
|
|
1544
|
+
}
|
|
1545
|
+
var MINUTE_MS2 = 6e4;
|
|
1546
|
+
function mergeOutcomes(verdicts, pulls, asOf) {
|
|
1547
|
+
const byPr = /* @__PURE__ */ new Map();
|
|
1548
|
+
for (const v of verdicts) if (v.verdict === "MERGE") byPr.set(prKey(v), [...byPr.get(prKey(v)) ?? [], v]);
|
|
1549
|
+
const pullByKey = new Map(pulls.map((p) => [prKey(p), p]));
|
|
1550
|
+
return [...byPr.values()].map((held) => outcome(held, pullByKey.get(prKey(held[0])), asOf));
|
|
1551
|
+
}
|
|
1552
|
+
function prKey(row) {
|
|
1553
|
+
return `${row.repo}#${row.pr}`;
|
|
1554
|
+
}
|
|
1555
|
+
function outcome(held, pull, asOf) {
|
|
1556
|
+
const { repo, pr } = held[0];
|
|
1557
|
+
if (!pull) return { repo, pr, status: "unknown", verdict: null, minutes: null };
|
|
1558
|
+
const atHead = held.filter((v) => sameSha(v.head, pull.headSha)).sort((a, b) => a.at.localeCompare(b.at))[0];
|
|
1559
|
+
const mergedBy = pull.mergedAt !== null && pull.mergedAt <= asOf;
|
|
1560
|
+
if (!atHead) return { repo, pr, status: pull.mergedAt !== null || pull.state === "open" ? "stale-head" : "closed", verdict: null, minutes: null };
|
|
1561
|
+
if (mergedBy) return { repo, pr, status: "merged", verdict: atHead, minutes: Math.max(0, minutesBetween(atHead.at, pull.mergedAt)) };
|
|
1562
|
+
if (pull.state === "open" || pull.mergedAt !== null) return { repo, pr, status: "open", verdict: atHead, minutes: minutesBetween(atHead.at, asOf) };
|
|
1563
|
+
return { repo, pr, status: "closed", verdict: atHead, minutes: null };
|
|
1564
|
+
}
|
|
1565
|
+
function sameSha(a, b) {
|
|
1566
|
+
return a.startsWith(b) || b.startsWith(a);
|
|
1567
|
+
}
|
|
1568
|
+
function minutesBetween(from, to) {
|
|
1569
|
+
return (Date.parse(to) - Date.parse(from)) / MINUTE_MS2;
|
|
1570
|
+
}
|
|
1571
|
+
var minutes = z5.number().nullable();
|
|
1572
|
+
var latencyStatsSchema = z5.object({
|
|
1573
|
+
merged: z5.number().int().nonnegative(),
|
|
1574
|
+
medianMin: minutes,
|
|
1575
|
+
meanMin: minutes,
|
|
1576
|
+
p90Min: minutes,
|
|
1577
|
+
maxMin: minutes,
|
|
1578
|
+
/** Open PRs holding MERGE at their current head; their ages are lower bounds on the wait. */
|
|
1579
|
+
censored: z5.number().int().nonnegative(),
|
|
1580
|
+
oldestCensoredMin: minutes,
|
|
1581
|
+
censoredPrMinutes: z5.number().nonnegative(),
|
|
1582
|
+
/** Median over merged waits and censored ages together: a lower bound on the true median. */
|
|
1583
|
+
medianWithCensoredMin: minutes
|
|
1584
|
+
});
|
|
1585
|
+
function latencyStats(outcomes) {
|
|
1586
|
+
const merged = sorted(outcomes.filter((o) => o.status === "merged").map((o) => o.minutes));
|
|
1587
|
+
const censored = sorted(outcomes.filter((o) => o.status === "open").map((o) => o.minutes));
|
|
1588
|
+
return {
|
|
1589
|
+
merged: merged.length,
|
|
1590
|
+
medianMin: median(merged),
|
|
1591
|
+
meanMin: merged.length === 0 ? null : round(merged.reduce((a, b) => a + b, 0) / merged.length),
|
|
1592
|
+
p90Min: nearestRank(merged, 0.9),
|
|
1593
|
+
maxMin: merged.at(-1) === void 0 ? null : round(merged.at(-1)),
|
|
1594
|
+
censored: censored.length,
|
|
1595
|
+
oldestCensoredMin: censored.at(-1) === void 0 ? null : round(censored.at(-1)),
|
|
1596
|
+
censoredPrMinutes: round(censored.reduce((a, b) => a + b, 0)),
|
|
1597
|
+
medianWithCensoredMin: median(sorted([...merged, ...censored]))
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
function sorted(values) {
|
|
1601
|
+
return values.sort((a, b) => a - b);
|
|
1602
|
+
}
|
|
1603
|
+
function median(values) {
|
|
1604
|
+
if (values.length === 0) return null;
|
|
1605
|
+
const mid = Math.floor(values.length / 2);
|
|
1606
|
+
return round(values.length % 2 === 1 ? values[mid] : (values[mid - 1] + values[mid]) / 2);
|
|
1607
|
+
}
|
|
1608
|
+
function nearestRank(values, p) {
|
|
1609
|
+
return values.length === 0 ? null : round(values[Math.ceil(p * values.length) - 1]);
|
|
1610
|
+
}
|
|
1611
|
+
function round(value) {
|
|
1612
|
+
return Math.round(value * 10) / 10;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// src/blocked-flow-denials.ts
|
|
1616
|
+
var DENIAL = /^Permission for this action (?:was|has been) denied by the Claude Code auto mode classifier\. Reason: \[([^\]]+)\]/;
|
|
1617
|
+
var BASH_ACTIONS = [
|
|
1618
|
+
["merge", /\bgh pr merge\b|\/pulls\/\d+\/merge\b/],
|
|
1619
|
+
["retire", /\bagent[ _-]retire\b|\bagent\s+retire\b/],
|
|
1620
|
+
["worktree-remove", /\bgit\b.*\bworktree (?:remove|prune)\b/],
|
|
1621
|
+
["branch-delete", /\bgit branch -[dD]\b|\bpush\b.*--delete\b|\/git\/refs\/heads\/\S*.*-X DELETE|-X DELETE\b.*\/git\/refs\//],
|
|
1622
|
+
["push", /\bgit\b.*\bpush\b/],
|
|
1623
|
+
["github-write", /\bgh (?:pr|issue) (?:create|edit|comment|close)\b|\bgh api\b.*(?:-X|--method)\s*(?:POST|PATCH|PUT|DELETE)\b/],
|
|
1624
|
+
["service-restart", /\bservice restart\b|\blaunchctl\b/],
|
|
1625
|
+
["process-kill", /\b(?:pkill|kill)\b/],
|
|
1626
|
+
["file-write", /\s>>?\s*(?!\/dev\/null|&)\S|\bopen\([^)]*["'][wa]["']\)|\bsed -i\b/]
|
|
1627
|
+
];
|
|
1628
|
+
var FILE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
1629
|
+
function classifyDeniedAction(tool, input) {
|
|
1630
|
+
if (tool === "Bash") return bashAction(String(input?.command ?? ""));
|
|
1631
|
+
if (FILE_TOOLS.has(tool)) return "file-edit";
|
|
1632
|
+
if (tool.startsWith("mcp__")) return `mcp:${tool.split("__").at(-1)}`;
|
|
1633
|
+
return `tool:${tool}`;
|
|
1634
|
+
}
|
|
1635
|
+
function bashAction(command) {
|
|
1636
|
+
return BASH_ACTIONS.find(([, pattern]) => pattern.test(command))?.[0] ?? "bash-other";
|
|
1637
|
+
}
|
|
1638
|
+
function parseDenials(lines2, seat) {
|
|
1639
|
+
const calls = /* @__PURE__ */ new Map();
|
|
1640
|
+
const denials = [];
|
|
1641
|
+
for (const line of lines2) {
|
|
1642
|
+
if (!line.includes('"tool_use"') && !line.includes("auto mode classifier")) continue;
|
|
1643
|
+
const entry = parseLine(line);
|
|
1644
|
+
for (const block of blocksOf(entry)) {
|
|
1645
|
+
if (block.type === "tool_use" && block.id) calls.set(block.id, { tool: block.name ?? "unknown", input: block.input });
|
|
1646
|
+
const reason = block.type === "tool_result" ? DENIAL.exec(resultText(block.content))?.[1] : void 0;
|
|
1647
|
+
if (reason === void 0) continue;
|
|
1648
|
+
const call = calls.get(block.tool_use_id ?? "") ?? { tool: "unknown", input: null };
|
|
1649
|
+
const toolUseId = block.tool_use_id ? { toolUseId: block.tool_use_id } : {};
|
|
1650
|
+
denials.push({ seat, at: entry?.timestamp ?? "", reason, action: classifyDeniedAction(call.tool, call.input), tool: call.tool, ...toolUseId });
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return denials;
|
|
1654
|
+
}
|
|
1655
|
+
function dedupeDenials(denials) {
|
|
1656
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1657
|
+
return denials.filter((d) => {
|
|
1658
|
+
if (d.toolUseId === void 0) return true;
|
|
1659
|
+
const key = `${d.seat}\0${d.toolUseId}`;
|
|
1660
|
+
if (seen.has(key)) return false;
|
|
1661
|
+
seen.add(key);
|
|
1662
|
+
return true;
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
function parseLine(line) {
|
|
1666
|
+
try {
|
|
1667
|
+
return JSON.parse(line);
|
|
1668
|
+
} catch {
|
|
1669
|
+
return null;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
function blocksOf(entry) {
|
|
1673
|
+
const content = entry?.message?.content;
|
|
1674
|
+
return Array.isArray(content) ? content : [];
|
|
1675
|
+
}
|
|
1676
|
+
function resultText(content) {
|
|
1677
|
+
if (typeof content === "string") return content;
|
|
1678
|
+
if (!Array.isArray(content)) return "";
|
|
1679
|
+
const first = content.find((part) => part.type === "text");
|
|
1680
|
+
return first?.text ?? "";
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// src/blocked-flow-idle.ts
|
|
1684
|
+
var UNSTATED_REASON = "unstated";
|
|
1685
|
+
var TICK_HOLD_MAX_MIN = 60;
|
|
1686
|
+
var STAMP = /^(\d{1,2}):(\d{2})(?::(\d{2}))?\s/;
|
|
1687
|
+
var IMPL = /\bimpl(?:ementers?)?\s+(\d+)\s*\/\s*(\d+)/i;
|
|
1688
|
+
var REASON = /\b(?:no dispatch|dispatch 0)\s*:\s*([^.;(]+)/i;
|
|
1689
|
+
function parseSeatJournal(text, seat, date, utcOffsetMin) {
|
|
1690
|
+
const ticks = [];
|
|
1691
|
+
let lastAt = null;
|
|
1692
|
+
text.split("\n").forEach((raw, index) => {
|
|
1693
|
+
const stamp = STAMP.exec(raw);
|
|
1694
|
+
if (!stamp) return;
|
|
1695
|
+
const at = journalTime(date, stamp, utcOffsetMin);
|
|
1696
|
+
lastAt = at;
|
|
1697
|
+
const impl = IMPL.exec(raw);
|
|
1698
|
+
if (!impl) return;
|
|
1699
|
+
const used = Number(impl[1]);
|
|
1700
|
+
const cap = Number(impl[2]);
|
|
1701
|
+
const reason = used < cap ? REASON.exec(raw)?.[1]?.trim().toLowerCase() ?? UNSTATED_REASON : "";
|
|
1702
|
+
ticks.push({ seat, at, used, cap, reason, line: index + 1 });
|
|
1703
|
+
});
|
|
1704
|
+
return { ticks, lastAt };
|
|
1705
|
+
}
|
|
1706
|
+
function journalTime(date, stamp, utcOffsetMin) {
|
|
1707
|
+
const [year, month, day] = date.split("-").map(Number);
|
|
1708
|
+
const local = Date.UTC(year, month - 1, day, Number(stamp[1]), Number(stamp[2]), Number(stamp[3] ?? 0));
|
|
1709
|
+
return new Date(local - utcOffsetMin * 6e4).toISOString();
|
|
1710
|
+
}
|
|
1711
|
+
function idleSlotMinutes(journals, window) {
|
|
1712
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1713
|
+
for (const journal of journals) {
|
|
1714
|
+
journal.ticks.forEach((tick, i) => {
|
|
1715
|
+
const free = Math.max(0, tick.cap - tick.used);
|
|
1716
|
+
const held = heldMinutes(tick.at, journal.ticks[i + 1]?.at ?? journal.lastAt ?? tick.at, window);
|
|
1717
|
+
if (free === 0 || held <= 0) return;
|
|
1718
|
+
const key = `${tick.seat}\0${tick.reason}`;
|
|
1719
|
+
const row = rows.get(key) ?? { seat: tick.seat, reason: tick.reason, slotMinutes: 0, ticks: 0, lines: [] };
|
|
1720
|
+
rows.set(key, { ...row, slotMinutes: row.slotMinutes + free * held, ticks: row.ticks + 1, lines: [...row.lines, tick.line] });
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
return [...rows.values()].map((r) => ({ ...r, slotMinutes: Math.round(r.slotMinutes) })).sort((a, b) => b.slotMinutes - a.slotMinutes);
|
|
1724
|
+
}
|
|
1725
|
+
function heldMinutes(from, to, window) {
|
|
1726
|
+
const start = Math.max(Date.parse(from), window.since ? Date.parse(window.since) : -Infinity);
|
|
1727
|
+
const capped = Math.min(Date.parse(to), Date.parse(from) + TICK_HOLD_MAX_MIN * 6e4);
|
|
1728
|
+
const end = Math.min(capped, window.until ? Date.parse(window.until) : Infinity);
|
|
1729
|
+
return Math.max(0, (end - start) / 6e4);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// src/blocked-flow.ts
|
|
1733
|
+
import { z as z6 } from "zod";
|
|
1734
|
+
var BLOCKED_FLOW_SOURCES = {
|
|
1735
|
+
verdicts: {
|
|
1736
|
+
command: `sqlite3 -readonly <events.db> "SELECT id, ts, target, body FROM events WHERE kind='message' AND body LIKE 'Verdict:%'"`,
|
|
1737
|
+
field: "events.ts, events.target, events.body lines Verdict:, PR:, Head:"
|
|
1738
|
+
},
|
|
1739
|
+
pulls: { command: "gh api repos/<owner>/<repo>/pulls/<n>", field: ".state, .merged_at, .head.sha" },
|
|
1740
|
+
denials: {
|
|
1741
|
+
command: "grep 'auto mode classifier. Reason: \\[' <seat transcript>.jsonl",
|
|
1742
|
+
field: "tool_result.content Reason: [<class>], joined to tool_use.input by tool_use_id"
|
|
1743
|
+
},
|
|
1744
|
+
journals: { command: "grep -nE 'impl [0-9]+/[0-9]+' <seat journal>.md", field: "impl <used>/<cap>, No dispatch: <reason>" }
|
|
1745
|
+
};
|
|
1746
|
+
var source = z6.enum(["verdicts", "pulls", "denials", "journals"]);
|
|
1747
|
+
var count5 = z6.number().int().nonnegative();
|
|
1748
|
+
var latencyRowSchema = z6.object({
|
|
1749
|
+
repo: z6.string(),
|
|
1750
|
+
prs: count5,
|
|
1751
|
+
staleHead: count5,
|
|
1752
|
+
closed: count5,
|
|
1753
|
+
unknown: count5,
|
|
1754
|
+
all: latencyStatsSchema,
|
|
1755
|
+
before: latencyStatsSchema.optional(),
|
|
1756
|
+
after: latencyStatsSchema.optional()
|
|
1757
|
+
});
|
|
1758
|
+
var openRowSchema = z6.object({
|
|
1759
|
+
repo: z6.string(),
|
|
1760
|
+
pr: count5,
|
|
1761
|
+
head: z6.string(),
|
|
1762
|
+
seat: z6.string(),
|
|
1763
|
+
verdictAt: z6.string(),
|
|
1764
|
+
ageMin: z6.number(),
|
|
1765
|
+
eventId: count5
|
|
1766
|
+
});
|
|
1767
|
+
var blockedFlowSchema = z6.object({
|
|
1768
|
+
asOf: z6.string(),
|
|
1769
|
+
window: z6.object({ since: z6.string().optional(), until: z6.string().optional() }),
|
|
1770
|
+
splitAt: z6.string().optional(),
|
|
1771
|
+
sources: z6.record(source, z6.object({ command: z6.string(), field: z6.string() })),
|
|
1772
|
+
verdictToMerge: z6.object({ cites: z6.array(source), rows: z6.array(latencyRowSchema) }),
|
|
1773
|
+
openHoldingMerge: z6.object({ cites: z6.array(source), prMinutes: z6.number(), rows: z6.array(openRowSchema) }),
|
|
1774
|
+
denials: z6.object({
|
|
1775
|
+
cites: z6.array(source),
|
|
1776
|
+
total: count5,
|
|
1777
|
+
rows: z6.array(z6.object({ reason: z6.string(), action: z6.string(), seat: z6.string(), count: count5 }))
|
|
1778
|
+
}),
|
|
1779
|
+
idleSlots: z6.object({
|
|
1780
|
+
cites: z6.array(source),
|
|
1781
|
+
holdMaxMin: z6.number(),
|
|
1782
|
+
total: z6.number(),
|
|
1783
|
+
rows: z6.array(z6.object({ seat: z6.string(), reason: z6.string(), slotMinutes: z6.number(), ticks: count5, lines: z6.array(count5) }))
|
|
1784
|
+
})
|
|
1785
|
+
});
|
|
1786
|
+
var ALL_REPOS = "all";
|
|
1787
|
+
function blockedFlowReport(input) {
|
|
1788
|
+
const window = input.window ?? {};
|
|
1789
|
+
const inScope = scopeTest(input.seats, window);
|
|
1790
|
+
const outcomes = mergeOutcomes(input.verdicts.filter((v) => inScope(v.seat, v.at)), input.pulls, input.asOf);
|
|
1791
|
+
const open = openRows(outcomes);
|
|
1792
|
+
const journals = input.journals.map((j) => ({ ...j, ticks: j.ticks.filter((t) => inScope(t.seat)) }));
|
|
1793
|
+
const idle = idleSlotMinutes(journals, window);
|
|
1794
|
+
return {
|
|
1795
|
+
asOf: input.asOf,
|
|
1796
|
+
window,
|
|
1797
|
+
...input.splitAt ? { splitAt: input.splitAt } : {},
|
|
1798
|
+
sources: BLOCKED_FLOW_SOURCES,
|
|
1799
|
+
verdictToMerge: { cites: ["verdicts", "pulls"], rows: latencyRows(outcomes, input.splitAt) },
|
|
1800
|
+
openHoldingMerge: { cites: ["verdicts", "pulls"], prMinutes: round(open.reduce((sum2, r) => sum2 + r.ageMin, 0)), rows: open },
|
|
1801
|
+
denials: denialSection(dedupeDenials(input.denials).filter((d) => inScope(d.seat, d.at))),
|
|
1802
|
+
idleSlots: { cites: ["journals"], holdMaxMin: TICK_HOLD_MAX_MIN, total: idle.reduce((sum2, r) => sum2 + r.slotMinutes, 0), rows: idle }
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
function scopeTest(seats, window) {
|
|
1806
|
+
const named = seats ? new Set(seats) : null;
|
|
1807
|
+
return (seat, at) => (!named || named.has(seat)) && (at === void 0 || (!window.since || at >= window.since) && (!window.until || at < window.until));
|
|
1808
|
+
}
|
|
1809
|
+
function latencyRows(outcomes, splitAt) {
|
|
1810
|
+
const repos = [...new Set(outcomes.map((o) => o.repo))];
|
|
1811
|
+
const rows = repos.map((repo) => latencyRow(repo, outcomes.filter((o) => o.repo === repo), splitAt));
|
|
1812
|
+
return [...rows.sort((a, b) => b.prs - a.prs || a.repo.localeCompare(b.repo)), latencyRow(ALL_REPOS, outcomes, splitAt)];
|
|
1813
|
+
}
|
|
1814
|
+
function latencyRow(repo, outcomes, splitAt) {
|
|
1815
|
+
const status = (s) => outcomes.filter((o) => o.status === s).length;
|
|
1816
|
+
const row = { repo, prs: outcomes.length, staleHead: status("stale-head"), closed: status("closed"), unknown: status("unknown"), all: latencyStats(outcomes) };
|
|
1817
|
+
if (!splitAt) return row;
|
|
1818
|
+
const timed = outcomes.filter((o) => o.verdict !== null);
|
|
1819
|
+
return { ...row, before: latencyStats(timed.filter((o) => o.verdict.at < splitAt)), after: latencyStats(timed.filter((o) => o.verdict.at >= splitAt)) };
|
|
1820
|
+
}
|
|
1821
|
+
function openRows(outcomes) {
|
|
1822
|
+
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);
|
|
1823
|
+
}
|
|
1824
|
+
function denialSection(denials) {
|
|
1825
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1826
|
+
for (const d of denials) {
|
|
1827
|
+
const key = [d.reason, d.action, d.seat].join("\0");
|
|
1828
|
+
const row = counts.get(key) ?? { reason: d.reason, action: d.action, seat: d.seat, count: 0 };
|
|
1829
|
+
counts.set(key, { ...row, count: row.count + 1 });
|
|
1830
|
+
}
|
|
1831
|
+
const rows = [...counts.values()].sort((a, b) => b.count - a.count || a.reason.localeCompare(b.reason) || a.action.localeCompare(b.action));
|
|
1832
|
+
return { cites: ["denials"], total: denials.length, rows };
|
|
1833
|
+
}
|
|
1834
|
+
function renderBlockedFlowText(report) {
|
|
1835
|
+
const scope = `Blocked flow as of ${report.asOf}${report.window.since ? `, since ${report.window.since}` : ""}${report.window.until ? `, until ${report.window.until}` : ""}`;
|
|
1836
|
+
const latency = [latencyTable("Verdict to merge, minutes from the first MERGE at the final head [verdictToMerge.rows[].all]", report.verdictToMerge.rows, "all")];
|
|
1837
|
+
if (report.splitAt) latency.push(...["before", "after"].map((part) => latencyTable(`Verdict to merge, verdicts ${part} ${report.splitAt} [verdictToMerge.rows[].${part}]`, report.verdictToMerge.rows, part)));
|
|
1838
|
+
return [scope, ...latency, openTable(report), denialTable(report), idleTable(report), sourceLines(report), LIST_PRICE_CAVEAT].join("\n\n") + "\n";
|
|
1839
|
+
}
|
|
1840
|
+
function latencyTable(title, rows, part) {
|
|
1841
|
+
const cells = rows.map((r) => [r.repo, r.prs, ...statCells(r[part]), r.staleHead]);
|
|
1842
|
+
return table(title, ["repo", "prs", "merged", "median", "mean", "p90", "max", "open", "oldest open", "median incl. open", "stale head"], cells);
|
|
1843
|
+
}
|
|
1844
|
+
function statCells(s) {
|
|
1845
|
+
const n = (v) => v === null ? "-" : v;
|
|
1846
|
+
return [s.merged, n(s.medianMin), n(s.meanMin), n(s.p90Min), n(s.maxMin), s.censored, n(s.oldestCensoredMin), n(s.medianWithCensoredMin)];
|
|
1847
|
+
}
|
|
1848
|
+
function openTable(report) {
|
|
1849
|
+
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}`]);
|
|
1850
|
+
const title = `Open PRs holding MERGE at their current head, ${report.openHoldingMerge.prMinutes} PR-minutes [openHoldingMerge.rows[].ageMin]`;
|
|
1851
|
+
return table(title, ["repo", "pr", "head", "seat", "verdict at", "age min", "verdict"], rows);
|
|
1852
|
+
}
|
|
1853
|
+
function denialTable(report) {
|
|
1854
|
+
const rows = report.denials.rows.map((r) => [r.reason, r.action, r.seat, r.count]);
|
|
1855
|
+
return table(`Classifier denials, ${report.denials.total} in all [denials.rows[].count]`, ["reason", "action", "seat", "count"], rows);
|
|
1856
|
+
}
|
|
1857
|
+
function idleTable(report) {
|
|
1858
|
+
const rows = report.idleSlots.rows.map((r) => [r.seat, r.reason, r.slotMinutes, r.ticks, r.lines.join(",")]);
|
|
1859
|
+
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]`;
|
|
1860
|
+
return table(title, ["seat", "reason", "slot-min", "ticks", "journal lines"], rows);
|
|
1861
|
+
}
|
|
1862
|
+
function sourceLines(report) {
|
|
1863
|
+
return ["Sources", ...Object.entries(report.sources).map(([name, s]) => `${name}: ${s.command} (${s.field})`)].join("\n");
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// src/liveness-broker.ts
|
|
1867
|
+
function parseBrokerLog(lines2) {
|
|
1868
|
+
const entries = [];
|
|
1869
|
+
let line = 0;
|
|
1870
|
+
for (const text of lines2) {
|
|
1871
|
+
line += 1;
|
|
1872
|
+
const fields = text.startsWith("{") ? parseObject(text) : null;
|
|
1873
|
+
if (fields && typeof fields.ts === "string" && typeof fields.event === "string") entries.push({ line, ts: fields.ts, event: fields.event, fields });
|
|
1874
|
+
}
|
|
1875
|
+
return entries;
|
|
1876
|
+
}
|
|
1877
|
+
function parseObject(text) {
|
|
1878
|
+
try {
|
|
1879
|
+
const value = JSON.parse(text);
|
|
1880
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
1881
|
+
} catch {
|
|
1882
|
+
return null;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
function stringField(entry, key) {
|
|
1886
|
+
const value = entry.fields[key];
|
|
1887
|
+
return typeof value === "string" ? value : null;
|
|
1888
|
+
}
|
|
1889
|
+
function splitNames(to) {
|
|
1890
|
+
return (to ?? "").split(",").map((name) => name.trim()).filter((name) => name.length > 0);
|
|
1891
|
+
}
|
|
1892
|
+
function minutesBetween2(from, to) {
|
|
1893
|
+
return Math.round((Date.parse(to) - Date.parse(from)) / 6e3) / 10;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// src/liveness-routes.ts
|
|
1897
|
+
var INBOX_KINDS = /* @__PURE__ */ new Set(["answer", "decided"]);
|
|
1898
|
+
function routeMisses(entries) {
|
|
1899
|
+
return entries.filter((e) => e.event === "route").flatMap((entry) => {
|
|
1900
|
+
const base = { line: entry.line, at: entry.ts, from: stringField(entry, "from") ?? "", kind: stringField(entry, "kind") ?? "" };
|
|
1901
|
+
return missedRecipients(entry).map(({ recipient, miss }) => ({ ...base, recipient, miss }));
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
function missedRecipients(entry) {
|
|
1905
|
+
const to = stringField(entry, "to");
|
|
1906
|
+
const kind = stringField(entry, "kind") ?? "";
|
|
1907
|
+
if (to === null || to.startsWith("tag ") || kind === "broadcast") return [];
|
|
1908
|
+
const named = splitNames(to);
|
|
1909
|
+
if (entry.fields.delivered === false) {
|
|
1910
|
+
const miss = entry.fields.held !== void 0 || INBOX_KINDS.has(kind) ? "queued" : "failed";
|
|
1911
|
+
return named.map((recipient) => ({ recipient, miss }));
|
|
1912
|
+
}
|
|
1913
|
+
const reached = entry.fields.recipients;
|
|
1914
|
+
if (entry.fields.delivered !== true || !Array.isArray(reached)) return [];
|
|
1915
|
+
return named.filter((name) => !reached.includes(name)).map((recipient) => ({ recipient, miss: "partial" }));
|
|
1916
|
+
}
|
|
1917
|
+
function routeFailureRows(misses) {
|
|
1918
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1919
|
+
for (const m of misses) {
|
|
1920
|
+
const row = rows.get(m.recipient) ?? { recipient: m.recipient, failed: 0, partial: 0, queued: 0, first: m.at, last: m.at, lines: [] };
|
|
1921
|
+
rows.set(m.recipient, { ...row, [m.miss]: row[m.miss] + 1, last: m.at, lines: [...row.lines, m.line] });
|
|
1922
|
+
}
|
|
1923
|
+
const total = (r) => r.failed + r.partial + r.queued;
|
|
1924
|
+
return [...rows.values()].sort((a, b) => total(b) - total(a) || a.recipient.localeCompare(b.recipient));
|
|
1925
|
+
}
|
|
1926
|
+
function countMisses(misses) {
|
|
1927
|
+
return { failed: misses.filter((m) => m.miss === "failed").length, partial: misses.filter((m) => m.miss === "partial").length, queued: misses.filter((m) => m.miss === "queued").length };
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
// src/liveness-dark.ts
|
|
1931
|
+
var DARK_MIN = 5;
|
|
1932
|
+
var TELEPORT_EVENTS = /* @__PURE__ */ new Set(["teleport_started", "teleport_completed", "teleport_failed", "teleport_aborted"]);
|
|
1933
|
+
function darkGaps(entries, misses, asOf) {
|
|
1934
|
+
return gapsByLifecycle(entries, asOf).filter((gap) => minutesBetween2(gap.from, gap.to ?? asOf) > DARK_MIN).map((gap) => toDarkGap(gap, misses, asOf)).filter((gap) => gap.to !== null || gap.failedRoutes + gap.partialRoutes + gap.queuedRoutes > 0).sort((a, b) => a.from.localeCompare(b.from) || a.seat.localeCompare(b.seat));
|
|
1935
|
+
}
|
|
1936
|
+
function gapsByLifecycle(entries, asOf) {
|
|
1937
|
+
const states = /* @__PURE__ */ new Map();
|
|
1938
|
+
const gaps = [];
|
|
1939
|
+
const keep = (state, gap) => {
|
|
1940
|
+
if (!state.exited || gap.teleportLines.length > 0) gaps.push(gap);
|
|
1941
|
+
};
|
|
1942
|
+
for (const entry of entries) {
|
|
1943
|
+
const seat = stringField(entry, "name");
|
|
1944
|
+
if (seat === null || entry.ts >= asOf) continue;
|
|
1945
|
+
const state = states.get(seat) ?? { open: null, teleportLines: [], exited: false };
|
|
1946
|
+
states.set(seat, state);
|
|
1947
|
+
step(state, entry, (gap) => keep(state, { ...gap, seat, to: entry.ts, closeLine: entry.line }));
|
|
1948
|
+
}
|
|
1949
|
+
for (const [seat, state] of states) if (state.open) keep(state, { ...state.open, seat, to: null });
|
|
1950
|
+
return gaps;
|
|
1951
|
+
}
|
|
1952
|
+
function step(state, entry, close) {
|
|
1953
|
+
if (entry.event === "agent_exited") {
|
|
1954
|
+
state.exited ||= exitedCleanly(entry);
|
|
1955
|
+
} else if (TELEPORT_EVENTS.has(entry.event)) {
|
|
1956
|
+
state.teleportLines.push(entry.line);
|
|
1957
|
+
state.open?.teleportLines.push(entry.line);
|
|
1958
|
+
} else if (entry.event === "deregistered" && !state.open) {
|
|
1959
|
+
state.open = { from: entry.ts, lines: [entry.line], teleportLines: [...state.teleportLines] };
|
|
1960
|
+
} else if (entry.event === "registered") {
|
|
1961
|
+
if (state.open) close(state.open);
|
|
1962
|
+
state.open = null;
|
|
1963
|
+
state.teleportLines = [];
|
|
1964
|
+
state.exited = false;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
function exitedCleanly(entry) {
|
|
1968
|
+
return entry.fields.code === 0 && entry.fields.inferred !== true;
|
|
1969
|
+
}
|
|
1970
|
+
function toDarkGap(gap, misses, asOf) {
|
|
1971
|
+
const end = gap.to ?? asOf;
|
|
1972
|
+
const missed = misses.filter((m) => m.recipient === gap.seat && m.at >= gap.from && m.at < end);
|
|
1973
|
+
const counts = countMisses(missed);
|
|
1974
|
+
return {
|
|
1975
|
+
seat: gap.seat,
|
|
1976
|
+
from: gap.from,
|
|
1977
|
+
to: gap.to,
|
|
1978
|
+
minutes: minutesBetween2(gap.from, end),
|
|
1979
|
+
teleport: gap.teleportLines.length > 0,
|
|
1980
|
+
failedRoutes: counts.failed,
|
|
1981
|
+
partialRoutes: counts.partial,
|
|
1982
|
+
queuedRoutes: counts.queued,
|
|
1983
|
+
lines: [...gap.lines, ...gap.closeLine === void 0 ? [] : [gap.closeLine], ...gap.teleportLines],
|
|
1984
|
+
routeLines: missed.map((m) => m.line)
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// src/liveness-exits.ts
|
|
1989
|
+
var UNKNOWN_PROFILE = "unknown";
|
|
1990
|
+
function unreportedExitRows(entries, spawns) {
|
|
1991
|
+
const byAgent = new Map(spawns.map((s) => [s.agentId, s]));
|
|
1992
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1993
|
+
for (const entry of entries.filter((e) => e.event === "unreported-exit")) {
|
|
1994
|
+
const agentId = stringField(entry, "agentId") ?? "";
|
|
1995
|
+
const spawn = byAgent.get(agentId);
|
|
1996
|
+
const profile = spawn?.profile ?? UNKNOWN_PROFILE;
|
|
1997
|
+
const exit = {
|
|
1998
|
+
at: entry.ts,
|
|
1999
|
+
line: entry.line,
|
|
2000
|
+
agentId,
|
|
2001
|
+
name: stringField(entry, "name") ?? spawn?.name ?? "",
|
|
2002
|
+
spawner: stringField(entry, "spawner") ?? "",
|
|
2003
|
+
lastAction: stringField(entry, "lastAction") ?? "",
|
|
2004
|
+
spawnEventId: spawn?.eventId ?? null
|
|
2005
|
+
};
|
|
2006
|
+
rows.set(profile, [...rows.get(profile) ?? [], exit]);
|
|
2007
|
+
}
|
|
2008
|
+
return [...rows.entries()].map(([profile, exits]) => ({ profile, count: exits.length, exits })).sort((a, b) => b.count - a.count || a.profile.localeCompare(b.profile));
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// src/liveness-prompts.ts
|
|
2012
|
+
var PROMPT_STALE_MIN = 10;
|
|
2013
|
+
function stalePromptRows(lastEvents, broker, asOf) {
|
|
2014
|
+
const gone = goneSinceRestart(broker, asOf);
|
|
2015
|
+
return lastEvents.filter((e) => e.kind === "approval_request" && e.at < asOf && e.endEventId === null && !gone(e)).map((e) => ({ agent: e.actor, at: e.at, ageMin: minutesBetween2(e.at, asOf), tool: e.tool, eventId: e.eventId, resolutionEventId: e.resolutionEventId })).filter((row) => row.ageMin > PROMPT_STALE_MIN).sort((a, b) => b.ageMin - a.ageMin || a.agent.localeCompare(b.agent));
|
|
2016
|
+
}
|
|
2017
|
+
function goneSinceRestart(broker, asOf) {
|
|
2018
|
+
const before = broker.filter((e) => e.ts < asOf);
|
|
2019
|
+
const restart = before.filter((e) => e.event === "broker_started").at(-1)?.ts;
|
|
2020
|
+
if (restart === void 0) return () => false;
|
|
2021
|
+
const live = new Set(before.filter((e) => e.event === "registered" && e.ts >= restart).map((e) => stringField(e, "name")));
|
|
2022
|
+
return (e) => e.at < restart && !live.has(e.actor);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// src/liveness.ts
|
|
2026
|
+
import { z as z7 } from "zod";
|
|
2027
|
+
var LIVENESS_SOURCES = {
|
|
2028
|
+
registrations: {
|
|
2029
|
+
command: `grep -nE '"event":"(registered|deregistered|agent_exited|teleport_started|teleport_completed|teleport_failed|teleport_aborted)"' <broker.log>`,
|
|
2030
|
+
field: "ts, event, name; cited as broker.log line numbers; a gap with a clean agent_exited (code 0, not inferred) and no teleport is a resume"
|
|
2031
|
+
},
|
|
2032
|
+
routes: { command: `grep -n '"event":"route"' <broker.log>`, field: "to, delivered, recipients" },
|
|
2033
|
+
exits: { command: `grep -n '"event":"unreported-exit"' <broker.log>`, field: "agentId, name, spawner, lastAction" },
|
|
2034
|
+
spawns: {
|
|
2035
|
+
command: `sqlite3 -readonly <events.db> "SELECT id, msg_id, target, json_extract(meta,'$.profile') FROM events WHERE kind='agent_spawned'"`,
|
|
2036
|
+
field: "events.msg_id (agent id), events.meta.profile"
|
|
2037
|
+
},
|
|
2038
|
+
prompts: {
|
|
2039
|
+
command: `sqlite3 -readonly <events.db> "SELECT e.* FROM events e JOIN (SELECT actor, max(id) id FROM events WHERE ts < <asOf> AND kind != 'resolution' GROUP BY actor) l ON e.id = l.id WHERE e.kind='approval_request'"`,
|
|
2040
|
+
field: "events.ts, events.actor, events.meta.tool_name; a resolution row's events.ref; skipped after the actor's agent_exited or agent_retired, or a broker_started it never re-registered after"
|
|
2041
|
+
}
|
|
2042
|
+
};
|
|
2043
|
+
var source2 = z7.enum(["registrations", "routes", "exits", "spawns", "prompts"]);
|
|
2044
|
+
var count6 = z7.number().int().nonnegative();
|
|
2045
|
+
var lines = z7.array(count6);
|
|
2046
|
+
var darkRow = z7.object({
|
|
2047
|
+
seat: z7.string(),
|
|
2048
|
+
from: z7.string(),
|
|
2049
|
+
to: z7.string().nullable(),
|
|
2050
|
+
minutes: z7.number(),
|
|
2051
|
+
teleport: z7.boolean(),
|
|
2052
|
+
failedRoutes: count6,
|
|
2053
|
+
partialRoutes: count6,
|
|
2054
|
+
queuedRoutes: count6,
|
|
2055
|
+
lines,
|
|
2056
|
+
routeLines: lines
|
|
2057
|
+
});
|
|
2058
|
+
var exitRow = z7.object({ at: z7.string(), line: count6, agentId: z7.string(), name: z7.string(), spawner: z7.string(), lastAction: z7.string(), spawnEventId: count6.nullable() });
|
|
2059
|
+
var promptRow = z7.object({ agent: z7.string(), at: z7.string(), ageMin: z7.number(), tool: z7.string().nullable(), eventId: count6, resolutionEventId: count6.nullable() });
|
|
2060
|
+
var livenessSchema = z7.object({
|
|
2061
|
+
asOf: z7.string(),
|
|
2062
|
+
window: z7.object({ since: z7.string().optional(), until: z7.string().optional() }),
|
|
2063
|
+
thresholds: z7.object({ darkMin: z7.number(), promptStaleMin: z7.number() }),
|
|
2064
|
+
sources: z7.record(source2, z7.object({ command: z7.string(), field: z7.string() })),
|
|
2065
|
+
darkSeats: z7.object({ cites: z7.array(source2), withTeleport: count6, withoutTeleport: count6, rows: z7.array(darkRow) }),
|
|
2066
|
+
routeFailures: z7.object({
|
|
2067
|
+
cites: z7.array(source2),
|
|
2068
|
+
failed: count6,
|
|
2069
|
+
partial: count6,
|
|
2070
|
+
queued: count6,
|
|
2071
|
+
rows: z7.array(z7.object({ recipient: z7.string(), failed: count6, partial: count6, queued: count6, first: z7.string(), last: z7.string(), lines }))
|
|
2072
|
+
}),
|
|
2073
|
+
unreportedExits: z7.object({ cites: z7.array(source2), total: count6, rows: z7.array(z7.object({ profile: z7.string(), count: count6, exits: z7.array(exitRow) })) }),
|
|
2074
|
+
stalePrompts: z7.object({ cites: z7.array(source2), rows: z7.array(promptRow), resolvedRows: z7.array(promptRow) })
|
|
2075
|
+
});
|
|
2076
|
+
function livenessReport(input) {
|
|
2077
|
+
const window = input.window ?? {};
|
|
2078
|
+
const keep = scopeTest2(input.seats, window);
|
|
2079
|
+
const broker = input.broker.filter((e) => e.ts < input.asOf);
|
|
2080
|
+
const misses = routeMisses(broker);
|
|
2081
|
+
const dark = darkGaps(broker, misses, input.asOf).filter((g) => keep([g.seat], g.from));
|
|
2082
|
+
const routeMissesInScope = misses.filter((m) => keep([m.recipient], m.at));
|
|
2083
|
+
const routes = routeFailureRows(routeMissesInScope);
|
|
2084
|
+
const prompts = stalePromptRows(input.lastEvents, broker, input.asOf).filter((p) => !input.seats || input.seats.includes(p.agent));
|
|
2085
|
+
const exits = unreportedExitRows(broker.filter((e) => keep([stringField(e, "name") ?? "", stringField(e, "spawner") ?? ""], e.ts)), input.spawns);
|
|
2086
|
+
return {
|
|
2087
|
+
asOf: input.asOf,
|
|
2088
|
+
window,
|
|
2089
|
+
thresholds: { darkMin: DARK_MIN, promptStaleMin: PROMPT_STALE_MIN },
|
|
2090
|
+
sources: LIVENESS_SOURCES,
|
|
2091
|
+
darkSeats: { cites: ["registrations", "routes"], withTeleport: dark.filter((g) => g.teleport).length, withoutTeleport: dark.filter((g) => !g.teleport).length, rows: dark },
|
|
2092
|
+
routeFailures: { cites: ["routes"], ...countMisses(routeMissesInScope), rows: routes },
|
|
2093
|
+
unreportedExits: { cites: ["exits", "spawns"], total: sum(exits.map((r) => r.count)), rows: exits },
|
|
2094
|
+
stalePrompts: { cites: ["prompts"], rows: prompts.filter((p) => p.resolutionEventId === null), resolvedRows: prompts.filter((p) => p.resolutionEventId !== null) }
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
function scopeTest2(seats, window) {
|
|
2098
|
+
const named = seats ? new Set(seats) : null;
|
|
2099
|
+
return (names, at) => (!named || names.some((n) => named.has(n))) && (!window.since || at >= window.since) && (!window.until || at < window.until);
|
|
2100
|
+
}
|
|
2101
|
+
function sum(values) {
|
|
2102
|
+
return values.reduce((total, v) => total + v, 0);
|
|
2103
|
+
}
|
|
2104
|
+
var CITED_LINES = 4;
|
|
2105
|
+
function cite(prefix, ids) {
|
|
2106
|
+
if (ids.length === 0) return "-";
|
|
2107
|
+
const shown = ids.slice(0, CITED_LINES).map((id) => `${prefix}${id}`).join(",");
|
|
2108
|
+
return ids.length > CITED_LINES ? `${shown} +${ids.length - CITED_LINES}` : shown;
|
|
2109
|
+
}
|
|
2110
|
+
function renderLivenessText(report) {
|
|
2111
|
+
const scope = `Liveness as of ${report.asOf}${report.window.since ? `, since ${report.window.since}` : ""}${report.window.until ? `, until ${report.window.until}` : ""}`;
|
|
2112
|
+
const sources = ["Sources", ...Object.entries(report.sources).map(([name, s]) => `${name}: ${s.command} (${s.field})`)].join("\n");
|
|
2113
|
+
return [scope, darkTable(report), routeTable(report), exitTable(report), promptTable(report), sources, LIST_PRICE_CAVEAT].join("\n\n") + "\n";
|
|
2114
|
+
}
|
|
2115
|
+
function darkTable(report) {
|
|
2116
|
+
const { rows, withTeleport, withoutTeleport } = report.darkSeats;
|
|
2117
|
+
const cells = rows.map((r) => [r.seat, r.from, r.to ?? "still dark", r.minutes, r.teleport ? "yes" : "no", r.failedRoutes, r.partialRoutes, r.queuedRoutes, cite("broker.log:", [...r.lines, ...r.routeLines])]);
|
|
2118
|
+
const title = `Seats dark over ${report.thresholds.darkMin} min, ${withTeleport} with a teleport and ${withoutTeleport} without [darkSeats.rows[]]`;
|
|
2119
|
+
return table(title, ["seat", "from", "to", "min", "teleport", "failed routes", "partial routes", "queued routes", "cites"], cells);
|
|
2120
|
+
}
|
|
2121
|
+
function routeTable(report) {
|
|
2122
|
+
const { rows, failed, partial, queued } = report.routeFailures;
|
|
2123
|
+
const cells = rows.map((r) => [r.recipient, r.failed, r.partial, r.queued, r.first, r.last, cite("broker.log:", r.lines)]);
|
|
2124
|
+
const title = `Routes that missed a recipient, ${failed} dropped, ${partial} partial and ${queued} held or queued for later [routeFailures.rows[]]`;
|
|
2125
|
+
return table(title, ["recipient", "failed", "partial", "queued", "first", "last", "cites"], cells);
|
|
2126
|
+
}
|
|
2127
|
+
function exitTable(report) {
|
|
2128
|
+
const cells = report.unreportedExits.rows.flatMap((r) => r.exits.map((e) => [r.profile, e.name, e.spawner, e.lastAction, e.at, cite("broker.log:", [e.line]), e.spawnEventId === null ? "-" : `events#${e.spawnEventId}`]));
|
|
2129
|
+
const title = `Unreported exits by profile, ${report.unreportedExits.total} in all [unreportedExits.rows[].exits[]]`;
|
|
2130
|
+
return table(title, ["profile", "agent", "spawner", "last action", "at", "exit", "spawn"], cells);
|
|
2131
|
+
}
|
|
2132
|
+
function promptTable(report) {
|
|
2133
|
+
const cells = (rows) => rows.map((r) => [r.agent, r.tool ?? "-", r.at, r.ageMin, `events#${r.eventId}`, r.resolutionEventId === null ? "-" : `events#${r.resolutionEventId}`]);
|
|
2134
|
+
const head = ["agent", "tool", "at", "age min", "prompt", "resolution"];
|
|
2135
|
+
const over = `over ${report.thresholds.promptStaleMin} min old`;
|
|
2136
|
+
return [
|
|
2137
|
+
table(`Agents whose last event is an unresolved permission prompt ${over} [stalePrompts.rows[]]`, head, cells(report.stalePrompts.rows)),
|
|
2138
|
+
table(`Agents silent since a resolved permission prompt ${over} [stalePrompts.resolvedRows[]]`, head, cells(report.stalePrompts.resolvedRows))
|
|
2139
|
+
].join("\n\n");
|
|
732
2140
|
}
|
|
733
2141
|
export {
|
|
2142
|
+
ACTION_CLASSES,
|
|
2143
|
+
AGENT_LIFECYCLE,
|
|
2144
|
+
ALL_REPOS,
|
|
2145
|
+
BLOCKED_FLOW_SOURCES,
|
|
2146
|
+
BOOT_TOOL,
|
|
734
2147
|
CHANNEL_CLUSTER_MS,
|
|
735
2148
|
CONTEXT_BANDS,
|
|
736
2149
|
CONTEXT_RESET_TOKENS,
|
|
737
2150
|
COORDINATOR_MERGE_DEDUPE_REQUESTS,
|
|
738
2151
|
COORDINATOR_MIN_EPISODE_REQUESTS,
|
|
739
2152
|
COORDINATOR_WAVE_QUIET_REQUESTS,
|
|
2153
|
+
COST_REPORT_SECTIONS,
|
|
2154
|
+
DARK_MIN,
|
|
2155
|
+
DEFAULT_ACTION_RULES,
|
|
2156
|
+
DEFAULT_CONFIGURED_K,
|
|
2157
|
+
DEFAULT_EPISODE_ROLES,
|
|
2158
|
+
DEFAULT_K_SWEEP,
|
|
2159
|
+
DEFAULT_MECHANICAL_CLASSES,
|
|
2160
|
+
DEFAULT_NO_ACTION_CLASSES,
|
|
2161
|
+
DEFAULT_REVIEWER_PRS,
|
|
2162
|
+
DEFAULT_REVIEWER_ROLE,
|
|
2163
|
+
DEFAULT_STANDING_ROLE,
|
|
740
2164
|
GAP_BANDS,
|
|
741
2165
|
HEURISTIC_VERSIONS,
|
|
742
2166
|
IDLE_GAP_MS,
|
|
743
2167
|
LIST_PRICE_CAVEAT,
|
|
2168
|
+
LIVENESS_SOURCES,
|
|
2169
|
+
POOLED_REVIEWERS,
|
|
744
2170
|
PRICE_TABLE,
|
|
745
2171
|
PRICE_TABLE_VERSION,
|
|
746
2172
|
PROFILE_ROLES,
|
|
2173
|
+
PROMPT_STALE_MIN,
|
|
2174
|
+
REBUILD_GAP_BANDS,
|
|
747
2175
|
STANDING_PEER_MIN_ASSIGNMENTS,
|
|
748
2176
|
STANDING_PEER_MIN_HOURS,
|
|
2177
|
+
TELEPORT_EVENT,
|
|
2178
|
+
TICK_HOLD_MAX_MIN,
|
|
749
2179
|
TOKEN_CLASSES,
|
|
2180
|
+
UNKNOWN_PROFILE,
|
|
2181
|
+
UNSTATED_REASON,
|
|
2182
|
+
WAKE_FROM_KINDS,
|
|
750
2183
|
assignmentCount,
|
|
751
2184
|
bandOf,
|
|
2185
|
+
blockedFlowReport,
|
|
2186
|
+
blockedFlowSchema,
|
|
752
2187
|
buildEpisodes,
|
|
2188
|
+
buildWakeEpisodes,
|
|
2189
|
+
cacheTtlReport,
|
|
2190
|
+
cacheTtlWhatIf,
|
|
2191
|
+
cacheTtlWhatIfSchema,
|
|
2192
|
+
classifyDeniedAction,
|
|
2193
|
+
classifyRequest,
|
|
753
2194
|
classifySession,
|
|
754
2195
|
contextBand,
|
|
2196
|
+
costPerRequest,
|
|
755
2197
|
costReport,
|
|
756
2198
|
costReportSchema,
|
|
2199
|
+
countMisses,
|
|
2200
|
+
darkGaps,
|
|
2201
|
+
dedupeDenials,
|
|
2202
|
+
episodeNames,
|
|
757
2203
|
findPrice,
|
|
758
2204
|
gapBand,
|
|
2205
|
+
handoffThreshold,
|
|
2206
|
+
handoffThresholdSchema,
|
|
759
2207
|
heuristicFor,
|
|
2208
|
+
idleSlotMinutes,
|
|
760
2209
|
initiativeFromCwd,
|
|
2210
|
+
isBootAction,
|
|
2211
|
+
latencyStats,
|
|
2212
|
+
latencyStatsSchema,
|
|
2213
|
+
livenessReport,
|
|
2214
|
+
livenessSchema,
|
|
2215
|
+
mergeOutcomes,
|
|
2216
|
+
parseBrokerLog,
|
|
2217
|
+
parseDenials,
|
|
2218
|
+
parseSeatJournal,
|
|
2219
|
+
parseTeleportEvents,
|
|
2220
|
+
parseVerdict,
|
|
2221
|
+
prKey,
|
|
761
2222
|
priceRequest,
|
|
762
2223
|
readEpisodeInput,
|
|
2224
|
+
readRequestToolCalls,
|
|
2225
|
+
readTtlRows,
|
|
2226
|
+
renderBlockedFlowText,
|
|
2227
|
+
renderCacheTtlText,
|
|
2228
|
+
renderCostReportSections,
|
|
763
2229
|
renderCostReportText,
|
|
2230
|
+
renderLivenessText,
|
|
764
2231
|
roleFromProfile,
|
|
2232
|
+
routeFailureRows,
|
|
2233
|
+
routeMisses,
|
|
2234
|
+
scopeFilter,
|
|
765
2235
|
sessionInitiative,
|
|
766
2236
|
sessionRole,
|
|
2237
|
+
stalePromptRows,
|
|
2238
|
+
summarizeWakeEpisodes,
|
|
2239
|
+
sweepK,
|
|
2240
|
+
unreportedExitRows,
|
|
2241
|
+
wakeEpisodesSchema,
|
|
767
2242
|
workerRole,
|
|
768
2243
|
writeEpisodes
|
|
769
2244
|
};
|