@thehammer/danx-session-telemetry 0.1.44 → 0.1.46
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/dispatch-usage.d.ts +48 -0
- package/dist/dispatch-usage.js +75 -2
- package/dist/stats-wire.d.ts +0 -14
- package/dist/stats-wire.js +1 -9
- package/package.json +1 -1
- package/dist/.source-hash +0 -1
package/dist/dispatch-usage.d.ts
CHANGED
|
@@ -68,6 +68,13 @@ export declare class DispatchUsage {
|
|
|
68
68
|
* its own resume point instead of regressing it.
|
|
69
69
|
*/
|
|
70
70
|
private readonly ackedCursor;
|
|
71
|
+
/**
|
|
72
|
+
* DX-3972 - per source, the `acked` the server holds for this dispatch: the `acked` of the last ACKNOWLEDGED
|
|
73
|
+
* report that carried the source. It is NOT {@link DispatchUsage.ackedCursor}: that is what the NEXT report
|
|
74
|
+
* claims (one write behind `delivered`), this is what the LAST one claimed. The terminal catch-up
|
|
75
|
+
* ({@link DispatchUsage.flushFinal}) is needed exactly while this lags the watcher's `delivered`.
|
|
76
|
+
*/
|
|
77
|
+
private readonly storedAcked;
|
|
71
78
|
/**
|
|
72
79
|
* DX-2954 (AC 34324) — the cursors this process STARTED from, reported
|
|
73
80
|
* verbatim for every source the watcher has not opened yet until one write
|
|
@@ -157,6 +164,18 @@ export declare class DispatchUsage {
|
|
|
157
164
|
* excess rule) and every side-effecting subscriber skips it — so the cost is
|
|
158
165
|
* one window of re-read, never a double count or a duplicate side effect.
|
|
159
166
|
*
|
|
167
|
+
* ## The terminal write closes the lag (DX-3972)
|
|
168
|
+
*
|
|
169
|
+
* A restart is a crash, and a crash's window is the cost above. A dispatch that
|
|
170
|
+
* ENDS is not one: its watcher is drained and stopped, so every line it read has been
|
|
171
|
+
* fed to this collector and nothing will raise a reply any more. The terminal
|
|
172
|
+
* {@link DispatchUsage.flushFinal} therefore reports `acked` equal to `delivered` for
|
|
173
|
+
* every source it has opened (see {@link DispatchUsage.catchUpReports}), and a
|
|
174
|
+
* `--resume` of the session starts at the end of what this dispatch read instead of
|
|
175
|
+
* replaying the whole file. Without it a dispatch with ONE line-bearing write ended at
|
|
176
|
+
* `{acked: 0, delivered: EOF}`, and every resume of the session re-read the whole
|
|
177
|
+
* transcript and re-shipped it into its own `dispatch_logs`.
|
|
178
|
+
*
|
|
160
179
|
* ## The process-start write carries the STARTING cursors (AC 34324)
|
|
161
180
|
*
|
|
162
181
|
* `attachMonitoringStack` deliberately does NOT await `watcher.start()` —
|
|
@@ -195,6 +214,20 @@ export declare class DispatchUsage {
|
|
|
195
214
|
* is why a write is not forced merely because cursors EXIST.
|
|
196
215
|
*/
|
|
197
216
|
private cursorsMoved;
|
|
217
|
+
/**
|
|
218
|
+
* DX-3972 - the TERMINAL write's reports: the tick's reports with every source this process has read claiming its
|
|
219
|
+
* whole `delivered` as `acked`, and the claim state AS OF that offset (the tracker's current state belongs to
|
|
220
|
+
* `delivered`, so it describes the new `acked` exactly, never a later position). Sources the watcher has not
|
|
221
|
+
* opened (a start cursor reported verbatim) are untouched: this process read nothing there to claim.
|
|
222
|
+
*/
|
|
223
|
+
private catchUpReports;
|
|
224
|
+
/**
|
|
225
|
+
* DX-3972 - whether the terminal write has anything to say: the server's `acked` still lags what the catch-up now
|
|
226
|
+
* claims. `ackedCursor` (the NEXT tick's claim) cannot tell, because a dispatch whose last write reported
|
|
227
|
+
* `delivered` with an older `acked` has `ackedCursor === delivered` and still lags; {@link DispatchUsage.storedAcked}
|
|
228
|
+
* is what the server holds. A source never reported at all has no entry, so it counts as lagging.
|
|
229
|
+
*/
|
|
230
|
+
private storedAckedLags;
|
|
198
231
|
/**
|
|
199
232
|
* Feed one raw transcript line of the usage feed set (assistant, user, attachment, system), in file order.
|
|
200
233
|
* DX-3964 - the usage collector is fed from the watcher's collector-only `onUsageLine` subscription, never from
|
|
@@ -221,6 +254,21 @@ export declare class DispatchUsage {
|
|
|
221
254
|
* acknowledged ahead of the entries it covers.
|
|
222
255
|
*/
|
|
223
256
|
flush(sink: StatsSink, dispatchId: string): Promise<FlushOutcome>;
|
|
257
|
+
/**
|
|
258
|
+
* DX-3972 - the TERMINAL write: {@link DispatchUsage.flush} that also catches the stored `acked` up to
|
|
259
|
+
* `delivered` for every source (see {@link DispatchUsage.catchUpReports}). Call it once, when the dispatch ends,
|
|
260
|
+
* after the watcher is drained and stopped (that is the whole precondition: every line it read has reached this
|
|
261
|
+
* collector, so `delivered` is covered by the entries this very write carries), and BEFORE the row goes terminal
|
|
262
|
+
* (the server refuses a terminal dispatch's stats, which would drop the catch-up silently). A write that does not
|
|
263
|
+
* land promotes nothing, exactly as in a tick; a dispatch already caught up sends no write.
|
|
264
|
+
*
|
|
265
|
+
* It does NOT wait for the dispatch's log shipper: the spawn path flushes it before this call, the reattach path
|
|
266
|
+
* after (`reattach.ts` `_cleanup`), so on a reattach `acked` can claim a line whose `dispatch_logs` row is still in
|
|
267
|
+
* the shipper's buffer. The shipper is best-effort today and a resume's shipper skips replays either way, so
|
|
268
|
+
* nothing is lost that was not already; acking only after the log line is acknowledged is DX-2956's change.
|
|
269
|
+
*/
|
|
270
|
+
flushFinal(sink: StatsSink, dispatchId: string): Promise<FlushOutcome>;
|
|
271
|
+
private write;
|
|
224
272
|
/** The dispatch's totals as the server last returned them. */
|
|
225
273
|
totals(): UsageTotals;
|
|
226
274
|
/** Window tokens (input + output + cache writes) of the server totals. */
|
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-3972 - per source, the `acked` the server holds for this dispatch: the `acked` of the last ACKNOWLEDGED
|
|
68
|
+
* report that carried the source. It is NOT {@link DispatchUsage.ackedCursor}: that is what the NEXT report
|
|
69
|
+
* claims (one write behind `delivered`), this is what the LAST one claimed. The terminal catch-up
|
|
70
|
+
* ({@link DispatchUsage.flushFinal}) is needed exactly while this lags the watcher's `delivered`.
|
|
71
|
+
*/
|
|
72
|
+
storedAcked = new Map();
|
|
66
73
|
/**
|
|
67
74
|
* DX-2954 (AC 34324) — the cursors this process STARTED from, reported
|
|
68
75
|
* verbatim for every source the watcher has not opened yet until one write
|
|
@@ -186,6 +193,18 @@ export class DispatchUsage {
|
|
|
186
193
|
* excess rule) and every side-effecting subscriber skips it — so the cost is
|
|
187
194
|
* one window of re-read, never a double count or a duplicate side effect.
|
|
188
195
|
*
|
|
196
|
+
* ## The terminal write closes the lag (DX-3972)
|
|
197
|
+
*
|
|
198
|
+
* A restart is a crash, and a crash's window is the cost above. A dispatch that
|
|
199
|
+
* ENDS is not one: its watcher is drained and stopped, so every line it read has been
|
|
200
|
+
* fed to this collector and nothing will raise a reply any more. The terminal
|
|
201
|
+
* {@link DispatchUsage.flushFinal} therefore reports `acked` equal to `delivered` for
|
|
202
|
+
* every source it has opened (see {@link DispatchUsage.catchUpReports}), and a
|
|
203
|
+
* `--resume` of the session starts at the end of what this dispatch read instead of
|
|
204
|
+
* replaying the whole file. Without it a dispatch with ONE line-bearing write ended at
|
|
205
|
+
* `{acked: 0, delivered: EOF}`, and every resume of the session re-read the whole
|
|
206
|
+
* transcript and re-shipped it into its own `dispatch_logs`.
|
|
207
|
+
*
|
|
189
208
|
* ## The process-start write carries the STARTING cursors (AC 34324)
|
|
190
209
|
*
|
|
191
210
|
* `attachMonitoringStack` deliberately does NOT await `watcher.start()` —
|
|
@@ -281,6 +300,32 @@ export class DispatchUsage {
|
|
|
281
300
|
}
|
|
282
301
|
return false;
|
|
283
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* DX-3972 - the TERMINAL write's reports: the tick's reports with every source this process has read claiming its
|
|
305
|
+
* whole `delivered` as `acked`, and the claim state AS OF that offset (the tracker's current state belongs to
|
|
306
|
+
* `delivered`, so it describes the new `acked` exactly, never a later position). Sources the watcher has not
|
|
307
|
+
* opened (a start cursor reported verbatim) are untouched: this process read nothing there to claim.
|
|
308
|
+
*/
|
|
309
|
+
catchUpReports(reports, deliveredAtSend, claimsAtSend) {
|
|
310
|
+
const caughtUp = { ...reports };
|
|
311
|
+
for (const [source, delivered] of deliveredAtSend) {
|
|
312
|
+
caughtUp[source] = { ...reports[source], acked: delivered, claims: claimsAtSend.get(source) };
|
|
313
|
+
}
|
|
314
|
+
return caughtUp;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* DX-3972 - whether the terminal write has anything to say: the server's `acked` still lags what the catch-up now
|
|
318
|
+
* claims. `ackedCursor` (the NEXT tick's claim) cannot tell, because a dispatch whose last write reported
|
|
319
|
+
* `delivered` with an older `acked` has `ackedCursor === delivered` and still lags; {@link DispatchUsage.storedAcked}
|
|
320
|
+
* is what the server holds. A source never reported at all has no entry, so it counts as lagging.
|
|
321
|
+
*/
|
|
322
|
+
storedAckedLags(reports) {
|
|
323
|
+
for (const [source, report] of Object.entries(reports)) {
|
|
324
|
+
if (report.pastEof !== undefined || report.acked !== this.storedAcked.get(source))
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
284
329
|
/**
|
|
285
330
|
* Feed one raw transcript line of the usage feed set (assistant, user, attachment, system), in file order.
|
|
286
331
|
* DX-3964 - the usage collector is fed from the watcher's collector-only `onUsageLine` subscription, never from
|
|
@@ -329,13 +374,36 @@ export class DispatchUsage {
|
|
|
329
374
|
* acknowledged ahead of the entries it covers.
|
|
330
375
|
*/
|
|
331
376
|
flush(sink, dispatchId) {
|
|
377
|
+
return this.write(sink, dispatchId, false);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* DX-3972 - the TERMINAL write: {@link DispatchUsage.flush} that also catches the stored `acked` up to
|
|
381
|
+
* `delivered` for every source (see {@link DispatchUsage.catchUpReports}). Call it once, when the dispatch ends,
|
|
382
|
+
* after the watcher is drained and stopped (that is the whole precondition: every line it read has reached this
|
|
383
|
+
* collector, so `delivered` is covered by the entries this very write carries), and BEFORE the row goes terminal
|
|
384
|
+
* (the server refuses a terminal dispatch's stats, which would drop the catch-up silently). A write that does not
|
|
385
|
+
* land promotes nothing, exactly as in a tick; a dispatch already caught up sends no write.
|
|
386
|
+
*
|
|
387
|
+
* It does NOT wait for the dispatch's log shipper: the spawn path flushes it before this call, the reattach path
|
|
388
|
+
* after (`reattach.ts` `_cleanup`), so on a reattach `acked` can claim a line whose `dispatch_logs` row is still in
|
|
389
|
+
* the shipper's buffer. The shipper is best-effort today and a resume's shipper skips replays either way, so
|
|
390
|
+
* nothing is lost that was not already; acking only after the log line is acknowledged is DX-2956's change.
|
|
391
|
+
*/
|
|
392
|
+
flushFinal(sink, dispatchId) {
|
|
393
|
+
return this.write(sink, dispatchId, true);
|
|
394
|
+
}
|
|
395
|
+
write(sink, dispatchId, catchUp) {
|
|
332
396
|
return this.queue(async () => {
|
|
333
397
|
if (this.refused)
|
|
334
398
|
return "refused";
|
|
335
399
|
this.flushBegan = true;
|
|
336
400
|
const changed = this.counter.takeChanged();
|
|
337
401
|
const batches = chunkEntries(changed);
|
|
338
|
-
const
|
|
402
|
+
const built = this.buildCursorReports();
|
|
403
|
+
const { deliveredAtSend, claimsAtSend, reportedResets, carriesStartCursors } = built;
|
|
404
|
+
const reports = catchUp
|
|
405
|
+
? this.catchUpReports(built.reports, deliveredAtSend, claimsAtSend)
|
|
406
|
+
: built.reports;
|
|
339
407
|
// TAKE the queued side data, exactly as `counter.takeChanged()` does, so a
|
|
340
408
|
// hook that fires during the await lands in the NEXT write rather than
|
|
341
409
|
// being silently dropped by a post-write reset of the live array.
|
|
@@ -361,7 +429,7 @@ export class DispatchUsage {
|
|
|
361
429
|
// whose last batch failed must still deliver it on a later, otherwise idle
|
|
362
430
|
// tick.
|
|
363
431
|
const haveSideData = carriesStartCursors ||
|
|
364
|
-
this.cursorsMoved(reports) ||
|
|
432
|
+
(catchUp ? this.storedAckedLags(reports) : this.cursorsMoved(reports)) ||
|
|
365
433
|
activities.length > 0 ||
|
|
366
434
|
(this.transcriptPath !== null && !this.transcriptPathSent);
|
|
367
435
|
if (batches.length === 0 && (!this.started || haveSideData))
|
|
@@ -390,6 +458,11 @@ export class DispatchUsage {
|
|
|
390
458
|
// there. A source the server REFUSED as a regression keeps its own
|
|
391
459
|
// stored value, so we must not promote ours past it.
|
|
392
460
|
const refused = new Set(res.cursorRegressions);
|
|
461
|
+
// DX-3972 - what the server now holds as each reported source's `acked` (a refused source kept its own).
|
|
462
|
+
for (const [source, report] of Object.entries(reports)) {
|
|
463
|
+
if (!refused.has(source))
|
|
464
|
+
this.storedAcked.set(source, report.acked);
|
|
465
|
+
}
|
|
393
466
|
// A source whose file was found truncated WHILE this write was in
|
|
394
467
|
// flight must NOT have its floor promoted. `recordPastEof` dropped
|
|
395
468
|
// that floor to 0 on purpose; re-raising it here would make the next
|
package/dist/stats-wire.d.ts
CHANGED
|
@@ -135,20 +135,6 @@ export type ActivityEndStatus = (typeof ACTIVITY_END_STATUSES)[number];
|
|
|
135
135
|
export declare const CURRENT_ACTIVITY_MAX_CHARS = 120;
|
|
136
136
|
export interface StatsPayload {
|
|
137
137
|
entries: LedgerEntry[];
|
|
138
|
-
/**
|
|
139
|
-
* DX-2963 — the worker applied the end-of-file rule (its own row or its
|
|
140
|
-
* predecessor's is pre-card), so the dispatch inherits pre-card lineage.
|
|
141
|
-
*
|
|
142
|
-
* DX-2954 RETIRED this field (DX-3286 removes it): the cursor start order replaces the rule it
|
|
143
|
-
* carried. It is still ACCEPTED, and ignored, for exactly one release — a
|
|
144
|
-
* deploy restarts the dashboard before it relaunches the workers, so for a
|
|
145
|
-
* window old workers send it to the new server, and refusing it would throw
|
|
146
|
-
* away their terminal status. A payload carrying it records one visible
|
|
147
|
-
* `stats_field_ignored` event per dispatch. The release after this one drops
|
|
148
|
-
* the field, the `usage_pre_card_lineage` column and the two
|
|
149
|
-
* `handleReadDispatch` fields together.
|
|
150
|
-
*/
|
|
151
|
-
preCardLineage?: true;
|
|
152
138
|
/**
|
|
153
139
|
* DX-2954 — this collector's cursors for every source it has opened. Optional
|
|
154
140
|
* because an old worker in the deploy window sends none; absent means
|
package/dist/stats-wire.js
CHANGED
|
@@ -46,13 +46,10 @@ const USAGE_KEYS = ["input", "output", "cacheRead", "cacheWrite5m", "cacheWrite1
|
|
|
46
46
|
* consumer) fails loud instead of being silently dropped — the pre-DX-2954
|
|
47
47
|
* parser rebuilt the object field by field and swallowed every extra key.
|
|
48
48
|
*
|
|
49
|
-
* `
|
|
50
|
-
* {@link StatsPayload}); `transcriptPath` is the ONE key allowed to carry a
|
|
51
|
-
* filesystem path.
|
|
49
|
+
* `transcriptPath` is the ONE key allowed to carry a filesystem path.
|
|
52
50
|
*/
|
|
53
51
|
const STATS_PAYLOAD_KEYS = new Set([
|
|
54
52
|
"entries",
|
|
55
|
-
"preCardLineage",
|
|
56
53
|
"sourceCursors",
|
|
57
54
|
"activities",
|
|
58
55
|
"transcriptPath",
|
|
@@ -364,9 +361,6 @@ export function parseStatsPayload(body) {
|
|
|
364
361
|
if (b.entries.length > STATS_BATCH_CAP) {
|
|
365
362
|
throw new StatsPayloadError(`entries exceeds the cap of ${STATS_BATCH_CAP}`);
|
|
366
363
|
}
|
|
367
|
-
if (b.preCardLineage !== undefined && b.preCardLineage !== true) {
|
|
368
|
-
throw new StatsPayloadError("preCardLineage must be true when present");
|
|
369
|
-
}
|
|
370
364
|
if (b.transcriptPath !== undefined && (typeof b.transcriptPath !== "string" || !b.transcriptPath)) {
|
|
371
365
|
throw new StatsPayloadError("transcriptPath must be a non-empty string when present");
|
|
372
366
|
}
|
|
@@ -427,8 +421,6 @@ export function parseStatsPayload(body) {
|
|
|
427
421
|
};
|
|
428
422
|
});
|
|
429
423
|
const payload = { entries };
|
|
430
|
-
if (b.preCardLineage === true)
|
|
431
|
-
payload.preCardLineage = true;
|
|
432
424
|
if (b.sourceCursors !== undefined)
|
|
433
425
|
payload.sourceCursors = parseSourceCursors(b.sourceCursors);
|
|
434
426
|
if (b.activities !== undefined)
|
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.46",
|
|
4
4
|
"description": "Shared Claude Code transcript collector (DX-2962) — incremental JSONL tailing for the danxbot worker and the danx-dashboard-mcp MCP server's relay, so both run identical collector code.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/dist/.source-hash
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
a9b13a1f70c9a71a4aafa832d67aadc10e9e2c998beaf0230f13903d39c2d4c4
|