sidekick-shared 0.27.1 → 0.27.3
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/CHANGELOG.md +46 -0
- package/README.md +35 -8
- package/dist/aggregation/EventAggregator.d.ts +12 -2
- package/dist/aggregation/EventAggregator.js +49 -19
- package/dist/browser.d.ts +2 -1
- package/dist/browser.js +9 -2
- package/dist/extractors/tokenUsage.js +1 -0
- package/dist/formatters/sessionDump.d.ts +10 -2
- package/dist/formatters/sessionDump.js +45 -23
- package/dist/index.d.ts +8 -4
- package/dist/index.js +31 -10
- package/dist/node.d.ts +3 -1
- package/dist/node.js +8 -2
- package/dist/parsers/codexParser.d.ts +22 -2
- package/dist/parsers/codexParser.js +64 -5
- package/dist/parsers/subagentScanner.d.ts +9 -0
- package/dist/parsers/subagentScanner.js +45 -12
- package/dist/parsers/subagentStatsCache.d.ts +17 -0
- package/dist/parsers/subagentStatsCache.js +82 -0
- package/dist/parsers/subagentTraceParser.js +19 -10
- package/dist/promptHistory.d.ts +82 -10
- package/dist/promptHistory.js +654 -183
- package/dist/providers/claudeCode.js +8 -41
- package/dist/providers/codex.js +28 -14
- package/dist/providers/codexDatabase.d.ts +16 -1
- package/dist/providers/codexDatabase.js +49 -1
- package/dist/providers/openCode.js +30 -4
- package/dist/report/htmlReportGenerator.js +33 -11
- package/dist/report/sessionReportInputs.d.ts +3 -1
- package/dist/report/sessionReportInputs.js +2 -0
- package/dist/report/types.d.ts +5 -0
- package/dist/schemas/sessionEvent.d.ts +30 -6
- package/dist/schemas/sessionEvent.js +7 -1
- package/dist/schemas/stateFile.d.ts +2 -0
- package/dist/schemas/stateFile.js +1 -0
- package/dist/sessionTokenTotals.d.ts +46 -0
- package/dist/sessionTokenTotals.js +138 -0
- package/dist/stateFile.d.ts +7 -0
- package/dist/tokenSummary.d.ts +27 -1
- package/dist/tokenSummary.js +50 -3
- package/dist/types/codex.d.ts +4 -0
- package/dist/types/sessionEvent.d.ts +22 -0
- package/dist/usage/claudeUsageDedupe.d.ts +53 -0
- package/dist/usage/claudeUsageDedupe.js +195 -0
- package/dist/usage/subagentUsage.d.ts +13 -0
- package/dist/usage/subagentUsage.js +21 -0
- package/dist/usage/usageEvents.d.ts +11 -2
- package/dist/usage/usageEvents.js +79 -8
- package/dist/watchers/eventBridge.js +5 -0
- package/dist/watchers/jsonlWatcher.d.ts +5 -0
- package/dist/watchers/jsonlWatcher.js +40 -5
- package/dist/watchers/types.d.ts +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Memoizes per-transcript subagent stats by file size and mtime.
|
|
4
|
+
*
|
|
5
|
+
* Dashboards rescan a session's subagents on a timer, and one parent can own
|
|
6
|
+
* a hundred subagent transcripts of several MB each. An unchanged transcript
|
|
7
|
+
* now costs one `stat` instead of a full parse. Callers treat the returned
|
|
8
|
+
* stats as read-only.
|
|
9
|
+
*
|
|
10
|
+
* Node-only: stats files.
|
|
11
|
+
*
|
|
12
|
+
* @module parsers/subagentStatsCache
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.cachedSubagentStats = cachedSubagentStats;
|
|
49
|
+
exports.clearSubagentStatsCache = clearSubagentStatsCache;
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const MAX_ENTRIES = 1024;
|
|
52
|
+
const cache = new Map();
|
|
53
|
+
/** Stats for one subagent transcript, parsed again only when the file changed. */
|
|
54
|
+
function cachedSubagentStats(filePath, compute) {
|
|
55
|
+
let stat;
|
|
56
|
+
try {
|
|
57
|
+
stat = fs.statSync(filePath);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
cache.delete(filePath);
|
|
61
|
+
return compute();
|
|
62
|
+
}
|
|
63
|
+
const hit = cache.get(filePath);
|
|
64
|
+
if (hit && hit.size === stat.size && hit.mtimeMs === stat.mtimeMs) {
|
|
65
|
+
cache.delete(filePath);
|
|
66
|
+
cache.set(filePath, hit);
|
|
67
|
+
return hit.stats;
|
|
68
|
+
}
|
|
69
|
+
const stats = compute();
|
|
70
|
+
cache.delete(filePath);
|
|
71
|
+
cache.set(filePath, { size: stat.size, mtimeMs: stat.mtimeMs, stats });
|
|
72
|
+
if (cache.size > MAX_ENTRIES) {
|
|
73
|
+
const oldest = cache.keys().next().value;
|
|
74
|
+
if (oldest !== undefined)
|
|
75
|
+
cache.delete(oldest);
|
|
76
|
+
}
|
|
77
|
+
return stats;
|
|
78
|
+
}
|
|
79
|
+
/** @internal Test hook. */
|
|
80
|
+
function clearSubagentStatsCache() {
|
|
81
|
+
cache.clear();
|
|
82
|
+
}
|
|
@@ -52,6 +52,9 @@ const fs = __importStar(require("fs"));
|
|
|
52
52
|
const path = __importStar(require("path"));
|
|
53
53
|
const toolSummary_1 = require("../formatters/toolSummary");
|
|
54
54
|
const noiseClassifier_1 = require("../formatters/noiseClassifier");
|
|
55
|
+
const claudeUsageDedupe_1 = require("../usage/claudeUsageDedupe");
|
|
56
|
+
const subagentUsage_1 = require("../usage/subagentUsage");
|
|
57
|
+
const subagentScanner_1 = require("./subagentScanner");
|
|
55
58
|
// ── Constants ──
|
|
56
59
|
const AGENT_FILE_PATTERN = /^agent-(.+)\.jsonl$/;
|
|
57
60
|
// ── Public API ──
|
|
@@ -101,8 +104,8 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
101
104
|
const toolCalls = [];
|
|
102
105
|
let agentType;
|
|
103
106
|
let description;
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
const tokens = { agentId, toolCalls: [], inputTokens: 0, outputTokens: 0 };
|
|
108
|
+
const usageDeduper = new claudeUsageDedupe_1.ClaudeUsageDeduper();
|
|
106
109
|
let startTime;
|
|
107
110
|
let endTime;
|
|
108
111
|
for (const line of lines) {
|
|
@@ -153,11 +156,10 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
153
156
|
isHardNoise: hardNoise,
|
|
154
157
|
});
|
|
155
158
|
// Extract token usage
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
159
|
+
// Split lines of one response repeat its usage; count each response once.
|
|
160
|
+
const usage = (0, claudeUsageDedupe_1.dedupedRawClaudeUsage)(raw, usageDeduper);
|
|
161
|
+
if (usage)
|
|
162
|
+
(0, subagentUsage_1.addUsageToSubagent)(tokens, usage);
|
|
161
163
|
// Extract tool calls
|
|
162
164
|
if (raw.type === 'assistant' && Array.isArray(raw.message?.content)) {
|
|
163
165
|
for (const block of raw.message.content) {
|
|
@@ -194,7 +196,10 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
194
196
|
// Skip malformed lines
|
|
195
197
|
}
|
|
196
198
|
}
|
|
197
|
-
|
|
199
|
+
const meta = (0, subagentScanner_1.readAgentMeta)(filePath);
|
|
200
|
+
agentType = meta.agentType ?? agentType;
|
|
201
|
+
description = meta.description ?? description;
|
|
202
|
+
if (events.length === 0 && !agentType && !description && !tokens.totalTokens) {
|
|
198
203
|
return null;
|
|
199
204
|
}
|
|
200
205
|
const durationMs = startTime && endTime ? endTime.getTime() - startTime.getTime() : undefined;
|
|
@@ -209,8 +214,12 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
209
214
|
agentType,
|
|
210
215
|
description,
|
|
211
216
|
toolCalls,
|
|
212
|
-
inputTokens,
|
|
213
|
-
outputTokens,
|
|
217
|
+
inputTokens: tokens.inputTokens,
|
|
218
|
+
outputTokens: tokens.outputTokens,
|
|
219
|
+
cacheReadTokens: tokens.cacheReadTokens ?? 0,
|
|
220
|
+
cacheWriteTokens: tokens.cacheWriteTokens ?? 0,
|
|
221
|
+
reasoningTokens: tokens.reasoningTokens ?? 0,
|
|
222
|
+
totalTokens: tokens.totalTokens ?? 0,
|
|
214
223
|
startTime,
|
|
215
224
|
endTime,
|
|
216
225
|
durationMs,
|
package/dist/promptHistory.d.ts
CHANGED
|
@@ -8,24 +8,37 @@
|
|
|
8
8
|
* or somewhere inside one. Every bound stops cleanly and is reported in
|
|
9
9
|
* `boundsHit`; nothing is silently truncated and no bound throws.
|
|
10
10
|
*
|
|
11
|
+
* Files are streamed in fixed-size chunks and every pass stops at a complete
|
|
12
|
+
* line, so a large or busy log is read across several calls: the cursor holds
|
|
13
|
+
* where each file stopped and the next call continues from there. A single
|
|
14
|
+
* record over `maxRecordBytes` is skipped without being buffered and reported
|
|
15
|
+
* in `exclusions`, the only way a record is permanently left out.
|
|
16
|
+
*
|
|
11
17
|
* A prompt can be written before its answer. The cursor then keeps the byte
|
|
12
|
-
* offset of that prompt's line
|
|
13
|
-
*
|
|
18
|
+
* offset of that prompt's line and its answer-tracking state (never text), and
|
|
19
|
+
* the prompt is returned again under the same identity once its first
|
|
14
20
|
* answering call brings model or usage (`metadataStatus` says how settled).
|
|
15
21
|
*/
|
|
16
22
|
import { type HumanPromptProvider } from './humanPrompt';
|
|
17
23
|
export type PromptHistoryProvider = HumanPromptProvider;
|
|
18
|
-
export type PromptHistoryBound = 'maxSessions' | 'maxFileBytes' | 'maxTotalBytes' | 'deadline' | 'aborted';
|
|
24
|
+
export type PromptHistoryBound = 'maxSessions' | 'maxFileBytes' | 'maxTotalBytes' | 'maxEntries' | 'deadline' | 'aborted';
|
|
19
25
|
export interface PromptHistoryBounds {
|
|
20
26
|
/** Session files read in one call (unchanged files do not count). Default 1000. */
|
|
21
27
|
maxSessions?: number;
|
|
22
28
|
/**
|
|
23
|
-
* Bytes
|
|
24
|
-
*
|
|
29
|
+
* Bytes read from one file in one call. Default 16 MiB. A file with more to
|
|
30
|
+
* read stops at a line boundary and continues on the next call.
|
|
25
31
|
*/
|
|
26
32
|
maxFileBytes?: number;
|
|
27
33
|
/** Bytes read across all files in this call. Default 256 MiB. */
|
|
28
34
|
maxTotalBytes?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Largest single log record that is parsed. Default 16 MiB. A longer record
|
|
37
|
+
* is skipped without being buffered and reported in `exclusions`.
|
|
38
|
+
*/
|
|
39
|
+
maxRecordBytes?: number;
|
|
40
|
+
/** Entries returned by one call. Default 10 000. */
|
|
41
|
+
maxEntries?: number;
|
|
29
42
|
/** Wall-clock budget for the call. Default 30 000 ms. */
|
|
30
43
|
deadlineMs?: number;
|
|
31
44
|
}
|
|
@@ -39,18 +52,36 @@ export interface PromptHistoryResumeState {
|
|
|
39
52
|
/** `session_meta` has been seen. */
|
|
40
53
|
meta?: boolean;
|
|
41
54
|
}
|
|
55
|
+
/** Answer metadata gathered so far for an open prompt; never text. */
|
|
56
|
+
export interface PromptHistoryAnswerState {
|
|
57
|
+
status: 'pending' | 'provisional';
|
|
58
|
+
/** Claude message id of the first answering call, once one has been seen. */
|
|
59
|
+
id?: string;
|
|
60
|
+
model?: string;
|
|
61
|
+
usage?: PromptHistoryUsage;
|
|
62
|
+
}
|
|
42
63
|
/**
|
|
43
64
|
* The last emitted prompt of a file whose answering call is not final yet.
|
|
44
|
-
* Holds a
|
|
65
|
+
* Holds a position and answer metadata, never prompt or answer text.
|
|
45
66
|
*/
|
|
46
67
|
export interface PromptHistoryPendingPrompt {
|
|
47
68
|
ordinal: number;
|
|
48
|
-
/** Byte offset where the prompt's log line starts
|
|
69
|
+
/** Byte offset where the prompt's log line starts. */
|
|
49
70
|
offset: number;
|
|
71
|
+
/** Byte length of the prompt's log line, without its newline. */
|
|
72
|
+
lineBytes?: number;
|
|
73
|
+
/** Timestamp of the prompt line, used to verify it when it is read again. */
|
|
74
|
+
timestamp?: string;
|
|
50
75
|
/** Digest of the model, usage, and status last returned for this prompt. */
|
|
51
76
|
emitted: string;
|
|
52
77
|
/** Codex parse state as it was just before the prompt line. */
|
|
53
78
|
state?: PromptHistoryResumeState;
|
|
79
|
+
/**
|
|
80
|
+
* Answer tracking to continue from the session offset. Absent only on a
|
|
81
|
+
* session carried unchanged from a version 2 cursor, which replays from
|
|
82
|
+
* `offset` the next time it is read.
|
|
83
|
+
*/
|
|
84
|
+
answer?: PromptHistoryAnswerState;
|
|
54
85
|
}
|
|
55
86
|
export interface PromptHistoryCursorSession {
|
|
56
87
|
provider: PromptHistoryProvider;
|
|
@@ -59,19 +90,30 @@ export interface PromptHistoryCursorSession {
|
|
|
59
90
|
mtimeMs: number;
|
|
60
91
|
/** Ordinal of the last human prompt seen in this file, or -1 when none. */
|
|
61
92
|
lastOrdinal: number;
|
|
62
|
-
/** Byte offset
|
|
93
|
+
/** Byte offset where the next pass starts reading. */
|
|
63
94
|
offset: number;
|
|
64
95
|
state?: PromptHistoryResumeState;
|
|
65
96
|
pending?: PromptHistoryPendingPrompt;
|
|
97
|
+
/** A bound stopped the last pass before the end of the file. */
|
|
98
|
+
backlog?: true;
|
|
99
|
+
/** `offset` is inside a record over `maxRecordBytes`; skip to its newline. */
|
|
100
|
+
skipping?: true;
|
|
101
|
+
/** Digest of the file's first bytes, used to detect a rewritten file. */
|
|
102
|
+
head?: {
|
|
103
|
+
bytes: number;
|
|
104
|
+
hash: string;
|
|
105
|
+
};
|
|
66
106
|
}
|
|
67
107
|
/**
|
|
68
108
|
* JSON-serializable resume point. Keys are opaque and never contain paths, but
|
|
69
109
|
* values can (Codex `state.cwd`), so treat the cursor as sensitive local state.
|
|
70
|
-
* Version 1
|
|
110
|
+
* Version 1 and 2 cursors are accepted as input; output is always version 3.
|
|
71
111
|
*/
|
|
72
112
|
export interface PromptHistoryCursor {
|
|
73
|
-
version:
|
|
113
|
+
version: 3;
|
|
74
114
|
sessions: Record<string, PromptHistoryCursorSession>;
|
|
115
|
+
/** Opaque key of the file the next call starts with, after a call-wide stop. */
|
|
116
|
+
next?: string;
|
|
75
117
|
}
|
|
76
118
|
/**
|
|
77
119
|
* How settled an entry's `model` / `usage` are:
|
|
@@ -112,14 +154,35 @@ export interface PromptHistoryEntry {
|
|
|
112
154
|
*/
|
|
113
155
|
metadataStatus: PromptHistoryMetadataStatus;
|
|
114
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* A log record permanently left out. Records over `maxRecordBytes` and lines
|
|
159
|
+
* that are not valid JSON are never parsed, so any prompt inside is lost, and
|
|
160
|
+
* the ordinals of later prompts in that file do not count it.
|
|
161
|
+
*/
|
|
162
|
+
export interface PromptHistoryExclusion {
|
|
163
|
+
provider: PromptHistoryProvider;
|
|
164
|
+
sessionId: string;
|
|
165
|
+
reason: 'oversizedRecord' | 'malformedRecord';
|
|
166
|
+
/** Byte offset where the record starts. */
|
|
167
|
+
offset: number;
|
|
168
|
+
}
|
|
115
169
|
export interface PromptHistoryStats {
|
|
116
170
|
sessionsScanned: number;
|
|
117
171
|
sessionsSkippedUnchanged: number;
|
|
118
172
|
/** Scanned sessions with human prompts but none inside the allowed roots. */
|
|
119
173
|
sessionsOutOfScope: number;
|
|
174
|
+
/** Files whose `maxFileBytes` budget ran out; they continue on the next call. */
|
|
120
175
|
filesOverSizeLimit: number;
|
|
176
|
+
/** Scanned sessions left with unread bytes by any bound; they continue next call. */
|
|
177
|
+
sessionsWithBacklog: number;
|
|
178
|
+
/** Sessions whose file was truncated or rewritten and was read again from the start. */
|
|
179
|
+
sessionsRewritten: number;
|
|
121
180
|
/** Codex sessions skipped as non-interactive (subagent, fork, unknown source). */
|
|
122
181
|
sessionsNotInteractive: number;
|
|
182
|
+
/** Records skipped for exceeding `maxRecordBytes` (see `exclusions`). */
|
|
183
|
+
recordsOverSizeLimit: number;
|
|
184
|
+
/** Complete lines that were not valid JSON (see `exclusions`). */
|
|
185
|
+
recordsMalformed: number;
|
|
123
186
|
/** Human prompts dropped because the log line had no usable timestamp. */
|
|
124
187
|
promptsMissingTimestamp: number;
|
|
125
188
|
/** Human prompts dropped because their cwd was missing or outside the roots. */
|
|
@@ -143,7 +206,16 @@ export interface PromptHistoryResult {
|
|
|
143
206
|
entries: PromptHistoryEntry[];
|
|
144
207
|
cursor: PromptHistoryCursor;
|
|
145
208
|
stats: PromptHistoryStats;
|
|
209
|
+
/** Resumable stops: calling again with the returned cursor continues past them. */
|
|
146
210
|
boundsHit: PromptHistoryBound[];
|
|
211
|
+
/** Permanent exclusions found in this call (at most 1000; stats count all). */
|
|
212
|
+
exclusions: PromptHistoryExclusion[];
|
|
213
|
+
/** A bound left bytes or files unread; call again with the cursor to continue. */
|
|
214
|
+
hasMore: boolean;
|
|
147
215
|
}
|
|
216
|
+
/** Test hooks; not part of the public entry points. */
|
|
217
|
+
export declare const __promptHistoryTesting: {
|
|
218
|
+
setReadChunkBytes(bytes: number | null): void;
|
|
219
|
+
};
|
|
148
220
|
/** Collect human-typed prompts from Claude Code and Codex logs under the given roots. */
|
|
149
221
|
export declare function collectPromptHistory(options: CollectPromptHistoryOptions): Promise<PromptHistoryResult>;
|