@thehammer/danx-session-telemetry 0.1.15 → 0.1.16
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/dist/activity-observer.d.ts +18 -12
- package/dist/dispatch-usage.d.ts +32 -14
- package/dist/dispatch-usage.js +56 -20
- package/package.json +1 -1
|
@@ -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,13 @@ 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;
|
|
46
53
|
/** Past-EOF resets to report once, then forget. */
|
|
47
54
|
private pastEof;
|
|
48
55
|
/**
|
|
@@ -118,25 +125,36 @@ export declare class DispatchUsage {
|
|
|
118
125
|
* excess rule) and every side-effecting subscriber skips it — so the cost is
|
|
119
126
|
* one window of re-read, never a double count or a duplicate side effect.
|
|
120
127
|
*
|
|
121
|
-
* ##
|
|
128
|
+
* ## The process-start write carries the STARTING cursors (AC 34324)
|
|
122
129
|
*
|
|
123
130
|
* `attachMonitoringStack` deliberately does NOT await `watcher.start()` —
|
|
124
131
|
* file discovery polls for up to 180 s and must not block the spawn — and the
|
|
125
132
|
* process-start stats write fires immediately after it. At that moment the
|
|
126
|
-
* watcher has opened no file, so `cursorState()` is empty
|
|
127
|
-
*
|
|
128
|
-
*
|
|
133
|
+
* watcher has opened no file, so its live `cursorState()` is empty.
|
|
134
|
+
*
|
|
135
|
+
* So every source in the cursors this process STARTED from that the watcher
|
|
136
|
+
* has not opened yet is reported VERBATIM — the exact `{acked, delivered}`
|
|
137
|
+
* pair it resumed from — until one write carrying them has landed. That is
|
|
138
|
+
* what makes "every attach persists its starting cursors" true on the FIRST
|
|
139
|
+
* write rather than one stats tick (15-30 s) later: a reattach of this row
|
|
140
|
+
* inside its first interval reads them back (start-order rule 1) and starts
|
|
141
|
+
* exactly where this process did, instead of finding no cursor, resolving
|
|
142
|
+
* rule 4 and skipping every line written since spawn. A fresh session's
|
|
143
|
+
* starting cursor is explicit too (`resolveStartCursors` rule 3 returns the
|
|
144
|
+
* main source at 0), and a `--resume`'s is its predecessor's (rule 2).
|
|
145
|
+
*
|
|
146
|
+
* Verbatim is the only honest claim for a source this process has not read:
|
|
147
|
+
* a higher `acked` would let a restart skip the replay window it has not
|
|
148
|
+
* re-credited, a lower one is refused as a regression. For the same reason
|
|
149
|
+
* the start write does not PROMOTE such a source's acknowledged floor — it
|
|
150
|
+
* delivered nothing there — so it stays out of `deliveredAtSend`.
|
|
129
151
|
*
|
|
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.
|
|
152
|
+
* The one case with nothing to carry is rule 4 (a continuing dispatch with no
|
|
153
|
+
* cursor for ANY source): its start is "each existing file's end at
|
|
154
|
+
* discovery", which does not exist until discovery runs. Its cursors reach
|
|
155
|
+
* the server on the first write after discovery. Rule 4 is the pre-card
|
|
156
|
+
* deploy transition only — every dispatch since this card stores cursors on
|
|
157
|
+
* its first write — so that bounded window is the one the card accepts.
|
|
140
158
|
*/
|
|
141
159
|
private buildCursorReports;
|
|
142
160
|
/**
|
package/dist/dispatch-usage.js
CHANGED
|
@@ -63,6 +63,13 @@ 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;
|
|
66
73
|
/** Past-EOF resets to report once, then forget. */
|
|
67
74
|
pastEof = [];
|
|
68
75
|
/**
|
|
@@ -87,6 +94,7 @@ export class DispatchUsage {
|
|
|
87
94
|
*/
|
|
88
95
|
attachCursors(readCursorState, startCursors) {
|
|
89
96
|
this.readCursorState = readCursorState;
|
|
97
|
+
this.startCursors = startCursors;
|
|
90
98
|
for (const [source, cursor] of Object.entries(startCursors)) {
|
|
91
99
|
this.ackedCursor.set(source, cursor.acked);
|
|
92
100
|
}
|
|
@@ -156,34 +164,56 @@ export class DispatchUsage {
|
|
|
156
164
|
* excess rule) and every side-effecting subscriber skips it — so the cost is
|
|
157
165
|
* one window of re-read, never a double count or a duplicate side effect.
|
|
158
166
|
*
|
|
159
|
-
* ##
|
|
167
|
+
* ## The process-start write carries the STARTING cursors (AC 34324)
|
|
160
168
|
*
|
|
161
169
|
* `attachMonitoringStack` deliberately does NOT await `watcher.start()` —
|
|
162
170
|
* file discovery polls for up to 180 s and must not block the spawn — and the
|
|
163
171
|
* process-start stats write fires immediately after it. At that moment the
|
|
164
|
-
* watcher has opened no file, so `cursorState()` is empty
|
|
165
|
-
*
|
|
166
|
-
*
|
|
172
|
+
* watcher has opened no file, so its live `cursorState()` is empty.
|
|
173
|
+
*
|
|
174
|
+
* So every source in the cursors this process STARTED from that the watcher
|
|
175
|
+
* has not opened yet is reported VERBATIM — the exact `{acked, delivered}`
|
|
176
|
+
* pair it resumed from — until one write carrying them has landed. That is
|
|
177
|
+
* what makes "every attach persists its starting cursors" true on the FIRST
|
|
178
|
+
* write rather than one stats tick (15-30 s) later: a reattach of this row
|
|
179
|
+
* inside its first interval reads them back (start-order rule 1) and starts
|
|
180
|
+
* exactly where this process did, instead of finding no cursor, resolving
|
|
181
|
+
* rule 4 and skipping every line written since spawn. A fresh session's
|
|
182
|
+
* starting cursor is explicit too (`resolveStartCursors` rule 3 returns the
|
|
183
|
+
* main source at 0), and a `--resume`'s is its predecessor's (rule 2).
|
|
184
|
+
*
|
|
185
|
+
* Verbatim is the only honest claim for a source this process has not read:
|
|
186
|
+
* a higher `acked` would let a restart skip the replay window it has not
|
|
187
|
+
* re-credited, a lower one is refused as a regression. For the same reason
|
|
188
|
+
* the start write does not PROMOTE such a source's acknowledged floor — it
|
|
189
|
+
* delivered nothing there — so it stays out of `deliveredAtSend`.
|
|
167
190
|
*
|
|
168
|
-
* The
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* for the deploy transition. Closing it properly means persisting the starting
|
|
175
|
-
* cursor on discovery rather than on the next tick — a watcher-discovery
|
|
176
|
-
* callback wired into the ordered stats channel — which is a separate change,
|
|
177
|
-
* not something to bolt onto the flush path.
|
|
191
|
+
* The one case with nothing to carry is rule 4 (a continuing dispatch with no
|
|
192
|
+
* cursor for ANY source): its start is "each existing file's end at
|
|
193
|
+
* discovery", which does not exist until discovery runs. Its cursors reach
|
|
194
|
+
* the server on the first write after discovery. Rule 4 is the pre-card
|
|
195
|
+
* deploy transition only — every dispatch since this card stores cursors on
|
|
196
|
+
* its first write — so that bounded window is the one the card accepts.
|
|
178
197
|
*/
|
|
179
198
|
buildCursorReports() {
|
|
180
199
|
const reports = {};
|
|
181
200
|
const deliveredAtSend = new Map();
|
|
182
|
-
if (!this.readCursorState)
|
|
183
|
-
return { reports, deliveredAtSend, reportedResets: [] };
|
|
201
|
+
if (!this.readCursorState) {
|
|
202
|
+
return { reports, deliveredAtSend, reportedResets: [], carriesStartCursors: false };
|
|
203
|
+
}
|
|
184
204
|
const resetBySource = new Map(this.pastEof.map((p) => [p.source, p]));
|
|
185
205
|
const reportedResets = [];
|
|
186
|
-
|
|
206
|
+
const live = this.readCursorState();
|
|
207
|
+
let carriesStartCursors = false;
|
|
208
|
+
if (!this.startCursorsPersisted) {
|
|
209
|
+
for (const [source, start] of Object.entries(this.startCursors)) {
|
|
210
|
+
if (source in live)
|
|
211
|
+
continue; // the live state supersedes it once opened
|
|
212
|
+
reports[source] = { acked: start.acked, delivered: start.delivered };
|
|
213
|
+
carriesStartCursors = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const [source, state] of Object.entries(live)) {
|
|
187
217
|
// `acked` floors at the source's START offset: a source opened at EOF
|
|
188
218
|
// under rule 4 must never claim byte 0 as acknowledged, or the next
|
|
189
219
|
// restart would read the whole file and re-sum the history rule 4 skipped.
|
|
@@ -201,7 +231,7 @@ export class DispatchUsage {
|
|
|
201
231
|
// source that has left `cursorState()` (its tail dropped on ENOENT) would
|
|
202
232
|
// otherwise be taken and forgotten, leaving the stored `acked` at a value the
|
|
203
233
|
// file can never satisfy.
|
|
204
|
-
return { reports, deliveredAtSend, reportedResets };
|
|
234
|
+
return { reports, deliveredAtSend, reportedResets, carriesStartCursors };
|
|
205
235
|
}
|
|
206
236
|
/**
|
|
207
237
|
* DX-2954 — whether the cursor half has anything new to say. An IDLE dispatch
|
|
@@ -261,7 +291,7 @@ export class DispatchUsage {
|
|
|
261
291
|
return "refused";
|
|
262
292
|
const changed = this.counter.takeChanged();
|
|
263
293
|
const batches = chunkEntries(changed);
|
|
264
|
-
const { reports, deliveredAtSend, reportedResets } = this.buildCursorReports();
|
|
294
|
+
const { reports, deliveredAtSend, reportedResets, carriesStartCursors } = this.buildCursorReports();
|
|
265
295
|
// TAKE the queued side data, exactly as `counter.takeChanged()` does, so a
|
|
266
296
|
// hook that fires during the await lands in the NEXT write rather than
|
|
267
297
|
// being silently dropped by a post-write reset of the live array.
|
|
@@ -282,7 +312,12 @@ export class DispatchUsage {
|
|
|
282
312
|
const pastEofSent = reportedResets;
|
|
283
313
|
const sentResets = new Set(reportedResets);
|
|
284
314
|
this.pastEof = this.pastEof.filter((p) => !sentResets.has(p));
|
|
285
|
-
|
|
315
|
+
// `carriesStartCursors` forces the write on its own: a starting pair like
|
|
316
|
+
// `{acked: 0, delivered: 0}` never looks "moved", and a process-start write
|
|
317
|
+
// whose last batch failed must still deliver it on a later, otherwise idle
|
|
318
|
+
// tick.
|
|
319
|
+
const haveSideData = carriesStartCursors ||
|
|
320
|
+
this.cursorsMoved(reports) ||
|
|
286
321
|
activities.length > 0 ||
|
|
287
322
|
(this.transcriptPath !== null && !this.transcriptPathSent);
|
|
288
323
|
if (batches.length === 0 && (!this.started || haveSideData))
|
|
@@ -325,6 +360,7 @@ export class DispatchUsage {
|
|
|
325
360
|
}
|
|
326
361
|
if (payload.transcriptPath !== undefined)
|
|
327
362
|
this.transcriptPathSent = true;
|
|
363
|
+
this.startCursorsPersisted = true;
|
|
328
364
|
}
|
|
329
365
|
}
|
|
330
366
|
catch (err) {
|
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.16",
|
|
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",
|