sidekick-shared 0.29.0 → 0.30.0
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 +48 -0
- package/README.md +198 -56
- package/dist/aggregation/EventAggregator.d.ts +96 -2
- package/dist/aggregation/EventAggregator.js +535 -42
- package/dist/aggregation/snapshot.d.ts +37 -8
- package/dist/aggregation/snapshot.js +31 -9
- package/dist/aggregation/types.d.ts +38 -1
- package/dist/browser.d.ts +9 -1
- package/dist/browser.js +29 -3
- package/dist/context/sessionContext.js +8 -0
- package/dist/esm/aggregation/EventAggregator.js +535 -42
- package/dist/esm/aggregation/snapshot.js +31 -9
- package/dist/esm/browser.js +3 -0
- package/dist/esm/context/sessionContext.js +8 -0
- package/dist/esm/fileContinuity.js +149 -0
- package/dist/esm/formatters/toolSummary.js +4 -1
- package/dist/esm/index.js +7 -2
- package/dist/esm/modelCatalog.js +5 -4
- package/dist/esm/modelContext.js +4 -3
- package/dist/esm/modelInfo.js +4 -3
- package/dist/esm/node.js +3 -1
- package/dist/esm/observedContextWindows.js +2 -1
- package/dist/esm/ownKey.js +26 -0
- package/dist/esm/parsers/codexParser.js +372 -7
- package/dist/esm/parsers/openCodeParser.js +2 -1
- package/dist/esm/parsers/subagentEndState.js +61 -0
- package/dist/esm/parsers/subagentScanner.js +37 -12
- package/dist/esm/parsers/subagentTraceParser.js +103 -37
- package/dist/esm/pricingCatalog.js +3 -2
- package/dist/esm/promptHistory.js +22 -11
- package/dist/esm/providers/claudeCode.js +60 -29
- package/dist/esm/providers/codex.js +235 -201
- package/dist/esm/providers/codexCompressedReader.js +184 -0
- package/dist/esm/providers/codexDatabase.js +47 -2
- package/dist/esm/providers/codexRollouts.js +20 -2
- package/dist/esm/providers/codexSubagents.js +456 -0
- package/dist/esm/providers/compressedRollouts.js +330 -0
- package/dist/esm/providers/factory.js +4 -1
- package/dist/esm/providers/rolloutWalker.js +32 -4
- package/dist/esm/schemas/sessionEvent.js +63 -14
- package/dist/esm/sessionMonitor.js +7 -1
- package/dist/esm/sessionPreviews.js +9 -0
- package/dist/esm/sessionPromptHistory.js +5 -3
- package/dist/esm/subagentActivity.js +125 -0
- package/dist/esm/subagentDisplayState.js +81 -0
- package/dist/esm/subagentSpawn.js +363 -0
- package/dist/esm/testing/codexSubagentFixtures.js +336 -0
- package/dist/esm/toolCallOrigins.js +17 -10
- package/dist/esm/toolCategory.js +6 -2
- package/dist/esm/transcript.js +8 -0
- package/dist/esm/turns/assistantTurn.js +6 -2
- package/dist/esm/usage/claudeUsageDedupe.js +68 -0
- package/dist/esm/usage/usageEvents.js +4 -1
- package/dist/esm/watchers/eventBridge.js +45 -0
- package/dist/esm/watchers/factory.js +5 -3
- package/dist/esm/watchers/jsonlTail.js +82 -48
- package/dist/esm/watchers/jsonlWatcher.js +61 -4
- package/dist/esm/watchers/providerReaderWatcher.js +18 -4
- package/dist/esm/zaiQuota.js +2 -1
- package/dist/fileContinuity.d.ts +95 -0
- package/dist/fileContinuity.js +187 -0
- package/dist/formatters/toolSummary.js +4 -1
- package/dist/index.d.ts +19 -7
- package/dist/index.js +48 -10
- package/dist/modelCatalog.js +5 -4
- package/dist/modelContext.js +4 -3
- package/dist/modelInfo.js +4 -3
- package/dist/node.d.ts +5 -1
- package/dist/node.js +18 -2
- package/dist/observedContextWindows.js +2 -1
- package/dist/ownKey.d.ts +16 -0
- package/dist/ownKey.js +30 -0
- package/dist/parsers/codexParser.d.ts +107 -0
- package/dist/parsers/codexParser.js +373 -7
- package/dist/parsers/openCodeParser.js +2 -1
- package/dist/parsers/subagentEndState.d.ts +20 -0
- package/dist/parsers/subagentEndState.js +65 -0
- package/dist/parsers/subagentScanner.d.ts +19 -6
- package/dist/parsers/subagentScanner.js +37 -12
- package/dist/parsers/subagentTraceParser.d.ts +33 -5
- package/dist/parsers/subagentTraceParser.js +103 -37
- package/dist/pricingCatalog.js +3 -2
- package/dist/promptHistory.d.ts +13 -3
- package/dist/promptHistory.js +22 -11
- package/dist/providers/claudeCode.js +59 -28
- package/dist/providers/codex.d.ts +35 -5
- package/dist/providers/codex.js +232 -198
- package/dist/providers/codexCompressedReader.d.ts +58 -0
- package/dist/providers/codexCompressedReader.js +221 -0
- package/dist/providers/codexDatabase.d.ts +25 -0
- package/dist/providers/codexDatabase.js +48 -2
- package/dist/providers/codexRollouts.d.ts +16 -2
- package/dist/providers/codexRollouts.js +20 -2
- package/dist/providers/codexSubagents.d.ts +66 -0
- package/dist/providers/codexSubagents.js +495 -0
- package/dist/providers/compressedRollouts.d.ts +114 -0
- package/dist/providers/compressedRollouts.js +380 -0
- package/dist/providers/factory.d.ts +5 -0
- package/dist/providers/factory.js +4 -1
- package/dist/providers/rolloutWalker.d.ts +15 -2
- package/dist/providers/rolloutWalker.js +33 -4
- package/dist/providers/types.d.ts +36 -1
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/sessionEvent.d.ts +51 -10
- package/dist/schemas/sessionEvent.js +63 -14
- package/dist/sessionMonitor.d.ts +6 -0
- package/dist/sessionMonitor.js +7 -1
- package/dist/sessionPreviews.js +9 -0
- package/dist/sessionPromptHistory.d.ts +5 -0
- package/dist/sessionPromptHistory.js +5 -36
- package/dist/subagentActivity.d.ts +91 -0
- package/dist/subagentActivity.js +132 -0
- package/dist/subagentDisplayState.d.ts +81 -0
- package/dist/subagentDisplayState.js +86 -0
- package/dist/subagentSpawn.d.ts +175 -0
- package/dist/subagentSpawn.js +385 -0
- package/dist/testing/codexSubagentFixtures.d.ts +48 -0
- package/dist/testing/codexSubagentFixtures.js +344 -0
- package/dist/toolCallOrigins.d.ts +5 -0
- package/dist/toolCallOrigins.js +17 -10
- package/dist/toolCategory.js +6 -2
- package/dist/transcript.js +8 -0
- package/dist/turns/assistantTurn.d.ts +4 -0
- package/dist/turns/assistantTurn.js +6 -2
- package/dist/types/codex.d.ts +71 -2
- package/dist/types/sessionEvent.d.ts +38 -3
- package/dist/usage/claudeUsageDedupe.d.ts +31 -0
- package/dist/usage/claudeUsageDedupe.js +68 -0
- package/dist/usage/usageEvents.d.ts +4 -1
- package/dist/usage/usageEvents.js +4 -1
- package/dist/watchers/eventBridge.js +45 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +5 -3
- package/dist/watchers/jsonlTail.d.ts +27 -4
- package/dist/watchers/jsonlTail.js +84 -48
- package/dist/watchers/jsonlWatcher.d.ts +22 -1
- package/dist/watchers/jsonlWatcher.js +60 -3
- package/dist/watchers/providerReaderWatcher.d.ts +7 -0
- package/dist/watchers/providerReaderWatcher.js +18 -4
- package/dist/watchers/types.d.ts +12 -0
- package/dist/zaiQuota.js +2 -1
- package/package.json +1 -1
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
* (not just stats) for drill-down trace visualization. Applies noise classification
|
|
6
6
|
* and tool summary formatting from the formatters module.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* 1.
|
|
10
|
-
* 2.
|
|
11
|
-
*
|
|
8
|
+
* Parent-child linking, exact links first:
|
|
9
|
+
* 1. Meta `parentAgentId` (Claude Code writes it for nested spawns)
|
|
10
|
+
* 2. The child's spawning tool_use id (meta `toolUseId`) found in another
|
|
11
|
+
* trace's `Agent` / `Task` calls
|
|
12
|
+
* Only traces without that spawn metadata (older Claude Code) fall back to:
|
|
13
|
+
* 3. Team summary: parse <teammate-message> XML blocks
|
|
14
|
+
* 4. Positional fallback: timestamp ordering + proximity
|
|
12
15
|
*
|
|
13
16
|
* @module parsers/subagentTraceParser
|
|
14
17
|
*/
|
|
@@ -19,6 +22,9 @@ import { isHardNoise, classifyMessage, getSoftNoiseReason } from '../formatters/
|
|
|
19
22
|
import { ClaudeUsageDeduper, dedupedRawClaudeUsage } from '../usage/claudeUsageDedupe';
|
|
20
23
|
import { addUsageToSubagent } from '../usage/subagentUsage';
|
|
21
24
|
import { readAgentMeta } from './subagentScanner';
|
|
25
|
+
import { SubagentEndTracker } from './subagentEndState';
|
|
26
|
+
import { isSubagentSpawnTool } from '../subagentSpawn';
|
|
27
|
+
import { ownValue } from '../ownKey';
|
|
22
28
|
// ── Constants ──
|
|
23
29
|
const AGENT_FILE_PATTERN = /^agent-(.+)\.jsonl$/;
|
|
24
30
|
// ── Public API ──
|
|
@@ -50,10 +56,8 @@ export function scanSubagentTraces(sessionDir, sessionId) {
|
|
|
50
56
|
if (trace)
|
|
51
57
|
traces.push(trace);
|
|
52
58
|
}
|
|
53
|
-
// Link parent-child relationships
|
|
54
|
-
linkTraces(traces);
|
|
55
|
-
// Return only top-level traces (those without parents)
|
|
56
|
-
return traces.filter((t) => !t.parentToolUseId);
|
|
59
|
+
// Link parent-child relationships; returns the traces with no parent agent.
|
|
60
|
+
return linkTraces(traces);
|
|
57
61
|
}
|
|
58
62
|
catch {
|
|
59
63
|
return [];
|
|
@@ -70,6 +74,7 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
70
74
|
let description;
|
|
71
75
|
const tokens = { agentId, toolCalls: [], inputTokens: 0, outputTokens: 0 };
|
|
72
76
|
const usageDeduper = new ClaudeUsageDeduper();
|
|
77
|
+
const endTracker = new SubagentEndTracker();
|
|
73
78
|
let startTime;
|
|
74
79
|
let endTime;
|
|
75
80
|
for (const line of lines) {
|
|
@@ -79,6 +84,7 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
79
84
|
if (!startTime)
|
|
80
85
|
startTime = timestamp;
|
|
81
86
|
endTime = timestamp;
|
|
87
|
+
endTracker.observe(raw);
|
|
82
88
|
// Build SessionEvent from raw JSONL
|
|
83
89
|
const sessionEvent = rawToSessionEvent(raw);
|
|
84
90
|
if (!sessionEvent)
|
|
@@ -133,8 +139,8 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
133
139
|
input: block.input || {},
|
|
134
140
|
timestamp,
|
|
135
141
|
});
|
|
136
|
-
// Extract agent info from Task
|
|
137
|
-
if (block.name
|
|
142
|
+
// Extract agent info from an Agent/Task spawn
|
|
143
|
+
if (isSubagentSpawnTool(block.name) && block.input) {
|
|
138
144
|
if (block.input.subagent_type && !agentType) {
|
|
139
145
|
agentType = String(block.input.subagent_type);
|
|
140
146
|
}
|
|
@@ -171,6 +177,16 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
171
177
|
agentId,
|
|
172
178
|
agentType,
|
|
173
179
|
description,
|
|
180
|
+
filePath,
|
|
181
|
+
...(meta.toolUseId
|
|
182
|
+
? { spawnToolUseId: meta.toolUseId, parentToolUseId: meta.toolUseId }
|
|
183
|
+
: {}),
|
|
184
|
+
...(meta.parentAgentId ? { parentAgentId: meta.parentAgentId } : {}),
|
|
185
|
+
...(meta.requestShape ? { background: meta.requestShape === 'background' } : {}),
|
|
186
|
+
...(meta.finished !== undefined ? { finished: meta.finished } : {}),
|
|
187
|
+
...(meta.stoppedByUser !== undefined ? { stoppedByUser: meta.stoppedByUser } : {}),
|
|
188
|
+
...(endTime ? { lastActivityAt: endTime } : {}),
|
|
189
|
+
endedCleanly: endTracker.endedCleanly(),
|
|
174
190
|
events,
|
|
175
191
|
children: [],
|
|
176
192
|
stats: {
|
|
@@ -187,6 +203,7 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
187
203
|
startTime,
|
|
188
204
|
endTime,
|
|
189
205
|
durationMs,
|
|
206
|
+
endedCleanly: endTracker.endedCleanly(),
|
|
190
207
|
},
|
|
191
208
|
};
|
|
192
209
|
}
|
|
@@ -195,23 +212,61 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
195
212
|
}
|
|
196
213
|
}
|
|
197
214
|
// ── Parent-Child Linking ──
|
|
215
|
+
/**
|
|
216
|
+
* Nests child traces under their parents and returns the traces with no
|
|
217
|
+
* parent agent. Exact metadata links come first; the heuristics only place
|
|
218
|
+
* traces Claude Code recorded no spawn metadata for.
|
|
219
|
+
*/
|
|
198
220
|
function linkTraces(traces) {
|
|
199
221
|
const traceById = new Map();
|
|
200
222
|
for (const trace of traces) {
|
|
201
223
|
traceById.set(trace.agentId, trace);
|
|
202
224
|
}
|
|
203
|
-
|
|
204
|
-
|
|
225
|
+
const parentOf = new Map();
|
|
226
|
+
const attach = (parent, child) => {
|
|
227
|
+
if (parent === child || parentOf.has(child))
|
|
228
|
+
return false;
|
|
229
|
+
// Never nest an agent under its own descendant (malformed metadata).
|
|
230
|
+
for (let up = parent; up; up = parentOf.get(up)) {
|
|
231
|
+
if (up === child)
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
parentOf.set(child, parent);
|
|
235
|
+
parent.children.push(child);
|
|
236
|
+
return true;
|
|
237
|
+
};
|
|
238
|
+
// Phase 1: the meta file names the parent agent
|
|
239
|
+
for (const child of traces) {
|
|
240
|
+
const parent = child.parentAgentId ? traceById.get(child.parentAgentId) : undefined;
|
|
241
|
+
if (parent)
|
|
242
|
+
attach(parent, child);
|
|
243
|
+
}
|
|
244
|
+
// Phase 2: the child's spawning call is in another trace
|
|
245
|
+
const spawnOwner = new Map();
|
|
205
246
|
for (const trace of traces) {
|
|
206
|
-
for (const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// The tool_use_id from this call should appear as a result in the parent
|
|
210
|
-
// but we can link via timestamp proximity to child traces
|
|
211
|
-
}
|
|
247
|
+
for (const id of spawnToolUseIdsOf(trace)) {
|
|
248
|
+
if (!spawnOwner.has(id))
|
|
249
|
+
spawnOwner.set(id, trace);
|
|
212
250
|
}
|
|
213
251
|
}
|
|
214
|
-
|
|
252
|
+
for (const child of traces) {
|
|
253
|
+
if (parentOf.has(child) || !child.spawnToolUseId)
|
|
254
|
+
continue;
|
|
255
|
+
const parent = spawnOwner.get(child.spawnToolUseId);
|
|
256
|
+
if (parent && attach(parent, child))
|
|
257
|
+
child.parentAgentId ??= parent.agentId;
|
|
258
|
+
}
|
|
259
|
+
// Heuristic links set parentAgentId and, as before, parentToolUseId to the
|
|
260
|
+
// parent's agent id.
|
|
261
|
+
const unresolved = (trace) => !parentOf.has(trace) && !trace.spawnToolUseId && !trace.parentAgentId;
|
|
262
|
+
const linkHeuristically = (parent, child) => {
|
|
263
|
+
if (!attach(parent, child))
|
|
264
|
+
return false;
|
|
265
|
+
child.parentAgentId = parent.agentId;
|
|
266
|
+
child.parentToolUseId = parent.agentId;
|
|
267
|
+
return true;
|
|
268
|
+
};
|
|
269
|
+
// Phase 3: Team summary linking
|
|
215
270
|
for (const trace of traces) {
|
|
216
271
|
for (const traceEvent of trace.events) {
|
|
217
272
|
const content = traceEvent.event.message?.content;
|
|
@@ -219,45 +274,56 @@ function linkTraces(traces) {
|
|
|
219
274
|
// Parse teammate agent IDs from XML blocks
|
|
220
275
|
const matches = content.matchAll(/agent[_-]id['":\s]+([a-f0-9]+)/gi);
|
|
221
276
|
for (const match of matches) {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
child.parentToolUseId = trace.agentId;
|
|
226
|
-
trace.children.push(child);
|
|
227
|
-
}
|
|
277
|
+
const child = traceById.get(match[1]);
|
|
278
|
+
if (child && unresolved(child))
|
|
279
|
+
linkHeuristically(trace, child);
|
|
228
280
|
}
|
|
229
281
|
}
|
|
230
282
|
}
|
|
231
283
|
}
|
|
232
|
-
// Phase
|
|
284
|
+
// Phase 4: Positional fallback — timestamp proximity
|
|
233
285
|
// Traces without parents that fall within another trace's time window
|
|
234
|
-
const
|
|
235
|
-
for (const child of parentless) {
|
|
286
|
+
for (const child of traces.filter(unresolved)) {
|
|
236
287
|
if (!child.stats.startTime)
|
|
237
288
|
continue;
|
|
238
289
|
const childStart = child.stats.startTime.getTime();
|
|
239
|
-
// Find a trace that
|
|
290
|
+
// Find a top-level trace that spawned an agent right before this child started
|
|
240
291
|
for (const potential of traces) {
|
|
241
|
-
if (potential === child)
|
|
292
|
+
if (potential === child || parentOf.has(potential))
|
|
242
293
|
continue;
|
|
243
|
-
if (potential.parentToolUseId)
|
|
244
|
-
continue; // Already linked
|
|
245
294
|
for (const traceEvent of potential.events) {
|
|
246
295
|
const evt = traceEvent.event;
|
|
247
|
-
if (evt.type === 'assistant' && evt.tool?.name
|
|
296
|
+
if (evt.type === 'assistant' && isSubagentSpawnTool(evt.tool?.name)) {
|
|
248
297
|
const taskTime = new Date(evt.timestamp).getTime();
|
|
249
298
|
// Within 2 seconds before child start
|
|
250
299
|
if (taskTime <= childStart && childStart - taskTime < 2000) {
|
|
251
|
-
child
|
|
252
|
-
potential.children.push(child);
|
|
300
|
+
linkHeuristically(potential, child);
|
|
253
301
|
break;
|
|
254
302
|
}
|
|
255
303
|
}
|
|
256
304
|
}
|
|
257
|
-
if (child
|
|
305
|
+
if (parentOf.has(child))
|
|
258
306
|
break;
|
|
259
307
|
}
|
|
260
308
|
}
|
|
309
|
+
return traces.filter((trace) => !parentOf.has(trace));
|
|
310
|
+
}
|
|
311
|
+
/** Ids of the `Agent` / `Task` calls a trace made. */
|
|
312
|
+
function spawnToolUseIdsOf(trace) {
|
|
313
|
+
const ids = [];
|
|
314
|
+
for (const traceEvent of trace.events) {
|
|
315
|
+
const content = traceEvent.event.message?.content;
|
|
316
|
+
if (!Array.isArray(content))
|
|
317
|
+
continue;
|
|
318
|
+
for (const block of content) {
|
|
319
|
+
if (block?.type === 'tool_use' &&
|
|
320
|
+
typeof block.id === 'string' &&
|
|
321
|
+
isSubagentSpawnTool(block.name)) {
|
|
322
|
+
ids.push(block.id);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return ids;
|
|
261
327
|
}
|
|
262
328
|
// ── Helpers ──
|
|
263
329
|
function rawToSessionEvent(raw) {
|
|
@@ -272,7 +338,7 @@ function rawToSessionEvent(raw) {
|
|
|
272
338
|
tool_result: 'tool_result',
|
|
273
339
|
summary: 'summary',
|
|
274
340
|
};
|
|
275
|
-
const mappedType = typeMap
|
|
341
|
+
const mappedType = ownValue(typeMap, type);
|
|
276
342
|
if (!mappedType)
|
|
277
343
|
return null;
|
|
278
344
|
const message = raw.message;
|
|
@@ -20,6 +20,7 @@ import { promises as fs } from 'node:fs';
|
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { _setCatalogContextWindows } from './modelContext';
|
|
22
22
|
import { _setPricingOverrides, getPricingCatalogInfo } from './modelInfo';
|
|
23
|
+
import { setOwnValue } from './ownKey';
|
|
23
24
|
import { getConfigDir } from './paths';
|
|
24
25
|
import { atomicWriteJson } from './writers/atomic';
|
|
25
26
|
// ── Constants ──
|
|
@@ -117,7 +118,7 @@ export function normalizeLiteLlmCatalog(raw) {
|
|
|
117
118
|
continue;
|
|
118
119
|
// Catalog keys use both `provider/model` and bare `model` forms. Record both
|
|
119
120
|
// the raw key and the bare-model form so lookups work regardless of caller.
|
|
120
|
-
out
|
|
121
|
+
setOwnValue(out, key, pricing);
|
|
121
122
|
const bare = stripProviderPrefix(key);
|
|
122
123
|
if (bare && bare !== key && !out[bare]) {
|
|
123
124
|
out[bare] = pricing;
|
|
@@ -154,7 +155,7 @@ export function normalizeLiteLlmContextWindows(raw) {
|
|
|
154
155
|
const window = entry.max_input_tokens;
|
|
155
156
|
if (typeof window !== 'number' || !Number.isFinite(window) || window <= 0)
|
|
156
157
|
continue;
|
|
157
|
-
out
|
|
158
|
+
setOwnValue(out, key, window);
|
|
158
159
|
const bare = stripProviderPrefix(key);
|
|
159
160
|
if (bare && bare !== key) {
|
|
160
161
|
const seen = aliasWindows.get(bare) ?? new Set();
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
* answering call brings model or usage (`metadataStatus` says how settled).
|
|
25
25
|
*/
|
|
26
26
|
import { createHash } from 'crypto';
|
|
27
|
-
import * as fs from 'fs';
|
|
28
27
|
import * as path from 'path';
|
|
29
28
|
import { HUMAN_CODEX_SOURCES, humanPromptText } from './humanPrompt';
|
|
30
29
|
import { listClaudeSessionFilesAsync } from './providers/claudeListing';
|
|
31
30
|
import { listCodexSessionFilesAsync } from './providers/codexRollouts';
|
|
31
|
+
import { canReadCompressedRollouts, openRolloutReadHandle, statRolloutContent, } from './providers/compressedRollouts';
|
|
32
32
|
import { DirectoryListingCache } from './providers/directoryListingCache';
|
|
33
33
|
import { realpathOrNull, resolveWorkspaceRoots } from './workspaceRoots';
|
|
34
34
|
import { fileHeadOf, HEAD_BYTES, headMatches } from './fileHead';
|
|
@@ -105,7 +105,9 @@ export async function collectPromptHistory(options) {
|
|
|
105
105
|
return finish();
|
|
106
106
|
const sinceMs = options.since ? options.since.getTime() : null;
|
|
107
107
|
const providers = options.providers ?? ALL_PROVIDERS;
|
|
108
|
-
const candidates = rotate(await listCandidates(providers, listingRoots, sinceMs
|
|
108
|
+
const candidates = rotate(await listCandidates(providers, listingRoots, sinceMs, {
|
|
109
|
+
includeCompressedRollouts: options.includeCompressedRollouts,
|
|
110
|
+
}), previousNext);
|
|
109
111
|
const moved = movedSessionKeys(previous, candidates);
|
|
110
112
|
const context = {
|
|
111
113
|
roots,
|
|
@@ -141,7 +143,8 @@ export async function collectPromptHistory(options) {
|
|
|
141
143
|
}
|
|
142
144
|
let stat;
|
|
143
145
|
try {
|
|
144
|
-
|
|
146
|
+
// A compressed rollout's size is its decompressed content's.
|
|
147
|
+
stat = await statRolloutContent(candidate.filePath);
|
|
145
148
|
}
|
|
146
149
|
catch {
|
|
147
150
|
continue;
|
|
@@ -246,7 +249,7 @@ export async function collectPromptHistory(options) {
|
|
|
246
249
|
*/
|
|
247
250
|
export async function readPromptOrdinalMarks(provider, sessionId, filePath) {
|
|
248
251
|
const marks = [];
|
|
249
|
-
const stat = await
|
|
252
|
+
const stat = await statRolloutContent(filePath);
|
|
250
253
|
const unbounded = Number.MAX_SAFE_INTEGER;
|
|
251
254
|
const context = {
|
|
252
255
|
roots: [],
|
|
@@ -435,19 +438,24 @@ function scopedCwd(cwd, roots, realpaths) {
|
|
|
435
438
|
return null;
|
|
436
439
|
}
|
|
437
440
|
/** @internal */
|
|
438
|
-
export async function listCandidates(providers, listingRoots, sinceMs) {
|
|
441
|
+
export async function listCandidates(providers, listingRoots, sinceMs, options = {}) {
|
|
439
442
|
const byPath = new Map();
|
|
440
443
|
const listOptions = sinceMs !== null ? { since: sinceMs } : {};
|
|
441
444
|
for (const provider of [...new Set(providers)]) {
|
|
442
445
|
if (!ALL_PROVIDERS.includes(provider))
|
|
443
446
|
continue;
|
|
444
447
|
// Listings are only candidate supersets; scope is re-checked per prompt.
|
|
445
|
-
const list = provider === 'claude-code' ? listClaudeSessionFilesAsync : listCodexSessionFilesAsync;
|
|
446
448
|
const cache = new DirectoryListingCache();
|
|
447
449
|
for (const root of listingRoots) {
|
|
448
450
|
let files;
|
|
449
451
|
try {
|
|
450
|
-
files =
|
|
452
|
+
files =
|
|
453
|
+
provider === 'claude-code'
|
|
454
|
+
? await listClaudeSessionFilesAsync(root, listOptions, cache)
|
|
455
|
+
: await listCodexSessionFilesAsync(root, {
|
|
456
|
+
...listOptions,
|
|
457
|
+
includeCompressed: options.includeCompressedRollouts === true && canReadCompressedRollouts(),
|
|
458
|
+
}, cache);
|
|
451
459
|
}
|
|
452
460
|
catch {
|
|
453
461
|
files = [];
|
|
@@ -469,11 +477,11 @@ export async function listCandidates(providers, listingRoots, sinceMs) {
|
|
|
469
477
|
compareStrings(left.sessionId, right.sessionId) ||
|
|
470
478
|
compareStrings(left.key, right.key));
|
|
471
479
|
}
|
|
472
|
-
/** True when the file's first bytes still match a recorded head. */
|
|
480
|
+
/** True when the file's first bytes (a compressed rollout's content) still match a recorded head. */
|
|
473
481
|
async function fileStartsWith(filePath, head) {
|
|
474
482
|
let handle;
|
|
475
483
|
try {
|
|
476
|
-
handle = await
|
|
484
|
+
({ handle } = await openRolloutReadHandle(filePath));
|
|
477
485
|
return headMatches(head, await readAt(handle, 0, Math.min(HEAD_BYTES, head.bytes)));
|
|
478
486
|
}
|
|
479
487
|
catch {
|
|
@@ -546,8 +554,11 @@ async function verifyFile(handle, size, session) {
|
|
|
546
554
|
return { head: fileHeadOf(head), bytesRead };
|
|
547
555
|
}
|
|
548
556
|
/** @internal */
|
|
549
|
-
export async function scanFile(candidate,
|
|
550
|
-
|
|
557
|
+
export async function scanFile(candidate, fileStat, prior, budget, context) {
|
|
558
|
+
// A compressed rollout is scanned over its decompressed content.
|
|
559
|
+
const opened = await openRolloutReadHandle(candidate.filePath);
|
|
560
|
+
const handle = opened.handle;
|
|
561
|
+
const stat = opened.size === null ? fileStat : { size: opened.size, mtimeMs: fileStat.mtimeMs };
|
|
551
562
|
try {
|
|
552
563
|
let bytesRead = 0;
|
|
553
564
|
const verified = await verifyFile(handle, stat.size, prior);
|
|
@@ -13,7 +13,8 @@ import { StringDecoder } from 'string_decoder';
|
|
|
13
13
|
import { readSessionContextSnapshot } from '../context/sessionContext';
|
|
14
14
|
import { readSessionFileStats } from '../sessionStats';
|
|
15
15
|
import { JsonlParser } from '../parsers/jsonl';
|
|
16
|
-
import {
|
|
16
|
+
import { FileContinuity } from '../fileContinuity';
|
|
17
|
+
import { ClaudeUsageDeduper, normalizeClaudeUsage, } from '../usage/claudeUsageDedupe';
|
|
17
18
|
import { ProviderDiagnosticTracker } from './diagnostics';
|
|
18
19
|
import { DirectoryListingCache, splitWatchedPath } from './directoryListingCache';
|
|
19
20
|
import { encodeWorkspacePath as encodeWsPath, getSessionDirectory as getSessionDir, discoverSessionDirectory as discoverSessionDir, findActiveSession as findActiveSessionPath, findAllSessions as findAllSessionPaths, findSessionsInDirectory as findSessionsInDir, decodeEncodedPath, getAllProjectFolders as getAllProjectFoldersRaw, findSessionFilesWithStats, } from '../parsers/sessionPathResolver';
|
|
@@ -110,7 +111,10 @@ class ClaudeCodeReader {
|
|
|
110
111
|
/** Cursor after the last complete line, safe to persist across restarts. */
|
|
111
112
|
committedPosition = 0;
|
|
112
113
|
events = [];
|
|
113
|
-
|
|
114
|
+
/** Why the last `readNew()` started over from offset 0, if it did. */
|
|
115
|
+
_lastReset = null;
|
|
116
|
+
/** Whether the file is still the one the positions refer to. */
|
|
117
|
+
continuity = new FileContinuity();
|
|
114
118
|
decoder = new StringDecoder('utf8');
|
|
115
119
|
/** Split lines of one response repeat its usage; count each response once. */
|
|
116
120
|
usageDeduper;
|
|
@@ -119,7 +123,8 @@ class ClaudeCodeReader {
|
|
|
119
123
|
this.usageDeduper = usageDeduper ?? new ClaudeUsageDeduper();
|
|
120
124
|
this.parser = new JsonlParser({
|
|
121
125
|
onEvent: (raw) => {
|
|
122
|
-
|
|
126
|
+
// Mid-turn task notifications finish background subagents.
|
|
127
|
+
for (const event of extractSessionEvents(raw, { includeTaskNotifications: true })) {
|
|
123
128
|
this.events.push(this.usageDeduper.apply(normalizeClaudeUsage(event)));
|
|
124
129
|
}
|
|
125
130
|
},
|
|
@@ -130,37 +135,28 @@ class ClaudeCodeReader {
|
|
|
130
135
|
}
|
|
131
136
|
readNew() {
|
|
132
137
|
this.events = [];
|
|
133
|
-
this.
|
|
138
|
+
this._lastReset = null;
|
|
139
|
+
let fd = null;
|
|
134
140
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const stats = fs.
|
|
141
|
+
// Open first and stat the descriptor: the size and identity checked
|
|
142
|
+
// are those of the file actually read, even if the path is replaced.
|
|
143
|
+
fd = fs.openSync(this.sessionPath, 'r');
|
|
144
|
+
const stats = fs.fstatSync(fd);
|
|
145
|
+
const reason = this.continuity.check(fd, stats, this.filePosition, this.committedPosition);
|
|
146
|
+
// Truncated, rewritten, or replaced: what was read no longer describes
|
|
147
|
+
// the file, so start over from its beginning.
|
|
148
|
+
if (reason !== null)
|
|
149
|
+
this.restart(reason);
|
|
150
|
+
this.continuity.adopt(stats);
|
|
139
151
|
const currentSize = stats.size;
|
|
140
|
-
// Handle truncation
|
|
141
|
-
if (currentSize < this.filePosition) {
|
|
142
|
-
this._wasTruncated = true;
|
|
143
|
-
this.filePosition = 0;
|
|
144
|
-
this.committedPosition = 0;
|
|
145
|
-
this.parser.reset();
|
|
146
|
-
this.decoder = new StringDecoder('utf8');
|
|
147
|
-
this.usageDeduper.reset();
|
|
148
|
-
}
|
|
149
152
|
// No new content
|
|
150
153
|
if (currentSize <= this.filePosition) {
|
|
151
|
-
return
|
|
154
|
+
return this.events;
|
|
152
155
|
}
|
|
153
156
|
// Read new bytes from last position
|
|
154
|
-
const fd = fs.openSync(this.sessionPath, 'r');
|
|
155
157
|
const bufferSize = currentSize - this.filePosition;
|
|
156
158
|
const buffer = Buffer.alloc(bufferSize);
|
|
157
|
-
|
|
158
|
-
try {
|
|
159
|
-
bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
|
|
160
|
-
}
|
|
161
|
-
finally {
|
|
162
|
-
fs.closeSync(fd);
|
|
163
|
-
}
|
|
159
|
+
const bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
|
|
164
160
|
const bytes = buffer.subarray(0, bytesRead);
|
|
165
161
|
const newline = bytes.lastIndexOf(0x0a);
|
|
166
162
|
if (newline >= 0)
|
|
@@ -170,10 +166,31 @@ class ClaudeCodeReader {
|
|
|
170
166
|
this.filePosition += bytesRead;
|
|
171
167
|
}
|
|
172
168
|
catch (error) {
|
|
173
|
-
|
|
169
|
+
if (error?.code !== 'ENOENT') {
|
|
170
|
+
console.error(`ClaudeCodeReader: error reading ${this.sessionPath}: ${error}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
if (fd !== null) {
|
|
175
|
+
try {
|
|
176
|
+
fs.closeSync(fd);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Ignore close errors.
|
|
180
|
+
}
|
|
181
|
+
}
|
|
174
182
|
}
|
|
175
183
|
return this.events;
|
|
176
184
|
}
|
|
185
|
+
/** Starts over from offset 0 of a file that was truncated, rewritten, or replaced. */
|
|
186
|
+
restart(reason) {
|
|
187
|
+
this._lastReset = { reason, previousPosition: this.committedPosition };
|
|
188
|
+
this.filePosition = 0;
|
|
189
|
+
this.committedPosition = 0;
|
|
190
|
+
this.parser.reset();
|
|
191
|
+
this.decoder = new StringDecoder('utf8');
|
|
192
|
+
this.usageDeduper.reset();
|
|
193
|
+
}
|
|
177
194
|
readAll() {
|
|
178
195
|
this.reset();
|
|
179
196
|
return this.readNew();
|
|
@@ -184,7 +201,8 @@ class ClaudeCodeReader {
|
|
|
184
201
|
this.parser.reset();
|
|
185
202
|
this.decoder = new StringDecoder('utf8');
|
|
186
203
|
this.usageDeduper.reset();
|
|
187
|
-
this.
|
|
204
|
+
this._lastReset = null;
|
|
205
|
+
this.continuity.forget();
|
|
188
206
|
}
|
|
189
207
|
exists() {
|
|
190
208
|
return fs.existsSync(this.sessionPath);
|
|
@@ -204,9 +222,22 @@ class ClaudeCodeReader {
|
|
|
204
222
|
this.committedPosition = position;
|
|
205
223
|
this.parser.reset();
|
|
206
224
|
this.decoder = new StringDecoder('utf8');
|
|
225
|
+
// The position comes from elsewhere (a snapshot): adopt the file the next
|
|
226
|
+
// read finds rather than compare it with one read before.
|
|
227
|
+
this.continuity.forget();
|
|
207
228
|
}
|
|
208
229
|
wasTruncated() {
|
|
209
|
-
return this.
|
|
230
|
+
return this._lastReset !== null;
|
|
231
|
+
}
|
|
232
|
+
lastReset() {
|
|
233
|
+
return this._lastReset ? { ...this._lastReset } : null;
|
|
234
|
+
}
|
|
235
|
+
/** The usage deduper's recent message ids, so a resumed reader does not recount a split response. */
|
|
236
|
+
getResumeState() {
|
|
237
|
+
return this.usageDeduper.exportState();
|
|
238
|
+
}
|
|
239
|
+
restoreResumeState(state) {
|
|
240
|
+
return this.usageDeduper.importState(state);
|
|
210
241
|
}
|
|
211
242
|
}
|
|
212
243
|
/**
|