@50arms/session-telemetry 0.0.0-stage → 0.1.66

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.
Files changed (45) hide show
  1. package/README.md +68 -2
  2. package/dist/.source-hash +1 -0
  3. package/dist/activity-observer.d.ts +186 -0
  4. package/dist/activity-observer.js +917 -0
  5. package/dist/api-tool-names.d.ts +12 -0
  6. package/dist/api-tool-names.js +19 -0
  7. package/dist/browser.d.ts +29 -0
  8. package/dist/browser.js +10 -0
  9. package/dist/catalog-prices.d.ts +27 -0
  10. package/dist/catalog-prices.js +31 -0
  11. package/dist/claim-tracker.d.ts +53 -0
  12. package/dist/claim-tracker.js +213 -0
  13. package/dist/dispatch-usage.d.ts +321 -0
  14. package/dist/dispatch-usage.js +657 -0
  15. package/dist/git-head.d.ts +1 -0
  16. package/dist/git-head.js +19 -0
  17. package/dist/index.d.ts +35 -0
  18. package/dist/index.js +49 -0
  19. package/dist/internal-logger.d.ts +16 -0
  20. package/dist/internal-logger.js +27 -0
  21. package/dist/ledger-timing.d.ts +26 -0
  22. package/dist/ledger-timing.js +31 -0
  23. package/dist/pricing.d.ts +93 -0
  24. package/dist/pricing.js +77 -0
  25. package/dist/real-work.d.ts +13 -0
  26. package/dist/real-work.js +57 -0
  27. package/dist/session-log-watcher.d.ts +616 -0
  28. package/dist/session-log-watcher.js +1285 -0
  29. package/dist/session-title-marks.d.ts +2 -0
  30. package/dist/session-title-marks.js +16 -0
  31. package/dist/stats-wire.d.ts +345 -0
  32. package/dist/stats-wire.js +636 -0
  33. package/dist/subagent-live.d.ts +36 -0
  34. package/dist/subagent-live.js +153 -0
  35. package/dist/tool-summary.d.ts +19 -0
  36. package/dist/tool-summary.js +58 -0
  37. package/dist/transcript-timing.d.ts +100 -0
  38. package/dist/transcript-timing.js +231 -0
  39. package/dist/types.d.ts +25 -0
  40. package/dist/types.js +1 -0
  41. package/dist/usage-ledger.d.ts +256 -0
  42. package/dist/usage-ledger.js +581 -0
  43. package/dist/write-queue.d.ts +9 -0
  44. package/dist/write-queue.js +16 -0
  45. package/package.json +43 -4
