@thehammer/danx-session-telemetry 0.1.15 → 0.1.17
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.
|
@@ -7,16 +7,17 @@
|
|
|
7
7
|
* signal danxbot owns end-to-end: the session-log watcher discovering a
|
|
8
8
|
* sub-agent's transcript file and then tailing it.
|
|
9
9
|
*
|
|
10
|
-
* ## Why the transcript,
|
|
10
|
+
* ## Why the transcript, and where the hooks come in
|
|
11
11
|
*
|
|
12
12
|
* Claude Code's `SubagentStart` / `SubagentStop` hooks carry strictly more (an
|
|
13
|
-
* `agent_type` before the sub-agent has written anything
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
13
|
+
* `agent_type` before the sub-agent has written anything, and — the part this
|
|
14
|
+
* module cannot supply — a FINISH). Those hooks ship from the danxbot plugin in
|
|
15
|
+
* the claude-plugins repo, and DX-3284 builds them, next. They were not built in
|
|
16
|
+
* DX-2954 itself, but not because a plugin change cannot ship from a danxbot
|
|
17
|
+
* card: DX-2954's own `transcript_path` change shipped as claude-plugins
|
|
18
|
+
* `c0c134d`. Keying the running set solely on them before they exist would ship
|
|
19
|
+
* a set that is permanently empty (`subagent_count` 0 on every dispatch, a
|
|
20
|
+
* REGRESSION against the transcript-derived counter this replaced).
|
|
20
21
|
*
|
|
21
22
|
* So the transcript is the producer, and the hooks are an ENRICHMENT layered on
|
|
22
23
|
* the same rows:
|
|
@@ -24,7 +25,13 @@
|
|
|
24
25
|
* | Signal | Sub-agent start | Sub-agent finish | Background script |
|
|
25
26
|
* |---|---|---|---|
|
|
26
27
|
* | Transcript (here) | file discovered | dispatch terminal (`finishOpenActivities`) | not visible |
|
|
27
|
-
* | Hooks (DX-3284) | `SubagentStart` | `SubagentStop` | `PostToolUse`
|
|
28
|
+
* | Hooks (DX-3284) | `SubagentStart` | `SubagentStop` | `PostToolUse` start; end at dispatch terminal (no documented completion event) |
|
|
29
|
+
*
|
|
30
|
+
* **Until DX-3284 lands, "running" over-counts.** With no finish signal, every
|
|
31
|
+
* sub-agent this dispatch ever started stays in the running set until the
|
|
32
|
+
* dispatch itself ends, and the silent-too-long warning fires
|
|
33
|
+
* `RUNNING_ACTIVITY_SILENT_AFTER_MS` after a sub-agent that has in fact
|
|
34
|
+
* finished. `SubagentStop` is what closes that gap.
|
|
28
35
|
*
|
|
29
36
|
* ## Why the two producers cannot double-count
|
|
30
37
|
*
|
|
@@ -41,9 +48,8 @@
|
|
|
41
48
|
* signal — a finished sub-agent's file simply stops growing, which is
|
|
42
49
|
* indistinguishable from a slow one. Guessing a finish from an idle gap would
|
|
43
50
|
* mark a long-thinking sub-agent as complete. So open rows are closed exactly
|
|
44
|
-
* once, at the dispatch's own terminal transition (`finishOpenActivities`),
|
|
45
|
-
*
|
|
46
|
-
* `quietest_running_at` is for.
|
|
51
|
+
* once, at the dispatch's own terminal transition (`finishOpenActivities`),
|
|
52
|
+
* unless DX-3284's `SubagentStop` hook closes a row first.
|
|
47
53
|
*/
|
|
48
54
|
import type { ActivityChange } from "./stats-wire.js";
|
|
49
55
|
import type { EntryMeta, SourceDiscoveredInfo } from "./session-log-watcher.js";
|
package/dist/dispatch-usage.d.ts
CHANGED
|
@@ -43,6 +43,20 @@ export declare class DispatchUsage {
|
|
|
43
43
|
* its own resume point instead of regressing it.
|
|
44
44
|
*/
|
|
45
45
|
private readonly ackedCursor;
|
|
46
|
+
/**
|
|
47
|
+
* DX-2954 (AC 34324) — the cursors this process STARTED from, reported
|
|
48
|
+
* verbatim for every source the watcher has not opened yet until one write
|
|
49
|
+
* carrying them has landed. See {@link DispatchUsage.buildCursorReports}.
|
|
50
|
+
*/
|
|
51
|
+
private startCursors;
|
|
52
|
+
private startCursorsPersisted;
|
|
53
|
+
/**
|
|
54
|
+
* DX-2954 — set the moment the first flush begins. `attachCursors` after that
|
|
55
|
+
* point would set the starting cursors AFTER a write that carried none, and
|
|
56
|
+
* `startCursorsPersisted` (set by any landed write) would then keep them from
|
|
57
|
+
* ever being carried — the exact AC 34324 loss, silently. So it fails loud.
|
|
58
|
+
*/
|
|
59
|
+
private flushBegan;
|
|
46
60
|
/** Past-EOF resets to report once, then forget. */
|
|
47
61
|
private pastEof;
|
|
48
62
|
/**
|
|
@@ -118,25 +132,36 @@ export declare class DispatchUsage {
|
|
|
118
132
|
* excess rule) and every side-effecting subscriber skips it — so the cost is
|
|
119
133
|
* one window of re-read, never a double count or a duplicate side effect.
|
|
120
134
|
*
|
|
121
|
-
* ##
|
|
135
|
+
* ## The process-start write carries the STARTING cursors (AC 34324)
|
|
122
136
|
*
|
|
123
137
|
* `attachMonitoringStack` deliberately does NOT await `watcher.start()` —
|
|
124
138
|
* file discovery polls for up to 180 s and must not block the spawn — and the
|
|
125
139
|
* process-start stats write fires immediately after it. At that moment the
|
|
126
|
-
* watcher has opened no file, so `cursorState()` is empty
|
|
127
|
-
*
|
|
128
|
-
*
|
|
140
|
+
* watcher has opened no file, so its live `cursorState()` is empty.
|
|
141
|
+
*
|
|
142
|
+
* So every source in the cursors this process STARTED from that the watcher
|
|
143
|
+
* has not opened yet is reported VERBATIM — the exact `{acked, delivered}`
|
|
144
|
+
* pair it resumed from — until a write that carries them has landed. That is
|
|
145
|
+
* what makes "every attach persists its starting cursors" true on the FIRST
|
|
146
|
+
* write rather than one stats tick (15-30 s) later: a reattach of this row
|
|
147
|
+
* inside its first interval reads them back (start-order rule 1) and starts
|
|
148
|
+
* exactly where this process did, instead of finding no cursor, resolving
|
|
149
|
+
* rule 4 and skipping every line written since spawn. A fresh session's
|
|
150
|
+
* starting cursor is explicit too (`resolveStartCursors` rule 3 returns the
|
|
151
|
+
* main source at 0), and a `--resume`'s is its predecessor's (rule 2).
|
|
152
|
+
*
|
|
153
|
+
* Verbatim is the only honest claim for a source this process has not read:
|
|
154
|
+
* a higher `acked` would let a restart skip the replay window it has not
|
|
155
|
+
* re-credited, a lower one is refused as a regression. For the same reason
|
|
156
|
+
* the start write does not PROMOTE such a source's acknowledged floor — it
|
|
157
|
+
* delivered nothing there — so it stays out of `deliveredAtSend`.
|
|
129
158
|
*
|
|
130
|
-
* The
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* for the deploy transition. Closing it properly means persisting the starting
|
|
137
|
-
* cursor on discovery rather than on the next tick — a watcher-discovery
|
|
138
|
-
* callback wired into the ordered stats channel — which is a separate change,
|
|
139
|
-
* not something to bolt onto the flush path.
|
|
159
|
+
* The one case with nothing to carry is rule 4 (a continuing dispatch with no
|
|
160
|
+
* cursor for ANY source): its start is "each existing file's end at
|
|
161
|
+
* discovery", which does not exist until discovery runs. Its cursors reach
|
|
162
|
+
* the server on the first write after discovery. Rule 4 is the pre-card
|
|
163
|
+
* deploy transition only — every dispatch since this card stores cursors on
|
|
164
|
+
* its first write — so that bounded window is the one the card accepts.
|
|
140
165
|
*/
|
|
141
166
|
private buildCursorReports;
|
|
142
167
|
/**
|
package/dist/dispatch-usage.js
CHANGED
|
@@ -63,6 +63,20 @@ export class DispatchUsage {
|
|
|
63
63
|
* its own resume point instead of regressing it.
|
|
64
64
|
*/
|
|
65
65
|
ackedCursor = new Map();
|
|
66
|
+
/**
|
|
67
|
+
* DX-2954 (AC 34324) — the cursors this process STARTED from, reported
|
|
68
|
+
* verbatim for every source the watcher has not opened yet until one write
|
|
69
|
+
* carrying them has landed. See {@link DispatchUsage.buildCursorReports}.
|
|
70
|
+
*/
|
|
71
|
+
startCursors = {};
|
|
72
|
+
startCursorsPersisted = false;
|
|
73
|
+
/**
|
|
74
|
+
* DX-2954 — set the moment the first flush begins. `attachCursors` after that
|
|
75
|
+
* point would set the starting cursors AFTER a write that carried none, and
|
|
76
|
+
* `startCursorsPersisted` (set by any landed write) would then keep them from
|
|
77
|
+
* ever being carried — the exact AC 34324 loss, silently. So it fails loud.
|
|
78
|
+
*/
|
|
79
|
+
flushBegan = false;
|
|
66
80
|
/** Past-EOF resets to report once, then forget. */
|
|
67
81
|
pastEof = [];
|
|
68
82
|
/**
|
|
@@ -86,7 +100,13 @@ export class DispatchUsage {
|
|
|
86
100
|
* each write.
|
|
87
101
|
*/
|
|
88
102
|
attachCursors(readCursorState, startCursors) {
|
|
103
|
+
// DX-2954 (AC 34324): the starting cursors ride the process-start write, so
|
|
104
|
+
// they must be attached BEFORE any write is attempted.
|
|
105
|
+
if (this.flushBegan) {
|
|
106
|
+
throw new Error("DispatchUsage.attachCursors: called after the first stats write began — the starting cursors would never be persisted");
|
|
107
|
+
}
|
|
89
108
|
this.readCursorState = readCursorState;
|
|
109
|
+
this.startCursors = startCursors;
|
|
90
110
|
for (const [source, cursor] of Object.entries(startCursors)) {
|
|
91
111
|
this.ackedCursor.set(source, cursor.acked);
|
|
92
112
|
}
|
|
@@ -156,34 +176,56 @@ export class DispatchUsage {
|
|
|
156
176
|
* excess rule) and every side-effecting subscriber skips it — so the cost is
|
|
157
177
|
* one window of re-read, never a double count or a duplicate side effect.
|
|
158
178
|
*
|
|
159
|
-
* ##
|
|
179
|
+
* ## The process-start write carries the STARTING cursors (AC 34324)
|
|
160
180
|
*
|
|
161
181
|
* `attachMonitoringStack` deliberately does NOT await `watcher.start()` —
|
|
162
182
|
* file discovery polls for up to 180 s and must not block the spawn — and the
|
|
163
183
|
* process-start stats write fires immediately after it. At that moment the
|
|
164
|
-
* watcher has opened no file, so `cursorState()` is empty
|
|
165
|
-
* reports. Cursors first reach the server on the write AFTER discovery, one
|
|
166
|
-
* stats tick (15-30 s) later.
|
|
184
|
+
* watcher has opened no file, so its live `cursorState()` is empty.
|
|
167
185
|
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
186
|
+
* So every source in the cursors this process STARTED from that the watcher
|
|
187
|
+
* has not opened yet is reported VERBATIM — the exact `{acked, delivered}`
|
|
188
|
+
* pair it resumed from — until a write that carries them has landed. That is
|
|
189
|
+
* what makes "every attach persists its starting cursors" true on the FIRST
|
|
190
|
+
* write rather than one stats tick (15-30 s) later: a reattach of this row
|
|
191
|
+
* inside its first interval reads them back (start-order rule 1) and starts
|
|
192
|
+
* exactly where this process did, instead of finding no cursor, resolving
|
|
193
|
+
* rule 4 and skipping every line written since spawn. A fresh session's
|
|
194
|
+
* starting cursor is explicit too (`resolveStartCursors` rule 3 returns the
|
|
195
|
+
* main source at 0), and a `--resume`'s is its predecessor's (rule 2).
|
|
196
|
+
*
|
|
197
|
+
* Verbatim is the only honest claim for a source this process has not read:
|
|
198
|
+
* a higher `acked` would let a restart skip the replay window it has not
|
|
199
|
+
* re-credited, a lower one is refused as a regression. For the same reason
|
|
200
|
+
* the start write does not PROMOTE such a source's acknowledged floor — it
|
|
201
|
+
* delivered nothing there — so it stays out of `deliveredAtSend`.
|
|
202
|
+
*
|
|
203
|
+
* The one case with nothing to carry is rule 4 (a continuing dispatch with no
|
|
204
|
+
* cursor for ANY source): its start is "each existing file's end at
|
|
205
|
+
* discovery", which does not exist until discovery runs. Its cursors reach
|
|
206
|
+
* the server on the first write after discovery. Rule 4 is the pre-card
|
|
207
|
+
* deploy transition only — every dispatch since this card stores cursors on
|
|
208
|
+
* its first write — so that bounded window is the one the card accepts.
|
|
178
209
|
*/
|
|
179
210
|
buildCursorReports() {
|
|
180
211
|
const reports = {};
|
|
181
212
|
const deliveredAtSend = new Map();
|
|
182
|
-
if (!this.readCursorState)
|
|
183
|
-
return { reports, deliveredAtSend, reportedResets: [] };
|
|
213
|
+
if (!this.readCursorState) {
|
|
214
|
+
return { reports, deliveredAtSend, reportedResets: [], carriesStartCursors: false };
|
|
215
|
+
}
|
|
184
216
|
const resetBySource = new Map(this.pastEof.map((p) => [p.source, p]));
|
|
185
217
|
const reportedResets = [];
|
|
186
|
-
|
|
218
|
+
const live = this.readCursorState();
|
|
219
|
+
let carriesStartCursors = false;
|
|
220
|
+
if (!this.startCursorsPersisted) {
|
|
221
|
+
for (const [source, start] of Object.entries(this.startCursors)) {
|
|
222
|
+
if (source in live)
|
|
223
|
+
continue; // the live state supersedes it once opened
|
|
224
|
+
reports[source] = { acked: start.acked, delivered: start.delivered };
|
|
225
|
+
carriesStartCursors = true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const [source, state] of Object.entries(live)) {
|
|
187
229
|
// `acked` floors at the source's START offset: a source opened at EOF
|
|
188
230
|
// under rule 4 must never claim byte 0 as acknowledged, or the next
|
|
189
231
|
// restart would read the whole file and re-sum the history rule 4 skipped.
|
|
@@ -201,7 +243,7 @@ export class DispatchUsage {
|
|
|
201
243
|
// source that has left `cursorState()` (its tail dropped on ENOENT) would
|
|
202
244
|
// otherwise be taken and forgotten, leaving the stored `acked` at a value the
|
|
203
245
|
// file can never satisfy.
|
|
204
|
-
return { reports, deliveredAtSend, reportedResets };
|
|
246
|
+
return { reports, deliveredAtSend, reportedResets, carriesStartCursors };
|
|
205
247
|
}
|
|
206
248
|
/**
|
|
207
249
|
* DX-2954 — whether the cursor half has anything new to say. An IDLE dispatch
|
|
@@ -259,9 +301,10 @@ export class DispatchUsage {
|
|
|
259
301
|
return this.queue(async () => {
|
|
260
302
|
if (this.refused)
|
|
261
303
|
return "refused";
|
|
304
|
+
this.flushBegan = true;
|
|
262
305
|
const changed = this.counter.takeChanged();
|
|
263
306
|
const batches = chunkEntries(changed);
|
|
264
|
-
const { reports, deliveredAtSend, reportedResets } = this.buildCursorReports();
|
|
307
|
+
const { reports, deliveredAtSend, reportedResets, carriesStartCursors } = this.buildCursorReports();
|
|
265
308
|
// TAKE the queued side data, exactly as `counter.takeChanged()` does, so a
|
|
266
309
|
// hook that fires during the await lands in the NEXT write rather than
|
|
267
310
|
// being silently dropped by a post-write reset of the live array.
|
|
@@ -282,7 +325,12 @@ export class DispatchUsage {
|
|
|
282
325
|
const pastEofSent = reportedResets;
|
|
283
326
|
const sentResets = new Set(reportedResets);
|
|
284
327
|
this.pastEof = this.pastEof.filter((p) => !sentResets.has(p));
|
|
285
|
-
|
|
328
|
+
// `carriesStartCursors` forces the write on its own: a starting pair like
|
|
329
|
+
// `{acked: 0, delivered: 0}` never looks "moved", and a process-start write
|
|
330
|
+
// whose last batch failed must still deliver it on a later, otherwise idle
|
|
331
|
+
// tick.
|
|
332
|
+
const haveSideData = carriesStartCursors ||
|
|
333
|
+
this.cursorsMoved(reports) ||
|
|
286
334
|
activities.length > 0 ||
|
|
287
335
|
(this.transcriptPath !== null && !this.transcriptPathSent);
|
|
288
336
|
if (batches.length === 0 && (!this.started || haveSideData))
|
|
@@ -325,6 +373,10 @@ export class DispatchUsage {
|
|
|
325
373
|
}
|
|
326
374
|
if (payload.transcriptPath !== undefined)
|
|
327
375
|
this.transcriptPathSent = true;
|
|
376
|
+
// Safe on ANY landed write: `attachCursors` refuses to run after the
|
|
377
|
+
// first flush began, so every starting cursor was either carried by
|
|
378
|
+
// this write or is superseded by a live one that this write reported.
|
|
379
|
+
this.startCursorsPersisted = true;
|
|
328
380
|
}
|
|
329
381
|
}
|
|
330
382
|
catch (err) {
|
|
@@ -76,7 +76,7 @@ export interface SessionLogWatcherOptions {
|
|
|
76
76
|
* That lost every line between the last write and the restart; a cursor does
|
|
77
77
|
* not.
|
|
78
78
|
*/
|
|
79
|
-
startCursors
|
|
79
|
+
startCursors: Record<string, SourceCursorStart>;
|
|
80
80
|
/**
|
|
81
81
|
* DX-2954 — rule 4 of the start order: start every ALREADY-EXISTING file at
|
|
82
82
|
* its current end.
|
|
@@ -507,7 +507,7 @@ export class SessionLogWatcher {
|
|
|
507
507
|
this.sessionId = options.sessionId;
|
|
508
508
|
this.dispatchId = options.dispatchId;
|
|
509
509
|
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
510
|
-
this.startCursors = options.startCursors
|
|
510
|
+
this.startCursors = options.startCursors;
|
|
511
511
|
this.startExistingFilesAtEof = options.startExistingFilesAtEof ?? false;
|
|
512
512
|
this.onCursorPastEof = options.onCursorPastEof;
|
|
513
513
|
this.onSourceDiscovered = options.onSourceDiscovered;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thehammer/danx-session-telemetry",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "Shared Claude Code transcript collector (DX-2962) — incremental JSONL tailing for the danxbot worker and the danx-dashboard-mcp desktop bridge, so both run identical collector code.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|