@titan-design/session-analytics 0.0.0 → 0.2.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 +31 -6
- package/dist/index.d.ts +255 -1
- package/dist/index.js +612 -1
- package/dist/index.js.map +1 -1
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# @titan-design/session-analytics
|
|
2
2
|
|
|
3
|
-
Pricing, session classification
|
|
4
|
-
|
|
3
|
+
Pricing, session classification, roles, episodes, banding and the standing cost report for
|
|
4
|
+
mined Claude Code sessions. The pricing, classification, role, episode and band functions are pure. `costReport`
|
|
5
|
+
reads a session graph through a read-only connection and never writes it; `writeEpisodes` is
|
|
6
|
+
the one function here that writes.
|
|
5
7
|
|
|
6
|
-
Tier 2 of the titan-platform DAG. It
|
|
7
|
-
|
|
8
|
+
Tier 2 of the titan-platform DAG. It depends on `@titan-design/session-graph` for table
|
|
9
|
+
names and on `@titan-design/store-sqlite` for the `Db` type. `zod` is a peer dependency.
|
|
8
10
|
|
|
9
11
|
```ts
|
|
10
12
|
import { classifySession, contextBand, priceRequest } from "@titan-design/session-analytics";
|
|
@@ -25,13 +27,36 @@ priceRequest(
|
|
|
25
27
|
- `classifySession(facts)` — `agent_spawned`, `human_interactive`, `headless_sdk` or
|
|
26
28
|
`other_headless`, plus `coordinator` or `adhoc` for human sessions.
|
|
27
29
|
- `CONTEXT_BANDS`, `GAP_BANDS`, `bandOf`, `contextBand`, `gapBand`.
|
|
30
|
+
- `costReport(db, { since, until, days, top, transcriptsDiscovered, facetVersion })` — the
|
|
31
|
+
standing cost report as one JSON object, and `costReportSchema`, its zod schema.
|
|
32
|
+
- `renderCostReportText(report)` — the same report as plain-text tables, ending with
|
|
33
|
+
`LIST_PRICE_CAVEAT` and the price-table and coverage footer.
|
|
34
|
+
- `roleFromProfile`, `workerRole(facts)`, `sessionRole(classification, facts)` — worker-v1
|
|
35
|
+
roles, including the standing-peer overlay.
|
|
36
|
+
- `buildEpisodes(input, "worker-v1" | "coordinator-v1")` (pure), `readEpisodeInput`,
|
|
37
|
+
`writeEpisodes(graph, sessionIds)` and `assignmentCount(rows)` — provisional episode
|
|
38
|
+
segmentation, written through session-graph's `replaceEpisodes`.
|
|
39
|
+
- `initiativeFromCwd(cwd)`, `sessionInitiative(tasks, cwd)` — a session's initiative from its
|
|
40
|
+
task edges, falling back to the `cf_analyze.py` cwd rule.
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
```ts
|
|
43
|
+
import { openDatabase } from "@titan-design/store-sqlite";
|
|
44
|
+
import { costReport, renderCostReportText } from "@titan-design/session-analytics";
|
|
45
|
+
|
|
46
|
+
const report = costReport(openDatabase(graphPath, { readonly: true }), { days: 7 });
|
|
47
|
+
process.stdout.write(renderCostReportText(report));
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Things that will bite you
|
|
30
51
|
|
|
31
52
|
Fable's cache read is **0.025** of its input rate, not the 0.1 every other model uses.
|
|
32
53
|
Misreading it overstated the 2026-09-20 audit by about $1,700.
|
|
33
54
|
|
|
34
55
|
An unknown model is **unpriced**: `priced: false` and zero cost. There is no default row,
|
|
35
|
-
because defaulting bills a new model at an old model's rate without saying so.
|
|
56
|
+
because defaulting bills a new model at an old model's rate without saying so. The cost
|
|
57
|
+
report lists such models under `unpricedModels`.
|
|
58
|
+
|
|
59
|
+
The cost report prices through the graph's `price` table, not through `PRICE_TABLE`. A graph
|
|
60
|
+
whose price rows were never synced reports every request as unpriced.
|
|
36
61
|
|
|
37
62
|
Full reference: `site/reference/session-analytics.md`.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { Db } from '@titan-design/store-sqlite';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { EpisodeRow, SessionGraph } from '@titan-design/session-graph';
|
|
4
|
+
|
|
1
5
|
/** USD per million tokens. Fitted against 392 `cost-state` rows in the 2026-09-20 audit. */
|
|
2
6
|
interface PriceRow {
|
|
3
7
|
/** Matched against the model string by longest prefix. */
|
|
@@ -88,4 +92,254 @@ declare function bandOf(value: number, bands: readonly Band[]): string | null;
|
|
|
88
92
|
declare function contextBand(tokens: number): string | null;
|
|
89
93
|
declare function gapBand(millis: number): string | null;
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
/** Initiatives a session's task edges name, with how many edges name each. */
|
|
96
|
+
interface TaskInitiative {
|
|
97
|
+
initiative: string;
|
|
98
|
+
edges: number;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A task edge names the initiative outright, so it wins over the cwd guess. Among several,
|
|
102
|
+
* the most-named initiative wins and a tie goes to the alphabetically first, for a stable report.
|
|
103
|
+
*/
|
|
104
|
+
declare function sessionInitiative(tasks: readonly TaskInitiative[], cwd: string | null | undefined): string;
|
|
105
|
+
/** The cwd rule from `cf_analyze.py` `initiative()`. A worktree under a repo counts as that repo. */
|
|
106
|
+
declare function initiativeFromCwd(cwd: string | null | undefined): string;
|
|
107
|
+
|
|
108
|
+
/** ISO bounds on request `ts`: `since` inclusive, `until` exclusive, null for open. */
|
|
109
|
+
interface ReportWindow {
|
|
110
|
+
since: string | null;
|
|
111
|
+
until: string | null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
declare const bucketSchema: z.ZodObject<{
|
|
115
|
+
key: z.ZodString;
|
|
116
|
+
requests: z.ZodNumber;
|
|
117
|
+
sessions: z.ZodNumber;
|
|
118
|
+
costUsd: z.ZodNumber;
|
|
119
|
+
}, z.core.$strip>;
|
|
120
|
+
declare const cellSchema: z.ZodObject<{
|
|
121
|
+
wakeCause: z.ZodString;
|
|
122
|
+
gapBand: z.ZodString;
|
|
123
|
+
requests: z.ZodNumber;
|
|
124
|
+
costUsd: z.ZodNumber;
|
|
125
|
+
}, z.core.$strip>;
|
|
126
|
+
declare const TOKEN_CLASSES: readonly ["input", "cache_read", "cache_write_5m", "cache_write_1h", "output"];
|
|
127
|
+
type TokenClass = (typeof TOKEN_CLASSES)[number];
|
|
128
|
+
declare const costReportSchema: z.ZodObject<{
|
|
129
|
+
window: z.ZodObject<{
|
|
130
|
+
since: z.ZodNullable<z.ZodString>;
|
|
131
|
+
until: z.ZodNullable<z.ZodString>;
|
|
132
|
+
}, z.core.$strip>;
|
|
133
|
+
priceTableVersion: z.ZodNullable<z.ZodNumber>;
|
|
134
|
+
totals: z.ZodObject<{
|
|
135
|
+
requests: z.ZodNumber;
|
|
136
|
+
sessions: z.ZodNumber;
|
|
137
|
+
costUsd: z.ZodNumber;
|
|
138
|
+
unpricedRequests: z.ZodNumber;
|
|
139
|
+
}, z.core.$strip>;
|
|
140
|
+
byTokenClass: z.ZodArray<z.ZodObject<{
|
|
141
|
+
tokenClass: z.ZodEnum<{
|
|
142
|
+
input: "input";
|
|
143
|
+
cache_read: "cache_read";
|
|
144
|
+
cache_write_5m: "cache_write_5m";
|
|
145
|
+
cache_write_1h: "cache_write_1h";
|
|
146
|
+
output: "output";
|
|
147
|
+
}>;
|
|
148
|
+
tokens: z.ZodNumber;
|
|
149
|
+
costUsd: z.ZodNumber;
|
|
150
|
+
}, z.core.$strip>>;
|
|
151
|
+
byAccount: z.ZodArray<z.ZodObject<{
|
|
152
|
+
key: z.ZodString;
|
|
153
|
+
requests: z.ZodNumber;
|
|
154
|
+
sessions: z.ZodNumber;
|
|
155
|
+
costUsd: z.ZodNumber;
|
|
156
|
+
}, z.core.$strip>>;
|
|
157
|
+
byModel: z.ZodArray<z.ZodObject<{
|
|
158
|
+
key: z.ZodString;
|
|
159
|
+
requests: z.ZodNumber;
|
|
160
|
+
sessions: z.ZodNumber;
|
|
161
|
+
costUsd: z.ZodNumber;
|
|
162
|
+
}, z.core.$strip>>;
|
|
163
|
+
byClass: z.ZodArray<z.ZodObject<{
|
|
164
|
+
key: z.ZodString;
|
|
165
|
+
requests: z.ZodNumber;
|
|
166
|
+
sessions: z.ZodNumber;
|
|
167
|
+
costUsd: z.ZodNumber;
|
|
168
|
+
}, z.core.$strip>>;
|
|
169
|
+
byRole: z.ZodArray<z.ZodObject<{
|
|
170
|
+
key: z.ZodString;
|
|
171
|
+
requests: z.ZodNumber;
|
|
172
|
+
sessions: z.ZodNumber;
|
|
173
|
+
costUsd: z.ZodNumber;
|
|
174
|
+
}, z.core.$strip>>;
|
|
175
|
+
byEpisodeCount: z.ZodArray<z.ZodObject<{
|
|
176
|
+
key: z.ZodString;
|
|
177
|
+
requests: z.ZodNumber;
|
|
178
|
+
sessions: z.ZodNumber;
|
|
179
|
+
costUsd: z.ZodNumber;
|
|
180
|
+
}, z.core.$strip>>;
|
|
181
|
+
byInitiative: z.ZodArray<z.ZodObject<{
|
|
182
|
+
key: z.ZodString;
|
|
183
|
+
requests: z.ZodNumber;
|
|
184
|
+
sessions: z.ZodNumber;
|
|
185
|
+
costUsd: z.ZodNumber;
|
|
186
|
+
}, z.core.$strip>>;
|
|
187
|
+
byContextBand: z.ZodArray<z.ZodObject<{
|
|
188
|
+
key: z.ZodString;
|
|
189
|
+
requests: z.ZodNumber;
|
|
190
|
+
sessions: z.ZodNumber;
|
|
191
|
+
costUsd: z.ZodNumber;
|
|
192
|
+
}, z.core.$strip>>;
|
|
193
|
+
byWakeCause: z.ZodArray<z.ZodObject<{
|
|
194
|
+
key: z.ZodString;
|
|
195
|
+
requests: z.ZodNumber;
|
|
196
|
+
sessions: z.ZodNumber;
|
|
197
|
+
costUsd: z.ZodNumber;
|
|
198
|
+
midLoopRequests: z.ZodNumber;
|
|
199
|
+
midLoopCostUsd: z.ZodNumber;
|
|
200
|
+
parts: z.ZodArray<z.ZodObject<{
|
|
201
|
+
key: z.ZodString;
|
|
202
|
+
requests: z.ZodNumber;
|
|
203
|
+
sessions: z.ZodNumber;
|
|
204
|
+
costUsd: z.ZodNumber;
|
|
205
|
+
midLoopRequests: z.ZodNumber;
|
|
206
|
+
midLoopCostUsd: z.ZodNumber;
|
|
207
|
+
}, z.core.$strip>>;
|
|
208
|
+
}, z.core.$strip>>;
|
|
209
|
+
wakeCauseByGapBand: z.ZodArray<z.ZodObject<{
|
|
210
|
+
wakeCause: z.ZodString;
|
|
211
|
+
gapBand: z.ZodString;
|
|
212
|
+
requests: z.ZodNumber;
|
|
213
|
+
costUsd: z.ZodNumber;
|
|
214
|
+
}, z.core.$strip>>;
|
|
215
|
+
coldRebuild: z.ZodObject<{
|
|
216
|
+
requests: z.ZodNumber;
|
|
217
|
+
costUsd: z.ZodNumber;
|
|
218
|
+
byGapBandAndCause: z.ZodArray<z.ZodObject<{
|
|
219
|
+
wakeCause: z.ZodString;
|
|
220
|
+
gapBand: z.ZodString;
|
|
221
|
+
requests: z.ZodNumber;
|
|
222
|
+
costUsd: z.ZodNumber;
|
|
223
|
+
}, z.core.$strip>>;
|
|
224
|
+
}, z.core.$strip>;
|
|
225
|
+
compactions: z.ZodObject<{
|
|
226
|
+
total: z.ZodNumber;
|
|
227
|
+
manual: z.ZodNumber;
|
|
228
|
+
auto: z.ZodNumber;
|
|
229
|
+
midLoop: z.ZodNumber;
|
|
230
|
+
droppedTokens: z.ZodNumber;
|
|
231
|
+
}, z.core.$strip>;
|
|
232
|
+
topSessions: z.ZodArray<z.ZodObject<{
|
|
233
|
+
sessionId: z.ZodString;
|
|
234
|
+
sessionClass: z.ZodString;
|
|
235
|
+
role: z.ZodString;
|
|
236
|
+
initiative: z.ZodString;
|
|
237
|
+
account: z.ZodString;
|
|
238
|
+
requests: z.ZodNumber;
|
|
239
|
+
costUsd: z.ZodNumber;
|
|
240
|
+
}, z.core.$strip>>;
|
|
241
|
+
unpricedModels: z.ZodArray<z.ZodObject<{
|
|
242
|
+
model: z.ZodString;
|
|
243
|
+
requests: z.ZodNumber;
|
|
244
|
+
tokens: z.ZodNumber;
|
|
245
|
+
}, z.core.$strip>>;
|
|
246
|
+
coverage: z.ZodObject<{
|
|
247
|
+
transcriptsIndexed: z.ZodNumber;
|
|
248
|
+
transcriptsDiscovered: z.ZodNullable<z.ZodNumber>;
|
|
249
|
+
facetBacklog: z.ZodNumber;
|
|
250
|
+
}, z.core.$strip>;
|
|
251
|
+
}, z.core.$strip>;
|
|
252
|
+
type CostReport = z.infer<typeof costReportSchema>;
|
|
253
|
+
type CostBucket = z.infer<typeof bucketSchema>;
|
|
254
|
+
type WakeCauseBucket = CostReport["byWakeCause"][number];
|
|
255
|
+
type WakeGapCell = z.infer<typeof cellSchema>;
|
|
256
|
+
interface CostReportOptions {
|
|
257
|
+
/** ISO timestamp or date, inclusive. Overrides `days`. */
|
|
258
|
+
since?: string;
|
|
259
|
+
/** ISO timestamp or date, exclusive. */
|
|
260
|
+
until?: string;
|
|
261
|
+
/** Window length ending at `until`, or at `now` when `until` is absent. */
|
|
262
|
+
days?: number;
|
|
263
|
+
now?: Date;
|
|
264
|
+
/** Sessions listed in `topSessions`. */
|
|
265
|
+
top?: number;
|
|
266
|
+
/** Transcripts the caller discovered on disk; the graph cannot know this. */
|
|
267
|
+
transcriptsDiscovered?: number;
|
|
268
|
+
/** The extraction version an audit facet must reach to count as current. */
|
|
269
|
+
facetVersion?: number;
|
|
270
|
+
}
|
|
271
|
+
/** Reads the graph and never writes it, so a read-only connection is enough. */
|
|
272
|
+
declare function costReport(db: Db, options?: CostReportOptions): CostReport;
|
|
273
|
+
|
|
274
|
+
/** Two rule sets on purpose (design section 18): workers segment by assignment, coordinators by work phase. */
|
|
275
|
+
type Heuristic = "worker-v1" | "coordinator-v1";
|
|
276
|
+
declare const HEURISTIC_VERSIONS: Readonly<Record<Heuristic, number>>;
|
|
277
|
+
interface EpisodeRequest {
|
|
278
|
+
offset: number;
|
|
279
|
+
ts: string;
|
|
280
|
+
contextTokens: number;
|
|
281
|
+
wakeCause: string | null;
|
|
282
|
+
}
|
|
283
|
+
interface EpisodeInbound {
|
|
284
|
+
offset: number;
|
|
285
|
+
ts: string;
|
|
286
|
+
cause: string;
|
|
287
|
+
}
|
|
288
|
+
interface EpisodeSignal {
|
|
289
|
+
offset: number;
|
|
290
|
+
ts: string;
|
|
291
|
+
signal: string;
|
|
292
|
+
}
|
|
293
|
+
interface EpisodeInput {
|
|
294
|
+
requests: readonly EpisodeRequest[];
|
|
295
|
+
inbounds: readonly EpisodeInbound[];
|
|
296
|
+
signals: readonly EpisodeSignal[];
|
|
297
|
+
/** An agent-chat spawn has a brief; anything else opens at `session_start`. */
|
|
298
|
+
spawned: boolean;
|
|
299
|
+
}
|
|
300
|
+
declare const IDLE_GAP_MS: number;
|
|
301
|
+
declare const CHANNEL_CLUSTER_MS: number;
|
|
302
|
+
declare const COORDINATOR_MIN_EPISODE_REQUESTS = 8;
|
|
303
|
+
declare const COORDINATOR_MERGE_DEDUPE_REQUESTS = 15;
|
|
304
|
+
declare const COORDINATOR_WAVE_QUIET_REQUESTS = 15;
|
|
305
|
+
declare const CONTEXT_RESET_TOKENS = 20000;
|
|
306
|
+
/** Pure: the same rows always cut the same way. Rows come back in episode order. */
|
|
307
|
+
declare function buildEpisodes(input: EpisodeInput, heuristic: Heuristic): EpisodeRow[];
|
|
308
|
+
/** Workers are segmented; human sessions get the coordinator rule; headless runs get none. */
|
|
309
|
+
declare function heuristicFor(sessionClass: SessionClass): Heuristic | null;
|
|
310
|
+
/** The worker report's `n_assignments`: episodes an assignment opened, which is what the standing-peer overlay counts. */
|
|
311
|
+
declare function assignmentCount(rows: readonly Pick<EpisodeRow, "openedBy">[]): number;
|
|
312
|
+
/** Main-thread requests only: a sidechain's requests belong to its subagent, not to the session's episodes. */
|
|
313
|
+
declare function readEpisodeInput(db: Db, sessionId: string, spawned: boolean): EpisodeInput;
|
|
314
|
+
interface WrittenEpisodes {
|
|
315
|
+
sessionId: string;
|
|
316
|
+
heuristic: Heuristic;
|
|
317
|
+
episodes: number;
|
|
318
|
+
}
|
|
319
|
+
/** Segments each session with its class's heuristic and replaces that heuristic's rows. Headless sessions are skipped. */
|
|
320
|
+
declare function writeEpisodes(graph: SessionGraph, sessionIds: readonly string[]): WrittenEpisodes[];
|
|
321
|
+
|
|
322
|
+
/** worker-v1's role vocabulary, from worker-forensics-report.md section 2. */
|
|
323
|
+
type WorkerRole = "implementer" | "reviewer" | "researcher" | "planner" | "standing_peer" | "unknown";
|
|
324
|
+
/** Spawn profile to role, as the worker report's table (wf_analyze.py:79-91) maps it. */
|
|
325
|
+
declare const PROFILE_ROLES: Readonly<Record<string, WorkerRole>>;
|
|
326
|
+
declare const STANDING_PEER_MIN_HOURS = 12;
|
|
327
|
+
declare const STANDING_PEER_MIN_ASSIGNMENTS = 2;
|
|
328
|
+
interface WorkerFacts {
|
|
329
|
+
profile?: string | null;
|
|
330
|
+
/** First request to last request, over the whole session rather than a report window. */
|
|
331
|
+
lifetimeMs: number;
|
|
332
|
+
/** worker-v1 episodes opened by an assignment (the brief or a channel follow-up), not by an idle gap. */
|
|
333
|
+
assignments: number;
|
|
334
|
+
}
|
|
335
|
+
declare function roleFromProfile(profile: string | null | undefined): WorkerRole;
|
|
336
|
+
/** A worker that outlives its first assignment is a standing peer whatever it was spawned as (wf_analyze.py:94-103). */
|
|
337
|
+
declare function workerRole(facts: WorkerFacts): WorkerRole;
|
|
338
|
+
/** The report's role key: `coordinator` or `adhoc` for humans, `worker:<role>` for spawned agents, else the class. */
|
|
339
|
+
declare function sessionRole(classification: SessionClassification, worker: WorkerFacts): string;
|
|
340
|
+
|
|
341
|
+
declare const 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.";
|
|
342
|
+
/** The same report as plain-text tables, one section per report field, caveat and footer last. */
|
|
343
|
+
declare function renderCostReportText(report: CostReport): string;
|
|
344
|
+
|
|
345
|
+
export { type Band, CHANNEL_CLUSTER_MS, CONTEXT_BANDS, CONTEXT_RESET_TOKENS, COORDINATOR_MERGE_DEDUPE_REQUESTS, COORDINATOR_MIN_EPISODE_REQUESTS, COORDINATOR_WAVE_QUIET_REQUESTS, type CostBucket, type CostReport, type CostReportOptions, type EpisodeInbound, type EpisodeInput, type EpisodeRequest, type EpisodeSignal, GAP_BANDS, HEURISTIC_VERSIONS, type Heuristic, type HumanRole, IDLE_GAP_MS, LIST_PRICE_CAVEAT, PRICE_TABLE, PRICE_TABLE_VERSION, PROFILE_ROLES, type PriceRow, type PricedRequest, type ReportWindow, type RequestTokens, STANDING_PEER_MIN_ASSIGNMENTS, STANDING_PEER_MIN_HOURS, type SessionClass, type SessionClassification, type SessionFacts, type SessionOrigin, TOKEN_CLASSES, type TaskInitiative, type TokenClass, type WakeCauseBucket, type WakeGapCell, type WorkerFacts, type WorkerRole, type WrittenEpisodes, assignmentCount, bandOf, buildEpisodes, classifySession, contextBand, costReport, costReportSchema, findPrice, gapBand, heuristicFor, initiativeFromCwd, priceRequest, readEpisodeInput, renderCostReportText, roleFromProfile, sessionInitiative, sessionRole, workerRole, writeEpisodes };
|
package/dist/index.js
CHANGED
|
@@ -119,16 +119,627 @@ function contextBand(tokens) {
|
|
|
119
119
|
function gapBand(millis) {
|
|
120
120
|
return bandOf(millis, GAP_BANDS);
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
// src/cost-report.ts
|
|
124
|
+
import { z } from "zod";
|
|
125
|
+
|
|
126
|
+
// src/cost-report-queries.ts
|
|
127
|
+
import { AUDIT_FACET, EPISODE_TABLE, FACET_TABLE, KIT } from "@titan-design/session-graph";
|
|
128
|
+
var IN_WINDOW = "(@since IS NULL OR ts >= @since) AND (@until IS NULL OR ts < @until)";
|
|
129
|
+
var IN_SESSIONS = "session_id IN (SELECT value FROM json_each(@ids))";
|
|
130
|
+
var COST_ROWS = `
|
|
131
|
+
SELECT session_id AS sessionId, model,
|
|
132
|
+
input_tokens AS inputTokens, cache_read_tokens AS cacheReadTokens,
|
|
133
|
+
CASE WHEN cache_creation_5m + cache_creation_1h = 0 THEN cache_creation_tokens ELSE cache_creation_5m END AS cacheWrite5mTokens,
|
|
134
|
+
cache_creation_1h AS cacheWrite1hTokens, output_tokens AS outputTokens,
|
|
135
|
+
input_cost_usd AS inputCostUsd, cache_read_cost_usd AS cacheReadCostUsd,
|
|
136
|
+
cache_write_5m_cost_usd AS cacheWrite5mCostUsd, cache_write_1h_cost_usd AS cacheWrite1hCostUsd,
|
|
137
|
+
output_cost_usd AS outputCostUsd, cost_usd AS costUsd, priced, is_cold AS isCold,
|
|
138
|
+
context_band AS contextBand, gap_band AS gapBand, wake_cause AS wakeCause, wake_delivery AS wakeDelivery
|
|
139
|
+
FROM request_cost WHERE ${IN_WINDOW}
|
|
140
|
+
`;
|
|
141
|
+
function readCostRows(db, window) {
|
|
142
|
+
const rows = db.prepare(COST_ROWS).all(window);
|
|
143
|
+
return rows.map((row) => ({ ...row, priced: row.priced === 1, isCold: row.isCold === 1 }));
|
|
144
|
+
}
|
|
145
|
+
function readCompactions(db, window) {
|
|
146
|
+
const sql = `SELECT trigger, mid_loop AS midLoop, dropped_tokens AS droppedTokens FROM compaction WHERE ${IN_WINDOW}`;
|
|
147
|
+
const rows = db.prepare(sql).all(window);
|
|
148
|
+
return rows.map((row) => ({ ...row, midLoop: row.midLoop === 1 }));
|
|
149
|
+
}
|
|
150
|
+
function readPriceTableVersion(db) {
|
|
151
|
+
return db.prepare("SELECT MAX(table_version) AS version FROM price").get().version;
|
|
152
|
+
}
|
|
153
|
+
function readCoverage(db, facetVersion) {
|
|
154
|
+
const indexed = db.prepare(`SELECT COUNT(*) AS n FROM "${KIT.watermark}"`).get();
|
|
155
|
+
const backlog = db.prepare(
|
|
156
|
+
`SELECT COUNT(*) AS n FROM "${KIT.watermark}" t LEFT JOIN "${FACET_TABLE}" f
|
|
157
|
+
ON f.transcript_id = t.source_id AND f.facet = @facet AND (@version IS NULL OR f.version >= @version)
|
|
158
|
+
WHERE f.transcript_id IS NULL`
|
|
159
|
+
).get({ facet: AUDIT_FACET, version: facetVersion });
|
|
160
|
+
return { transcriptsIndexed: indexed.n, facetBacklog: backlog.n };
|
|
161
|
+
}
|
|
162
|
+
function readSessionContexts(db, sessionIds) {
|
|
163
|
+
const ids = JSON.stringify(sessionIds);
|
|
164
|
+
const contexts = new Map(sessionIds.map((id) => [id, emptyContext()]));
|
|
165
|
+
addSessionRows(db, ids, contexts);
|
|
166
|
+
addOrigins(db, ids, contexts);
|
|
167
|
+
addInbounds(db, ids, contexts);
|
|
168
|
+
addSignals(db, ids, contexts);
|
|
169
|
+
addTaskInitiatives(db, ids, contexts);
|
|
170
|
+
addLifetimes(db, ids, contexts);
|
|
171
|
+
addEpisodes(db, ids, contexts);
|
|
172
|
+
return contexts;
|
|
173
|
+
}
|
|
174
|
+
function emptyContext() {
|
|
175
|
+
return { facts: { inboundKinds: [], signalKinds: [], humanTurnCount: 0 }, cwd: null, account: null, tasks: [], lifetimeMs: 0, episodes: [] };
|
|
176
|
+
}
|
|
177
|
+
function addSessionRows(db, ids, contexts) {
|
|
178
|
+
const sql = `SELECT session_id AS sessionId, start_type AS startType, cwd, seed_prompt AS seedPrompt, account FROM session WHERE ${IN_SESSIONS}`;
|
|
179
|
+
for (const row of db.prepare(sql).all({ ids })) {
|
|
180
|
+
const context = contexts.get(row.sessionId);
|
|
181
|
+
context.cwd = row.cwd;
|
|
182
|
+
context.account = row.account;
|
|
183
|
+
context.facts.startType = row.startType;
|
|
184
|
+
context.facts.firstUserText = row.seedPrompt;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function addOrigins(db, ids, contexts) {
|
|
188
|
+
const sql = `SELECT session_id AS sessionId, depth, parent_name AS parentName, origin_kind AS originKind, profile FROM session_origin WHERE ${IN_SESSIONS}`;
|
|
189
|
+
for (const { sessionId, depth, ...origin } of db.prepare(sql).all({ ids })) {
|
|
190
|
+
contexts.get(sessionId).facts.origin = { ...origin, depth: depth ?? 0 };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function addInbounds(db, ids, contexts) {
|
|
194
|
+
const sql = `SELECT session_id AS sessionId, cause, SUM(delivery = 'turn_start') AS turnStarts FROM inbound WHERE ${IN_SESSIONS} GROUP BY session_id, cause`;
|
|
195
|
+
for (const row of db.prepare(sql).all({ ids })) {
|
|
196
|
+
const facts = contexts.get(row.sessionId).facts;
|
|
197
|
+
facts.inboundKinds = [...facts.inboundKinds ?? [], row.cause];
|
|
198
|
+
if (row.cause === "human_typed") facts.humanTurnCount = row.turnStarts;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function addSignals(db, ids, contexts) {
|
|
202
|
+
const sql = `SELECT DISTINCT session_id AS sessionId, signal FROM session_signal WHERE ${IN_SESSIONS}`;
|
|
203
|
+
for (const row of db.prepare(sql).all({ ids })) {
|
|
204
|
+
const facts = contexts.get(row.sessionId).facts;
|
|
205
|
+
facts.signalKinds = [...facts.signalKinds ?? [], row.signal];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function addTaskInitiatives(db, ids, contexts) {
|
|
209
|
+
const sql = `
|
|
210
|
+
SELECT substr(e.source_ref, 9) AS sessionId, t.initiative, COUNT(*) AS edges
|
|
211
|
+
FROM "${KIT.edge}" e JOIN task t ON t.task_ref = e.target_ref
|
|
212
|
+
WHERE e.relation = 'ran' AND e.t_expired IS NULL AND t.initiative IS NOT NULL
|
|
213
|
+
AND e.source_ref IN (SELECT 'session:' || value FROM json_each(@ids))
|
|
214
|
+
GROUP BY e.source_ref, t.initiative`;
|
|
215
|
+
for (const row of db.prepare(sql).all({ ids })) {
|
|
216
|
+
contexts.get(row.sessionId).tasks.push({ initiative: row.initiative, edges: row.edges });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function addLifetimes(db, ids, contexts) {
|
|
220
|
+
const sql = `SELECT session_id AS sessionId, MIN(ts) AS first, MAX(ts) AS last FROM request_dedup WHERE ${IN_SESSIONS} AND is_sidechain = 0 GROUP BY session_id`;
|
|
221
|
+
for (const row of db.prepare(sql).all({ ids })) {
|
|
222
|
+
contexts.get(row.sessionId).lifetimeMs = Date.parse(row.last) - Date.parse(row.first);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function addEpisodes(db, ids, contexts) {
|
|
226
|
+
const sql = `SELECT session_id AS sessionId, heuristic, opened_by AS openedBy FROM "${EPISODE_TABLE}" WHERE ${IN_SESSIONS} ORDER BY session_id, heuristic, episode_index`;
|
|
227
|
+
for (const { sessionId, ...episode } of db.prepare(sql).all({ ids })) {
|
|
228
|
+
contexts.get(sessionId).episodes.push(episode);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/episodes.ts
|
|
233
|
+
import { replaceEpisodes } from "@titan-design/session-graph";
|
|
234
|
+
var HEURISTIC_VERSIONS = { "worker-v1": 1, "coordinator-v1": 1 };
|
|
235
|
+
var MINUTE_MS = 6e4;
|
|
236
|
+
var IDLE_GAP_MS = 30 * MINUTE_MS;
|
|
237
|
+
var CHANNEL_CLUSTER_MS = 10 * MINUTE_MS;
|
|
238
|
+
var COORDINATOR_MIN_EPISODE_REQUESTS = 8;
|
|
239
|
+
var COORDINATOR_MERGE_DEDUPE_REQUESTS = 15;
|
|
240
|
+
var COORDINATOR_WAVE_QUIET_REQUESTS = 15;
|
|
241
|
+
var CONTEXT_RESET_TOKENS = 2e4;
|
|
242
|
+
var DELIVERABLE_SIGNALS = /* @__PURE__ */ new Set(["status_report", "pr_create", "commit", "doc_written", "task_wrap"]);
|
|
243
|
+
var ASSIGNMENT_OPENERS = /* @__PURE__ */ new Set(["brief", "channel_followup"]);
|
|
244
|
+
function buildEpisodes(input, heuristic) {
|
|
245
|
+
return heuristic === "worker-v1" ? workerEpisodes(input) : coordinatorEpisodes(input);
|
|
246
|
+
}
|
|
247
|
+
function heuristicFor(sessionClass) {
|
|
248
|
+
if (sessionClass === "agent_spawned") return "worker-v1";
|
|
249
|
+
if (sessionClass === "human_interactive") return "coordinator-v1";
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
function assignmentCount(rows) {
|
|
253
|
+
return rows.filter((row) => ASSIGNMENT_OPENERS.has(row.openedBy)).length;
|
|
254
|
+
}
|
|
255
|
+
function workerEpisodes(input) {
|
|
256
|
+
const state = { drafts: [], lastRequestMs: null, lastChannelMs: null };
|
|
257
|
+
for (const event of timeline(input)) {
|
|
258
|
+
const current = state.drafts.at(-1);
|
|
259
|
+
const reason = current ? workerBoundary(event, current, state) : input.spawned ? "brief" : "session_start";
|
|
260
|
+
if (reason && (!current || current.hasRequest)) state.drafts.push(openDraft(event, reason, state.drafts.length));
|
|
261
|
+
absorb(state.drafts.at(-1), event);
|
|
262
|
+
if (event.kind === "request") state.lastRequestMs = event.ms;
|
|
263
|
+
if (isChannel(event)) state.lastChannelMs = event.ms;
|
|
264
|
+
}
|
|
265
|
+
return state.drafts.map((draft) => draft.row);
|
|
266
|
+
}
|
|
267
|
+
function workerBoundary(event, current, state) {
|
|
268
|
+
if (event.kind === "request" && state.lastRequestMs !== null && event.ms - state.lastRequestMs >= IDLE_GAP_MS) return "idle_gap";
|
|
269
|
+
if (!isChannel(event)) return null;
|
|
270
|
+
const startsCluster = state.lastChannelMs === null || event.ms - state.lastChannelMs > CHANNEL_CLUSTER_MS;
|
|
271
|
+
return startsCluster && current.row.firstStatusOffset != null ? "channel_followup" : null;
|
|
272
|
+
}
|
|
273
|
+
function isChannel(event) {
|
|
274
|
+
return event.kind === "inbound" && event.cause === "channel_message";
|
|
275
|
+
}
|
|
276
|
+
function openDraft(event, openedBy, episodeIndex) {
|
|
277
|
+
const assignment = ASSIGNMENT_OPENERS.has(openedBy) && event.kind === "inbound" ? event.offset : null;
|
|
278
|
+
const row = {
|
|
279
|
+
episodeIndex,
|
|
280
|
+
heuristicVersion: HEURISTIC_VERSIONS["worker-v1"],
|
|
281
|
+
startedAt: event.ts,
|
|
282
|
+
endedAt: event.ts,
|
|
283
|
+
startOffset: event.offset,
|
|
284
|
+
endOffset: event.offset,
|
|
285
|
+
openedBy,
|
|
286
|
+
assignmentOffset: assignment,
|
|
287
|
+
firstDeliverableOffset: null,
|
|
288
|
+
firstDeliverableSignal: null,
|
|
289
|
+
firstStatusOffset: null
|
|
290
|
+
};
|
|
291
|
+
return { row, hasRequest: false };
|
|
292
|
+
}
|
|
293
|
+
function absorb(draft, event) {
|
|
294
|
+
draft.row.endedAt = event.ts;
|
|
295
|
+
draft.row.endOffset = event.offset;
|
|
296
|
+
if (event.kind === "request") draft.hasRequest = true;
|
|
297
|
+
if (event.kind !== "signal") return;
|
|
298
|
+
if (draft.row.firstDeliverableOffset == null && DELIVERABLE_SIGNALS.has(event.signal)) {
|
|
299
|
+
draft.row.firstDeliverableOffset = event.offset;
|
|
300
|
+
draft.row.firstDeliverableSignal = event.signal;
|
|
301
|
+
}
|
|
302
|
+
if (draft.row.firstStatusOffset == null && event.signal === "status_report") draft.row.firstStatusOffset = event.offset;
|
|
303
|
+
}
|
|
304
|
+
function timeline(input) {
|
|
305
|
+
const events = [
|
|
306
|
+
...input.requests.map((r) => ({ kind: "request", offset: r.offset, ts: r.ts, ms: Date.parse(r.ts) })),
|
|
307
|
+
...input.inbounds.map((i) => ({ kind: "inbound", offset: i.offset, ts: i.ts, ms: Date.parse(i.ts), cause: i.cause })),
|
|
308
|
+
...input.signals.map((s) => ({ kind: "signal", offset: s.offset, ts: s.ts, ms: Date.parse(s.ts), signal: s.signal }))
|
|
309
|
+
];
|
|
310
|
+
return events.sort((a, b) => a.ms - b.ms || a.offset - b.offset);
|
|
311
|
+
}
|
|
312
|
+
function coordinatorEpisodes(input) {
|
|
313
|
+
const turns = toTurns(input);
|
|
314
|
+
if (turns.length === 0) return [];
|
|
315
|
+
const opens = [{ at: 0, reason: "session_start" }, ...dropShort(coordinatorBoundaries(turns), turns.length)];
|
|
316
|
+
return opens.map((open, index) => {
|
|
317
|
+
const first = turns[open.at].request;
|
|
318
|
+
const last = turns[(opens[index + 1]?.at ?? turns.length) - 1].request;
|
|
319
|
+
return {
|
|
320
|
+
episodeIndex: index,
|
|
321
|
+
heuristicVersion: HEURISTIC_VERSIONS["coordinator-v1"],
|
|
322
|
+
startedAt: first.ts,
|
|
323
|
+
endedAt: last.ts,
|
|
324
|
+
startOffset: first.offset,
|
|
325
|
+
endOffset: last.offset,
|
|
326
|
+
openedBy: open.reason
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
function coordinatorBoundaries(turns) {
|
|
331
|
+
const bounds = [];
|
|
332
|
+
let spawnedSince = false;
|
|
333
|
+
let lastMerge = -Infinity;
|
|
334
|
+
for (let i = 1; i < turns.length; i++) {
|
|
335
|
+
const reason = coordinatorTrigger(turns, i, lastMerge) ?? (spawnedSince && waveComplete(turns, i) ? "spawn_wave_complete" : null);
|
|
336
|
+
if (turns[i - 1].signals.has("pr_merge")) lastMerge = i;
|
|
337
|
+
if (reason) {
|
|
338
|
+
bounds.push({ at: i, reason });
|
|
339
|
+
spawnedSince = false;
|
|
340
|
+
}
|
|
341
|
+
if (turns[i].signals.has("agent_spawn")) spawnedSince = true;
|
|
342
|
+
}
|
|
343
|
+
return bounds;
|
|
344
|
+
}
|
|
345
|
+
function coordinatorTrigger(turns, i, lastMerge) {
|
|
346
|
+
const [a, b] = [turns[i - 1], turns[i]];
|
|
347
|
+
if (b.ms - a.ms >= IDLE_GAP_MS) return "idle_gap";
|
|
348
|
+
if (a.signals.has("pr_merge") && i - lastMerge > COORDINATOR_MERGE_DEDUPE_REQUESTS) return "pr_merge";
|
|
349
|
+
if (a.signals.has("task_wrap") || a.signals.has("task_done")) return "task_wrap";
|
|
350
|
+
const drop = a.request.contextTokens - b.request.contextTokens;
|
|
351
|
+
if (a.request.contextTokens > 0 && drop > CONTEXT_RESET_TOKENS) return "context_reset";
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
function waveComplete(turns, i) {
|
|
355
|
+
return hasChatTraffic(turns[i - 1]) && !turns.slice(i, i + COORDINATOR_WAVE_QUIET_REQUESTS).some(hasChatTraffic);
|
|
356
|
+
}
|
|
357
|
+
function hasChatTraffic(turn) {
|
|
358
|
+
return turn.request.wakeCause === "channel_message" || turn.signals.has("chat_send") || turn.signals.has("agent_spawn");
|
|
359
|
+
}
|
|
360
|
+
function dropShort(bounds, total) {
|
|
361
|
+
const kept = [];
|
|
362
|
+
let last = 0;
|
|
363
|
+
for (const bound of bounds) {
|
|
364
|
+
if (bound.at - last < COORDINATOR_MIN_EPISODE_REQUESTS || total - bound.at < COORDINATOR_MIN_EPISODE_REQUESTS) continue;
|
|
365
|
+
kept.push(bound);
|
|
366
|
+
last = bound.at;
|
|
367
|
+
}
|
|
368
|
+
return kept;
|
|
369
|
+
}
|
|
370
|
+
function toTurns(input) {
|
|
371
|
+
const turns = input.requests.map((request) => ({ request, ms: Date.parse(request.ts), signals: /* @__PURE__ */ new Set() })).sort((a, b) => a.ms - b.ms || a.request.offset - b.request.offset);
|
|
372
|
+
for (const signal of input.signals) ownerOf(turns, signal)?.signals.add(signal.signal);
|
|
373
|
+
return turns;
|
|
374
|
+
}
|
|
375
|
+
function ownerOf(turns, signal) {
|
|
376
|
+
const ms = Date.parse(signal.ts);
|
|
377
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
378
|
+
const turn = turns[i];
|
|
379
|
+
if (turn.ms < ms || turn.ms === ms && turn.request.offset <= signal.offset) return turn;
|
|
380
|
+
}
|
|
381
|
+
return turns[0];
|
|
382
|
+
}
|
|
383
|
+
var IN_SESSION = "session_id = ?";
|
|
384
|
+
function readEpisodeInput(db, sessionId, spawned) {
|
|
385
|
+
const requests = db.prepare(`SELECT byte_offset AS offset, ts, context_tokens AS contextTokens, wake_cause AS wakeCause FROM request_dedup WHERE ${IN_SESSION} AND is_sidechain = 0`).all(sessionId);
|
|
386
|
+
const inbounds = db.prepare(`SELECT byte_offset AS offset, ts, cause FROM inbound WHERE ${IN_SESSION}`).all(sessionId);
|
|
387
|
+
const signals = db.prepare(`SELECT byte_offset AS offset, ts, signal FROM session_signal WHERE ${IN_SESSION}`).all(sessionId);
|
|
388
|
+
return { requests, inbounds, signals, spawned };
|
|
389
|
+
}
|
|
390
|
+
function writeEpisodes(graph, sessionIds) {
|
|
391
|
+
const written = [];
|
|
392
|
+
for (const [sessionId, context] of readSessionContexts(graph.db, sessionIds)) {
|
|
393
|
+
const { sessionClass } = classifySession(context.facts);
|
|
394
|
+
const heuristic = heuristicFor(sessionClass);
|
|
395
|
+
if (!heuristic) continue;
|
|
396
|
+
const rows = buildEpisodes(readEpisodeInput(graph.db, sessionId, sessionClass === "agent_spawned"), heuristic);
|
|
397
|
+
written.push({ sessionId, heuristic, episodes: replaceEpisodes(graph, sessionId, heuristic, rows) });
|
|
398
|
+
}
|
|
399
|
+
return written;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/initiative.ts
|
|
403
|
+
function sessionInitiative(tasks, cwd) {
|
|
404
|
+
const best = [...tasks].sort((a, b) => b.edges - a.edges || a.initiative.localeCompare(b.initiative))[0];
|
|
405
|
+
return best ? best.initiative : initiativeFromCwd(cwd);
|
|
406
|
+
}
|
|
407
|
+
function initiativeFromCwd(cwd) {
|
|
408
|
+
if (!cwd) return "unknown";
|
|
409
|
+
const repo = /\/projects\/([^/]+)/.exec(cwd);
|
|
410
|
+
if (repo) return `repo:${repo[1]}`;
|
|
411
|
+
const activeWork = /active-work\/([^/]+)/.exec(cwd);
|
|
412
|
+
if (activeWork) return `active-work:${activeWork[1]}`;
|
|
413
|
+
if (/ac-fork-\w+/.test(cwd)) return "ac-fork(tmp)";
|
|
414
|
+
const trimmed = cwd.replace(/\/+$/, "");
|
|
415
|
+
return `other:${trimmed.slice(trimmed.lastIndexOf("/") + 1)}`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/roles.ts
|
|
419
|
+
var PROFILE_ROLES = {
|
|
420
|
+
implementer: "implementer",
|
|
421
|
+
"implementer-lite": "implementer",
|
|
422
|
+
"relay-implementer": "implementer",
|
|
423
|
+
"beat-builder": "implementer",
|
|
424
|
+
"fable-optimizer": "implementer",
|
|
425
|
+
reviewer: "reviewer",
|
|
426
|
+
"relay-reviewer": "reviewer",
|
|
427
|
+
researcher: "researcher",
|
|
428
|
+
explorer: "researcher",
|
|
429
|
+
"fable-architect": "planner",
|
|
430
|
+
peer: "standing_peer"
|
|
431
|
+
};
|
|
432
|
+
var STANDING_PEER_MIN_HOURS = 12;
|
|
433
|
+
var STANDING_PEER_MIN_ASSIGNMENTS = 2;
|
|
434
|
+
var HOUR_MS = 36e5;
|
|
435
|
+
function roleFromProfile(profile) {
|
|
436
|
+
return PROFILE_ROLES[(profile ?? "").toLowerCase()] ?? "unknown";
|
|
437
|
+
}
|
|
438
|
+
function workerRole(facts) {
|
|
439
|
+
const outlived = facts.lifetimeMs >= STANDING_PEER_MIN_HOURS * HOUR_MS && facts.assignments >= STANDING_PEER_MIN_ASSIGNMENTS;
|
|
440
|
+
return outlived ? "standing_peer" : roleFromProfile(facts.profile);
|
|
441
|
+
}
|
|
442
|
+
function sessionRole(classification, worker) {
|
|
443
|
+
if (classification.humanRole) return classification.humanRole;
|
|
444
|
+
if (classification.sessionClass === "agent_spawned") return `worker:${workerRole(worker)}`;
|
|
445
|
+
return classification.sessionClass;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/cost-report.ts
|
|
449
|
+
var count = z.number().int().nonnegative();
|
|
450
|
+
var bucketSchema = z.object({ key: z.string(), requests: count, sessions: count, costUsd: z.number() });
|
|
451
|
+
var wakePartSchema = bucketSchema.extend({ midLoopRequests: count, midLoopCostUsd: z.number() });
|
|
452
|
+
var cellSchema = z.object({ wakeCause: z.string(), gapBand: z.string(), requests: count, costUsd: z.number() });
|
|
453
|
+
var TOKEN_CLASSES = ["input", "cache_read", "cache_write_5m", "cache_write_1h", "output"];
|
|
454
|
+
var costReportSchema = z.object({
|
|
455
|
+
window: z.object({ since: z.string().nullable(), until: z.string().nullable() }),
|
|
456
|
+
priceTableVersion: z.number().int().nullable(),
|
|
457
|
+
totals: z.object({ requests: count, sessions: count, costUsd: z.number(), unpricedRequests: count }),
|
|
458
|
+
byTokenClass: z.array(z.object({ tokenClass: z.enum(TOKEN_CLASSES), tokens: count, costUsd: z.number() })),
|
|
459
|
+
byAccount: z.array(bucketSchema),
|
|
460
|
+
byModel: z.array(bucketSchema),
|
|
461
|
+
byClass: z.array(bucketSchema),
|
|
462
|
+
byRole: z.array(bucketSchema),
|
|
463
|
+
/** Sessions by how many episodes their class's heuristic cut them into; `none` when not yet segmented. */
|
|
464
|
+
byEpisodeCount: z.array(bucketSchema),
|
|
465
|
+
byInitiative: z.array(bucketSchema),
|
|
466
|
+
byContextBand: z.array(bucketSchema),
|
|
467
|
+
byWakeCause: z.array(wakePartSchema.extend({ parts: z.array(wakePartSchema) })),
|
|
468
|
+
wakeCauseByGapBand: z.array(cellSchema),
|
|
469
|
+
coldRebuild: z.object({ requests: count, costUsd: z.number(), byGapBandAndCause: z.array(cellSchema) }),
|
|
470
|
+
compactions: z.object({ total: count, manual: count, auto: count, midLoop: count, droppedTokens: count }),
|
|
471
|
+
topSessions: z.array(
|
|
472
|
+
z.object({
|
|
473
|
+
sessionId: z.string(),
|
|
474
|
+
sessionClass: z.string(),
|
|
475
|
+
role: z.string(),
|
|
476
|
+
initiative: z.string(),
|
|
477
|
+
account: z.string(),
|
|
478
|
+
requests: count,
|
|
479
|
+
costUsd: z.number()
|
|
480
|
+
})
|
|
481
|
+
),
|
|
482
|
+
unpricedModels: z.array(z.object({ model: z.string(), requests: count, tokens: count })),
|
|
483
|
+
coverage: z.object({ transcriptsIndexed: count, transcriptsDiscovered: count.nullable(), facetBacklog: count })
|
|
484
|
+
});
|
|
485
|
+
var HUMAN_CAUSES = ["human_typed", "ask_user_answer"];
|
|
486
|
+
var NONE = "none";
|
|
487
|
+
var DEFAULT_TOP = 10;
|
|
488
|
+
var EPISODE_COUNT_CAP = 5;
|
|
489
|
+
var DAY_MS = 864e5;
|
|
490
|
+
function costReport(db, options = {}) {
|
|
491
|
+
const window = resolveWindow(options);
|
|
492
|
+
const costRows = readCostRows(db, window);
|
|
493
|
+
const sessionIds = [...new Set(costRows.map((row) => row.sessionId))];
|
|
494
|
+
const tags = tagSessions(readSessionContexts(db, sessionIds));
|
|
495
|
+
const rows = costRows.map((row) => ({ ...row, ...tags.get(row.sessionId) }));
|
|
496
|
+
return {
|
|
497
|
+
window,
|
|
498
|
+
priceTableVersion: readPriceTableVersion(db),
|
|
499
|
+
totals: { ...sumOf(rows), sessions: sessionIds.length, unpricedRequests: rows.filter((row) => !row.priced).length },
|
|
500
|
+
byTokenClass: tokenClasses(rows),
|
|
501
|
+
...dimensionBuckets(rows),
|
|
502
|
+
byWakeCause: wakeCauses(rows),
|
|
503
|
+
wakeCauseByGapBand: gapCells(rows),
|
|
504
|
+
coldRebuild: coldRebuild(rows),
|
|
505
|
+
compactions: compactionCounts(readCompactions(db, window)),
|
|
506
|
+
topSessions: topSessions(rows, options.top ?? DEFAULT_TOP),
|
|
507
|
+
unpricedModels: unpricedModels(rows),
|
|
508
|
+
coverage: { ...readCoverage(db, options.facetVersion ?? null), transcriptsDiscovered: options.transcriptsDiscovered ?? null }
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function resolveWindow(options) {
|
|
512
|
+
const until = options.until ?? null;
|
|
513
|
+
if (options.since !== void 0) return { since: options.since, until };
|
|
514
|
+
if (options.days === void 0) return { since: null, until };
|
|
515
|
+
const end = until ? Date.parse(until) : (options.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
516
|
+
return { since: new Date(end - options.days * DAY_MS).toISOString(), until };
|
|
517
|
+
}
|
|
518
|
+
function tagSessions(contexts) {
|
|
519
|
+
const tags = /* @__PURE__ */ new Map();
|
|
520
|
+
for (const [sessionId, context] of contexts) {
|
|
521
|
+
const classification = classifySession(context.facts);
|
|
522
|
+
const episodes = context.episodes.filter((episode) => episode.heuristic === heuristicFor(classification.sessionClass));
|
|
523
|
+
const worker = { profile: context.facts.origin?.profile, lifetimeMs: context.lifetimeMs, assignments: assignmentCount(episodes) };
|
|
524
|
+
tags.set(sessionId, {
|
|
525
|
+
sessionClass: classification.sessionClass,
|
|
526
|
+
role: sessionRole(classification, worker),
|
|
527
|
+
episodeCount: episodeCountKey(episodes.length),
|
|
528
|
+
initiative: sessionInitiative(context.tasks, context.cwd),
|
|
529
|
+
account: context.account ?? "unknown"
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
return tags;
|
|
533
|
+
}
|
|
534
|
+
function episodeCountKey(episodes) {
|
|
535
|
+
if (episodes === 0) return NONE;
|
|
536
|
+
return episodes >= EPISODE_COUNT_CAP ? `${EPISODE_COUNT_CAP}+` : String(episodes);
|
|
537
|
+
}
|
|
538
|
+
function sumOf(rows) {
|
|
539
|
+
return { requests: rows.length, costUsd: rows.reduce((sum, row) => sum + row.costUsd, 0) };
|
|
540
|
+
}
|
|
541
|
+
function tokenClasses(rows) {
|
|
542
|
+
const pick = {
|
|
543
|
+
input: (row) => [row.inputTokens, row.inputCostUsd],
|
|
544
|
+
cache_read: (row) => [row.cacheReadTokens, row.cacheReadCostUsd],
|
|
545
|
+
cache_write_5m: (row) => [row.cacheWrite5mTokens, row.cacheWrite5mCostUsd],
|
|
546
|
+
cache_write_1h: (row) => [row.cacheWrite1hTokens, row.cacheWrite1hCostUsd],
|
|
547
|
+
output: (row) => [row.outputTokens, row.outputCostUsd]
|
|
548
|
+
};
|
|
549
|
+
return TOKEN_CLASSES.map((tokenClass) => {
|
|
550
|
+
const pairs = rows.map(pick[tokenClass]);
|
|
551
|
+
return { tokenClass, tokens: pairs.reduce((sum, [tokens]) => sum + tokens, 0), costUsd: pairs.reduce((sum, [, cost]) => sum + cost, 0) };
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
function dimensionBuckets(rows) {
|
|
555
|
+
return {
|
|
556
|
+
byAccount: bucketsBy(rows, (row) => row.account),
|
|
557
|
+
byModel: bucketsBy(rows, (row) => row.model),
|
|
558
|
+
byClass: bucketsBy(rows, (row) => row.sessionClass),
|
|
559
|
+
byRole: bucketsBy(rows, (row) => row.role),
|
|
560
|
+
byEpisodeCount: bucketsBy(rows, (row) => row.episodeCount),
|
|
561
|
+
byInitiative: bucketsBy(rows, (row) => row.initiative),
|
|
562
|
+
byContextBand: bucketsBy(rows, (row) => row.contextBand)
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function bucketsBy(rows, keyOf) {
|
|
566
|
+
const groups = groupRows(rows, keyOf);
|
|
567
|
+
return [...groups].map(([key, members]) => ({ key, ...sumOf(members), sessions: new Set(members.map((row) => row.sessionId)).size })).sort(byCostThenKey);
|
|
568
|
+
}
|
|
569
|
+
function groupRows(rows, keyOf) {
|
|
570
|
+
const groups = /* @__PURE__ */ new Map();
|
|
571
|
+
for (const row of rows) {
|
|
572
|
+
const key = keyOf(row);
|
|
573
|
+
const members = groups.get(key);
|
|
574
|
+
if (members) members.push(row);
|
|
575
|
+
else groups.set(key, [row]);
|
|
576
|
+
}
|
|
577
|
+
return groups;
|
|
578
|
+
}
|
|
579
|
+
function byCostThenKey(a, b) {
|
|
580
|
+
return b.costUsd - a.costUsd || a.key.localeCompare(b.key);
|
|
581
|
+
}
|
|
582
|
+
function wakeCauses(rows) {
|
|
583
|
+
const causeOf = (row) => row.wakeCause ?? NONE;
|
|
584
|
+
const groupOf = (row) => HUMAN_CAUSES.includes(causeOf(row)) ? "human" : causeOf(row);
|
|
585
|
+
return [...groupRows(rows, groupOf)].map(([key, members]) => ({
|
|
586
|
+
...wakePart(key, members),
|
|
587
|
+
parts: key === "human" ? [...groupRows(members, causeOf)].map(([cause, part]) => wakePart(cause, part)).sort(byCostThenKey) : []
|
|
588
|
+
})).sort(byCostThenKey);
|
|
589
|
+
}
|
|
590
|
+
function wakePart(key, rows) {
|
|
591
|
+
const midLoop = rows.filter((row) => row.wakeDelivery === "mid_loop");
|
|
592
|
+
const sessions = new Set(rows.map((row) => row.sessionId)).size;
|
|
593
|
+
return { key, ...sumOf(rows), sessions, midLoopRequests: midLoop.length, midLoopCostUsd: sumOf(midLoop).costUsd };
|
|
594
|
+
}
|
|
595
|
+
function gapCells(rows) {
|
|
596
|
+
const keyOf = (row) => JSON.stringify([row.wakeCause ?? NONE, row.gapBand ?? NONE]);
|
|
597
|
+
return [...groupRows(rows, keyOf)].map(([key, members]) => {
|
|
598
|
+
const [wakeCause, gapBand2] = JSON.parse(key);
|
|
599
|
+
return { wakeCause, gapBand: gapBand2, ...sumOf(members) };
|
|
600
|
+
}).sort((a, b) => b.costUsd - a.costUsd || a.wakeCause.localeCompare(b.wakeCause) || a.gapBand.localeCompare(b.gapBand));
|
|
601
|
+
}
|
|
602
|
+
function coldRebuild(rows) {
|
|
603
|
+
const cold = rows.filter((row) => row.isCold);
|
|
604
|
+
return { ...sumOf(cold), byGapBandAndCause: gapCells(cold) };
|
|
605
|
+
}
|
|
606
|
+
function compactionCounts(rows) {
|
|
607
|
+
return {
|
|
608
|
+
total: rows.length,
|
|
609
|
+
manual: rows.filter((row) => row.trigger === "manual").length,
|
|
610
|
+
auto: rows.filter((row) => row.trigger === "auto").length,
|
|
611
|
+
midLoop: rows.filter((row) => row.midLoop).length,
|
|
612
|
+
droppedTokens: rows.reduce((sum, row) => sum + (row.droppedTokens ?? 0), 0)
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function topSessions(rows, top) {
|
|
616
|
+
return [...groupRows(rows, (row) => row.sessionId)].map(([sessionId, members]) => {
|
|
617
|
+
const { sessionClass, role, initiative, account } = members[0];
|
|
618
|
+
return { sessionId, sessionClass, role, initiative, account, ...sumOf(members) };
|
|
619
|
+
}).sort((a, b) => b.costUsd - a.costUsd || a.sessionId.localeCompare(b.sessionId)).slice(0, top);
|
|
620
|
+
}
|
|
621
|
+
function unpricedModels(rows) {
|
|
622
|
+
const unpriced = rows.filter((row) => !row.priced);
|
|
623
|
+
return [...groupRows(unpriced, (row) => row.model)].map(([model, members]) => ({ model, requests: members.length, tokens: members.reduce((sum, row) => sum + tokensOf(row), 0) })).sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model));
|
|
624
|
+
}
|
|
625
|
+
function tokensOf(row) {
|
|
626
|
+
return row.inputTokens + row.cacheReadTokens + row.cacheWrite5mTokens + row.cacheWrite1hTokens + row.outputTokens;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/render-text.ts
|
|
630
|
+
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.";
|
|
631
|
+
function renderCostReportText(report) {
|
|
632
|
+
const sections = [
|
|
633
|
+
header(report),
|
|
634
|
+
table("By token class", ["class", "tokens", "cost"], report.byTokenClass.map((row) => [row.tokenClass, row.tokens, usd(row.costUsd)])),
|
|
635
|
+
bucketTable("By account", report.byAccount, report.totals.costUsd),
|
|
636
|
+
bucketTable("By model", report.byModel, report.totals.costUsd),
|
|
637
|
+
bucketTable("By class", report.byClass, report.totals.costUsd),
|
|
638
|
+
bucketTable("By role", report.byRole, report.totals.costUsd),
|
|
639
|
+
bucketTable("By episode count", report.byEpisodeCount, report.totals.costUsd),
|
|
640
|
+
bucketTable("By initiative", report.byInitiative, report.totals.costUsd),
|
|
641
|
+
bucketTable("By context band", report.byContextBand, report.totals.costUsd),
|
|
642
|
+
wakeCauseTable(report),
|
|
643
|
+
cellTable("Wake cause by gap band", report.wakeCauseByGapBand),
|
|
644
|
+
cellTable(`Cold rebuilds: ${report.coldRebuild.requests} requests, ${usd(report.coldRebuild.costUsd)}`, report.coldRebuild.byGapBandAndCause),
|
|
645
|
+
compactionTable(report.compactions),
|
|
646
|
+
topSessionTable(report.topSessions),
|
|
647
|
+
unpricedTable(report.unpricedModels),
|
|
648
|
+
[LIST_PRICE_CAVEAT, footer(report)].join("\n")
|
|
649
|
+
];
|
|
650
|
+
return sections.join("\n\n") + "\n";
|
|
651
|
+
}
|
|
652
|
+
function header(report) {
|
|
653
|
+
const { since, until } = report.window;
|
|
654
|
+
const { requests, sessions, costUsd } = report.totals;
|
|
655
|
+
return `Cost report, ${since ?? "beginning"} to ${until ?? "now"}
|
|
656
|
+
Total ${usd(costUsd)} over ${requests} requests in ${sessions} sessions`;
|
|
657
|
+
}
|
|
658
|
+
function bucketTable(title, buckets, total) {
|
|
659
|
+
const rows = buckets.map((b) => [b.key, b.requests, b.sessions, usd(b.costUsd), share(b.costUsd, total)]);
|
|
660
|
+
return table(title, ["key", "requests", "sessions", "cost", "share"], rows);
|
|
661
|
+
}
|
|
662
|
+
function wakeCauseTable(report) {
|
|
663
|
+
const rows = report.byWakeCause.flatMap((group) => [
|
|
664
|
+
[group.key, group.requests, usd(group.costUsd), group.midLoopRequests, usd(group.midLoopCostUsd)],
|
|
665
|
+
...group.parts.map((part) => [` ${part.key}`, part.requests, usd(part.costUsd), part.midLoopRequests, usd(part.midLoopCostUsd)])
|
|
666
|
+
]);
|
|
667
|
+
return table("By wake cause", ["cause", "requests", "cost", "mid-loop", "mid-loop cost"], rows);
|
|
668
|
+
}
|
|
669
|
+
function cellTable(title, cells) {
|
|
670
|
+
return table(title, ["cause", "gap", "requests", "cost"], cells.map((c) => [c.wakeCause, c.gapBand, c.requests, usd(c.costUsd)]));
|
|
671
|
+
}
|
|
672
|
+
function compactionTable(c) {
|
|
673
|
+
return table("Compactions", ["total", "manual", "auto", "mid-loop", "dropped tokens"], [[c.total, c.manual, c.auto, c.midLoop, c.droppedTokens]]);
|
|
674
|
+
}
|
|
675
|
+
function topSessionTable(sessions) {
|
|
676
|
+
const rows = sessions.map((s) => [s.sessionId, s.sessionClass, s.role, s.initiative, s.requests, usd(s.costUsd)]);
|
|
677
|
+
return table("Top sessions", ["session", "class", "role", "initiative", "requests", "cost"], rows);
|
|
678
|
+
}
|
|
679
|
+
function unpricedTable(models) {
|
|
680
|
+
if (models.length === 0) return "Unpriced models: none";
|
|
681
|
+
return table("Unpriced models (counted at zero cost)", ["model", "requests", "tokens"], models.map((m) => [m.model, m.requests, m.tokens]));
|
|
682
|
+
}
|
|
683
|
+
function footer(report) {
|
|
684
|
+
const { transcriptsIndexed, transcriptsDiscovered, facetBacklog } = report.coverage;
|
|
685
|
+
const discovered = transcriptsDiscovered === null ? "" : ` of ${transcriptsDiscovered} discovered`;
|
|
686
|
+
const version = report.priceTableVersion === null ? "none" : `v${report.priceTableVersion}`;
|
|
687
|
+
const stale = report.priceTableVersion !== null && report.priceTableVersion !== PRICE_TABLE_VERSION ? ` (package has v${PRICE_TABLE_VERSION})` : "";
|
|
688
|
+
return `Price table ${version}${stale}. Coverage: ${transcriptsIndexed} transcripts indexed${discovered}, facet backlog ${facetBacklog}.`;
|
|
689
|
+
}
|
|
690
|
+
function table(title, head, rows) {
|
|
691
|
+
if (rows.length === 0) return `${title}: none`;
|
|
692
|
+
const text = [head, ...rows].map((row) => row.map(String));
|
|
693
|
+
const widths = head.map((_, i) => Math.max(...text.map((row) => row[i].length)));
|
|
694
|
+
const rightAligned = head.map((_, i) => rows.every((row) => isNumeric(row[i])));
|
|
695
|
+
const pad = (cell, i) => rightAligned[i] ? cell.padStart(widths[i]) : cell.padEnd(widths[i]);
|
|
696
|
+
const lines = text.map((row) => row.map(pad).join(" ").trimEnd());
|
|
697
|
+
return [title, ...lines].join("\n");
|
|
698
|
+
}
|
|
699
|
+
function isNumeric(cell) {
|
|
700
|
+
return typeof cell === "number" || /^\$|%$/.test(cell);
|
|
701
|
+
}
|
|
702
|
+
function usd(value) {
|
|
703
|
+
return `$${value.toFixed(2)}`;
|
|
704
|
+
}
|
|
705
|
+
function share(value, total) {
|
|
706
|
+
return total > 0 ? `${(value / total * 100).toFixed(1)}%` : "-";
|
|
707
|
+
}
|
|
122
708
|
export {
|
|
709
|
+
CHANNEL_CLUSTER_MS,
|
|
123
710
|
CONTEXT_BANDS,
|
|
711
|
+
CONTEXT_RESET_TOKENS,
|
|
712
|
+
COORDINATOR_MERGE_DEDUPE_REQUESTS,
|
|
713
|
+
COORDINATOR_MIN_EPISODE_REQUESTS,
|
|
714
|
+
COORDINATOR_WAVE_QUIET_REQUESTS,
|
|
124
715
|
GAP_BANDS,
|
|
716
|
+
HEURISTIC_VERSIONS,
|
|
717
|
+
IDLE_GAP_MS,
|
|
718
|
+
LIST_PRICE_CAVEAT,
|
|
125
719
|
PRICE_TABLE,
|
|
126
720
|
PRICE_TABLE_VERSION,
|
|
721
|
+
PROFILE_ROLES,
|
|
722
|
+
STANDING_PEER_MIN_ASSIGNMENTS,
|
|
723
|
+
STANDING_PEER_MIN_HOURS,
|
|
724
|
+
TOKEN_CLASSES,
|
|
725
|
+
assignmentCount,
|
|
127
726
|
bandOf,
|
|
727
|
+
buildEpisodes,
|
|
128
728
|
classifySession,
|
|
129
729
|
contextBand,
|
|
730
|
+
costReport,
|
|
731
|
+
costReportSchema,
|
|
130
732
|
findPrice,
|
|
131
733
|
gapBand,
|
|
132
|
-
|
|
734
|
+
heuristicFor,
|
|
735
|
+
initiativeFromCwd,
|
|
736
|
+
priceRequest,
|
|
737
|
+
readEpisodeInput,
|
|
738
|
+
renderCostReportText,
|
|
739
|
+
roleFromProfile,
|
|
740
|
+
sessionInitiative,
|
|
741
|
+
sessionRole,
|
|
742
|
+
workerRole,
|
|
743
|
+
writeEpisodes
|
|
133
744
|
};
|
|
134
745
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/prices.ts","../src/price-request.ts","../src/classify-session.ts","../src/bands.ts"],"sourcesContent":["/** USD per million tokens. Fitted against 392 `cost-state` rows in the 2026-09-20 audit. */\nexport interface PriceRow {\n /** Matched against the model string by longest prefix. */\n modelPrefix: string;\n /** ISO date. The latest row at or before a request's timestamp wins. */\n effectiveFrom: string;\n input: number;\n cacheRead: number;\n cacheWrite5m: number;\n cacheWrite1h: number;\n output: number;\n}\n\n/** Bump on any edit to PRICE_TABLE. The cost report prints it in its footer. */\nexport const PRICE_TABLE_VERSION = 1;\n\nconst OPUS = { input: 5.0, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10.0, output: 25.0 };\nconst SONNET = { input: 2.0, cacheRead: 0.2, cacheWrite5m: 2.5, cacheWrite1h: 4.0, output: 10.0 };\n// Fable's cache read is 0.025 of input, not the 0.1 every other model uses. Reading it as\n// 0.1 overstated the 2026-09-20 audit by about $1,700; `fable-cache-read.test` pins the ratio.\nconst FABLE = { input: 10.0, cacheRead: 0.25, cacheWrite5m: 12.5, cacheWrite1h: 20.0, output: 50.0 };\nconst HAIKU = { input: 1.0, cacheRead: 0.1, cacheWrite5m: 1.25, cacheWrite1h: 2.0, output: 5.0 };\nconst FREE = { input: 0, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 0 };\n\nconst GENESIS = \"2026-01-01\";\n\nexport const PRICE_TABLE: readonly PriceRow[] = [\n { modelPrefix: \"claude-opus-5\", effectiveFrom: GENESIS, ...OPUS },\n { modelPrefix: \"claude-opus-4-8\", effectiveFrom: GENESIS, ...OPUS },\n { modelPrefix: \"claude-sonnet-5\", effectiveFrom: GENESIS, ...SONNET },\n { modelPrefix: \"claude-fable-5-1\", effectiveFrom: GENESIS, ...FABLE },\n { modelPrefix: \"claude-fable-5\", effectiveFrom: GENESIS, ...FABLE },\n { modelPrefix: \"claude-haiku-4-5\", effectiveFrom: GENESIS, ...HAIKU },\n { modelPrefix: \"<synthetic>\", effectiveFrom: GENESIS, ...FREE },\n];\n\n/**\n * The longest prefix that matches the model, then the latest row effective at `ts`.\n * Returns null for a model no row covers: an unknown model is unpriced, never defaulted,\n * because a default silently prices a new model at an old model's rate.\n */\nexport function findPrice(model: string, ts: string, prices: readonly PriceRow[] = PRICE_TABLE): PriceRow | null {\n let best: PriceRow | null = null;\n for (const row of prices) {\n if (!model.startsWith(row.modelPrefix)) continue;\n if (row.effectiveFrom > ts) continue;\n if (best && !isBetterMatch(row, best)) continue;\n best = row;\n }\n return best;\n}\n\nfunction isBetterMatch(row: PriceRow, best: PriceRow): boolean {\n if (row.modelPrefix.length !== best.modelPrefix.length) return row.modelPrefix.length > best.modelPrefix.length;\n return row.effectiveFrom > best.effectiveFrom;\n}\n","import type { PriceRow } from \"./prices.js\";\nimport { PRICE_TABLE, findPrice } from \"./prices.js\";\n\n/** Raw token counts for one API request, as the transcript reports them. */\nexport interface RequestTokens {\n inputTokens?: number;\n cacheReadTokens?: number;\n /** Total cache creation. Used only when the 5m/1h split is absent. */\n cacheCreationTokens?: number;\n cacheCreation5mTokens?: number;\n cacheCreation1hTokens?: number;\n outputTokens?: number;\n}\n\nexport interface PricedRequest {\n inputUsd: number;\n cacheReadUsd: number;\n cacheWrite5mUsd: number;\n cacheWrite1hUsd: number;\n outputUsd: number;\n costUsd: number;\n /** False when no price row covers the model. Every component is then zero. */\n priced: boolean;\n}\n\nconst UNPRICED: PricedRequest = {\n inputUsd: 0,\n cacheReadUsd: 0,\n cacheWrite5mUsd: 0,\n cacheWrite1hUsd: 0,\n outputUsd: 0,\n costUsd: 0,\n priced: false,\n};\n\nexport function priceRequest(\n tokens: RequestTokens,\n model: string,\n ts: string,\n prices: readonly PriceRow[] = PRICE_TABLE,\n): PricedRequest {\n const row = findPrice(model, ts, prices);\n if (!row) return { ...UNPRICED };\n\n const { write5m, write1h } = splitCacheCreation(tokens);\n const inputUsd = perMTok(tokens.inputTokens, row.input);\n const cacheReadUsd = perMTok(tokens.cacheReadTokens, row.cacheRead);\n const cacheWrite5mUsd = perMTok(write5m, row.cacheWrite5m);\n const cacheWrite1hUsd = perMTok(write1h, row.cacheWrite1h);\n const outputUsd = perMTok(tokens.outputTokens, row.output);\n const costUsd = inputUsd + cacheReadUsd + cacheWrite5mUsd + cacheWrite1hUsd + outputUsd;\n return { inputUsd, cacheReadUsd, cacheWrite5mUsd, cacheWrite1hUsd, outputUsd, costUsd, priced: true };\n}\n\n/**\n * Older transcripts carry a cache_creation total with no ttl breakdown. Charging the\n * 5m rate there under-reads rather than over-reads, and 5m is the harness default.\n */\nfunction splitCacheCreation(tokens: RequestTokens): { write5m: number; write1h: number } {\n const write5m = tokens.cacheCreation5mTokens ?? 0;\n const write1h = tokens.cacheCreation1hTokens ?? 0;\n if (write5m > 0 || write1h > 0) return { write5m, write1h };\n return { write5m: tokens.cacheCreationTokens ?? 0, write1h: 0 };\n}\n\nfunction perMTok(tokens: number | undefined, ratePerMTok: number): number {\n return ((tokens ?? 0) * ratePerMTok) / 1_000_000;\n}\n","export type SessionClass = \"agent_spawned\" | \"human_interactive\" | \"headless_sdk\" | \"other_headless\";\n\n/** Human sessions only. A coordinator drives other agents; an adhoc session does not. */\nexport type HumanRole = \"coordinator\" | \"adhoc\";\n\n/** The agent-chat spawn record for a session, as the origin resolver returns it. */\nexport interface SessionOrigin {\n depth: number;\n parentName?: string | null;\n /** \"adopted\" and \"inherited\" mark a pane the human took over, not a spawned worker. */\n originKind?: string | null;\n profile?: string | null;\n}\n\nexport interface SessionFacts {\n /** The harness entrypoint. Agent-chat workers and `-p` miners both report \"sdk-cli\". */\n startType?: string | null;\n origin?: SessionOrigin | null;\n firstUserText?: string | null;\n humanTurnCount?: number;\n inboundKinds?: readonly string[];\n signalKinds?: readonly string[];\n}\n\nexport interface SessionClassification {\n sessionClass: SessionClass;\n reason: string;\n humanRole: HumanRole | null;\n}\n\nconst COORDINATOR_SIGNALS = new Set([\"agent_spawn\", \"chat_send\"]);\n\n/**\n * Rule order matters. The origin row wins over `startType` because agent-chat workers\n * run `claude -p` and so report \"sdk-cli\" exactly like a headless miner does.\n */\nexport function classifySession(facts: SessionFacts): SessionClassification {\n const [sessionClass, reason] = classOf(facts);\n const humanRole = sessionClass === \"human_interactive\" ? humanRoleOf(facts) : null;\n return { sessionClass, reason, humanRole };\n}\n\nfunction classOf(facts: SessionFacts): [SessionClass, string] {\n const origin = facts.origin;\n if (origin) {\n if (isAdoptedPane(origin)) return [\"human_interactive\", \"origin depth 0, human or adopted parent\"];\n return [\"agent_spawned\", \"origin depth >= 1\"];\n }\n if (facts.startType === \"sdk-cli\") return [\"headless_sdk\", \"start_type sdk-cli with no origin row\"];\n if (isProgrammaticPrompt(facts)) return [\"other_headless\", \"content-block prompt with <= 2 human turns\"];\n return [\"human_interactive\", \"no origin row, no headless marker\"];\n}\n\nfunction isAdoptedPane(origin: SessionOrigin): boolean {\n if (origin.depth !== 0) return false;\n if (origin.parentName === \"human\") return true;\n if (origin.originKind === \"adopted\" || origin.originKind === \"inherited\") return true;\n return !origin.profile;\n}\n\n/** Cron and RemoteTrigger daemons inject a structured content block, never typed prose. */\nfunction isProgrammaticPrompt(facts: SessionFacts): boolean {\n const text = (facts.firstUserText ?? \"\").trimStart();\n return text.startsWith(\"[{\") && (facts.humanTurnCount ?? 0) <= 2;\n}\n\nfunction humanRoleOf(facts: SessionFacts): HumanRole {\n if (facts.inboundKinds?.includes(\"channel_message\")) return \"coordinator\";\n if (facts.signalKinds?.some((kind) => COORDINATOR_SIGNALS.has(kind))) return \"coordinator\";\n return \"adhoc\";\n}\n","/** Half-open [lo, hi) in the band's own unit. */\nexport interface Band {\n lo: number;\n hi: number;\n name: string;\n}\n\n/** Context size in tokens. From `cf_analyze.py` BANDS. */\nexport const CONTEXT_BANDS: readonly Band[] = [\n { lo: 0, hi: 50_000, name: \"<50k\" },\n { lo: 50_000, hi: 100_000, name: \"50-100k\" },\n { lo: 100_000, hi: 200_000, name: \"100-200k\" },\n { lo: 200_000, hi: Number.POSITIVE_INFINITY, name: \"200k+\" },\n];\n\n/** Idle gap in milliseconds. From `cf_analyze.py` IDLE. */\nexport const GAP_BANDS: readonly Band[] = [\n { lo: 0, hi: 5 * 60_000, name: \"<5m\" },\n { lo: 5 * 60_000, hi: 60 * 60_000, name: \"5-60m\" },\n { lo: 60 * 60_000, hi: Number.POSITIVE_INFINITY, name: \">60m\" },\n];\n\n/** Null for a value no band covers, so a caller reports the gap instead of inventing one. */\nexport function bandOf(value: number, bands: readonly Band[]): string | null {\n for (const band of bands) {\n if (value >= band.lo && value < band.hi) return band.name;\n }\n return null;\n}\n\nexport function contextBand(tokens: number): string | null {\n return bandOf(tokens, CONTEXT_BANDS);\n}\n\nexport function gapBand(millis: number): string | null {\n return bandOf(millis, GAP_BANDS);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/prices.ts","../src/price-request.ts","../src/classify-session.ts","../src/bands.ts","../src/cost-report.ts","../src/cost-report-queries.ts","../src/episodes.ts","../src/initiative.ts","../src/roles.ts","../src/render-text.ts"],"sourcesContent":["/** USD per million tokens. Fitted against 392 `cost-state` rows in the 2026-09-20 audit. */\nexport interface PriceRow {\n /** Matched against the model string by longest prefix. */\n modelPrefix: string;\n /** ISO date. The latest row at or before a request's timestamp wins. */\n effectiveFrom: string;\n input: number;\n cacheRead: number;\n cacheWrite5m: number;\n cacheWrite1h: number;\n output: number;\n}\n\n/** Bump on any edit to PRICE_TABLE. The cost report prints it in its footer. */\nexport const PRICE_TABLE_VERSION = 1;\n\nconst OPUS = { input: 5.0, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10.0, output: 25.0 };\nconst SONNET = { input: 2.0, cacheRead: 0.2, cacheWrite5m: 2.5, cacheWrite1h: 4.0, output: 10.0 };\n// Fable's cache read is 0.025 of input, not the 0.1 every other model uses. Reading it as\n// 0.1 overstated the 2026-09-20 audit by about $1,700; `fable-cache-read.test` pins the ratio.\nconst FABLE = { input: 10.0, cacheRead: 0.25, cacheWrite5m: 12.5, cacheWrite1h: 20.0, output: 50.0 };\nconst HAIKU = { input: 1.0, cacheRead: 0.1, cacheWrite5m: 1.25, cacheWrite1h: 2.0, output: 5.0 };\nconst FREE = { input: 0, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 0 };\n\nconst GENESIS = \"2026-01-01\";\n\nexport const PRICE_TABLE: readonly PriceRow[] = [\n { modelPrefix: \"claude-opus-5\", effectiveFrom: GENESIS, ...OPUS },\n { modelPrefix: \"claude-opus-4-8\", effectiveFrom: GENESIS, ...OPUS },\n { modelPrefix: \"claude-sonnet-5\", effectiveFrom: GENESIS, ...SONNET },\n { modelPrefix: \"claude-fable-5-1\", effectiveFrom: GENESIS, ...FABLE },\n { modelPrefix: \"claude-fable-5\", effectiveFrom: GENESIS, ...FABLE },\n { modelPrefix: \"claude-haiku-4-5\", effectiveFrom: GENESIS, ...HAIKU },\n { modelPrefix: \"<synthetic>\", effectiveFrom: GENESIS, ...FREE },\n];\n\n/**\n * The longest prefix that matches the model, then the latest row effective at `ts`.\n * Returns null for a model no row covers: an unknown model is unpriced, never defaulted,\n * because a default silently prices a new model at an old model's rate.\n */\nexport function findPrice(model: string, ts: string, prices: readonly PriceRow[] = PRICE_TABLE): PriceRow | null {\n let best: PriceRow | null = null;\n for (const row of prices) {\n if (!model.startsWith(row.modelPrefix)) continue;\n if (row.effectiveFrom > ts) continue;\n if (best && !isBetterMatch(row, best)) continue;\n best = row;\n }\n return best;\n}\n\nfunction isBetterMatch(row: PriceRow, best: PriceRow): boolean {\n if (row.modelPrefix.length !== best.modelPrefix.length) return row.modelPrefix.length > best.modelPrefix.length;\n return row.effectiveFrom > best.effectiveFrom;\n}\n","import type { PriceRow } from \"./prices.js\";\nimport { PRICE_TABLE, findPrice } from \"./prices.js\";\n\n/** Raw token counts for one API request, as the transcript reports them. */\nexport interface RequestTokens {\n inputTokens?: number;\n cacheReadTokens?: number;\n /** Total cache creation. Used only when the 5m/1h split is absent. */\n cacheCreationTokens?: number;\n cacheCreation5mTokens?: number;\n cacheCreation1hTokens?: number;\n outputTokens?: number;\n}\n\nexport interface PricedRequest {\n inputUsd: number;\n cacheReadUsd: number;\n cacheWrite5mUsd: number;\n cacheWrite1hUsd: number;\n outputUsd: number;\n costUsd: number;\n /** False when no price row covers the model. Every component is then zero. */\n priced: boolean;\n}\n\nconst UNPRICED: PricedRequest = {\n inputUsd: 0,\n cacheReadUsd: 0,\n cacheWrite5mUsd: 0,\n cacheWrite1hUsd: 0,\n outputUsd: 0,\n costUsd: 0,\n priced: false,\n};\n\nexport function priceRequest(\n tokens: RequestTokens,\n model: string,\n ts: string,\n prices: readonly PriceRow[] = PRICE_TABLE,\n): PricedRequest {\n const row = findPrice(model, ts, prices);\n if (!row) return { ...UNPRICED };\n\n const { write5m, write1h } = splitCacheCreation(tokens);\n const inputUsd = perMTok(tokens.inputTokens, row.input);\n const cacheReadUsd = perMTok(tokens.cacheReadTokens, row.cacheRead);\n const cacheWrite5mUsd = perMTok(write5m, row.cacheWrite5m);\n const cacheWrite1hUsd = perMTok(write1h, row.cacheWrite1h);\n const outputUsd = perMTok(tokens.outputTokens, row.output);\n const costUsd = inputUsd + cacheReadUsd + cacheWrite5mUsd + cacheWrite1hUsd + outputUsd;\n return { inputUsd, cacheReadUsd, cacheWrite5mUsd, cacheWrite1hUsd, outputUsd, costUsd, priced: true };\n}\n\n/**\n * Older transcripts carry a cache_creation total with no ttl breakdown. Charging the\n * 5m rate there under-reads rather than over-reads, and 5m is the harness default.\n */\nfunction splitCacheCreation(tokens: RequestTokens): { write5m: number; write1h: number } {\n const write5m = tokens.cacheCreation5mTokens ?? 0;\n const write1h = tokens.cacheCreation1hTokens ?? 0;\n if (write5m > 0 || write1h > 0) return { write5m, write1h };\n return { write5m: tokens.cacheCreationTokens ?? 0, write1h: 0 };\n}\n\nfunction perMTok(tokens: number | undefined, ratePerMTok: number): number {\n return ((tokens ?? 0) * ratePerMTok) / 1_000_000;\n}\n","export type SessionClass = \"agent_spawned\" | \"human_interactive\" | \"headless_sdk\" | \"other_headless\";\n\n/** Human sessions only. A coordinator drives other agents; an adhoc session does not. */\nexport type HumanRole = \"coordinator\" | \"adhoc\";\n\n/** The agent-chat spawn record for a session, as the origin resolver returns it. */\nexport interface SessionOrigin {\n depth: number;\n parentName?: string | null;\n /** \"adopted\" and \"inherited\" mark a pane the human took over, not a spawned worker. */\n originKind?: string | null;\n profile?: string | null;\n}\n\nexport interface SessionFacts {\n /** The harness entrypoint. Agent-chat workers and `-p` miners both report \"sdk-cli\". */\n startType?: string | null;\n origin?: SessionOrigin | null;\n firstUserText?: string | null;\n humanTurnCount?: number;\n inboundKinds?: readonly string[];\n signalKinds?: readonly string[];\n}\n\nexport interface SessionClassification {\n sessionClass: SessionClass;\n reason: string;\n humanRole: HumanRole | null;\n}\n\nconst COORDINATOR_SIGNALS = new Set([\"agent_spawn\", \"chat_send\"]);\n\n/**\n * Rule order matters. The origin row wins over `startType` because agent-chat workers\n * run `claude -p` and so report \"sdk-cli\" exactly like a headless miner does.\n */\nexport function classifySession(facts: SessionFacts): SessionClassification {\n const [sessionClass, reason] = classOf(facts);\n const humanRole = sessionClass === \"human_interactive\" ? humanRoleOf(facts) : null;\n return { sessionClass, reason, humanRole };\n}\n\nfunction classOf(facts: SessionFacts): [SessionClass, string] {\n const origin = facts.origin;\n if (origin) {\n if (isAdoptedPane(origin)) return [\"human_interactive\", \"origin depth 0, human or adopted parent\"];\n return [\"agent_spawned\", \"origin depth >= 1\"];\n }\n if (facts.startType === \"sdk-cli\") return [\"headless_sdk\", \"start_type sdk-cli with no origin row\"];\n if (isProgrammaticPrompt(facts)) return [\"other_headless\", \"content-block prompt with <= 2 human turns\"];\n return [\"human_interactive\", \"no origin row, no headless marker\"];\n}\n\nfunction isAdoptedPane(origin: SessionOrigin): boolean {\n if (origin.depth !== 0) return false;\n if (origin.parentName === \"human\") return true;\n if (origin.originKind === \"adopted\" || origin.originKind === \"inherited\") return true;\n return !origin.profile;\n}\n\n/** Cron and RemoteTrigger daemons inject a structured content block, never typed prose. */\nfunction isProgrammaticPrompt(facts: SessionFacts): boolean {\n const text = (facts.firstUserText ?? \"\").trimStart();\n return text.startsWith(\"[{\") && (facts.humanTurnCount ?? 0) <= 2;\n}\n\nfunction humanRoleOf(facts: SessionFacts): HumanRole {\n if (facts.inboundKinds?.includes(\"channel_message\")) return \"coordinator\";\n if (facts.signalKinds?.some((kind) => COORDINATOR_SIGNALS.has(kind))) return \"coordinator\";\n return \"adhoc\";\n}\n","/** Half-open [lo, hi) in the band's own unit. */\nexport interface Band {\n lo: number;\n hi: number;\n name: string;\n}\n\n/** Context size in tokens. From `cf_analyze.py` BANDS. */\nexport const CONTEXT_BANDS: readonly Band[] = [\n { lo: 0, hi: 50_000, name: \"<50k\" },\n { lo: 50_000, hi: 100_000, name: \"50-100k\" },\n { lo: 100_000, hi: 200_000, name: \"100-200k\" },\n { lo: 200_000, hi: Number.POSITIVE_INFINITY, name: \"200k+\" },\n];\n\n/** Idle gap in milliseconds. From `cf_analyze.py` IDLE. */\nexport const GAP_BANDS: readonly Band[] = [\n { lo: 0, hi: 5 * 60_000, name: \"<5m\" },\n { lo: 5 * 60_000, hi: 60 * 60_000, name: \"5-60m\" },\n { lo: 60 * 60_000, hi: Number.POSITIVE_INFINITY, name: \">60m\" },\n];\n\n/** Null for a value no band covers, so a caller reports the gap instead of inventing one. */\nexport function bandOf(value: number, bands: readonly Band[]): string | null {\n for (const band of bands) {\n if (value >= band.lo && value < band.hi) return band.name;\n }\n return null;\n}\n\nexport function contextBand(tokens: number): string | null {\n return bandOf(tokens, CONTEXT_BANDS);\n}\n\nexport function gapBand(millis: number): string | null {\n return bandOf(millis, GAP_BANDS);\n}\n","import type { Db } from \"@titan-design/store-sqlite\";\nimport { z } from \"zod\";\nimport { classifySession, type SessionClass } from \"./classify-session.js\";\nimport {\n readCompactions,\n readCostRows,\n readCoverage,\n readPriceTableVersion,\n readSessionContexts,\n type CompactionRow,\n type CostRow,\n type ReportWindow,\n type SessionContext,\n} from \"./cost-report-queries.js\";\nimport { assignmentCount, heuristicFor } from \"./episodes.js\";\nimport { sessionInitiative } from \"./initiative.js\";\nimport { sessionRole } from \"./roles.js\";\n\nconst count = z.number().int().nonnegative();\nconst bucketSchema = z.object({ key: z.string(), requests: count, sessions: count, costUsd: z.number() });\nconst wakePartSchema = bucketSchema.extend({ midLoopRequests: count, midLoopCostUsd: z.number() });\nconst cellSchema = z.object({ wakeCause: z.string(), gapBand: z.string(), requests: count, costUsd: z.number() });\n\nexport const TOKEN_CLASSES = [\"input\", \"cache_read\", \"cache_write_5m\", \"cache_write_1h\", \"output\"] as const;\nexport type TokenClass = (typeof TOKEN_CLASSES)[number];\n\nexport const costReportSchema = z.object({\n window: z.object({ since: z.string().nullable(), until: z.string().nullable() }),\n priceTableVersion: z.number().int().nullable(),\n totals: z.object({ requests: count, sessions: count, costUsd: z.number(), unpricedRequests: count }),\n byTokenClass: z.array(z.object({ tokenClass: z.enum(TOKEN_CLASSES), tokens: count, costUsd: z.number() })),\n byAccount: z.array(bucketSchema),\n byModel: z.array(bucketSchema),\n byClass: z.array(bucketSchema),\n byRole: z.array(bucketSchema),\n /** Sessions by how many episodes their class's heuristic cut them into; `none` when not yet segmented. */\n byEpisodeCount: z.array(bucketSchema),\n byInitiative: z.array(bucketSchema),\n byContextBand: z.array(bucketSchema),\n byWakeCause: z.array(wakePartSchema.extend({ parts: z.array(wakePartSchema) })),\n wakeCauseByGapBand: z.array(cellSchema),\n coldRebuild: z.object({ requests: count, costUsd: z.number(), byGapBandAndCause: z.array(cellSchema) }),\n compactions: z.object({ total: count, manual: count, auto: count, midLoop: count, droppedTokens: count }),\n topSessions: z.array(\n z.object({\n sessionId: z.string(),\n sessionClass: z.string(),\n role: z.string(),\n initiative: z.string(),\n account: z.string(),\n requests: count,\n costUsd: z.number(),\n }),\n ),\n unpricedModels: z.array(z.object({ model: z.string(), requests: count, tokens: count })),\n coverage: z.object({ transcriptsIndexed: count, transcriptsDiscovered: count.nullable(), facetBacklog: count }),\n});\n\nexport type CostReport = z.infer<typeof costReportSchema>;\nexport type CostBucket = z.infer<typeof bucketSchema>;\nexport type WakeCauseBucket = CostReport[\"byWakeCause\"][number];\nexport type WakeGapCell = z.infer<typeof cellSchema>;\n\nexport interface CostReportOptions {\n /** ISO timestamp or date, inclusive. Overrides `days`. */\n since?: string;\n /** ISO timestamp or date, exclusive. */\n until?: string;\n /** Window length ending at `until`, or at `now` when `until` is absent. */\n days?: number;\n now?: Date;\n /** Sessions listed in `topSessions`. */\n top?: number;\n /** Transcripts the caller discovered on disk; the graph cannot know this. */\n transcriptsDiscovered?: number;\n /** The extraction version an audit facet must reach to count as current. */\n facetVersion?: number;\n}\n\n/** AskUserQuestion answers are the human answering, so they count under `human` (decisions Q4). */\nconst HUMAN_CAUSES: readonly string[] = [\"human_typed\", \"ask_user_answer\"];\nconst NONE = \"none\";\nconst DEFAULT_TOP = 10;\nconst EPISODE_COUNT_CAP = 5;\nconst DAY_MS = 86_400_000;\n\ninterface SessionTags {\n sessionClass: SessionClass;\n role: string;\n episodeCount: string;\n initiative: string;\n account: string;\n}\n\ntype TaggedRow = CostRow & SessionTags;\n\n/** Reads the graph and never writes it, so a read-only connection is enough. */\nexport function costReport(db: Db, options: CostReportOptions = {}): CostReport {\n const window = resolveWindow(options);\n const costRows = readCostRows(db, window);\n const sessionIds = [...new Set(costRows.map((row) => row.sessionId))];\n const tags = tagSessions(readSessionContexts(db, sessionIds));\n const rows = costRows.map((row) => ({ ...row, ...tags.get(row.sessionId)! }));\n return {\n window,\n priceTableVersion: readPriceTableVersion(db),\n totals: { ...sumOf(rows), sessions: sessionIds.length, unpricedRequests: rows.filter((row) => !row.priced).length },\n byTokenClass: tokenClasses(rows),\n ...dimensionBuckets(rows),\n byWakeCause: wakeCauses(rows),\n wakeCauseByGapBand: gapCells(rows),\n coldRebuild: coldRebuild(rows),\n compactions: compactionCounts(readCompactions(db, window)),\n topSessions: topSessions(rows, options.top ?? DEFAULT_TOP),\n unpricedModels: unpricedModels(rows),\n coverage: { ...readCoverage(db, options.facetVersion ?? null), transcriptsDiscovered: options.transcriptsDiscovered ?? null },\n };\n}\n\nexport function resolveWindow(options: CostReportOptions): ReportWindow {\n const until = options.until ?? null;\n if (options.since !== undefined) return { since: options.since, until };\n if (options.days === undefined) return { since: null, until };\n const end = until ? Date.parse(until) : (options.now ?? new Date()).getTime();\n return { since: new Date(end - options.days * DAY_MS).toISOString(), until };\n}\n\nfunction tagSessions(contexts: Map<string, SessionContext>): Map<string, SessionTags> {\n const tags = new Map<string, SessionTags>();\n for (const [sessionId, context] of contexts) {\n const classification = classifySession(context.facts);\n const episodes = context.episodes.filter((episode) => episode.heuristic === heuristicFor(classification.sessionClass));\n const worker = { profile: context.facts.origin?.profile, lifetimeMs: context.lifetimeMs, assignments: assignmentCount(episodes) };\n tags.set(sessionId, {\n sessionClass: classification.sessionClass,\n role: sessionRole(classification, worker),\n episodeCount: episodeCountKey(episodes.length),\n initiative: sessionInitiative(context.tasks, context.cwd),\n account: context.account ?? \"unknown\",\n });\n }\n return tags;\n}\n\n/** The worker report's assignment buckets: 1, 2, 3, 4, 5+. */\nfunction episodeCountKey(episodes: number): string {\n if (episodes === 0) return NONE;\n return episodes >= EPISODE_COUNT_CAP ? `${EPISODE_COUNT_CAP}+` : String(episodes);\n}\n\nfunction sumOf(rows: readonly CostRow[]): { requests: number; costUsd: number } {\n return { requests: rows.length, costUsd: rows.reduce((sum, row) => sum + row.costUsd, 0) };\n}\n\nfunction tokenClasses(rows: readonly CostRow[]): CostReport[\"byTokenClass\"] {\n const pick: Record<TokenClass, (row: CostRow) => [number, number]> = {\n input: (row) => [row.inputTokens, row.inputCostUsd],\n cache_read: (row) => [row.cacheReadTokens, row.cacheReadCostUsd],\n cache_write_5m: (row) => [row.cacheWrite5mTokens, row.cacheWrite5mCostUsd],\n cache_write_1h: (row) => [row.cacheWrite1hTokens, row.cacheWrite1hCostUsd],\n output: (row) => [row.outputTokens, row.outputCostUsd],\n };\n return TOKEN_CLASSES.map((tokenClass) => {\n const pairs = rows.map(pick[tokenClass]);\n return { tokenClass, tokens: pairs.reduce((sum, [tokens]) => sum + tokens, 0), costUsd: pairs.reduce((sum, [, cost]) => sum + cost, 0) };\n });\n}\n\nfunction dimensionBuckets(rows: readonly TaggedRow[]) {\n return {\n byAccount: bucketsBy(rows, (row) => row.account),\n byModel: bucketsBy(rows, (row) => row.model),\n byClass: bucketsBy(rows, (row) => row.sessionClass),\n byRole: bucketsBy(rows, (row) => row.role),\n byEpisodeCount: bucketsBy(rows, (row) => row.episodeCount),\n byInitiative: bucketsBy(rows, (row) => row.initiative),\n byContextBand: bucketsBy(rows, (row) => row.contextBand),\n };\n}\n\n/** Most expensive first; ties by key so the report is stable. */\nfunction bucketsBy<T extends CostRow>(rows: readonly T[], keyOf: (row: T) => string): CostBucket[] {\n const groups = groupRows(rows, keyOf);\n return [...groups]\n .map(([key, members]) => ({ key, ...sumOf(members), sessions: new Set(members.map((row) => row.sessionId)).size }))\n .sort(byCostThenKey);\n}\n\nfunction groupRows<T>(rows: readonly T[], keyOf: (row: T) => string): Map<string, T[]> {\n const groups = new Map<string, T[]>();\n for (const row of rows) {\n const key = keyOf(row);\n const members = groups.get(key);\n if (members) members.push(row);\n else groups.set(key, [row]);\n }\n return groups;\n}\n\nfunction byCostThenKey(a: { key: string; costUsd: number }, b: { key: string; costUsd: number }): number {\n return b.costUsd - a.costUsd || a.key.localeCompare(b.key);\n}\n\nfunction wakeCauses(rows: readonly CostRow[]): WakeCauseBucket[] {\n const causeOf = (row: CostRow) => row.wakeCause ?? NONE;\n const groupOf = (row: CostRow) => (HUMAN_CAUSES.includes(causeOf(row)) ? \"human\" : causeOf(row));\n return [...groupRows(rows, groupOf)]\n .map(([key, members]) => ({\n ...wakePart(key, members),\n parts: key === \"human\" ? [...groupRows(members, causeOf)].map(([cause, part]) => wakePart(cause, part)).sort(byCostThenKey) : [],\n }))\n .sort(byCostThenKey);\n}\n\nfunction wakePart(key: string, rows: readonly CostRow[]) {\n const midLoop = rows.filter((row) => row.wakeDelivery === \"mid_loop\");\n const sessions = new Set(rows.map((row) => row.sessionId)).size;\n return { key, ...sumOf(rows), sessions, midLoopRequests: midLoop.length, midLoopCostUsd: sumOf(midLoop).costUsd };\n}\n\nfunction gapCells(rows: readonly CostRow[]): WakeGapCell[] {\n const keyOf = (row: CostRow) => JSON.stringify([row.wakeCause ?? NONE, row.gapBand ?? NONE]);\n return [...groupRows(rows, keyOf)]\n .map(([key, members]) => {\n const [wakeCause, gapBand] = JSON.parse(key) as [string, string];\n return { wakeCause, gapBand, ...sumOf(members) };\n })\n .sort((a, b) => b.costUsd - a.costUsd || a.wakeCause.localeCompare(b.wakeCause) || a.gapBand.localeCompare(b.gapBand));\n}\n\nfunction coldRebuild(rows: readonly CostRow[]): CostReport[\"coldRebuild\"] {\n const cold = rows.filter((row) => row.isCold);\n return { ...sumOf(cold), byGapBandAndCause: gapCells(cold) };\n}\n\nfunction compactionCounts(rows: readonly CompactionRow[]): CostReport[\"compactions\"] {\n return {\n total: rows.length,\n manual: rows.filter((row) => row.trigger === \"manual\").length,\n auto: rows.filter((row) => row.trigger === \"auto\").length,\n midLoop: rows.filter((row) => row.midLoop).length,\n droppedTokens: rows.reduce((sum, row) => sum + (row.droppedTokens ?? 0), 0),\n };\n}\n\nfunction topSessions(rows: readonly TaggedRow[], top: number): CostReport[\"topSessions\"] {\n return [...groupRows(rows, (row) => row.sessionId)]\n .map(([sessionId, members]) => {\n const { sessionClass, role, initiative, account } = members[0]!;\n return { sessionId, sessionClass, role, initiative, account, ...sumOf(members) };\n })\n .sort((a, b) => b.costUsd - a.costUsd || a.sessionId.localeCompare(b.sessionId))\n .slice(0, top);\n}\n\nfunction unpricedModels(rows: readonly CostRow[]): CostReport[\"unpricedModels\"] {\n const unpriced = rows.filter((row) => !row.priced);\n return [...groupRows(unpriced, (row) => row.model)]\n .map(([model, members]) => ({ model, requests: members.length, tokens: members.reduce((sum, row) => sum + tokensOf(row), 0) }))\n .sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model));\n}\n\nfunction tokensOf(row: CostRow): number {\n return row.inputTokens + row.cacheReadTokens + row.cacheWrite5mTokens + row.cacheWrite1hTokens + row.outputTokens;\n}\n","import { AUDIT_FACET, EPISODE_TABLE, FACET_TABLE, KIT } from \"@titan-design/session-graph\";\nimport type { Db } from \"@titan-design/store-sqlite\";\nimport type { SessionFacts } from \"./classify-session.js\";\nimport type { TaskInitiative } from \"./initiative.js\";\n\n/** ISO bounds on request `ts`: `since` inclusive, `until` exclusive, null for open. */\nexport interface ReportWindow {\n since: string | null;\n until: string | null;\n}\n\n/** One deduplicated, priced request as the `request_cost` view returns it. */\nexport interface CostRow {\n sessionId: string;\n model: string;\n inputTokens: number;\n cacheReadTokens: number;\n cacheWrite5mTokens: number;\n cacheWrite1hTokens: number;\n outputTokens: number;\n inputCostUsd: number;\n cacheReadCostUsd: number;\n cacheWrite5mCostUsd: number;\n cacheWrite1hCostUsd: number;\n outputCostUsd: number;\n costUsd: number;\n priced: boolean;\n isCold: boolean;\n contextBand: string;\n gapBand: string | null;\n wakeCause: string | null;\n wakeDelivery: string | null;\n}\n\n/** What the report needs about one session beyond its requests. */\nexport interface SessionContext {\n facts: SessionFacts;\n cwd: string | null;\n account: string | null;\n tasks: TaskInitiative[];\n /** First to last main-thread request over the whole session, not just the window. */\n lifetimeMs: number;\n /** Stored episode rows of every heuristic, in episode order. */\n episodes: StoredEpisode[];\n}\n\nexport interface StoredEpisode {\n heuristic: string;\n openedBy: string;\n}\n\nexport interface CompactionRow {\n trigger: string | null;\n midLoop: boolean;\n droppedTokens: number | null;\n}\n\nconst IN_WINDOW = \"(@since IS NULL OR ts >= @since) AND (@until IS NULL OR ts < @until)\";\nconst IN_SESSIONS = \"session_id IN (SELECT value FROM json_each(@ids))\";\n\n// The 5m write column mirrors the view: a split-less write is billed at the 5m rate.\nconst COST_ROWS = `\n SELECT session_id AS sessionId, model,\n input_tokens AS inputTokens, cache_read_tokens AS cacheReadTokens,\n CASE WHEN cache_creation_5m + cache_creation_1h = 0 THEN cache_creation_tokens ELSE cache_creation_5m END AS cacheWrite5mTokens,\n cache_creation_1h AS cacheWrite1hTokens, output_tokens AS outputTokens,\n input_cost_usd AS inputCostUsd, cache_read_cost_usd AS cacheReadCostUsd,\n cache_write_5m_cost_usd AS cacheWrite5mCostUsd, cache_write_1h_cost_usd AS cacheWrite1hCostUsd,\n output_cost_usd AS outputCostUsd, cost_usd AS costUsd, priced, is_cold AS isCold,\n context_band AS contextBand, gap_band AS gapBand, wake_cause AS wakeCause, wake_delivery AS wakeDelivery\n FROM request_cost WHERE ${IN_WINDOW}\n`;\n\nexport function readCostRows(db: Db, window: ReportWindow): CostRow[] {\n const rows = db.prepare(COST_ROWS).all(window) as (Omit<CostRow, \"priced\" | \"isCold\"> & { priced: number; isCold: number })[];\n return rows.map((row) => ({ ...row, priced: row.priced === 1, isCold: row.isCold === 1 }));\n}\n\nexport function readCompactions(db: Db, window: ReportWindow): CompactionRow[] {\n const sql = `SELECT trigger, mid_loop AS midLoop, dropped_tokens AS droppedTokens FROM compaction WHERE ${IN_WINDOW}`;\n const rows = db.prepare(sql).all(window) as (Omit<CompactionRow, \"midLoop\"> & { midLoop: number | null })[];\n return rows.map((row) => ({ ...row, midLoop: row.midLoop === 1 }));\n}\n\n/** The graph's own price version, which is what `request_cost` priced with; null before any price row exists. */\nexport function readPriceTableVersion(db: Db): number | null {\n return (db.prepare(\"SELECT MAX(table_version) AS version FROM price\").get() as { version: number | null }).version;\n}\n\n/** Indexed transcripts, and those whose audit facet is missing or older than `facetVersion`. */\nexport function readCoverage(db: Db, facetVersion: number | null): { transcriptsIndexed: number; facetBacklog: number } {\n const indexed = db.prepare(`SELECT COUNT(*) AS n FROM \"${KIT.watermark}\"`).get() as { n: number };\n const backlog = db\n .prepare(\n `SELECT COUNT(*) AS n FROM \"${KIT.watermark}\" t LEFT JOIN \"${FACET_TABLE}\" f\n ON f.transcript_id = t.source_id AND f.facet = @facet AND (@version IS NULL OR f.version >= @version)\n WHERE f.transcript_id IS NULL`,\n )\n .get({ facet: AUDIT_FACET, version: facetVersion }) as { n: number };\n return { transcriptsIndexed: indexed.n, facetBacklog: backlog.n };\n}\n\nexport function readSessionContexts(db: Db, sessionIds: readonly string[]): Map<string, SessionContext> {\n const ids = JSON.stringify(sessionIds);\n const contexts = new Map<string, SessionContext>(sessionIds.map((id) => [id, emptyContext()]));\n addSessionRows(db, ids, contexts);\n addOrigins(db, ids, contexts);\n addInbounds(db, ids, contexts);\n addSignals(db, ids, contexts);\n addTaskInitiatives(db, ids, contexts);\n addLifetimes(db, ids, contexts);\n addEpisodes(db, ids, contexts);\n return contexts;\n}\n\nfunction emptyContext(): SessionContext {\n return { facts: { inboundKinds: [], signalKinds: [], humanTurnCount: 0 }, cwd: null, account: null, tasks: [], lifetimeMs: 0, episodes: [] };\n}\n\nfunction addSessionRows(db: Db, ids: string, contexts: Map<string, SessionContext>): void {\n const sql = `SELECT session_id AS sessionId, start_type AS startType, cwd, seed_prompt AS seedPrompt, account FROM session WHERE ${IN_SESSIONS}`;\n type Row = { sessionId: string; startType: string | null; cwd: string | null; seedPrompt: string | null; account: string | null };\n for (const row of db.prepare(sql).all({ ids }) as Row[]) {\n const context = contexts.get(row.sessionId)!;\n context.cwd = row.cwd;\n context.account = row.account;\n context.facts.startType = row.startType;\n context.facts.firstUserText = row.seedPrompt;\n }\n}\n\nfunction addOrigins(db: Db, ids: string, contexts: Map<string, SessionContext>): void {\n const sql = `SELECT session_id AS sessionId, depth, parent_name AS parentName, origin_kind AS originKind, profile FROM session_origin WHERE ${IN_SESSIONS}`;\n type Row = { sessionId: string; depth: number | null; parentName: string | null; originKind: string | null; profile: string | null };\n for (const { sessionId, depth, ...origin } of db.prepare(sql).all({ ids }) as Row[]) {\n contexts.get(sessionId)!.facts.origin = { ...origin, depth: depth ?? 0 };\n }\n}\n\nfunction addInbounds(db: Db, ids: string, contexts: Map<string, SessionContext>): void {\n const sql = `SELECT session_id AS sessionId, cause, SUM(delivery = 'turn_start') AS turnStarts FROM inbound WHERE ${IN_SESSIONS} GROUP BY session_id, cause`;\n for (const row of db.prepare(sql).all({ ids }) as { sessionId: string; cause: string; turnStarts: number }[]) {\n const facts = contexts.get(row.sessionId)!.facts;\n facts.inboundKinds = [...(facts.inboundKinds ?? []), row.cause];\n if (row.cause === \"human_typed\") facts.humanTurnCount = row.turnStarts;\n }\n}\n\nfunction addSignals(db: Db, ids: string, contexts: Map<string, SessionContext>): void {\n const sql = `SELECT DISTINCT session_id AS sessionId, signal FROM session_signal WHERE ${IN_SESSIONS}`;\n for (const row of db.prepare(sql).all({ ids }) as { sessionId: string; signal: string }[]) {\n const facts = contexts.get(row.sessionId)!.facts;\n facts.signalKinds = [...(facts.signalKinds ?? []), row.signal];\n }\n}\n\nfunction addTaskInitiatives(db: Db, ids: string, contexts: Map<string, SessionContext>): void {\n const sql = `\n SELECT substr(e.source_ref, 9) AS sessionId, t.initiative, COUNT(*) AS edges\n FROM \"${KIT.edge}\" e JOIN task t ON t.task_ref = e.target_ref\n WHERE e.relation = 'ran' AND e.t_expired IS NULL AND t.initiative IS NOT NULL\n AND e.source_ref IN (SELECT 'session:' || value FROM json_each(@ids))\n GROUP BY e.source_ref, t.initiative`;\n for (const row of db.prepare(sql).all({ ids }) as ({ sessionId: string } & TaskInitiative)[]) {\n contexts.get(row.sessionId)!.tasks.push({ initiative: row.initiative, edges: row.edges });\n }\n}\n\nfunction addLifetimes(db: Db, ids: string, contexts: Map<string, SessionContext>): void {\n const sql = `SELECT session_id AS sessionId, MIN(ts) AS first, MAX(ts) AS last FROM request_dedup WHERE ${IN_SESSIONS} AND is_sidechain = 0 GROUP BY session_id`;\n for (const row of db.prepare(sql).all({ ids }) as { sessionId: string; first: string; last: string }[]) {\n contexts.get(row.sessionId)!.lifetimeMs = Date.parse(row.last) - Date.parse(row.first);\n }\n}\n\nfunction addEpisodes(db: Db, ids: string, contexts: Map<string, SessionContext>): void {\n const sql = `SELECT session_id AS sessionId, heuristic, opened_by AS openedBy FROM \"${EPISODE_TABLE}\" WHERE ${IN_SESSIONS} ORDER BY session_id, heuristic, episode_index`;\n for (const { sessionId, ...episode } of db.prepare(sql).all({ ids }) as ({ sessionId: string } & StoredEpisode)[]) {\n contexts.get(sessionId)!.episodes.push(episode);\n }\n}\n","import { replaceEpisodes, type EpisodeRow, type SessionGraph } from \"@titan-design/session-graph\";\nimport type { Db } from \"@titan-design/store-sqlite\";\nimport { classifySession, type SessionClass } from \"./classify-session.js\";\nimport { readSessionContexts } from \"./cost-report-queries.js\";\n\n/** Two rule sets on purpose (design section 18): workers segment by assignment, coordinators by work phase. */\nexport type Heuristic = \"worker-v1\" | \"coordinator-v1\";\nexport const HEURISTIC_VERSIONS: Readonly<Record<Heuristic, number>> = { \"worker-v1\": 1, \"coordinator-v1\": 1 };\n\nexport interface EpisodeRequest {\n offset: number;\n ts: string;\n contextTokens: number;\n wakeCause: string | null;\n}\nexport interface EpisodeInbound {\n offset: number;\n ts: string;\n cause: string;\n}\nexport interface EpisodeSignal {\n offset: number;\n ts: string;\n signal: string;\n}\nexport interface EpisodeInput {\n requests: readonly EpisodeRequest[];\n inbounds: readonly EpisodeInbound[];\n signals: readonly EpisodeSignal[];\n /** An agent-chat spawn has a brief; anything else opens at `session_start`. */\n spawned: boolean;\n}\n\nconst MINUTE_MS = 60_000;\nexport const IDLE_GAP_MS = 30 * MINUTE_MS;\nexport const CHANNEL_CLUSTER_MS = 10 * MINUTE_MS;\nexport const COORDINATOR_MIN_EPISODE_REQUESTS = 8;\nexport const COORDINATOR_MERGE_DEDUPE_REQUESTS = 15;\nexport const COORDINATOR_WAVE_QUIET_REQUESTS = 15;\nexport const CONTEXT_RESET_TOKENS = 20_000;\nconst DELIVERABLE_SIGNALS = new Set([\"status_report\", \"pr_create\", \"commit\", \"doc_written\", \"task_wrap\"]);\nconst ASSIGNMENT_OPENERS = new Set([\"brief\", \"channel_followup\"]);\n\n/** Pure: the same rows always cut the same way. Rows come back in episode order. */\nexport function buildEpisodes(input: EpisodeInput, heuristic: Heuristic): EpisodeRow[] {\n return heuristic === \"worker-v1\" ? workerEpisodes(input) : coordinatorEpisodes(input);\n}\n\n/** Workers are segmented; human sessions get the coordinator rule; headless runs get none. */\nexport function heuristicFor(sessionClass: SessionClass): Heuristic | null {\n if (sessionClass === \"agent_spawned\") return \"worker-v1\";\n if (sessionClass === \"human_interactive\") return \"coordinator-v1\";\n return null;\n}\n\n/** The worker report's `n_assignments`: episodes an assignment opened, which is what the standing-peer overlay counts. */\nexport function assignmentCount(rows: readonly Pick<EpisodeRow, \"openedBy\">[]): number {\n return rows.filter((row) => ASSIGNMENT_OPENERS.has(row.openedBy)).length;\n}\n\n// ---------------------------------------------------------------- worker-v1\n\ntype Event =\n | { kind: \"request\"; offset: number; ts: string; ms: number }\n | { kind: \"inbound\"; offset: number; ts: string; ms: number; cause: string }\n | { kind: \"signal\"; offset: number; ts: string; ms: number; signal: string };\n\ninterface Draft {\n row: EpisodeRow;\n hasRequest: boolean;\n}\n\ninterface WorkerState {\n drafts: Draft[];\n lastRequestMs: number | null;\n lastChannelMs: number | null;\n}\n\n/** worker-forensics-report.md section 5 and wf_analyze.py:241-251, with the status-report trigger from design section 9. */\nfunction workerEpisodes(input: EpisodeInput): EpisodeRow[] {\n const state: WorkerState = { drafts: [], lastRequestMs: null, lastChannelMs: null };\n for (const event of timeline(input)) {\n const current = state.drafts.at(-1);\n const reason = current ? workerBoundary(event, current, state) : input.spawned ? \"brief\" : \"session_start\";\n if (reason && (!current || current.hasRequest)) state.drafts.push(openDraft(event, reason, state.drafts.length));\n absorb(state.drafts.at(-1)!, event);\n if (event.kind === \"request\") state.lastRequestMs = event.ms;\n if (isChannel(event)) state.lastChannelMs = event.ms;\n }\n return state.drafts.map((draft) => draft.row);\n}\n\nfunction workerBoundary(event: Event, current: Draft, state: WorkerState): string | null {\n if (event.kind === \"request\" && state.lastRequestMs !== null && event.ms - state.lastRequestMs >= IDLE_GAP_MS) return \"idle_gap\";\n if (!isChannel(event)) return null;\n const startsCluster = state.lastChannelMs === null || event.ms - state.lastChannelMs > CHANNEL_CLUSTER_MS;\n return startsCluster && current.row.firstStatusOffset != null ? \"channel_followup\" : null;\n}\n\nfunction isChannel(event: Event): boolean {\n return event.kind === \"inbound\" && event.cause === \"channel_message\";\n}\n\nfunction openDraft(event: Event, openedBy: string, episodeIndex: number): Draft {\n const assignment = ASSIGNMENT_OPENERS.has(openedBy) && event.kind === \"inbound\" ? event.offset : null;\n const row: EpisodeRow = {\n episodeIndex,\n heuristicVersion: HEURISTIC_VERSIONS[\"worker-v1\"],\n startedAt: event.ts,\n endedAt: event.ts,\n startOffset: event.offset,\n endOffset: event.offset,\n openedBy,\n assignmentOffset: assignment,\n firstDeliverableOffset: null,\n firstDeliverableSignal: null,\n firstStatusOffset: null,\n };\n return { row, hasRequest: false };\n}\n\n/** The first commit is recorded as a deliverable but not as a report: commit fires early for implementers (221 of 411). */\nfunction absorb(draft: Draft, event: Event): void {\n draft.row.endedAt = event.ts;\n draft.row.endOffset = event.offset;\n if (event.kind === \"request\") draft.hasRequest = true;\n if (event.kind !== \"signal\") return;\n if (draft.row.firstDeliverableOffset == null && DELIVERABLE_SIGNALS.has(event.signal)) {\n draft.row.firstDeliverableOffset = event.offset;\n draft.row.firstDeliverableSignal = event.signal;\n }\n if (draft.row.firstStatusOffset == null && event.signal === \"status_report\") draft.row.firstStatusOffset = event.offset;\n}\n\n/** Offsets only order events inside one transcript, so time orders first and the offset breaks ties. */\nfunction timeline(input: EpisodeInput): Event[] {\n const events: Event[] = [\n ...input.requests.map((r) => ({ kind: \"request\" as const, offset: r.offset, ts: r.ts, ms: Date.parse(r.ts) })),\n ...input.inbounds.map((i) => ({ kind: \"inbound\" as const, offset: i.offset, ts: i.ts, ms: Date.parse(i.ts), cause: i.cause })),\n ...input.signals.map((s) => ({ kind: \"signal\" as const, offset: s.offset, ts: s.ts, ms: Date.parse(s.ts), signal: s.signal })),\n ];\n return events.sort((a, b) => a.ms - b.ms || a.offset - b.offset);\n}\n\n// ---------------------------------------------------------------- coordinator-v1\n\ninterface Turn {\n request: EpisodeRequest;\n ms: number;\n signals: Set<string>;\n}\n\ninterface Boundary {\n at: number;\n reason: string;\n}\n\n/** coordinator-forensics-report.md section 1, ported from co_analyze.py `segment` with `merge_lag` 0. */\nfunction coordinatorEpisodes(input: EpisodeInput): EpisodeRow[] {\n const turns = toTurns(input);\n if (turns.length === 0) return [];\n const opens = [{ at: 0, reason: \"session_start\" }, ...dropShort(coordinatorBoundaries(turns), turns.length)];\n return opens.map((open, index) => {\n const first = turns[open.at]!.request;\n const last = turns[(opens[index + 1]?.at ?? turns.length) - 1]!.request;\n return {\n episodeIndex: index,\n heuristicVersion: HEURISTIC_VERSIONS[\"coordinator-v1\"],\n startedAt: first.ts,\n endedAt: last.ts,\n startOffset: first.offset,\n endOffset: last.offset,\n openedBy: open.reason,\n };\n });\n}\n\nfunction coordinatorBoundaries(turns: readonly Turn[]): Boundary[] {\n const bounds: Boundary[] = [];\n let spawnedSince = false;\n let lastMerge = -Infinity;\n for (let i = 1; i < turns.length; i++) {\n const reason = coordinatorTrigger(turns, i, lastMerge) ?? (spawnedSince && waveComplete(turns, i) ? \"spawn_wave_complete\" : null);\n if (turns[i - 1]!.signals.has(\"pr_merge\")) lastMerge = i;\n if (reason) {\n bounds.push({ at: i, reason });\n spawnedSince = false;\n }\n if (turns[i]!.signals.has(\"agent_spawn\")) spawnedSince = true;\n }\n return bounds;\n}\n\n/** Checked in the source's order; the first that fires names the boundary. */\nfunction coordinatorTrigger(turns: readonly Turn[], i: number, lastMerge: number): string | null {\n const [a, b] = [turns[i - 1]!, turns[i]!];\n if (b.ms - a.ms >= IDLE_GAP_MS) return \"idle_gap\";\n if (a.signals.has(\"pr_merge\") && i - lastMerge > COORDINATOR_MERGE_DEDUPE_REQUESTS) return \"pr_merge\";\n if (a.signals.has(\"task_wrap\") || a.signals.has(\"task_done\")) return \"task_wrap\";\n const drop = a.request.contextTokens - b.request.contextTokens;\n if (a.request.contextTokens > 0 && drop > CONTEXT_RESET_TOKENS) return \"context_reset\";\n return null;\n}\n\n/** The last agent-chat traffic before a quiet run; the source also counted any agent-chat tool call, which only reaches the graph as `chat_send` or `agent_spawn`. */\nfunction waveComplete(turns: readonly Turn[], i: number): boolean {\n return hasChatTraffic(turns[i - 1]!) && !turns.slice(i, i + COORDINATOR_WAVE_QUIET_REQUESTS).some(hasChatTraffic);\n}\n\nfunction hasChatTraffic(turn: Turn): boolean {\n return turn.request.wakeCause === \"channel_message\" || turn.signals.has(\"chat_send\") || turn.signals.has(\"agent_spawn\");\n}\n\n/** No episode shorter than the minimum, at either end of the session. */\nfunction dropShort(bounds: readonly Boundary[], total: number): Boundary[] {\n const kept: Boundary[] = [];\n let last = 0;\n for (const bound of bounds) {\n if (bound.at - last < COORDINATOR_MIN_EPISODE_REQUESTS || total - bound.at < COORDINATOR_MIN_EPISODE_REQUESTS) continue;\n kept.push(bound);\n last = bound.at;\n }\n return kept;\n}\n\n/** A signal belongs to the latest request at or before it; one before any request goes to the first. */\nfunction toTurns(input: EpisodeInput): Turn[] {\n const turns = input.requests\n .map((request) => ({ request, ms: Date.parse(request.ts), signals: new Set<string>() }))\n .sort((a, b) => a.ms - b.ms || a.request.offset - b.request.offset);\n for (const signal of input.signals) ownerOf(turns, signal)?.signals.add(signal.signal);\n return turns;\n}\n\nfunction ownerOf(turns: readonly Turn[], signal: EpisodeSignal): Turn | undefined {\n const ms = Date.parse(signal.ts);\n for (let i = turns.length - 1; i >= 0; i--) {\n const turn = turns[i]!;\n if (turn.ms < ms || (turn.ms === ms && turn.request.offset <= signal.offset)) return turn;\n }\n return turns[0];\n}\n\n// ---------------------------------------------------------------- graph I/O\n\nconst IN_SESSION = \"session_id = ?\";\n\n/** Main-thread requests only: a sidechain's requests belong to its subagent, not to the session's episodes. */\nexport function readEpisodeInput(db: Db, sessionId: string, spawned: boolean): EpisodeInput {\n const requests = db\n .prepare(`SELECT byte_offset AS offset, ts, context_tokens AS contextTokens, wake_cause AS wakeCause FROM request_dedup WHERE ${IN_SESSION} AND is_sidechain = 0`)\n .all(sessionId) as EpisodeRequest[];\n const inbounds = db.prepare(`SELECT byte_offset AS offset, ts, cause FROM inbound WHERE ${IN_SESSION}`).all(sessionId) as EpisodeInbound[];\n const signals = db.prepare(`SELECT byte_offset AS offset, ts, signal FROM session_signal WHERE ${IN_SESSION}`).all(sessionId) as EpisodeSignal[];\n return { requests, inbounds, signals, spawned };\n}\n\nexport interface WrittenEpisodes {\n sessionId: string;\n heuristic: Heuristic;\n episodes: number;\n}\n\n/** Segments each session with its class's heuristic and replaces that heuristic's rows. Headless sessions are skipped. */\nexport function writeEpisodes(graph: SessionGraph, sessionIds: readonly string[]): WrittenEpisodes[] {\n const written: WrittenEpisodes[] = [];\n for (const [sessionId, context] of readSessionContexts(graph.db, sessionIds)) {\n const { sessionClass } = classifySession(context.facts);\n const heuristic = heuristicFor(sessionClass);\n if (!heuristic) continue;\n const rows = buildEpisodes(readEpisodeInput(graph.db, sessionId, sessionClass === \"agent_spawned\"), heuristic);\n written.push({ sessionId, heuristic, episodes: replaceEpisodes(graph, sessionId, heuristic, rows) });\n }\n return written;\n}\n","/** Initiatives a session's task edges name, with how many edges name each. */\nexport interface TaskInitiative {\n initiative: string;\n edges: number;\n}\n\n/**\n * A task edge names the initiative outright, so it wins over the cwd guess. Among several,\n * the most-named initiative wins and a tie goes to the alphabetically first, for a stable report.\n */\nexport function sessionInitiative(tasks: readonly TaskInitiative[], cwd: string | null | undefined): string {\n const best = [...tasks].sort((a, b) => b.edges - a.edges || a.initiative.localeCompare(b.initiative))[0];\n return best ? best.initiative : initiativeFromCwd(cwd);\n}\n\n/** The cwd rule from `cf_analyze.py` `initiative()`. A worktree under a repo counts as that repo. */\nexport function initiativeFromCwd(cwd: string | null | undefined): string {\n if (!cwd) return \"unknown\";\n const repo = /\\/projects\\/([^/]+)/.exec(cwd);\n if (repo) return `repo:${repo[1]}`;\n const activeWork = /active-work\\/([^/]+)/.exec(cwd);\n if (activeWork) return `active-work:${activeWork[1]}`;\n if (/ac-fork-\\w+/.test(cwd)) return \"ac-fork(tmp)\";\n const trimmed = cwd.replace(/\\/+$/, \"\");\n return `other:${trimmed.slice(trimmed.lastIndexOf(\"/\") + 1)}`;\n}\n","import type { SessionClassification } from \"./classify-session.js\";\n\n/** worker-v1's role vocabulary, from worker-forensics-report.md section 2. */\nexport type WorkerRole = \"implementer\" | \"reviewer\" | \"researcher\" | \"planner\" | \"standing_peer\" | \"unknown\";\n\n/** Spawn profile to role, as the worker report's table (wf_analyze.py:79-91) maps it. */\nexport const PROFILE_ROLES: Readonly<Record<string, WorkerRole>> = {\n implementer: \"implementer\",\n \"implementer-lite\": \"implementer\",\n \"relay-implementer\": \"implementer\",\n \"beat-builder\": \"implementer\",\n \"fable-optimizer\": \"implementer\",\n reviewer: \"reviewer\",\n \"relay-reviewer\": \"reviewer\",\n researcher: \"researcher\",\n explorer: \"researcher\",\n \"fable-architect\": \"planner\",\n peer: \"standing_peer\",\n};\n\nexport const STANDING_PEER_MIN_HOURS = 12;\nexport const STANDING_PEER_MIN_ASSIGNMENTS = 2;\nconst HOUR_MS = 3_600_000;\n\nexport interface WorkerFacts {\n profile?: string | null;\n /** First request to last request, over the whole session rather than a report window. */\n lifetimeMs: number;\n /** worker-v1 episodes opened by an assignment (the brief or a channel follow-up), not by an idle gap. */\n assignments: number;\n}\n\nexport function roleFromProfile(profile: string | null | undefined): WorkerRole {\n return PROFILE_ROLES[(profile ?? \"\").toLowerCase()] ?? \"unknown\";\n}\n\n/** A worker that outlives its first assignment is a standing peer whatever it was spawned as (wf_analyze.py:94-103). */\nexport function workerRole(facts: WorkerFacts): WorkerRole {\n const outlived = facts.lifetimeMs >= STANDING_PEER_MIN_HOURS * HOUR_MS && facts.assignments >= STANDING_PEER_MIN_ASSIGNMENTS;\n return outlived ? \"standing_peer\" : roleFromProfile(facts.profile);\n}\n\n/** The report's role key: `coordinator` or `adhoc` for humans, `worker:<role>` for spawned agents, else the class. */\nexport function sessionRole(classification: SessionClassification, worker: WorkerFacts): string {\n if (classification.humanRole) return classification.humanRole;\n if (classification.sessionClass === \"agent_spawned\") return `worker:${workerRole(worker)}`;\n return classification.sessionClass;\n}\n","import type { CostBucket, CostReport, WakeGapCell } from \"./cost-report.js\";\nimport { PRICE_TABLE_VERSION } from \"./prices.js\";\n\nexport const LIST_PRICE_CAVEAT =\n \"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.\";\n\ntype Cell = string | number;\n\n/** The same report as plain-text tables, one section per report field, caveat and footer last. */\nexport function renderCostReportText(report: CostReport): string {\n const sections = [\n header(report),\n table(\"By token class\", [\"class\", \"tokens\", \"cost\"], report.byTokenClass.map((row) => [row.tokenClass, row.tokens, usd(row.costUsd)])),\n bucketTable(\"By account\", report.byAccount, report.totals.costUsd),\n bucketTable(\"By model\", report.byModel, report.totals.costUsd),\n bucketTable(\"By class\", report.byClass, report.totals.costUsd),\n bucketTable(\"By role\", report.byRole, report.totals.costUsd),\n bucketTable(\"By episode count\", report.byEpisodeCount, report.totals.costUsd),\n bucketTable(\"By initiative\", report.byInitiative, report.totals.costUsd),\n bucketTable(\"By context band\", report.byContextBand, report.totals.costUsd),\n wakeCauseTable(report),\n cellTable(\"Wake cause by gap band\", report.wakeCauseByGapBand),\n cellTable(`Cold rebuilds: ${report.coldRebuild.requests} requests, ${usd(report.coldRebuild.costUsd)}`, report.coldRebuild.byGapBandAndCause),\n compactionTable(report.compactions),\n topSessionTable(report.topSessions),\n unpricedTable(report.unpricedModels),\n [LIST_PRICE_CAVEAT, footer(report)].join(\"\\n\"),\n ];\n return sections.join(\"\\n\\n\") + \"\\n\";\n}\n\nfunction header(report: CostReport): string {\n const { since, until } = report.window;\n const { requests, sessions, costUsd } = report.totals;\n return `Cost report, ${since ?? \"beginning\"} to ${until ?? \"now\"}\\nTotal ${usd(costUsd)} over ${requests} requests in ${sessions} sessions`;\n}\n\nfunction bucketTable(title: string, buckets: readonly CostBucket[], total: number): string {\n const rows = buckets.map((b) => [b.key, b.requests, b.sessions, usd(b.costUsd), share(b.costUsd, total)]);\n return table(title, [\"key\", \"requests\", \"sessions\", \"cost\", \"share\"], rows);\n}\n\n/** Q4: answers sit under `human`, with typed text and answers indented one level down. */\nfunction wakeCauseTable(report: CostReport): string {\n const rows = report.byWakeCause.flatMap((group) => [\n [group.key, group.requests, usd(group.costUsd), group.midLoopRequests, usd(group.midLoopCostUsd)],\n ...group.parts.map((part) => [` ${part.key}`, part.requests, usd(part.costUsd), part.midLoopRequests, usd(part.midLoopCostUsd)]),\n ]);\n return table(\"By wake cause\", [\"cause\", \"requests\", \"cost\", \"mid-loop\", \"mid-loop cost\"], rows);\n}\n\nfunction cellTable(title: string, cells: readonly WakeGapCell[]): string {\n return table(title, [\"cause\", \"gap\", \"requests\", \"cost\"], cells.map((c) => [c.wakeCause, c.gapBand, c.requests, usd(c.costUsd)]));\n}\n\nfunction compactionTable(c: CostReport[\"compactions\"]): string {\n return table(\"Compactions\", [\"total\", \"manual\", \"auto\", \"mid-loop\", \"dropped tokens\"], [[c.total, c.manual, c.auto, c.midLoop, c.droppedTokens]]);\n}\n\nfunction topSessionTable(sessions: CostReport[\"topSessions\"]): string {\n const rows = sessions.map((s) => [s.sessionId, s.sessionClass, s.role, s.initiative, s.requests, usd(s.costUsd)]);\n return table(\"Top sessions\", [\"session\", \"class\", \"role\", \"initiative\", \"requests\", \"cost\"], rows);\n}\n\nfunction unpricedTable(models: CostReport[\"unpricedModels\"]): string {\n if (models.length === 0) return \"Unpriced models: none\";\n return table(\"Unpriced models (counted at zero cost)\", [\"model\", \"requests\", \"tokens\"], models.map((m) => [m.model, m.requests, m.tokens]));\n}\n\nfunction footer(report: CostReport): string {\n const { transcriptsIndexed, transcriptsDiscovered, facetBacklog } = report.coverage;\n const discovered = transcriptsDiscovered === null ? \"\" : ` of ${transcriptsDiscovered} discovered`;\n const version = report.priceTableVersion === null ? \"none\" : `v${report.priceTableVersion}`;\n const stale = report.priceTableVersion !== null && report.priceTableVersion !== PRICE_TABLE_VERSION ? ` (package has v${PRICE_TABLE_VERSION})` : \"\";\n return `Price table ${version}${stale}. Coverage: ${transcriptsIndexed} transcripts indexed${discovered}, facet backlog ${facetBacklog}.`;\n}\n\nfunction table(title: string, head: readonly string[], rows: readonly (readonly Cell[])[]): string {\n if (rows.length === 0) return `${title}: none`;\n const text = [head, ...rows].map((row) => row.map(String));\n const widths = head.map((_, i) => Math.max(...text.map((row) => row[i]!.length)));\n const rightAligned = head.map((_, i) => rows.every((row) => isNumeric(row[i]!)));\n const pad = (cell: string, i: number) => (rightAligned[i] ? cell.padStart(widths[i]!) : cell.padEnd(widths[i]!));\n const lines = text.map((row) => row.map(pad).join(\" \").trimEnd());\n return [title, ...lines].join(\"\\n\");\n}\n\nfunction isNumeric(cell: Cell): boolean {\n return typeof cell === \"number\" || /^\\$|%$/.test(cell);\n}\n\nfunction usd(value: number): string {\n return `$${value.toFixed(2)}`;\n}\n\nfunction share(value: number, total: number): string {\n return total > 0 ? `${((value / total) * 100).toFixed(1)}%` : \"-\";\n}\n"],"mappings":"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","names":["gapBand"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@titan-design/session-analytics",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Pricing, session classification, banding and the cost report over mined session data",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -30,6 +30,16 @@
|
|
|
30
30
|
"publishConfig": {
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@titan-design/session-graph": "^0.8.0",
|
|
35
|
+
"@titan-design/store-sqlite": "^0.3.1"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"zod": "^4.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"zod": "^4.4.3"
|
|
42
|
+
},
|
|
33
43
|
"scripts": {
|
|
34
44
|
"build": "tsup",
|
|
35
45
|
"typecheck": "tsc --noEmit",
|