@@ -0,0 +1,321 @@
1
+ /**
2
+ * DX-2963 — a running dispatch's usage, worker side.
3
+ *
4
+ * The worker no longer sums anything. It feeds every transcript line into
5
+ * the ONE shared counter (`UsageLedgerCounter`, per-field maximum per reply)
6
+ * and ships the entries that are new or changed since the last acknowledged
7
+ * write to the server (`applyStats`), which derives every total from the
8
+ * ledger and hands them back. The worker's own view of the totals is what the
9
+ * server last returned — plus, for the token ceiling only, a local estimate of
10
+ * what it has read but the server has not acknowledged yet.
11
+ *
12
+ * Totals are taken from the server ONLY at process start (the first write,
13
+ * sent with no entries if there are none) and from each write's response —
14
+ * never seeded by hand (the reattach hand seeds this replaces).
15
+ *
16
+ * DB-free: runs in the DB-less worker. The sink decides the transport
17
+ * (HTTP on a registered worker, getPool-direct on the dashboard).
18
+ */
19
+ import { UsageLedgerCounter } from "./usage-ledger.js";
20
+ import type { CursorPastEofInfo, EntryMeta } from "./session-log-watcher.js";
21
+ import { type ActivityChange, type ActivityMessage, type ActivityProblemLink, type SourceCursors, type StatsPayload, type StatsResponse, type UsageTotals } from "./stats-wire.js";
22
+ /** Where stats writes go — both dispatch-row sinks implement it. */
23
+ export interface StatsSink {
24
+ applyStats(dispatchId: string, payload: StatsPayload): Promise<StatsResponse>;
25
+ }
26
+ export declare function zeroUsageTotals(): UsageTotals;
27
+ export type FlushOutcome = "ok" | "failed" | "refused";
28
+ /**
29
+ * DX-3962 - which transcript owner a collector is built for. It decides whether
30
+ * replies are attributed to cards, and it has no default: every construction names
31
+ * its path.
32
+ *
33
+ * - `"worker"` - a dispatched worker. NO claim tracker, and every entry's
34
+ * `issueId` is `null` whatever the transcript contains (a worker picks its card
35
+ * up server-side, its transcript has no pickup, and its dispatch row already
36
+ * carries the card). The server refuses a non-null `issueId` from a non-operator
37
+ * dispatch by refusing the WHOLE batch, which would stall the worker's stats,
38
+ * token ceiling and spend alert on every resend - so a worker is structurally
39
+ * unable to send one.
40
+ * - `"operator-bridge"` - the plan event relay of an operator session
41
+ * (`platform-mcp`'s `session-stats.ts`), the only path that attributes. The value keeps its old name on purpose: it is
42
+ * this package's published API (the MCP package passes it), so renaming it is a new telemetry release, not a text fix.
43
+ */
44
+ export type DispatchUsageMode = "worker" | "operator-bridge";
45
+ export declare class DispatchUsage {
46
+ readonly counter: UsageLedgerCounter;
47
+ /** DX-3962 - `null` in `"worker"` mode: the worker can never create a claim, so it can never send an `issueId`. */
48
+ private readonly claims;
49
+ /**
50
+ * DX-3962 - per source, the claim state as of its `acked` offset: what the next
51
+ * write reports with `acked`, promoted together with it. The state the tracker
52
+ * holds NOW belongs to `delivered`, which runs ahead.
53
+ */
54
+ private readonly ackedClaims;
55
+ private serverTotals;
56
+ private readonly acked;
57
+ private healthy;
58
+ private refused;
59
+ private started;
60
+ private readonly queue;
61
+ constructor(mode: DispatchUsageMode);
62
+ /** How to read the watcher's live per-source cursor state. */
63
+ private readCursorState;
64
+ /**
65
+ * Per source, the `delivered` offset as of the last ACKNOWLEDGED write — which
66
+ * is precisely what `acked` means. Seeded from the cursors this process
67
+ * resumed from, so a restart that writes before reading anything re-reports
68
+ * its own resume point instead of regressing it.
69
+ */
70
+ private readonly ackedCursor;
71
+ /**
72
+ * DX-3972 - per source, the `acked` the server holds for this dispatch: the `acked` of the last ACKNOWLEDGED
73
+ * report that carried the source. It is NOT {@link DispatchUsage.ackedCursor}: that is what the NEXT report
74
+ * claims (one write behind `delivered`), this is what the LAST one claimed. The terminal catch-up
75
+ * ({@link DispatchUsage.flushFinal}) is needed exactly while this lags the watcher's `delivered`.
76
+ */
77
+ private readonly storedAcked;
78
+ /**
79
+ * DX-2954 (AC 34324) — the cursors this process STARTED from, reported
80
+ * verbatim for every source the watcher has not opened yet until one write
81
+ * carrying them has landed. See {@link DispatchUsage.buildCursorReports}.
82
+ */
83
+ private startCursors;
84
+ private startCursorsPersisted;
85
+ /**
86
+ * DX-2954 — set the moment the first flush begins. `attachCursors` after that
87
+ * point would set the starting cursors AFTER a write that carried none, and
88
+ * `startCursorsPersisted` (set by any landed write) would then keep them from
89
+ * ever being carried — the exact AC 34324 loss, silently. So it fails loud.
90
+ */
91
+ private flushBegan;
92
+ /** Past-EOF resets to report once, then forget. */
93
+ private pastEof;
94
+ /**
95
+ * DX-2954 — replies this process read as REPLAYS. The server already counted
96
+ * them for THIS dispatch (they are below its stored `acked`), so they must not
97
+ * enter the unacknowledged excess the token-ceiling estimate adds on top of the
98
+ * server totals.
99
+ */
100
+ private readonly replayedMessageIds;
101
+ /** Running-set changes to report on the next write. */
102
+ private pendingActivities;
103
+ /** DX-5333 - sub-agent messages to report on the next write, one per `(activityId, messageKey)`. */
104
+ private pendingMessages;
105
+ /** DX-5333 - card problems sub-agents created, to report on the next write, one per `(activityId, problemRefNum)`. */
106
+ private pendingProblemLinks;
107
+ /** The session transcript path — the ONE path field the wire carries. */
108
+ private transcriptPath;
109
+ private transcriptPathSent;
110
+ /**
111
+ * DX-2954 — wire the cursor half up at attach.
112
+ *
113
+ * `startCursors` are the cursors this process RESUMED from
114
+ * (`resolveStartCursors`); their `acked` values seed this collector's own
115
+ * acknowledged floor. `readCursorState` reads the watcher's live offsets at
116
+ * each write.
117
+ */
118
+ attachCursors(readCursorState: () => Record<string, {
119
+ startOffset: number;
120
+ delivered: number;
121
+ }>, startCursors: SourceCursors): void;
122
+ /**
123
+ * DX-2954 — a stored cursor was past its file's end; report the reset on the
124
+ * next write so the server records a `transcript_cursor_past_eof` event and
125
+ * permits the one sanctioned backwards move of `acked`.
126
+ */
127
+ recordPastEof(info: CursorPastEofInfo): void;
128
+ /**
129
+ * DX-2954 — queue running-set changes for the next write.
130
+ *
131
+ * Kept COLLAPSED (one entry per `(kind, activityId)`) rather than appended
132
+ * raw, so the queue is bounded by how many activities the dispatch has, not by
133
+ * how much they moved since the last write. A producer reports an activity's
134
+ * full current state every time anything about it changes, and a chatty
135
+ * sub-agent advances its `lastActivityAt` continuously — appending those would
136
+ * grow without bound, eventually exceed `ACTIVITY_BATCH_CAP` and take the
137
+ * WHOLE stats write down (not just its activity part) to describe a handful of
138
+ * activities.
139
+ *
140
+ * `collapseActivityChanges` applies the server's own merge rule, so collapsing
141
+ * here reaches the identical stored row; it is not a lossy summarisation.
142
+ */
143
+ recordActivities(changes: readonly ActivityChange[]): void;
144
+ /**
145
+ * DX-5333 - queue sub-agent messages for the next write. One entry per `(activityId, messageKey)`: the server's insert is
146
+ * idempotent on that key, so a message read twice (a collector that re-reads a file) is sent once.
147
+ */
148
+ recordMessages(messages: readonly ActivityMessage[]): void;
149
+ /** DX-5333 - queue the card problems a sub-agent created for the next write (idempotent on the pair, like {@link DispatchUsage.recordMessages}). */
150
+ recordProblemLinks(links: readonly ActivityProblemLink[]): void;
151
+ /**
152
+ * DX-2954 — the session's own transcript path, recorded on the dispatch's
153
+ * `jsonl_path`. Sent once, with the first stats write that carries it; the
154
+ * server also keeps only the first path it is given, so a repeat is harmless.
155
+ */
156
+ setTranscriptPath(path: string): void;
157
+ /**
158
+ * DX-2954 — this write's cursor reports, plus the `delivered` value to promote
159
+ * to `acked` per source once the write is acknowledged.
160
+ *
161
+ * **`acked` deliberately LAGS `delivered` by one write.** A write reports the
162
+ * position it has read to (`delivered`) and the position whose entries a
163
+ * PREVIOUS write already had acknowledged (`acked`); only when THIS write
164
+ * succeeds does its own `delivered` become the next write's `acked`.
165
+ *
166
+ * The lag is not slack — it is what makes the pair safe. A reply's ledger entry
167
+ * can be RAISED after it was first reported (its usage grows as the reply
168
+ * streams), and a delivered line need not produce an entry at all (a
169
+ * `tool_result`), so "the server committed my last write" is NOT the same as
170
+ * "every line I have read is finally accounted for". Reporting `acked` one
171
+ * write behind is the conservative claim that is always true.
172
+ *
173
+ * What it costs: a restart re-reads the last write's window. That is exactly
174
+ * the window `EntryMeta.replay` covers — the ledger re-credits nothing (its
175
+ * excess rule) and every side-effecting subscriber skips it — so the cost is
176
+ * one window of re-read, never a double count or a duplicate side effect.
177
+ *
178
+ * ## The terminal write closes the lag (DX-3972)
179
+ *
180
+ * A restart is a crash, and a crash's window is the cost above. A dispatch that
181
+ * ENDS is not one: its watcher is drained and stopped, so every line it read has been
182
+ * fed to this collector and nothing will raise a reply any more. The terminal
183
+ * {@link DispatchUsage.flushFinal} therefore reports `acked` equal to `delivered` for
184
+ * every source it has opened (see {@link DispatchUsage.catchUpReports}), and a
185
+ * `--resume` of the session starts at the end of what this dispatch read instead of
186
+ * replaying the whole file. Without it a dispatch with ONE line-bearing write ended at
187
+ * `{acked: 0, delivered: EOF}`, and every resume of the session re-read the whole
188
+ * transcript and re-shipped it into its own `dispatch_logs`.
189
+ *
190
+ * ## The process-start write carries the STARTING cursors (AC 34324)
191
+ *
192
+ * `attachMonitoringStack` deliberately does NOT await `watcher.start()` —
193
+ * file discovery polls for up to 180 s and must not block the spawn — and the
194
+ * process-start stats write fires immediately after it. At that moment the
195
+ * watcher has opened no file, so its live `cursorState()` is empty.
196
+ *
197
+ * So every source in the cursors this process STARTED from that the watcher
198
+ * has not opened yet is reported VERBATIM — the exact `{acked, delivered}`
199
+ * pair it resumed from — until a write that carries them has landed. That is
200
+ * what makes "every attach persists its starting cursors" true on the FIRST
201
+ * write rather than one stats tick (15-30 s) later: a reattach of this row
202
+ * inside its first interval reads them back (start-order rule 1) and starts
203
+ * exactly where this process did, instead of finding no cursor, resolving
204
+ * rule 4 and skipping every line written since spawn. A fresh session's
205
+ * starting cursor is explicit too (`resolveStartCursors` rule 3 returns the
206
+ * main source at 0), and a `--resume`'s is its predecessor's (rule 2).
207
+ *
208
+ * Verbatim is the only honest claim for a source this process has not read:
209
+ * a higher `acked` would let a restart skip the replay window it has not
210
+ * re-credited, a lower one is refused as a regression. For the same reason
211
+ * the start write does not PROMOTE such a source's acknowledged floor — it
212
+ * delivered nothing there — so it stays out of `deliveredAtSend`.
213
+ *
214
+ * The one case with nothing to carry is rule 4 (a continuing dispatch with no
215
+ * cursor for ANY source): its start is "each existing file's end at
216
+ * discovery", which does not exist until discovery runs. Its cursors reach
217
+ * the server on the first write after discovery. Rule 4 is the pre-card
218
+ * deploy transition only — every dispatch since this card stores cursors on
219
+ * its first write — so that bounded window is the one the card accepts.
220
+ */
221
+ private buildCursorReports;
222
+ /**
223
+ * DX-2954 — whether the cursor half has anything new to say. An IDLE dispatch
224
+ * (no new lines, no hook events) must still cost zero writes per tick, which
225
+ * is why a write is not forced merely because cursors EXIST.
226
+ */
227
+ private cursorsMoved;
228
+ /**
229
+ * DX-3972 - the TERMINAL write's reports: the tick's reports with every source this process has read claiming its
230
+ * whole `delivered` as `acked`, and the claim state AS OF that offset (the tracker's current state belongs to
231
+ * `delivered`, so it describes the new `acked` exactly, never a later position). Sources the watcher has not
232
+ * opened (a start cursor reported verbatim) are untouched: this process read nothing there to claim.
233
+ */
234
+ private catchUpReports;
235
+ /**
236
+ * DX-3972 - whether the terminal write has anything to say: the server's `acked` still lags what the catch-up now
237
+ * claims. `ackedCursor` (the NEXT tick's claim) cannot tell, because a dispatch whose last write reported
238
+ * `delivered` with an older `acked` has `ackedCursor === delivered` and still lags; {@link DispatchUsage.storedAcked}
239
+ * is what the server holds. A source never reported at all has no entry, so it counts as lagging.
240
+ */
241
+ private storedAckedLags;
242
+ /**
243
+ * Feed one raw transcript line of the usage feed set (assistant, user, attachment, system), in file order.
244
+ * DX-3964 - the usage collector is fed from the watcher's collector-only `onUsageLine` subscription, never from
245
+ * `onEntry`: `convertJsonlEntry` drops human prompts, attachments and system lines, and widening it would hand
246
+ * every other `onEntry` subscriber a new volume of entries.
247
+ *
248
+ * `meta.replay` (DX-2954) marks a line re-read after a restart. The line STILL goes to the counter - the
249
+ * ledger is idempotent and dropping it would lose usage a restart is meant to recover - but a replayed
250
+ * assistant reply's id is remembered so the local ceiling estimate does not count it twice (see
251
+ * {@link DispatchUsage.unacknowledged}).
252
+ */
253
+ observeRawLine(raw: Record<string, unknown>, meta: EntryMeta): void;
254
+ /**
255
+ * Ship every changed entry in capped batches, one after another. The first
256
+ * call always writes (even with nothing to send) so the process takes the
257
+ * server totals at start. A failed batch re-queues itself and every later
258
+ * batch; a refusal (the dispatch went terminal) stops all later writes.
259
+ *
260
+ * DX-2954 — the write also carries the cursor half. A write happens when there
261
+ * are entries, OR the cursors moved, OR there are activity changes, OR it is
262
+ * the process-start write; an idle dispatch still writes nothing. Cursors,
263
+ * activity changes and the transcript path ride the LAST batch only, so a
264
+ * multi-batch backlog does not repeat them and the cursor can never be
265
+ * acknowledged ahead of the entries it covers.
266
+ */
267
+ flush(sink: StatsSink, dispatchId: string): Promise<FlushOutcome>;
268
+ /**
269
+ * DX-3972 - the TERMINAL write: {@link DispatchUsage.flush} that also catches the stored `acked` up to
270
+ * `delivered` for every source (see {@link DispatchUsage.catchUpReports}). Call it once, when the dispatch ends,
271
+ * after the watcher is drained and stopped (that is the whole precondition: every line it read has reached this
272
+ * collector, so `delivered` is covered by the entries this very write carries), and BEFORE the row goes terminal
273
+ * (the server refuses a terminal dispatch's stats, which would drop the catch-up silently). A write that does not
274
+ * land promotes nothing, exactly as in a tick; a dispatch already caught up sends no write.
275
+ *
276
+ * It does NOT wait for the dispatch's log shipper: the spawn path flushes it before this call, the reattach path
277
+ * after (`reattach.ts` `_cleanup`), so on a reattach `acked` can claim a line whose `dispatch_logs` row is still in
278
+ * the shipper's buffer. The shipper is best-effort today and a resume's shipper skips replays either way, so
279
+ * nothing is lost that was not already; acking only after the log line is acknowledged is DX-2956's change.
280
+ */
281
+ flushFinal(sink: StatsSink, dispatchId: string): Promise<FlushOutcome>;
282
+ private write;
283
+ /** The dispatch's totals as the server last returned them. */
284
+ totals(): UsageTotals;
285
+ /** Window tokens (input + output + cache writes) of the server totals. */
286
+ serverWindowTokens(): number;
287
+ /** Whether the most recent stats write succeeded. */
288
+ statsHealthy(): boolean;
289
+ /**
290
+ * The per-field excess of each local reply's maximum over what the server
291
+ * last acknowledged for it.
292
+ *
293
+ * DX-2954 — REPLAYED replies are excluded. The pre-DX-2954 comment here
294
+ * asserted that "every id this process observed was read at or after its
295
+ * starting cursor … so lines re-read on a restart never enter the estimate",
296
+ * which stopped being true the moment replays became a deliberate input: a
297
+ * replayed reply has no entry in `this.acked` at process start, so its FULL
298
+ * usage would be added on top of `serverTotals` that already contain it. That
299
+ * feeds a fabricated total to the token ceiling (`agent-status.ts` uses this
300
+ * estimate precisely when a write did NOT land) and to the context-overflow
301
+ * decision — either can stop a healthy dispatch.
302
+ */
303
+ private unacknowledged;
304
+ /**
305
+ * The dispatch's usage as far as this process knows it: the server totals
306
+ * plus the per-field excess this process has read but the server has not
307
+ * acknowledged. A replay block adds nothing (its excess over the
308
+ * acknowledged total is zero). For a job with no dispatch row (nothing is
309
+ * ever acknowledged) this is exactly the local counter's total.
310
+ */
311
+ estimate(): {
312
+ tokensIn: number;
313
+ tokensOut: number;
314
+ cacheRead: number;
315
+ cacheWrite: number;
316
+ };
317
+ /** Token-ceiling input: window tokens (input + output + cache writes) of {@link estimate}. */
318
+ windowTokensEstimate(): number;
319
+ /** All-field total of {@link estimate}. */
320
+ totalTokensEstimate(): number;
321
+ }