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.
Files changed (53) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +35 -8
  3. package/dist/aggregation/EventAggregator.d.ts +12 -2
  4. package/dist/aggregation/EventAggregator.js +49 -19
  5. package/dist/browser.d.ts +2 -1
  6. package/dist/browser.js +9 -2
  7. package/dist/extractors/tokenUsage.js +1 -0
  8. package/dist/formatters/sessionDump.d.ts +10 -2
  9. package/dist/formatters/sessionDump.js +45 -23
  10. package/dist/index.d.ts +8 -4
  11. package/dist/index.js +31 -10
  12. package/dist/node.d.ts +3 -1
  13. package/dist/node.js +8 -2
  14. package/dist/parsers/codexParser.d.ts +22 -2
  15. package/dist/parsers/codexParser.js +64 -5
  16. package/dist/parsers/subagentScanner.d.ts +9 -0
  17. package/dist/parsers/subagentScanner.js +45 -12
  18. package/dist/parsers/subagentStatsCache.d.ts +17 -0
  19. package/dist/parsers/subagentStatsCache.js +82 -0
  20. package/dist/parsers/subagentTraceParser.js +19 -10
  21. package/dist/promptHistory.d.ts +82 -10
  22. package/dist/promptHistory.js +654 -183
  23. package/dist/providers/claudeCode.js +8 -41
  24. package/dist/providers/codex.js +28 -14
  25. package/dist/providers/codexDatabase.d.ts +16 -1
  26. package/dist/providers/codexDatabase.js +49 -1
  27. package/dist/providers/openCode.js +30 -4
  28. package/dist/report/htmlReportGenerator.js +33 -11
  29. package/dist/report/sessionReportInputs.d.ts +3 -1
  30. package/dist/report/sessionReportInputs.js +2 -0
  31. package/dist/report/types.d.ts +5 -0
  32. package/dist/schemas/sessionEvent.d.ts +30 -6
  33. package/dist/schemas/sessionEvent.js +7 -1
  34. package/dist/schemas/stateFile.d.ts +2 -0
  35. package/dist/schemas/stateFile.js +1 -0
  36. package/dist/sessionTokenTotals.d.ts +46 -0
  37. package/dist/sessionTokenTotals.js +138 -0
  38. package/dist/stateFile.d.ts +7 -0
  39. package/dist/tokenSummary.d.ts +27 -1
  40. package/dist/tokenSummary.js +50 -3
  41. package/dist/types/codex.d.ts +4 -0
  42. package/dist/types/sessionEvent.d.ts +22 -0
  43. package/dist/usage/claudeUsageDedupe.d.ts +53 -0
  44. package/dist/usage/claudeUsageDedupe.js +195 -0
  45. package/dist/usage/subagentUsage.d.ts +13 -0
  46. package/dist/usage/subagentUsage.js +21 -0
  47. package/dist/usage/usageEvents.d.ts +11 -2
  48. package/dist/usage/usageEvents.js +79 -8
  49. package/dist/watchers/eventBridge.js +5 -0
  50. package/dist/watchers/jsonlWatcher.d.ts +5 -0
  51. package/dist/watchers/jsonlWatcher.js +40 -5
  52. package/dist/watchers/types.d.ts +5 -0
  53. 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
- let inputTokens = 0;
105
- let outputTokens = 0;
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
- if (raw.type === 'assistant' && raw.message?.usage) {
157
- const usage = raw.message.usage;
158
- inputTokens += usage.input_tokens || 0;
159
- outputTokens += usage.output_tokens || 0;
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
- if (events.length === 0 && !agentType && !description && inputTokens === 0) {
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,
@@ -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 (not its text), the next call replays from it,
13
- * and the prompt is returned again under the same identity once its first
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 a single file may need read in this pass. Default 16 MiB. A file over
24
- * it is skipped, and calling again with the same bounds skips it again.
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 replay position and a metadata digest, never prompt or answer text.
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; the next pass replays from here. */
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 just past the last complete line processed. */
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 cursors from 0.27.0 are accepted as input; output is always version 2.
110
+ * Version 1 and 2 cursors are accepted as input; output is always version 3.
71
111
  */
72
112
  export interface PromptHistoryCursor {
73
- version: 2;
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>;