@thehammer/danx-session-telemetry 0.1.27 → 0.1.31
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 +4 -0
- package/dist/.source-hash +1 -1
- package/dist/activity-observer.d.ts +7 -5
- package/dist/activity-observer.js +25 -15
- package/dist/claim-tracker.d.ts +6 -2
- package/dist/claim-tracker.js +9 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/pricing.d.ts +13 -0
- package/dist/pricing.js +16 -0
- package/dist/session-log-watcher.d.ts +7 -0
- package/dist/session-log-watcher.js +9 -0
- package/dist/stats-wire.d.ts +5 -3
- package/dist/stats-wire.js +3 -2
- package/dist/subagent-live.d.ts +34 -0
- package/dist/subagent-live.js +150 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,10 @@ inside the MCP package.
|
|
|
17
17
|
shape. Canonical home now; danxbot root's `src/types.ts` re-exports both
|
|
18
18
|
names verbatim.
|
|
19
19
|
- `tool-summary.ts` — human-readable summary builders the collector uses.
|
|
20
|
+
- `subagent-live.ts` — `createSubagentLiveView` (DX-4508): per-sub-agent snapshots
|
|
21
|
+
(tokens, cost, tool calls, model, current activity, card) computed by the same
|
|
22
|
+
activity observer, ledger counter and pricing rule the dashboard's numbers come
|
|
23
|
+
from; streamed by `danx-dashboard-mcp subagents-live`.
|
|
20
24
|
- `internal-logger.ts` — this package's own tiny logging shim (see its
|
|
21
25
|
header for why it does not reuse root's `src/logger.ts`).
|
|
22
26
|
- `index.ts` — the main entry (Node-only; imports `node:fs`/`node:path`/`node:os`).
|
package/dist/.source-hash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
ade91474df465c805cff25ba35ef8265728fbf4a6649e43ea3401b5812ed943c
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* | `lastActivityAt` | the timestamp of its latest `assistant` / `user` line |
|
|
38
38
|
* | `effort` | its latest `assistant` line's top-level `effort` |
|
|
39
39
|
* | `currentActivity` | its latest `tool_use` block, through {@link describeToolCall} |
|
|
40
|
-
* | `briefIssueId` | the card id its brief (the root `user` line)
|
|
40
|
+
* | `briefIssueId` | the card its spawning `Agent` call names: the first card id in its `description` (redacted), else the first anywhere in its brief (the root `user` line) — see {@link firstIssueId} (DX-4508) |
|
|
41
41
|
* | `finishedAt`, `endStatus` | the task notification Claude Code writes into the SPAWNER's transcript |
|
|
42
42
|
*
|
|
43
43
|
* Model, tokens, cost, tool-call count and the claimed card are NOT here: the
|
|
@@ -129,9 +129,11 @@ export declare function labelOf(value: unknown): string | null;
|
|
|
129
129
|
*/
|
|
130
130
|
export declare function describeToolCall(name: string, input: Record<string, unknown>): string;
|
|
131
131
|
/**
|
|
132
|
-
* DX-
|
|
133
|
-
*
|
|
134
|
-
*
|
|
132
|
+
* DX-4508 — the first card-id-shaped token in a text (`DX-4498`, also `DX-4498:`, `(DX-4498)`), or null when it has
|
|
133
|
+
* none. A sub-agent's card is the one its spawning `Agent` call names: its `description` ("Review DX-4498 code gates")
|
|
134
|
+
* first, else its brief ("Quality gate for card DX-4421 ..."), where the first-word rule DX-4498 shipped missed most
|
|
135
|
+
* real briefs. Only the SHAPE is checked: the server keeps the id only when it is a real card of the team, so a plan ref
|
|
136
|
+
* (`PLAN-2`) or rule ref (`R-45`) never reaches a reader.
|
|
135
137
|
*/
|
|
136
|
-
export declare function
|
|
138
|
+
export declare function firstIssueId(text: string): string | null;
|
|
137
139
|
export declare function createActivityObserver(deps: ActivityObserverDeps): ActivityObserver;
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* | `lastActivityAt` | the timestamp of its latest `assistant` / `user` line |
|
|
38
38
|
* | `effort` | its latest `assistant` line's top-level `effort` |
|
|
39
39
|
* | `currentActivity` | its latest `tool_use` block, through {@link describeToolCall} |
|
|
40
|
-
* | `briefIssueId` | the card id its brief (the root `user` line)
|
|
40
|
+
* | `briefIssueId` | the card its spawning `Agent` call names: the first card id in its `description` (redacted), else the first anywhere in its brief (the root `user` line) — see {@link firstIssueId} (DX-4508) |
|
|
41
41
|
* | `finishedAt`, `endStatus` | the task notification Claude Code writes into the SPAWNER's transcript |
|
|
42
42
|
*
|
|
43
43
|
* Model, tokens, cost, tool-call count and the claimed card are NOT here: the
|
|
@@ -188,16 +188,19 @@ export function describeToolCall(name, input) {
|
|
|
188
188
|
return text.length > CURRENT_ACTIVITY_MAX_CHARS ? `${text.slice(0, CURRENT_ACTIVITY_MAX_CHARS - 3)}...` : text;
|
|
189
189
|
}
|
|
190
190
|
/**
|
|
191
|
-
* DX-
|
|
192
|
-
*
|
|
193
|
-
*
|
|
191
|
+
* DX-4508 — the first card-id-shaped token in a text (`DX-4498`, also `DX-4498:`, `(DX-4498)`), or null when it has
|
|
192
|
+
* none. A sub-agent's card is the one its spawning `Agent` call names: its `description` ("Review DX-4498 code gates")
|
|
193
|
+
* first, else its brief ("Quality gate for card DX-4421 ..."), where the first-word rule DX-4498 shipped missed most
|
|
194
|
+
* real briefs. Only the SHAPE is checked: the server keeps the id only when it is a real card of the team, so a plan ref
|
|
195
|
+
* (`PLAN-2`) or rule ref (`R-45`) never reaches a reader.
|
|
194
196
|
*/
|
|
195
|
-
export function
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
export function firstIssueId(text) {
|
|
198
|
+
for (const token of text.split(/\s+/)) {
|
|
199
|
+
const bare = token.replace(WRAPPING_PUNCTUATION, "");
|
|
200
|
+
if (bare.length <= ISSUE_ID_MAX_LENGTH && ISSUE_ID_PATTERN.test(bare))
|
|
201
|
+
return bare;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
201
204
|
}
|
|
202
205
|
/** The text of a `user` line's content: the string itself, or its first text block. */
|
|
203
206
|
function userText(raw) {
|
|
@@ -276,6 +279,10 @@ export function createActivityObserver(deps) {
|
|
|
276
279
|
if (existing.description === null && description !== null) {
|
|
277
280
|
existing.description = description;
|
|
278
281
|
filled = true;
|
|
282
|
+
// DX-4508 — the description outranks the brief: a late sidecar naming a card replaces a brief-derived id.
|
|
283
|
+
const named = firstIssueId(description);
|
|
284
|
+
if (named !== null && named !== existing.briefIssueId)
|
|
285
|
+
existing.briefIssueId = named;
|
|
279
286
|
}
|
|
280
287
|
if (existing.parentActivityId === null && info.parentSource !== null) {
|
|
281
288
|
existing.parentActivityId = info.parentSource;
|
|
@@ -285,12 +292,13 @@ export function createActivityObserver(deps) {
|
|
|
285
292
|
report(existing);
|
|
286
293
|
return;
|
|
287
294
|
}
|
|
295
|
+
// DX-4498 — the label its spawning `Agent` call gave it, from the sidecar, redacted like every label.
|
|
296
|
+
const description = labelOf(info.description);
|
|
288
297
|
const change = {
|
|
289
298
|
kind: "agent",
|
|
290
299
|
activityId: info.source,
|
|
291
300
|
agentType: info.agentType,
|
|
292
|
-
|
|
293
|
-
description: labelOf(info.description),
|
|
301
|
+
description,
|
|
294
302
|
startedAt: info.discoveredAt,
|
|
295
303
|
lastActivityAt: info.discoveredAt,
|
|
296
304
|
finishedAt: null,
|
|
@@ -298,7 +306,9 @@ export function createActivityObserver(deps) {
|
|
|
298
306
|
effort: null,
|
|
299
307
|
currentActivity: null,
|
|
300
308
|
endStatus: null,
|
|
301
|
-
|
|
309
|
+
// DX-4508 — the card the description names (the REDACTED label: the one way a label leaves a transcript); the
|
|
310
|
+
// brief fills it in only while this is still null.
|
|
311
|
+
briefIssueId: description === null ? null : firstIssueId(description),
|
|
302
312
|
};
|
|
303
313
|
const pending = pendingEnds.get(info.source);
|
|
304
314
|
if (pending !== undefined) {
|
|
@@ -355,10 +365,10 @@ export function createActivityObserver(deps) {
|
|
|
355
365
|
}
|
|
356
366
|
if (raw.type === "user") {
|
|
357
367
|
// DX-4498 — the brief is the chain's root line (`parentUuid: null`), so a collector that resumes mid-file
|
|
358
|
-
// never mistakes a later prompt for it.
|
|
368
|
+
// never mistakes a later prompt for it. DX-4508 — read only while no card is known: a description's id wins.
|
|
359
369
|
if (change.briefIssueId === null && raw.parentUuid === null) {
|
|
360
370
|
const text = userText(raw);
|
|
361
|
-
const id = text === null ? null :
|
|
371
|
+
const id = text === null ? null : firstIssueId(text);
|
|
362
372
|
if (id !== null) {
|
|
363
373
|
change.briefIssueId = id;
|
|
364
374
|
changed = true;
|
package/dist/claim-tracker.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
/** The
|
|
2
|
-
export declare const
|
|
1
|
+
/** The `danxbot_api` tool of a dispatched worker's `danx-dashboard` server. */
|
|
2
|
+
export declare const DISPATCH_API_TOOL_NAME = "mcp__danx-dashboard__danxbot_api";
|
|
3
|
+
/** The `danxbot_api` tool of the danxbot plugin's server, which an operator session's sub-agent calls. */
|
|
4
|
+
export declare const PLUGIN_API_TOOL_NAME = "mcp__plugin_danxbot_danx-dashboard__danxbot_api";
|
|
5
|
+
/** Every tool name a sub-agent reaches the dashboard API through. */
|
|
6
|
+
export declare const DASHBOARD_API_TOOL_NAMES: ReadonlySet<string>;
|
|
3
7
|
/** A card id as the issue routes name it, e.g. `DX-3962`. Also the wire's validation for every card id a claim carries. */
|
|
4
8
|
export declare const ISSUE_ID_PATTERN: RegExp;
|
|
5
9
|
/** Longest card id a claim may carry (`dispatches.issue_id` is `VARCHAR(32)`). */
|
package/dist/claim-tracker.js
CHANGED
|
@@ -46,8 +46,14 @@
|
|
|
46
46
|
* Pure bookkeeping — no `node:` imports, so it rides the browser-safe subpath too.
|
|
47
47
|
*/
|
|
48
48
|
import { MAIN_SOURCE } from "./usage-ledger.js";
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
// DX-4578: both names are live, neither a fallback. A dispatched worker's clean-room config names the server `danx-dashboard`; an operator
|
|
50
|
+
// session's sub-agent reaches it through the danxbot plugin's server (`plugin:danxbot:danx-dashboard`, tools `mcp__plugin_danxbot_danx-dashboard__*`).
|
|
51
|
+
/** The `danxbot_api` tool of a dispatched worker's `danx-dashboard` server. */
|
|
52
|
+
export const DISPATCH_API_TOOL_NAME = "mcp__danx-dashboard__danxbot_api";
|
|
53
|
+
/** The `danxbot_api` tool of the danxbot plugin's server, which an operator session's sub-agent calls. */
|
|
54
|
+
export const PLUGIN_API_TOOL_NAME = "mcp__plugin_danxbot_danx-dashboard__danxbot_api";
|
|
55
|
+
/** Every tool name a sub-agent reaches the dashboard API through. */
|
|
56
|
+
export const DASHBOARD_API_TOOL_NAMES = new Set([DISPATCH_API_TOOL_NAME, PLUGIN_API_TOOL_NAME]);
|
|
51
57
|
/** A card id as the issue routes name it, e.g. `DX-3962`. Also the wire's validation for every card id a claim carries. */
|
|
52
58
|
export const ISSUE_ID_PATTERN = /^[A-Za-z][A-Za-z0-9]*-[0-9]+$/;
|
|
53
59
|
/** Longest card id a claim may carry (`dispatches.issue_id` is `VARCHAR(32)`). */
|
|
@@ -102,7 +108,7 @@ function resultSucceeded(block) {
|
|
|
102
108
|
}
|
|
103
109
|
/** The transition a `tool_use` block asks for, or `null` when it is not a claim or release of a card. */
|
|
104
110
|
function transitionOf(block) {
|
|
105
|
-
if (block.type !== "tool_use" || block.name !==
|
|
111
|
+
if (block.type !== "tool_use" || typeof block.name !== "string" || !DASHBOARD_API_TOOL_NAMES.has(block.name))
|
|
106
112
|
return null;
|
|
107
113
|
const input = block.input;
|
|
108
114
|
if (input === null || typeof input !== "object")
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -37,4 +37,6 @@ export * from "./stats-wire.js";
|
|
|
37
37
|
export * from "./claim-tracker.js";
|
|
38
38
|
export * from "./dispatch-usage.js";
|
|
39
39
|
export * from "./activity-observer.js";
|
|
40
|
+
// DX-4508 — the local live sub-agent view `danx-dashboard-mcp subagents-live` streams to the plugin pane.
|
|
41
|
+
export * from "./subagent-live.js";
|
|
40
42
|
export * from "./write-queue.js";
|
package/dist/pricing.d.ts
CHANGED
|
@@ -102,3 +102,16 @@ export type PriceResult = {
|
|
|
102
102
|
export declare function priceUsage(model: string, speed: string, tokens: PricedTokens): PriceResult;
|
|
103
103
|
/** True when `model` has a standard-speed price (used by tests + guards). */
|
|
104
104
|
export declare function isPricedModel(model: string): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* DX-4508 — one ledger credit's cost, exactly as the server's usage-ledger writer stores it (moved here from
|
|
107
|
+
* `src/dashboard/usage-ledger.ts` so the live sub-agent view, `subagent-live.ts`, prices a reply with the SAME rule
|
|
108
|
+
* instead of a copy): a `<synthetic>` reply costs nothing and is never unpriced; a reply with no model is unpriced only
|
|
109
|
+
* when it carries tokens; any other model goes through {@link priceUsage}, an unpriced one costing 0 and naming itself.
|
|
110
|
+
*/
|
|
111
|
+
export declare function priceLedgerCredit(model: string | null, speed: string, tokens: PricedTokens): {
|
|
112
|
+
costUsd: number;
|
|
113
|
+
unpriced: {
|
|
114
|
+
model: string;
|
|
115
|
+
speed: string;
|
|
116
|
+
} | null;
|
|
117
|
+
};
|
package/dist/pricing.js
CHANGED
|
@@ -170,3 +170,19 @@ export function isPricedModel(model) {
|
|
|
170
170
|
// `Object.hasOwn` can't be fooled by it. See `priceUsage`'s own comment.
|
|
171
171
|
return Object.hasOwn(PRICING, model);
|
|
172
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* DX-4508 — one ledger credit's cost, exactly as the server's usage-ledger writer stores it (moved here from
|
|
175
|
+
* `src/dashboard/usage-ledger.ts` so the live sub-agent view, `subagent-live.ts`, prices a reply with the SAME rule
|
|
176
|
+
* instead of a copy): a `<synthetic>` reply costs nothing and is never unpriced; a reply with no model is unpriced only
|
|
177
|
+
* when it carries tokens; any other model goes through {@link priceUsage}, an unpriced one costing 0 and naming itself.
|
|
178
|
+
*/
|
|
179
|
+
export function priceLedgerCredit(model, speed, tokens) {
|
|
180
|
+
if (model === SYNTHETIC_MODEL)
|
|
181
|
+
return { costUsd: 0, unpriced: null };
|
|
182
|
+
if (model === null) {
|
|
183
|
+
const zero = tokens.input === 0 && tokens.output === 0 && tokens.cacheRead === 0 && tokens.cacheWrite5m === 0 && tokens.cacheWrite1h === 0;
|
|
184
|
+
return zero ? { costUsd: 0, unpriced: null } : { costUsd: 0, unpriced: { model: "(none)", speed } };
|
|
185
|
+
}
|
|
186
|
+
const r = priceUsage(model, speed, tokens);
|
|
187
|
+
return r.kind === "priced" ? { costUsd: r.costUsd, unpriced: null } : { costUsd: 0, unpriced: { model: r.model, speed: r.speed } };
|
|
188
|
+
}
|
|
@@ -127,6 +127,12 @@ export interface SessionLogWatcherOptions {
|
|
|
127
127
|
* within it.
|
|
128
128
|
*/
|
|
129
129
|
onSourceDiscovered?: (info: SourceDiscoveredInfo) => void;
|
|
130
|
+
/**
|
|
131
|
+
* DX-4508 — one poll (the main transcript, sub-agent discovery, then every sub-agent file) has finished handing its
|
|
132
|
+
* lines to subscribers. The live sub-agent view (`danx-dashboard-mcp subagents-live`) prints one line per poll from
|
|
133
|
+
* it, so a reader sees every change one poll made together. A throw is contained like every consumer's.
|
|
134
|
+
*/
|
|
135
|
+
onPollComplete?: () => void;
|
|
130
136
|
}
|
|
131
137
|
/** DX-2954 — one source's stored cursor pair, as a collector starts from it. */
|
|
132
138
|
export interface SourceCursorStart {
|
|
@@ -344,6 +350,7 @@ export declare class SessionLogWatcher {
|
|
|
344
350
|
private readonly startExistingFilesAtEof;
|
|
345
351
|
private readonly onCursorPastEof;
|
|
346
352
|
private readonly onSourceDiscovered;
|
|
353
|
+
private readonly onPollComplete;
|
|
347
354
|
private subagentDiscoveryDone;
|
|
348
355
|
private running;
|
|
349
356
|
private polling;
|
|
@@ -495,6 +495,7 @@ export class SessionLogWatcher {
|
|
|
495
495
|
startExistingFilesAtEof;
|
|
496
496
|
onCursorPastEof;
|
|
497
497
|
onSourceDiscovered;
|
|
498
|
+
onPollComplete;
|
|
498
499
|
subagentDiscoveryDone = false;
|
|
499
500
|
running = false;
|
|
500
501
|
polling = false;
|
|
@@ -514,6 +515,7 @@ export class SessionLogWatcher {
|
|
|
514
515
|
this.startExistingFilesAtEof = options.startExistingFilesAtEof ?? false;
|
|
515
516
|
this.onCursorPastEof = options.onCursorPastEof;
|
|
516
517
|
this.onSourceDiscovered = options.onSourceDiscovered;
|
|
518
|
+
this.onPollComplete = options.onPollComplete;
|
|
517
519
|
}
|
|
518
520
|
/**
|
|
519
521
|
* DX-2954 — resolve one file's starting offset and replay boundary, per the
|
|
@@ -795,6 +797,13 @@ export class SessionLogWatcher {
|
|
|
795
797
|
finally {
|
|
796
798
|
this.polling = false;
|
|
797
799
|
}
|
|
800
|
+
// DX-4508 — only a poll that ran to its end: a stop mid-poll returns above and reports nothing.
|
|
801
|
+
try {
|
|
802
|
+
this.onPollComplete?.();
|
|
803
|
+
}
|
|
804
|
+
catch (err) {
|
|
805
|
+
log.error("onPollComplete consumer error:", err);
|
|
806
|
+
}
|
|
798
807
|
}
|
|
799
808
|
/** Read any new bytes from `state.filePath` and emit resulting entries. */
|
|
800
809
|
async pollFile(state) {
|
package/dist/stats-wire.d.ts
CHANGED
|
@@ -118,9 +118,11 @@ export interface ActivityChange {
|
|
|
118
118
|
currentActivity: string | null;
|
|
119
119
|
/** DX-4498 — how it ended, when an explicit end event named it; null while running and for an end that named none. */
|
|
120
120
|
endStatus: ActivityEndStatus | null;
|
|
121
|
-
/**
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
/**
|
|
122
|
+
* DX-4508 — the card its spawning `Agent` call names: the first card id in its (redacted) `description`, else the
|
|
123
|
+
* first anywhere in its brief; null when neither names one. The name stays `briefIssueId` (and the column
|
|
124
|
+
* `brief_issue_id`): renaming a required wire field would make every bridge already published fail the strict parser.
|
|
125
|
+
*/
|
|
124
126
|
briefIssueId: string | null;
|
|
125
127
|
}
|
|
126
128
|
/**
|
package/dist/stats-wire.js
CHANGED
|
@@ -122,8 +122,9 @@ export function collapseActivityChanges(changes) {
|
|
|
122
122
|
// DX-4498 — COALESCE(incoming, stored): the latest explicit end status wins (a notification read after the session's
|
|
123
123
|
// own close names how it really ended).
|
|
124
124
|
endStatus: c.endStatus ?? prior.endStatus,
|
|
125
|
-
// DX-
|
|
126
|
-
|
|
125
|
+
// DX-4508 — COALESCE(incoming, stored): the producer reports its best id and may improve it (a late sidecar's
|
|
126
|
+
// description outranks the brief); a null never erases one.
|
|
127
|
+
briefIssueId: c.briefIssueId ?? prior.briefIssueId,
|
|
127
128
|
});
|
|
128
129
|
}
|
|
129
130
|
return [...byKey.values()];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { EntryMeta, SourceDiscoveredInfo } from "./session-log-watcher.js";
|
|
2
|
+
import type { ActivityEndStatus } from "./stats-wire.js";
|
|
3
|
+
/** One sub-agent as the live view reports it. Every time is epoch ms. The plugin pane renders exactly this shape. */
|
|
4
|
+
export interface SubagentLiveSnapshot {
|
|
5
|
+
/** `agent-<agentId>`: its transcript basename, the ledger source key and the dashboard row's activity id. */
|
|
6
|
+
id: string;
|
|
7
|
+
/** The sub-agent that spawned it, or null for one the session itself spawned. */
|
|
8
|
+
parentId: string | null;
|
|
9
|
+
description: string | null;
|
|
10
|
+
agentType: string | null;
|
|
11
|
+
/** Its newest reply's model, never `<synthetic>`; null before its first reply. */
|
|
12
|
+
model: string | null;
|
|
13
|
+
effort: string | null;
|
|
14
|
+
startedAt: number;
|
|
15
|
+
lastActivityAt: number;
|
|
16
|
+
finishedAt: number | null;
|
|
17
|
+
endStatus: ActivityEndStatus | null;
|
|
18
|
+
/** input + output + cacheRead + cacheWrite5m + cacheWrite1h, summed over its replies. */
|
|
19
|
+
tokensTotal: number;
|
|
20
|
+
costUsd: number;
|
|
21
|
+
toolCallCount: number;
|
|
22
|
+
currentActivity: string | null;
|
|
23
|
+
/** The card its spawning `Agent` call names (description first, then brief); shape-checked only. */
|
|
24
|
+
cardId: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface SubagentLiveView {
|
|
27
|
+
/** Wire to the watcher's `onSourceDiscovered`. */
|
|
28
|
+
onSourceDiscovered: (info: SourceDiscoveredInfo) => void;
|
|
29
|
+
/** Wire to the watcher's `onUsageLine` (the raw line feed, main transcript included: it carries task notifications). */
|
|
30
|
+
onLine: (raw: Record<string, unknown>, meta: EntryMeta) => void;
|
|
31
|
+
/** The snapshots that changed since the last call (each differs from the one last returned for it); clears the set. */
|
|
32
|
+
takeChanged: () => SubagentLiveSnapshot[];
|
|
33
|
+
}
|
|
34
|
+
export declare function createSubagentLiveView(): SubagentLiveView;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-4508 — a LOCAL live view of a session's sub-agents, for the plugin's plan pane.
|
|
3
|
+
*
|
|
4
|
+
* The pane shows one card per sub-agent: what it is, what it is doing, and what it has spent. Its dashboard path
|
|
5
|
+
* (`GET /api/plan-sessions/:id/subagents`) lags about 80s behind the transcript (the bridge's 5s poll, its 15s
|
|
6
|
+
* heartbeat flush, the pane's 60s read), so the same facts are also computed here, on the operator's own machine, from
|
|
7
|
+
* the transcript files themselves, and streamed by `danx-dashboard-mcp subagents-live` within a poll of being written.
|
|
8
|
+
*
|
|
9
|
+
* ## One place computes each number
|
|
10
|
+
*
|
|
11
|
+
* Nothing here counts or prices on its own. It consumes the SAME two watcher feeds the bridge wires
|
|
12
|
+
* (`onSourceDiscovered`, `onUsageLine`) into the SAME producers:
|
|
13
|
+
*
|
|
14
|
+
* | Snapshot field | Computed by | As the dashboard read derives it from |
|
|
15
|
+
* |---|---|---|
|
|
16
|
+
* | `description`, `agentType`, `parentId`, `effort`, `currentActivity`, `finishedAt`, `endStatus`, `cardId` | {@link createActivityObserver} (`cardId` is its `briefIssueId`) | the `dispatch_activities` row the same observer feeds |
|
|
17
|
+
* | `tokensTotal` | {@link UsageLedgerCounter} (per-reply per-field maximum) via {@link ledgerLineFromRaw} | `usage_by_source[<id>]`, all five fields summed |
|
|
18
|
+
* | `costUsd` | {@link priceLedgerCredit} over each reply | `usage_by_source[<id>].costUsd` (the ledger writer prices each credit with it) |
|
|
19
|
+
* | `toolCallCount` | the counter's distinct `tool_use` ids per reply | `SUM(cardinality(tool_use_ids))` |
|
|
20
|
+
* | `model` | the counter's newest reply whose model is not {@link SYNTHETIC_MODEL} | the newest non-synthetic `dispatch_message_usage.model` |
|
|
21
|
+
*
|
|
22
|
+
* Cost is priced per reply over the reply's whole usage, where the server prices each CREDIT (the excess a resend
|
|
23
|
+
* adds); pricing is linear, so the two agree whenever a reply keeps one model and speed, which Claude Code always writes.
|
|
24
|
+
*
|
|
25
|
+
* ## Where the live view differs from the observer's own stamps
|
|
26
|
+
*
|
|
27
|
+
* `startedAt` and `lastActivityAt` come from the sub-agent's own transcript lines (its first and newest `assistant` /
|
|
28
|
+
* `user` line). The observer stamps a start at the moment its watcher DISCOVERED the file, which is right for the bridge
|
|
29
|
+
* (its first discovery is the real start, and the server keeps the earliest), but this view reads every file from byte
|
|
30
|
+
* 0 each time it starts, so a discovery stamp would show every finished sub-agent as having started, and been active,
|
|
31
|
+
* just now. The discovery stamp is used only for a sub-agent that has written no line yet.
|
|
32
|
+
*
|
|
33
|
+
* ## Memory
|
|
34
|
+
*
|
|
35
|
+
* Per sub-agent: the observer's one small state record, and the counter's one small entry per reply. No transcript
|
|
36
|
+
* text is held; the watcher reads each file's new bytes and drops them once parsed.
|
|
37
|
+
*/
|
|
38
|
+
import { createActivityObserver } from "./activity-observer.js";
|
|
39
|
+
import { SYNTHETIC_MODEL, priceLedgerCredit } from "./pricing.js";
|
|
40
|
+
import { MAIN_SOURCE, UsageLedgerCounter, ledgerLineFromRaw, totalTokensOfUsage } from "./usage-ledger.js";
|
|
41
|
+
/** The line's own timestamp in epoch ms, or null when it carries none that parses. */
|
|
42
|
+
function lineTime(raw) {
|
|
43
|
+
if (typeof raw.timestamp !== "string")
|
|
44
|
+
return null;
|
|
45
|
+
const ms = Date.parse(raw.timestamp);
|
|
46
|
+
return Number.isNaN(ms) ? null : ms;
|
|
47
|
+
}
|
|
48
|
+
export function createSubagentLiveView() {
|
|
49
|
+
const sources = new Map();
|
|
50
|
+
const dirty = new Set();
|
|
51
|
+
const observer = createActivityObserver({
|
|
52
|
+
// The observer reports a source's FULL current state on every change.
|
|
53
|
+
recordActivities: (changes) => {
|
|
54
|
+
for (const change of changes) {
|
|
55
|
+
const state = sources.get(change.activityId);
|
|
56
|
+
if (state === undefined) {
|
|
57
|
+
sources.set(change.activityId, {
|
|
58
|
+
activity: change,
|
|
59
|
+
counter: new UsageLedgerCounter(),
|
|
60
|
+
firstLineAt: null,
|
|
61
|
+
newestLineAt: null,
|
|
62
|
+
lastSent: null,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
state.activity = change;
|
|
67
|
+
}
|
|
68
|
+
dirty.add(change.activityId);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
const snapshot = (state) => {
|
|
73
|
+
let costUsd = 0;
|
|
74
|
+
let toolCallCount = 0;
|
|
75
|
+
let model = null;
|
|
76
|
+
let modelAt = -Infinity;
|
|
77
|
+
for (const entry of state.counter.all()) {
|
|
78
|
+
costUsd += priceLedgerCredit(entry.model, entry.speed, entry.usage).costUsd;
|
|
79
|
+
toolCallCount += entry.toolUseIds.length;
|
|
80
|
+
if (entry.model !== null && entry.model !== SYNTHETIC_MODEL && entry.repliedAt >= modelAt) {
|
|
81
|
+
model = entry.model;
|
|
82
|
+
modelAt = entry.repliedAt;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const a = state.activity;
|
|
86
|
+
const startedAt = state.firstLineAt ?? a.startedAt;
|
|
87
|
+
return {
|
|
88
|
+
id: a.activityId,
|
|
89
|
+
parentId: a.parentActivityId,
|
|
90
|
+
description: a.description,
|
|
91
|
+
agentType: a.agentType,
|
|
92
|
+
model,
|
|
93
|
+
effort: a.effort,
|
|
94
|
+
startedAt,
|
|
95
|
+
lastActivityAt: state.newestLineAt ?? startedAt,
|
|
96
|
+
finishedAt: a.finishedAt,
|
|
97
|
+
endStatus: a.endStatus,
|
|
98
|
+
tokensTotal: totalTokensOfUsage(state.counter.totals()),
|
|
99
|
+
costUsd,
|
|
100
|
+
toolCallCount,
|
|
101
|
+
currentActivity: a.currentActivity,
|
|
102
|
+
cardId: a.briefIssueId,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
onSourceDiscovered: observer.onSourceDiscovered,
|
|
107
|
+
onLine(raw, meta) {
|
|
108
|
+
// The observer first: it reads the main transcript's task notifications, and a line the ledger refuses (no
|
|
109
|
+
// timestamp) must not withhold them.
|
|
110
|
+
observer.onLine(raw, meta);
|
|
111
|
+
if (meta.source === MAIN_SOURCE)
|
|
112
|
+
return;
|
|
113
|
+
const state = sources.get(meta.source);
|
|
114
|
+
if (state === undefined)
|
|
115
|
+
return;
|
|
116
|
+
if (raw.type === "assistant" || raw.type === "user") {
|
|
117
|
+
const at = lineTime(raw);
|
|
118
|
+
if (at !== null) {
|
|
119
|
+
if (state.firstLineAt === null || at < state.firstLineAt) {
|
|
120
|
+
state.firstLineAt = at;
|
|
121
|
+
dirty.add(meta.source);
|
|
122
|
+
}
|
|
123
|
+
if (state.newestLineAt === null || at > state.newestLineAt) {
|
|
124
|
+
state.newestLineAt = at;
|
|
125
|
+
dirty.add(meta.source);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const line = ledgerLineFromRaw(raw, meta.source, meta.byteOffset);
|
|
130
|
+
if (line !== null && state.counter.observeLine(line))
|
|
131
|
+
dirty.add(meta.source);
|
|
132
|
+
},
|
|
133
|
+
takeChanged() {
|
|
134
|
+
const out = [];
|
|
135
|
+
for (const source of dirty) {
|
|
136
|
+
const state = sources.get(source);
|
|
137
|
+
if (state === undefined)
|
|
138
|
+
continue;
|
|
139
|
+
const snap = snapshot(state);
|
|
140
|
+
const json = JSON.stringify(snap);
|
|
141
|
+
if (json === state.lastSent)
|
|
142
|
+
continue;
|
|
143
|
+
state.lastSent = json;
|
|
144
|
+
out.push(snap);
|
|
145
|
+
}
|
|
146
|
+
dirty.clear();
|
|
147
|
+
return out;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
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.31",
|
|
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",
|