@thehammer/danx-session-telemetry 0.1.13 → 0.1.15
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 +1 -1
- package/dist/activity-observer.d.ts +77 -0
- package/dist/activity-observer.js +73 -0
- package/dist/dispatch-usage.d.ts +209 -0
- package/dist/dispatch-usage.js +430 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +11 -1
- package/dist/internal-logger.js +5 -2
- package/dist/session-log-watcher.d.ts +230 -35
- package/dist/session-log-watcher.js +373 -91
- package/dist/stats-wire.d.ts +227 -0
- package/dist/stats-wire.js +307 -0
- package/dist/usage-ledger.js +1 -1
- package/dist/write-queue.d.ts +9 -0
- package/dist/write-queue.js +16 -0
- package/package.json +1 -1
|
@@ -28,7 +28,21 @@ import type { AgentLogEntry } from "./types.js";
|
|
|
28
28
|
* wrong file (e.g., an active human session).
|
|
29
29
|
*/
|
|
30
30
|
export declare const DISPATCH_TAG_PREFIX = "<!-- danxbot-dispatch:";
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* A consumer of transcript lines.
|
|
33
|
+
*
|
|
34
|
+
* DX-2954 added the second argument. A consumer that ignores it behaves exactly
|
|
35
|
+
* as before; a consumer with a SIDE EFFECT must read `meta.replay` and skip a
|
|
36
|
+
* re-read line (see {@link EntryMeta.replay}).
|
|
37
|
+
*/
|
|
38
|
+
export type EntryConsumer = (entry: AgentLogEntry, meta: EntryMeta) => void;
|
|
39
|
+
/**
|
|
40
|
+
* DX-2954 — the ONE replay gate every side-effecting transcript subscriber goes
|
|
41
|
+
* through (Slack progress, the gpt-manager forwarder): it hands on only lines
|
|
42
|
+
* read for the FIRST time. A replay was already handed to the previous
|
|
43
|
+
* incarnation's subscribers, so acting on it again is a duplicate post/event.
|
|
44
|
+
*/
|
|
45
|
+
export declare function skipReplays(consumer: (entry: AgentLogEntry) => void): EntryConsumer;
|
|
32
46
|
export interface SessionLogWatcherOptions {
|
|
33
47
|
/** Working directory the agent was launched in. Used to derive the session directory. */
|
|
34
48
|
cwd: string;
|
|
@@ -46,32 +60,125 @@ export interface SessionLogWatcherOptions {
|
|
|
46
60
|
*/
|
|
47
61
|
dispatchId?: string;
|
|
48
62
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
63
|
+
* DX-2954 — where each source resumes: the cursors the SERVER has stored for
|
|
64
|
+
* this dispatch, keyed by source ({@link MAIN_SOURCE} for the main transcript,
|
|
65
|
+
* the sub-agent id for a sub-agent file).
|
|
66
|
+
*
|
|
67
|
+
* An ABSENT source key means no cursor is stored for it — never "start at 0".
|
|
68
|
+
* The two are different: 0 is a legitimate stored cursor meaning "start of
|
|
69
|
+
* file", and CP1 forbids defaulting a missing persisted value. See
|
|
70
|
+
* {@link SessionLogWatcherOptions.startExistingFilesAtEof} and the start-order
|
|
71
|
+
* table on {@link SessionLogWatcher} for what happens when a source has none.
|
|
72
|
+
*
|
|
73
|
+
* This REPLACED the pre-DX-2954 `fromEof` / `existingSubagentFilesFromEof`
|
|
74
|
+
* pair, which skipped straight to a file's end on every reattach and every
|
|
75
|
+
* `--resume` because there was nowhere to record how far a reader had got.
|
|
76
|
+
* That lost every line between the last write and the restart; a cursor does
|
|
77
|
+
* not.
|
|
78
|
+
*/
|
|
79
|
+
startCursors?: Record<string, SourceCursorStart>;
|
|
80
|
+
/**
|
|
81
|
+
* DX-2954 — rule 4 of the start order: start every ALREADY-EXISTING file at
|
|
82
|
+
* its current end.
|
|
83
|
+
*
|
|
84
|
+
* Set ONLY by a WORKER dispatch that continues a session and has no cursor
|
|
85
|
+
* for ANY source — a row from before this card shipped, or a `--resume` whose
|
|
86
|
+
* predecessor predates it. Those files hold a prior dispatch's accounting with
|
|
87
|
+
* no ledger rows to dedupe against, so reading them from 0 re-sums the whole
|
|
88
|
+
* session (the 421M-cache-read inflation of DX-2583). A file that appears
|
|
89
|
+
* LATER is genuinely new and still starts at 0.
|
|
90
|
+
*
|
|
91
|
+
* It must never be set alongside cursors for some sources: rule 5 says a
|
|
92
|
+
* sub-agent file that appeared after the last stats write, on a dispatch that
|
|
93
|
+
* HAS cursors, starts at 0 so none of its lines are lost.
|
|
62
94
|
*/
|
|
63
|
-
|
|
95
|
+
startExistingFilesAtEof?: boolean;
|
|
64
96
|
/**
|
|
65
|
-
* DX-
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* no
|
|
71
|
-
*
|
|
72
|
-
*
|
|
97
|
+
* DX-2954 — a stored cursor was PAST the end of its file (the file was
|
|
98
|
+
* replaced or truncated), so that source restarted at 0.
|
|
99
|
+
*
|
|
100
|
+
* Without this the watcher would silently tail nothing for the rest of the
|
|
101
|
+
* dispatch: its `size <= byteOffset` early return is indistinguishable from
|
|
102
|
+
* "no new bytes". The caller records a visible `transcript_cursor_past_eof`
|
|
103
|
+
* event with both numbers and marks the reset in its next stats write, which
|
|
104
|
+
* is the ONE sanctioned backwards move of a stored `acked`.
|
|
73
105
|
*/
|
|
74
|
-
|
|
106
|
+
onCursorPastEof?: (info: CursorPastEofInfo) => void;
|
|
107
|
+
/**
|
|
108
|
+
* DX-2954 — a sub-agent transcript just came under tail for the first time,
|
|
109
|
+
* i.e. a sub-agent of this dispatch is RUNNING.
|
|
110
|
+
*
|
|
111
|
+
* This is the transcript-side producer of the dispatch's running set. It
|
|
112
|
+
* reports the sub-agent's ledger source key, which is the same value its
|
|
113
|
+
* `dispatch_activities.activity_id` and its per-source token total join on —
|
|
114
|
+
* so a hook-side producer reporting the same sub-agent by its
|
|
115
|
+
* `transcript_path` resolves to this same id and upserts the SAME row rather
|
|
116
|
+
* than a duplicate.
|
|
117
|
+
*
|
|
118
|
+
* Fired once per source, from the discovery pass, for sub-agent files only —
|
|
119
|
+
* never for the main transcript, which is the dispatch itself, not an activity
|
|
120
|
+
* within it.
|
|
121
|
+
*/
|
|
122
|
+
onSourceDiscovered?: (info: SourceDiscoveredInfo) => void;
|
|
123
|
+
}
|
|
124
|
+
/** DX-2954 — one source's stored cursor pair, as a collector starts from it. */
|
|
125
|
+
export interface SourceCursorStart {
|
|
126
|
+
/** The resume point: read on from here. */
|
|
127
|
+
acked: number;
|
|
128
|
+
/**
|
|
129
|
+
* The highest byte already handed to subscribers. Lines between `acked` and
|
|
130
|
+
* this are re-read and handed on marked as REPLAYS, so side-effecting
|
|
131
|
+
* subscribers skip them while the ledger and the log shipper still see them.
|
|
132
|
+
*/
|
|
133
|
+
delivered: number;
|
|
134
|
+
}
|
|
135
|
+
/** DX-2954 — reported when a stored cursor was past its file's end. */
|
|
136
|
+
export interface CursorPastEofInfo {
|
|
137
|
+
source: string;
|
|
138
|
+
storedOffset: number;
|
|
139
|
+
fileSize: number;
|
|
140
|
+
}
|
|
141
|
+
/** DX-2954 — reported when a sub-agent's transcript comes under tail. */
|
|
142
|
+
export interface SourceDiscoveredInfo {
|
|
143
|
+
/** The sub-agent's ledger source key (its transcript file's basename). */
|
|
144
|
+
source: string;
|
|
145
|
+
/**
|
|
146
|
+
* The sub-agent's declared type, read from its own transcript. `null` when it
|
|
147
|
+
* has not written a line naming one yet — a hook-side report of the same
|
|
148
|
+
* sub-agent fills it in later without clobbering a value already stored.
|
|
149
|
+
*/
|
|
150
|
+
agentType: string | null;
|
|
151
|
+
/** Epoch ms this watcher first saw the file. */
|
|
152
|
+
discoveredAt: number;
|
|
153
|
+
/**
|
|
154
|
+
* The file already existed when this watcher attached (found on its FIRST
|
|
155
|
+
* discovery pass). A consumer attached to a dispatch that CONTINUES an earlier
|
|
156
|
+
* one (a `--resume`, a dispatcher or thread resume) uses this to tell its
|
|
157
|
+
* predecessor's sub-agents, which ended with that process, from its own.
|
|
158
|
+
*/
|
|
159
|
+
preexisting: boolean;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* DX-2954 — what a consumer learns about the line it is being handed, beyond
|
|
163
|
+
* the entry itself.
|
|
164
|
+
*/
|
|
165
|
+
export interface EntryMeta {
|
|
166
|
+
/** {@link MAIN_SOURCE} for the main transcript, else the sub-agent id. */
|
|
167
|
+
source: string;
|
|
168
|
+
/** Byte offset of the line's first byte in its file. */
|
|
169
|
+
byteOffset: number;
|
|
170
|
+
/**
|
|
171
|
+
* This line was already handed to a PREVIOUS incarnation's subscribers (it
|
|
172
|
+
* falls below the source's stored `delivered` cursor) and is being handed
|
|
173
|
+
* again after a restart.
|
|
174
|
+
*
|
|
175
|
+
* Every subscriber with a SIDE EFFECT must skip a replay — re-posting Slack
|
|
176
|
+
* progress, re-forwarding to gpt-manager, or re-running the API-error
|
|
177
|
+
* detector on a predecessor's last lines are all real, visible damage. The
|
|
178
|
+
* ledger and the log shipper DO process replays: both are idempotent, and
|
|
179
|
+
* skipping them is what would lose data.
|
|
180
|
+
*/
|
|
181
|
+
replay: boolean;
|
|
75
182
|
}
|
|
76
183
|
/**
|
|
77
184
|
* Encode a working directory to the subdir name Claude Code writes JSONL
|
|
@@ -186,6 +293,29 @@ export declare function convertJsonlEntry(raw: Record<string, unknown>, lastTime
|
|
|
186
293
|
entry: AgentLogEntry;
|
|
187
294
|
timestamp: number;
|
|
188
295
|
} | null;
|
|
296
|
+
/**
|
|
297
|
+
* DX-2954 — WHERE EACH FILE STARTS. One ordered rule set, applied per source by
|
|
298
|
+
* {@link SessionLogWatcher.resolveStart}; the caller's job is only to hand in
|
|
299
|
+
* the right `startCursors` / `startExistingFilesAtEof`.
|
|
300
|
+
*
|
|
301
|
+
* | # | Condition | Start |
|
|
302
|
+
* |---|---|---|
|
|
303
|
+
* | 1 | the dispatch has a stored cursor for this source | its `acked`, replaying up to its `delivered` |
|
|
304
|
+
* | 2 | a worker dispatch continuing a session: its predecessor's cursor for this source (the caller resolves it INTO `startCursors`, so it arrives as rule 1) | the predecessor's final `acked`, replaying to its final `delivered` |
|
|
305
|
+
* | 3 | the FIRST dispatch of its Claude session — a new worker session, or any operator dispatch (always the only one for its session id, including one opened by a mid-session `plan_connect` and a forked session's new id) | 0 |
|
|
306
|
+
* | 4 | a WORKER dispatch continuing a session with NO cursor for ANY source (a row from before this card; a `--resume` whose predecessor predates it) — `startExistingFilesAtEof` | every ALREADY-EXISTING file at its current end |
|
|
307
|
+
* | 5 | otherwise — in particular a sub-agent file that appeared AFTER the last stats write, on a dispatch that has cursors for other sources | 0 |
|
|
308
|
+
*
|
|
309
|
+
* Rules 1 and 2 arrive identically because "where does this source resume" has
|
|
310
|
+
* ONE answer; rule 4 is the only branch that skips content, and it is narrow by
|
|
311
|
+
* construction (it cannot fire when ANY source has a cursor, so rule 5 keeps a
|
|
312
|
+
* late sub-agent file's lines).
|
|
313
|
+
*
|
|
314
|
+
* Rules 3 and 5 both read from 0, but for different reasons, and both are
|
|
315
|
+
* correct: a fresh session's full usage SHOULD be counted, and a forked
|
|
316
|
+
* session's copied history is read and credited nothing (the ledger's excess
|
|
317
|
+
* rule already covers it).
|
|
318
|
+
*/
|
|
189
319
|
export declare class SessionLogWatcher {
|
|
190
320
|
private consumers;
|
|
191
321
|
private pollTimer;
|
|
@@ -197,13 +327,62 @@ export declare class SessionLogWatcher {
|
|
|
197
327
|
private sessionId;
|
|
198
328
|
private dispatchId;
|
|
199
329
|
private pollIntervalMs;
|
|
200
|
-
private
|
|
201
|
-
private
|
|
330
|
+
private readonly startCursors;
|
|
331
|
+
private readonly startExistingFilesAtEof;
|
|
332
|
+
private readonly onCursorPastEof;
|
|
333
|
+
private readonly onSourceDiscovered;
|
|
202
334
|
private subagentDiscoveryDone;
|
|
203
335
|
private running;
|
|
204
336
|
private polling;
|
|
205
337
|
private initSynthesized;
|
|
206
338
|
constructor(options: SessionLogWatcherOptions);
|
|
339
|
+
/**
|
|
340
|
+
* DX-2954 — resolve one file's starting offset and replay boundary, per the
|
|
341
|
+
* start order on {@link SessionLogWatcher}.
|
|
342
|
+
*
|
|
343
|
+
* `fileAlreadyExisted` distinguishes rule 4 (a file present when this watcher
|
|
344
|
+
* attached, on a continuing worker dispatch with no cursor anywhere) from
|
|
345
|
+
* rule 5 (a file that appeared later, which always starts at 0 so none of its
|
|
346
|
+
* lines are lost).
|
|
347
|
+
*/
|
|
348
|
+
private resolveStart;
|
|
349
|
+
/**
|
|
350
|
+
* DX-2954 — every open source's cursor state.
|
|
351
|
+
*
|
|
352
|
+
* `delivered` is the byte up to which this watcher has handed lines to
|
|
353
|
+
* subscribers, which the collector reports as the `delivered` half of its
|
|
354
|
+
* stats write. `startOffset` is where this watcher OPENED the file, and it is
|
|
355
|
+
* the floor for the `acked` half: a source that started at EOF under rule 4
|
|
356
|
+
* must never report `acked: 0`, or the next restart would read the whole file
|
|
357
|
+
* from the start and re-sum the history rule 4 exists to skip.
|
|
358
|
+
*/
|
|
359
|
+
cursorState(): Record<string, {
|
|
360
|
+
startOffset: number;
|
|
361
|
+
delivered: number;
|
|
362
|
+
}>;
|
|
363
|
+
/**
|
|
364
|
+
* DX-2954 — the main source's replay boundary (its stored `delivered` cursor),
|
|
365
|
+
* or 0 when it has none.
|
|
366
|
+
*
|
|
367
|
+
* Read with {@link SessionLogWatcher.mainStartOffset} to answer "did this
|
|
368
|
+
* watcher attach mid-session?". The offset alone is not enough: because `acked`
|
|
369
|
+
* lags `delivered` by one write, a crash right after the first cursor-bearing
|
|
370
|
+
* write leaves `{acked: 0, delivered: D}`, so the next attach starts at 0 while
|
|
371
|
+
* every byte up to D is a re-read.
|
|
372
|
+
*/
|
|
373
|
+
mainReplayUntil(): number;
|
|
374
|
+
/**
|
|
375
|
+
* DX-2954 — the main transcript's starting offset once discovery has run, or
|
|
376
|
+
* `null` before that.
|
|
377
|
+
*
|
|
378
|
+
* This is what "is this a mid-session attach?" is derived from now. The
|
|
379
|
+
* declared-loaded observer used to be told `reattached: true` explicitly;
|
|
380
|
+
* a non-zero main start is the same fact stated as an observation rather than
|
|
381
|
+
* a flag, and it is true for every case that matters (a reattach, a
|
|
382
|
+
* `--resume`, a resumed operator session) without a caller having to
|
|
383
|
+
* enumerate them.
|
|
384
|
+
*/
|
|
385
|
+
mainStartOffset(): number | null;
|
|
207
386
|
/** Register a consumer callback that receives each new AgentLogEntry. */
|
|
208
387
|
onEntry(consumer: EntryConsumer): void;
|
|
209
388
|
/** Returns the discovered parent session file path, or null if not yet found. */
|
|
@@ -264,15 +443,10 @@ export declare class SessionLogWatcher {
|
|
|
264
443
|
/**
|
|
265
444
|
* Adopt a parent session file and derive the sub-agent directory.
|
|
266
445
|
*
|
|
267
|
-
*
|
|
268
|
-
* the
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* with a delete that happens between discover and adopt) falls back to
|
|
272
|
-
* 0 so the watcher tails from the start; that is preferable to throwing
|
|
273
|
-
* during boot. `fromEof` does NOT skip sub-agent files: any sub-agent
|
|
274
|
-
* file claude writes AFTER reattach is genuinely new and should be
|
|
275
|
-
* tailed normally.
|
|
446
|
+
* DX-2954 — the starting offset comes from {@link SessionLogWatcher.resolveStart}
|
|
447
|
+
* (the cursor start order), not from a `fromEof` flag: the main source resumes
|
|
448
|
+
* at its stored `acked` cursor when it has one, and only a continuing worker
|
|
449
|
+
* dispatch with no cursor anywhere skips to the file's end.
|
|
276
450
|
*/
|
|
277
451
|
private setParentFile;
|
|
278
452
|
/**
|
|
@@ -297,6 +471,27 @@ export declare class SessionLogWatcher {
|
|
|
297
471
|
* `pollFile` — the inline call is the defense-in-depth that closes the race
|
|
298
472
|
* where bytes appear before `refreshPendingAgentTypes` has a chance to run.
|
|
299
473
|
*/
|
|
474
|
+
/**
|
|
475
|
+
* DX-2954 — report a running sub-agent to the consumer, normalizing the
|
|
476
|
+
* absent-type shape and CONTAINING any throw.
|
|
477
|
+
*
|
|
478
|
+
* The try/catch is not defensive padding; it mirrors `emitEntry`, which wraps
|
|
479
|
+
* every consumer for the same reason, and it is load-bearing here because of
|
|
480
|
+
* where this fires. `discoverNewSubagentFiles` has no `catch` of its own and
|
|
481
|
+
* `poll()` has `try/finally` without one, so an escaping throw would:
|
|
482
|
+
*
|
|
483
|
+
* - on the first pass (inside `start()`), kill the watcher for the whole
|
|
484
|
+
* dispatch — the caller only logs `start()`'s rejection;
|
|
485
|
+
* - on a later interval tick, become an UNHANDLED REJECTION, which Node 20
|
|
486
|
+
* turns into process exit — a worker crash caused by a consumer bug;
|
|
487
|
+
* - either way abort the discovery loop mid-list, skipping
|
|
488
|
+
* `subagentDiscoveryDone = true`, which leaves `firstPass` true forever and
|
|
489
|
+
* sends later-appearing files down rule 4 instead of rule 5 — losing
|
|
490
|
+
* exactly the lines that rule split exists to preserve.
|
|
491
|
+
*
|
|
492
|
+
* Reporting the running set must never cost the monitoring it rides on.
|
|
493
|
+
*/
|
|
494
|
+
private emitSourceDiscovered;
|
|
300
495
|
private refreshLineage;
|
|
301
496
|
/**
|
|
302
497
|
* Read agentType from the `<subagentId>.meta.json` sidecar. Returns
|