@thehammer/danx-session-telemetry 0.1.16 → 0.1.18

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.
@@ -11,36 +11,42 @@
11
11
  *
12
12
  * Claude Code's `SubagentStart` / `SubagentStop` hooks carry strictly more (an
13
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).
14
+ * module cannot supply — a FINISH). They ship from the danxbot plugin in the
15
+ * claude-plugins repo (DX-3284, `danxbot/scripts/activity-report.mjs`), run the
16
+ * `activity` subcommand of `@thehammer/danx-dashboard-mcp`, and reach the server
17
+ * through `POST /api/plan-sessions/me/activity` — which writes the SAME
18
+ * `dispatch_activities` rows through the same one server writer.
21
19
  *
22
- * So the transcript is the producer, and the hooks are an ENRICHMENT layered on
23
- * the same rows:
20
+ * The transcript stays the producer because it needs no hook to be installed: a
21
+ * session with no plugin, and every WORKER dispatch, still gets a start row for
22
+ * each sub-agent from here. The hooks are an ENRICHMENT layered on the same rows:
24
23
  *
25
24
  * | Signal | Sub-agent start | Sub-agent finish | Background script |
26
25
  * |---|---|---|---|
27
26
  * | Transcript (here) | file discovered | dispatch terminal (`finishOpenActivities`) | not visible |
