@titan-design/session-analytics 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,16 +21,41 @@ priceRequest(
21
21
  ## What it exports
22
22
 
23
23
  - `PRICE_TABLE`, `PRICE_TABLE_VERSION`, `findPrice(model, ts, prices?)` — USD per million
24
- tokens by longest model prefix, then the latest row effective at `ts`.
24
+ tokens by longest model prefix, then the latest row effective at `ts`. A prefix matches only
25
+ at a model-id boundary (the id, a `-YYYYMMDD` date, or a `[..]` suffix), so `claude-opus-5`
26
+ never prices `claude-opus-5-5`.
25
27
  - `priceRequest(tokens, model, ts, prices?)` — the five cost components, `costUsd` and
26
28
  `priced`.
27
29
  - `classifySession(facts)` — `agent_spawned`, `human_interactive`, `headless_sdk` or
28
30
  `other_headless`, plus `coordinator` or `adhoc` for human sessions.
29
31
  - `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
+ - `costReport(db, { since, until, days, top, transcriptsDiscovered, facetVersion, actionRules,
33
+ mechanicalClasses, episodeRoles, noActionClasses, brokerLogLines, handoff })` — the standing cost report as one JSON
34
+ object, and `costReportSchema`, its zod schema. `byAction` gives each role's cost by action
35
+ class, and `mechanicalShare` the cost of the `mechanicalClasses` (default
36
+ `DEFAULT_MECHANICAL_CLASSES`) over the window total, with each role's share over that role's
37
+ cost. `wakeEpisodes` cuts the wakes of the `episodeRoles` (default `DEFAULT_EPISODE_ROLES`:
38
+ `coordinator` and `worker:coordinator`) into episodes; see "Wake episodes" below.
39
+ `handoffThreshold` fits the handoff model per role and model; see "Handoff threshold" below.
40
+ - `buildWakeEpisodes`, `summarizeWakeEpisodes`, `episodeNames`, `wakeEpisodesSchema`,
41
+ `WAKE_FROM_KINDS`, `DEFAULT_NO_ACTION_CLASSES` — the pure pieces behind `wakeEpisodes`.
42
+ - `handoffThreshold(rows, teleports, agentSessions, options?)`, `sweepK`, `costPerRequest`,
43
+ `isBootAction`, `parseTeleportEvents`, `handoffThresholdSchema` — the pure pieces behind
44
+ `handoffThreshold`.
45
+ - `ACTION_CLASSES`, `DEFAULT_ACTION_RULES`, `DEFAULT_MECHANICAL_CLASSES`,
46
+ `classifyRequest(calls, rules?)` — one action class per request from its tool calls: the
47
+ first rule in list order that any call matches, `text-only` with no calls, `other` with no
48
+ match. Rules match a tool name, a Bash command head, or a read or written path.
49
+ - `readRequestToolCalls(db, window)` — each request's tool calls as `ActionCall`s, with the
50
+ `command_heads`, `file_read` and `file_write` signals session-read extracted for them.
32
51
  - `renderCostReportText(report)` — the same report as plain-text tables, ending with
33
52
  `LIST_PRICE_CAVEAT` and the price-table and coverage footer.
53
+ - `renderCostReportSections(report, sections)`, `COST_REPORT_SECTIONS` — chosen sections of that
54
+ text under the same header, with the caveat and footer last; one question's answer, not the
55
+ whole report.
56
+ - `scope` on `costReport` and `cacheTtlReport` options (`ReportScope`: `sessionIds`,
57
+ `agentPrefix` on agent-chat names, `roles`), and `scopeFilter(db, scope)` behind it — narrows
58
+ every request-keyed field to some sessions. Compactions and coverage stay window-wide.
34
59
  - `roleFromProfile`, `workerRole(facts)`, `sessionRole(classification, facts)` — worker-v1
35
60
  roles, including the standing-peer overlay.
36
61
  - `buildEpisodes(input, "worker-v1" | "coordinator-v1")` (pure), `readEpisodeInput`,
@@ -47,6 +72,89 @@ const report = costReport(openDatabase(graphPath, { readonly: true }), { days: 7
47
72
  process.stdout.write(renderCostReportText(report));
48
73
  ```
49
74
 
75
+ ## Wake episodes
76
+
77
+ An episode is one arrival that wakes a session and the requests after it, up to the next
78
+ arrival in the same transcript. An arrival is a turn-start record or a mid-loop delivery, the
79
+ `queued_command` that reaches a busy seat inside a tool loop. A tool result is never an arrival.
80
+ Mid-loop deliveries are most of a busy coordinator's events, so leaving them out would show a
81
+ handful of expensive wakes and hide the rest.
82
+
83
+ `wakeEpisodes.byCause` gives, per cause, episodes, mid-loop episodes, requests, cost,
84
+ `requestsPerEpisode`, `costPerEpisode`, and the no-action count and cost. Causes are
85
+ session-read's, except that `channel_system` is reported as `agent_lifecycle`: agent-chat's
86
+ notice that an agent exited or changed state. Requests in the window whose arrival came before
87
+ it are counted under `unattributed`.
88
+
89
+ **No-action rule.** An episode is no-action when every one of its requests has an action class
90
+ in `noActionClasses`, by default `read-investigate`, `text-only` and `other`. An episode with no
91
+ requests, such as the second report of a burst, is no-action. Anything else, including a
92
+ journal write, a PR check or a message, counts as action. The rule reads the TP-501 facet
93
+ rather than regexes over tool input, so the default action rules and a seat's own `actionRules`
94
+ decide it the same way they decide `byAction`.
95
+
96
+ **From.** Each episode has a sender kind. `broker` is agent-chat itself, the sender of the
97
+ lifecycle notice. `broadcast` is a message whose `msg_id` reached more than one session. `seat`
98
+ is a name carried by a top-level session or by a session spawned with a coordinator profile.
99
+ `agent` is any other named sender, and `none` is an arrival with no sender. `pairs` is the
100
+ sender-by-receiver matrix. A seat sender is named, and every other sender collapses to its kind,
101
+ because spawned agents have one-off names.
102
+
103
+ ## Handoff threshold
104
+
105
+ When should a session hand over to a fresh one? Each session's terms come from its own requests,
106
+ main thread only, priced from `PRICE_TABLE` rather than the graph's stored price table:
107
+
108
+ - **Boot cost B**: the cost of every request up to and including the first action, a dispatch,
109
+ a send (`BOOT_TOOL`) or a file write.
110
+ - **Boot fill f0**: the context tokens at that request.
111
+ - **Growth g**: the mean rise in fill per request after boot. Only rises count, so a
112
+ compaction's drop does not cancel the growth before it.
113
+ - **Read price p**: the model's cache-read rate.
114
+
115
+ A cycle runs from f0 to the threshold K in n = (K - f0) / g requests, so a request costs
116
+ B / n + p x (f0 + K) / 2. `cohorts` averages the terms over each role and model, sweeps K over
117
+ `DEFAULT_K_SWEEP` and reports the best K, the extra cost per request at each `configuredK`
118
+ (the charter's `teleport_k` and `retire_k`), and the same sweep in `halfBoot`, where only half
119
+ the boot is overhead. `sessions` carries the per-session terms and best K. A session with no
120
+ boot action, or on an unpriced model, counts in `unbootedSessions`.
121
+
122
+ `teleports` gives the exit fill of each handover: the fill of the outgoing session's last
123
+ request at or before the broker's `teleport_started` line. The package does not open the broker
124
+ log. Pass its lines as `brokerLogLines`; the `from` agent id maps to sessions through
125
+ `session_origin.agent_id`.
126
+
127
+ `reviewers` compares `reviewerPrs` reviews (default 10). A fresh reviewer per PR pays its boot
128
+ and its own reads each time. A standing reviewer, priced from the `standingRole` cohort, boots
129
+ once and then reads a context that every earlier PR grew. A row's requests per PR come from the
130
+ reviewer sessions on its own model; a standing model with no reviewers of its own takes the
131
+ mean of the newest reviewer cohort, the model whose latest session ends last; `requestsFrom`
132
+ names that model. With no reviewer session at all it is 0 and `requestsFrom` is `"pooled"`.
133
+ A session with no request after boot does not count toward its cohort's growth. A reviewer's
134
+ review often sits inside its boot (its first write or send comes late), so requests per PR
135
+ count only what follows it and can understate a reviewer that does its work before it writes.
136
+
137
+ The report reads a window, so a session that started before it has its boot cut short.
138
+
139
+ ## Cache TTL what-if
140
+
141
+ `cacheTtlReport(db, { since, until, days })` asks what `CLAUDE_CODE_PROMPT_CACHE_TTL=5m` would
142
+ have saved against the 1h TTL. `cacheTtlWhatIf(rows, prices?)` is the pure core over
143
+ `TtlRequestRow`s, which `readTtlRows(db, window)` reads with the cost report's roles and each
144
+ session's spawn profile. `cacheTtlWhatIfSchema` is its zod schema and `renderCacheTtlText` its
145
+ text form.
146
+
147
+ - **Reprice.** Every 1h write is priced at the 5m write rate instead, from `findPrice`.
148
+ - **Rebuild.** A request whose gap falls in `REBUILD_GAP_BANDS` (the `gapBand`s from 5 minutes
149
+ up) finds a 5m cache gone. Its cache read is charged again at the 5m write rate, less the read
150
+ it no longer pays. Past an hour the 1h cache had expired too, so the read is already near zero
151
+ and only the reprice applies.
152
+ - **Net.** Per role and per profile: reprice saving less rebuild cost, and the same per session.
153
+ `lossRoles` lists the roles whose net is negative, typically seats that wait on CI.
154
+
155
+ The gap is the request's `gap_ms` when the miner stored one. Otherwise it is the time since the
156
+ session's previous request on the same thread, which may fall before the window.
157
+
50
158
  ## Things that will bite you
51
159
 
52
160
  Fable's cache read is **0.025** of its input rate, not the 0.1 every other model uses.
@@ -57,6 +165,27 @@ because defaulting bills a new model at an old model's rate without saying so. T
57
165
  report lists such models under `unpricedModels`.
58
166
 
59
167
  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.
168
+ whose price rows were never synced reports every request as unpriced, and one synced from an
169
+ older `PRICE_TABLE` keeps pricing at the old rates. The cost report takes a caller-opened,
170
+ read-only graph and never writes it. Before reporting, the caller must run session-graph's
171
+ `reconcilePrices(graph, PRICE_TABLE, { tableVersion: PRICE_TABLE_VERSION, source: "session-analytics" })`
172
+ through a writable connection; `titan-miner` does so on every open, other openers do not.
173
+
174
+ The default action rules are generic. Rules that name a seat's own journal files or scorer
175
+ scripts belong in the caller's config, passed as `actionRules`, never in this package. The
176
+ defaults read session-read's `command_heads` signal, which keeps only a path's shape, so
177
+ `gh api -X PUT repos/o/r/pulls/5/merge` reaches the classifier as `gh api PUT pulls/merge`. No
178
+ default rule matches that head yet, so it is not a merge.
179
+
180
+ A tool call belongs to the latest request at or before it in its transcript, the request that
181
+ issued it. The `context_contribution` view maps the other way, to the request a block feeds.
182
+
183
+ session-read keeps `from` and `msg_id` from a channel tag but not its `broadcast` attribute, so
184
+ a broadcast is inferred from its `msg_id` reaching more than one session. A multicast to named
185
+ recipients counts as a broadcast too.
186
+
187
+ Coordinator seats spawned with the `opus-coordinator` profile report as `worker:coordinator`.
188
+ Before that profile was mapped they fell into `worker:unknown`, and the `coordinator` role held
189
+ only human-driven seats.
61
190
 
62
191
  Full reference: `site/reference/session-analytics.md`.