28
- * | Hooks (DX-3284) | `SubagentStart` | `SubagentStop` | `PostToolUse` start; end at dispatch terminal (no documented completion event) |
27
+ * | Hooks (DX-3284, operator sessions) | `SubagentStart` | `SubagentStop` | `PostToolUse` start; end at dispatch terminal (no documented completion event) |
29
28
  *
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.
29
+ * **Where "running" closes.** An operator session's sub-agent closes at its
30
+ * `SubagentStop` hook. A WORKER dispatch has no hook reporting yet
31
+ * (`/me/activity` refuses a worker's session id as `session_is_worker`), so its
32
+ * sub-agents stay in the running set until the dispatch itself ends, and the
33
+ * silent-too-long warning fires `RUNNING_ACTIVITY_SILENT_AFTER_MS` after a
34
+ * sub-agent that has in fact finished. A worker-side `SubagentStop` report is the
35
+ * fix, and is not built.
35
36
  *
36
37
  * ## Why the two producers cannot double-count
37
38
  *
38
39
  * Every row is an upsert keyed `(dispatch_id, kind, activity_id)`, and BOTH
39
40
  * producers derive `activity_id` from the same thing: the sub-agent's transcript
40
- * file basename. The watcher has it directly; a hook has it as the basename of
41
- * its `transcript_path`. That is also the key the ledger credits the sub-agent's
42
- * tokens under, which is how per-sub-agent tokens join to the running set
43
- * without this table carrying a token column.
41
+ * file basename, `agent-<agent_id>`. The watcher has it directly; a hook has the
42
+ * `agent_id` and builds the same basename from it (`subagentActivityId` in
43
+ * danx-dashboard-mcp's `activity.ts`). A hook's `transcript_path` is NOT the
44
+ * sub-agent's file — on these events it is the MAIN session's transcript, and only
45
+ * `SubagentStop` also carries the sub-agent's own as `agent_transcript_path`,
46
+ * which the subcommand uses to refuse a key that disagrees. That basename is also
47
+ * the key the ledger credits the sub-agent's tokens under, which is how
48
+ * per-sub-agent tokens join to the running set without this table carrying a
49
+ * token column.
44
50
  *
45
51
  * ## What this module deliberately does NOT do
46
52
  *
@@ -49,7 +55,7 @@
49
55
  * indistinguishable from a slow one. Guessing a finish from an idle gap would
50
56
  * mark a long-thinking sub-agent as complete. So open rows are closed exactly
51
57
  * once, at the dispatch's own terminal transition (`finishOpenActivities`),
52
- * unless DX-3284's `SubagentStop` hook closes a row first.
58
+ * unless the `SubagentStop` hook (DX-3284) closes a row first.
53
59
  */
54
60
  import type { ActivityChange } from "./stats-wire.js";
55
61
  import type { EntryMeta, SourceDiscoveredInfo } from "./session-log-watcher.js";
@@ -50,6 +50,13 @@ export declare class DispatchUsage {
50
50
  */
51
51
  private startCursors;
52
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;
53
60
  /** Past-EOF resets to report once, then forget. */
54
61
  private pastEof;
55
62
  /**
@@ -134,7 +141,7 @@ export declare class DispatchUsage {
134
141
  *
135
142
  * So every source in the cursors this process STARTED from that the watcher
136
143
  * 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
144
+ * pair it resumed from — until a write that carries them has landed. That is
138
145
  * what makes "every attach persists its starting cursors" true on the FIRST
139
146
  * write rather than one stats tick (15-30 s) later: a reattach of this row
140
147
  * inside its first interval reads them back (start-order rule 1) and starts
@@ -70,6 +70,13 @@ export class DispatchUsage {
70
70
  */
71
71
  startCursors = {};
72
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;
73
80
  /** Past-EOF resets to report once, then forget. */
74
81
  pastEof = [];
75
82
  /**
@@ -93,6 +100,11 @@ export class DispatchUsage {
93
100
  * each write.
94
101
  */
95
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
+ }
96
108
  this.readCursorState = readCursorState;
97
109
  this.startCursors = startCursors;
98
110
  for (const [source, cursor] of Object.entries(startCursors)) {
@@ -173,7 +185,7 @@ export class DispatchUsage {
173
185
  *
174
186
  * So every source in the cursors this process STARTED from that the watcher
175
187
  * 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
188
+ * pair it resumed from — until a write that carries them has landed. That is
177
189
  * what makes "every attach persists its starting cursors" true on the FIRST
178
190
  * write rather than one stats tick (15-30 s) later: a reattach of this row
179
191
  * inside its first interval reads them back (start-order rule 1) and starts
@@ -289,6 +301,7 @@ export class DispatchUsage {
289
301
  return this.queue(async () => {
290
302
  if (this.refused)
291
303
  return "refused";
304
+ this.flushBegan = true;
292
305
  const changed = this.counter.takeChanged();
293
306
  const batches = chunkEntries(changed);
294
307
  const { reports, deliveredAtSend, reportedResets, carriesStartCursors } = this.buildCursorReports();
@@ -360,6 +373,9 @@ export class DispatchUsage {
360
373
  }
361
374
  if (payload.transcriptPath !== undefined)
362
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.
363
379
  this.startCursorsPersisted = true;
364
380
  }
365
381
  }
@@ -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?: Record<string, SourceCursorStart>;
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;
@@ -61,12 +61,14 @@ export type SourceCursorReports = Record<string, SourceCursorReport>;
61
61
  * DX-2954 — the running set's two kinds. `agent` is a sub-agent, produced from
62
62
  * the TRANSCRIPT: one row per sub-agent transcript file the session-log watcher
63
63
  * discovers (`packages/danx-session-telemetry/src/activity-observer.ts`; orchestrator decision, DX-2954
64
- * comment 8004). `bash` is a background script; nothing produces it yet — the
65
- * hook-side producer (`PostToolUse` on a backgrounded Bash call) is DX-3284's.
66
- * No hook event observes a background shell FINISHING (verified against the
67
- * hooks reference — see the Step 0 note on DX-2954), so an open row of either
68
- * kind is closed by the dispatch's own terminal write, never by inferring an
69
- * end from transcript lines.
64
+ * comment 8004), and — for an operator session — from the plugin's
65
+ * `SubagentStart` / `SubagentStop` hooks, which add the finish. `bash` is a
66
+ * background script, produced only by the plugin's `PostToolUse` hook on a
67
+ * backgrounded Bash call (DX-3284; the transcript cannot see it). No hook event
68
+ * observes a background shell FINISHING (verified against the hooks reference —
69
+ * see the Step 0 note on DX-2954), so an open `bash` row is closed by the
70
+ * dispatch's own terminal write, never by inferring an end from transcript
71
+ * lines.
70
72
  */
71
73
  export declare const ACTIVITY_KINDS: readonly ["agent", "bash"];
72
74
  export type ActivityKind = (typeof ACTIVITY_KINDS)[number];
@@ -76,12 +78,13 @@ export type ActivityKind = (typeof ACTIVITY_KINDS)[number];
76
78
  * fires twice, or a batch resent after a failed flush, must not duplicate it.
77
79
  *
78
80
  * `activityId` for a sub-agent is its transcript file's basename
79
- * (`agent-<hash>`) — the SAME value the ledger credits its tokens under as
81
+ * (`agent-<agent_id>`) — the SAME value the ledger credits its tokens under as
80
82
  * `source`, which is how per-sub-agent tokens join to the running set without
81
- * this table carrying a token column. Any future producer (DX-3284's hooks)
82
- * must derive it the same way — the basename of the hook's `transcript_path`,
83
- * never the hook's `agent_id` — or it upserts a duplicate row. For a
84
- * background script it is the Bash call's `tool_use_id`.
83
+ * this table carrying a token column. A hook producer builds it from the hook's
84
+ * `agent_id` as `agent-${agent_id}` (never the bare `agent_id`, and never from
85
+ * `transcript_path`, which on these events is the MAIN session's transcript) or
86
+ * it upserts a duplicate row. For a background script it is the Bash call's
87
+ * `tool_use_id`.
85
88
  */
86
89
  export interface ActivityChange {
87
90
  kind: ActivityKind;
@@ -9,12 +9,14 @@ export const STATS_BATCH_CAP = 500;
9
9
  * DX-2954 — the running set's two kinds. `agent` is a sub-agent, produced from
10
10
  * the TRANSCRIPT: one row per sub-agent transcript file the session-log watcher
11
11
  * discovers (`packages/danx-session-telemetry/src/activity-observer.ts`; orchestrator decision, DX-2954
12
- * comment 8004). `bash` is a background script; nothing produces it yet — the
13
- * hook-side producer (`PostToolUse` on a backgrounded Bash call) is DX-3284's.
14
- * No hook event observes a background shell FINISHING (verified against the
15
- * hooks reference — see the Step 0 note on DX-2954), so an open row of either
16
- * kind is closed by the dispatch's own terminal write, never by inferring an
17
- * end from transcript lines.
12
+ * comment 8004), and — for an operator session — from the plugin's
13
+ * `SubagentStart` / `SubagentStop` hooks, which add the finish. `bash` is a
14
+ * background script, produced only by the plugin's `PostToolUse` hook on a
15
+ * backgrounded Bash call (DX-3284; the transcript cannot see it). No hook event
16
+ * observes a background shell FINISHING (verified against the hooks reference —
17
+ * see the Step 0 note on DX-2954), so an open `bash` row is closed by the
18
+ * dispatch's own terminal write, never by inferring an end from transcript
19
+ * lines.
18
20
  */
19
21
  export const ACTIVITY_KINDS = ["agent", "bash"];
20
22
  export function chunkEntries(entries, cap = STATS_BATCH_CAP) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-session-telemetry",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
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",