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
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex subagent discovery for `CodexProvider.scanSubagents()`.
|
|
3
|
+
*
|
|
4
|
+
* Every spawned thread below a session (children, their children, …), each
|
|
5
|
+
* with its rollout, the parent's spawn call that started it, its nickname and
|
|
6
|
+
* agent path, and stats from the child's own records. A forked child's
|
|
7
|
+
* rollout embeds the parent's session_meta and history before
|
|
8
|
+
* `session_meta.subagent_history_start_ordinal`; that prefix is the parent's
|
|
9
|
+
* and is not counted (the parser skips it, and so do the timestamps here).
|
|
10
|
+
*
|
|
11
|
+
* The spawn link comes from the parent's rollout: the `item_completed` →
|
|
12
|
+
* `SubAgentActivity` record with `kind: 'started'` names the spawn call id
|
|
13
|
+
* and the child thread. When a parent has no such record (older Codex), the
|
|
14
|
+
* spawn call whose output named the child's agent path
|
|
15
|
+
* (`{"task_name":"/root/x"}`) is used. Parent scans are cached and read
|
|
16
|
+
* incrementally as the parent rollout grows.
|
|
17
|
+
*
|
|
18
|
+
* Node-only: reads files.
|
|
19
|
+
*
|
|
20
|
+
* @module providers/codexSubagents
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from 'fs';
|
|
23
|
+
import * as path from 'path';
|
|
24
|
+
import { CodexRolloutParser } from '../parsers/codexParser';
|
|
25
|
+
import { cachedSubagentStats } from '../parsers/subagentStatsCache';
|
|
26
|
+
import { extractNormalizedUsage } from '../usageNormalization';
|
|
27
|
+
import { addUsageToSubagent } from '../usage/subagentUsage';
|
|
28
|
+
import { codexSpawnParentFromSource as spawnParentFromSource } from '../parsers/codexRolloutOrigin';
|
|
29
|
+
import { readSessionMeta } from './codexRollouts';
|
|
30
|
+
import { isCompressedRolloutPath, readRolloutBuffer, readRolloutText, resolveRolloutPath, } from './compressedRollouts';
|
|
31
|
+
import { extractRolloutSessionId } from './rolloutWalker';
|
|
32
|
+
const SPAWN_LINK_CACHE_LIMIT = 256;
|
|
33
|
+
const PENDING_SPAWN_LIMIT = 1024;
|
|
34
|
+
const spawnLinkCache = new Map();
|
|
35
|
+
/** @internal Test hook. */
|
|
36
|
+
export function clearCodexSpawnLinkCache() {
|
|
37
|
+
spawnLinkCache.clear();
|
|
38
|
+
}
|
|
39
|
+
function freshEntry(size, mtimeMs) {
|
|
40
|
+
return {
|
|
41
|
+
size,
|
|
42
|
+
mtimeMs,
|
|
43
|
+
offset: 0,
|
|
44
|
+
historyStart: undefined,
|
|
45
|
+
skipBelow: null,
|
|
46
|
+
pendingSpawns: new Set(),
|
|
47
|
+
links: { started: new Map(), spawnedPaths: new Map() },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function historyStartOf(meta) {
|
|
51
|
+
const value = meta?.subagent_history_start_ordinal;
|
|
52
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : null;
|
|
53
|
+
}
|
|
54
|
+
function record(value) {
|
|
55
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
56
|
+
? value
|
|
57
|
+
: undefined;
|
|
58
|
+
}
|
|
59
|
+
function nonEmpty(value) {
|
|
60
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
61
|
+
}
|
|
62
|
+
/** Applies one rollout line to a spawn-link scan; most lines are rejected by substring. */
|
|
63
|
+
function scanLine(entry, line) {
|
|
64
|
+
const isMeta = line.includes('"session_meta"');
|
|
65
|
+
const isActivity = line.includes('"SubAgentActivity"');
|
|
66
|
+
const isSpawnCall = line.includes('"spawn_agent"') && line.includes('"function_call"');
|
|
67
|
+
const isOutput = entry.pendingSpawns.size > 0 && line.includes('"function_call_output"');
|
|
68
|
+
if (!isMeta && !isActivity && !isSpawnCall && !isOutput)
|
|
69
|
+
return;
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(line);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const payload = record(parsed.payload);
|
|
78
|
+
if (!payload)
|
|
79
|
+
return;
|
|
80
|
+
if (parsed.type === 'session_meta') {
|
|
81
|
+
// As the parser: the own meta comes first; an embedded ancestor meta
|
|
82
|
+
// below the history start makes the records below it the parent's.
|
|
83
|
+
if (entry.historyStart === undefined) {
|
|
84
|
+
entry.historyStart = historyStartOf(payload);
|
|
85
|
+
}
|
|
86
|
+
else if (entry.skipBelow === null &&
|
|
87
|
+
typeof entry.historyStart === 'number' &&
|
|
88
|
+
typeof parsed.ordinal === 'number' &&
|
|
89
|
+
parsed.ordinal < entry.historyStart) {
|
|
90
|
+
entry.skipBelow = entry.historyStart;
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const skipBelow = entry.skipBelow;
|
|
95
|
+
if (skipBelow !== null && typeof parsed.ordinal === 'number' && parsed.ordinal < skipBelow) {
|
|
96
|
+
return; // the parent's history, embedded in a forked child
|
|
97
|
+
}
|
|
98
|
+
if (parsed.type === 'event_msg' && payload.type === 'item_completed') {
|
|
99
|
+
const item = record(payload.item);
|
|
100
|
+
if (item?.type !== 'SubAgentActivity' || item.kind !== 'started')
|
|
101
|
+
return;
|
|
102
|
+
const threadId = nonEmpty(item.agent_thread_id);
|
|
103
|
+
const callId = nonEmpty(item.id);
|
|
104
|
+
if (!threadId || !callId)
|
|
105
|
+
return;
|
|
106
|
+
const agentPath = nonEmpty(item.agent_path);
|
|
107
|
+
entry.links.started.set(threadId, { callId, ...(agentPath ? { agentPath } : {}) });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (parsed.type !== 'response_item')
|
|
111
|
+
return;
|
|
112
|
+
const callId = nonEmpty(payload.call_id);
|
|
113
|
+
if (!callId)
|
|
114
|
+
return;
|
|
115
|
+
if (payload.type === 'function_call' && payload.name === 'spawn_agent') {
|
|
116
|
+
entry.pendingSpawns.add(callId);
|
|
117
|
+
while (entry.pendingSpawns.size > PENDING_SPAWN_LIMIT) {
|
|
118
|
+
entry.pendingSpawns.delete(entry.pendingSpawns.values().next().value);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (payload.type === 'function_call_output' && entry.pendingSpawns.has(callId)) {
|
|
123
|
+
entry.pendingSpawns.delete(callId);
|
|
124
|
+
let output;
|
|
125
|
+
try {
|
|
126
|
+
output = JSON.parse(String(payload.output));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return; // e.g. "collab spawn failed: …"
|
|
130
|
+
}
|
|
131
|
+
const taskName = nonEmpty(record(output)?.task_name);
|
|
132
|
+
if (!taskName)
|
|
133
|
+
return;
|
|
134
|
+
const calls = entry.links.spawnedPaths.get(taskName) ?? [];
|
|
135
|
+
calls.push(callId);
|
|
136
|
+
entry.links.spawnedPaths.set(taskName, calls);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function remember(filePath, entry) {
|
|
140
|
+
spawnLinkCache.delete(filePath);
|
|
141
|
+
spawnLinkCache.set(filePath, entry);
|
|
142
|
+
while (spawnLinkCache.size > SPAWN_LINK_CACHE_LIMIT) {
|
|
143
|
+
spawnLinkCache.delete(spawnLinkCache.keys().next().value);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The spawn calls a rollout records (see {@link CodexSpawnLinks}). Cached per
|
|
148
|
+
* path; a growing `.jsonl` is read only past what was read before, a
|
|
149
|
+
* compressed one whole. `null` when the rollout cannot be read.
|
|
150
|
+
*/
|
|
151
|
+
export function readCodexSpawnLinks(filePath, options = {}) {
|
|
152
|
+
let stat;
|
|
153
|
+
try {
|
|
154
|
+
stat = fs.statSync(filePath);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
spawnLinkCache.delete(filePath);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
let entry = spawnLinkCache.get(filePath);
|
|
161
|
+
if (entry && entry.size === stat.size && entry.mtimeMs === stat.mtimeMs) {
|
|
162
|
+
remember(filePath, entry);
|
|
163
|
+
return entry.links;
|
|
164
|
+
}
|
|
165
|
+
if (isCompressedRolloutPath(filePath)) {
|
|
166
|
+
const content = readRolloutBuffer(filePath, options);
|
|
167
|
+
if (!content)
|
|
168
|
+
return null;
|
|
169
|
+
entry = freshEntry(stat.size, stat.mtimeMs);
|
|
170
|
+
for (const line of content.toString('utf8').split('\n'))
|
|
171
|
+
if (line)
|
|
172
|
+
scanLine(entry, line);
|
|
173
|
+
remember(filePath, entry);
|
|
174
|
+
return entry.links;
|
|
175
|
+
}
|
|
176
|
+
// A rollout that shrank was replaced: scan it again from the start.
|
|
177
|
+
if (!entry || stat.size < entry.offset || stat.size < entry.size) {
|
|
178
|
+
entry = freshEntry(stat.size, stat.mtimeMs);
|
|
179
|
+
}
|
|
180
|
+
let fd;
|
|
181
|
+
try {
|
|
182
|
+
fd = fs.openSync(filePath, 'r');
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const length = stat.size - entry.offset;
|
|
189
|
+
if (length > 0) {
|
|
190
|
+
const buffer = Buffer.alloc(length);
|
|
191
|
+
const bytesRead = fs.readSync(fd, buffer, 0, length, entry.offset);
|
|
192
|
+
const bytes = buffer.subarray(0, bytesRead);
|
|
193
|
+
const lastNewline = bytes.lastIndexOf(0x0a);
|
|
194
|
+
if (lastNewline >= 0) {
|
|
195
|
+
for (const line of bytes.toString('utf8', 0, lastNewline).split('\n')) {
|
|
196
|
+
if (line)
|
|
197
|
+
scanLine(entry, line);
|
|
198
|
+
}
|
|
199
|
+
entry.offset += lastNewline + 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
fs.closeSync(fd);
|
|
208
|
+
}
|
|
209
|
+
entry.size = stat.size;
|
|
210
|
+
entry.mtimeMs = stat.mtimeMs;
|
|
211
|
+
remember(filePath, entry);
|
|
212
|
+
return entry.links;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Stats of one subagent rollout from its own records: tool calls, tokens,
|
|
216
|
+
* and first/last timestamps after the embedded parent history (the child's
|
|
217
|
+
* own `session_meta` counts). Reads `.jsonl.zst` when the runtime can.
|
|
218
|
+
* `null` when the rollout is missing or cannot be read.
|
|
219
|
+
*/
|
|
220
|
+
export function buildCodexSubagentStats(agentId, rolloutPath, description, options = {}) {
|
|
221
|
+
if (!rolloutPath || !fs.existsSync(rolloutPath))
|
|
222
|
+
return null;
|
|
223
|
+
const content = readRolloutText(rolloutPath, options);
|
|
224
|
+
if (content === null)
|
|
225
|
+
return null;
|
|
226
|
+
const stats = {
|
|
227
|
+
agentId,
|
|
228
|
+
description: description || undefined,
|
|
229
|
+
toolCalls: [],
|
|
230
|
+
inputTokens: 0,
|
|
231
|
+
outputTokens: 0,
|
|
232
|
+
};
|
|
233
|
+
const parser = new CodexRolloutParser();
|
|
234
|
+
let sawMeta = false;
|
|
235
|
+
// The child's last own turn boundary: `task_complete` means it ended cleanly.
|
|
236
|
+
let lastTurnEvent;
|
|
237
|
+
for (const line of content.split('\n')) {
|
|
238
|
+
const trimmed = line.trim();
|
|
239
|
+
if (!trimmed)
|
|
240
|
+
continue;
|
|
241
|
+
let parsed;
|
|
242
|
+
try {
|
|
243
|
+
parsed = JSON.parse(trimmed);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
let events;
|
|
249
|
+
try {
|
|
250
|
+
events = parser.convertLine(parsed);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
continue; // Skip lines the parser rejects.
|
|
254
|
+
}
|
|
255
|
+
// Timestamps of the child's own records only, as the parser decides them.
|
|
256
|
+
let own;
|
|
257
|
+
if (parsed.type === 'session_meta') {
|
|
258
|
+
own = !sawMeta;
|
|
259
|
+
sawMeta = true;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
const end = parser.getInheritedHistoryEnd();
|
|
263
|
+
own = !(end !== null && typeof parsed.ordinal === 'number' && parsed.ordinal < end);
|
|
264
|
+
}
|
|
265
|
+
if (own && parsed.type === 'event_msg') {
|
|
266
|
+
const payloadType = parsed.payload?.type;
|
|
267
|
+
if (payloadType === 'task_started' ||
|
|
268
|
+
payloadType === 'task_complete' ||
|
|
269
|
+
payloadType === 'turn_aborted') {
|
|
270
|
+
lastTurnEvent = payloadType;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (own && parsed.timestamp) {
|
|
274
|
+
const ts = new Date(parsed.timestamp);
|
|
275
|
+
if (!Number.isNaN(ts.getTime())) {
|
|
276
|
+
if (!stats.startTime || ts < stats.startTime)
|
|
277
|
+
stats.startTime = ts;
|
|
278
|
+
if (!stats.endTime || ts > stats.endTime)
|
|
279
|
+
stats.endTime = ts;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const event of events) {
|
|
283
|
+
if (event.type === 'assistant' && Array.isArray(event.message.content)) {
|
|
284
|
+
for (const block of event.message.content) {
|
|
285
|
+
if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
286
|
+
stats.toolCalls.push({
|
|
287
|
+
name: block.name,
|
|
288
|
+
input: block.input || {},
|
|
289
|
+
timestamp: new Date(event.timestamp),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Duplicate token_count events are already dropped by the parser.
|
|
295
|
+
const usage = extractNormalizedUsage(event);
|
|
296
|
+
if (usage)
|
|
297
|
+
addUsageToSubagent(stats, usage);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (stats.startTime && stats.endTime) {
|
|
301
|
+
stats.durationMs = stats.endTime.getTime() - stats.startTime.getTime();
|
|
302
|
+
}
|
|
303
|
+
if (lastTurnEvent)
|
|
304
|
+
stats.endedCleanly = lastTurnEvent === 'task_complete';
|
|
305
|
+
return stats;
|
|
306
|
+
}
|
|
307
|
+
function spawnSource(source) {
|
|
308
|
+
let value = source;
|
|
309
|
+
if (typeof value === 'string') {
|
|
310
|
+
if (!value.startsWith('{'))
|
|
311
|
+
return undefined;
|
|
312
|
+
try {
|
|
313
|
+
value = JSON.parse(value);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return record(record(record(value)?.subagent)?.thread_spawn);
|
|
320
|
+
}
|
|
321
|
+
function childFromDbThread(thread, parentId, depth) {
|
|
322
|
+
const spawn = spawnSource(thread.source);
|
|
323
|
+
return {
|
|
324
|
+
id: thread.id,
|
|
325
|
+
parentId,
|
|
326
|
+
depth,
|
|
327
|
+
rolloutPath: thread.rollout_path,
|
|
328
|
+
description: thread.title || thread.agent_nickname,
|
|
329
|
+
agentType: thread.agent_role,
|
|
330
|
+
agentNickname: thread.agent_nickname ?? nonEmpty(spawn?.agent_nickname),
|
|
331
|
+
agentPath: thread.agent_path ?? nonEmpty(spawn?.agent_path),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/** Children below `sessionId` from the state database; empty when it knows none. */
|
|
335
|
+
function childrenFromDb(db, sessionId) {
|
|
336
|
+
const children = [];
|
|
337
|
+
const seen = new Set();
|
|
338
|
+
for (const spawned of db.getSpawnedDescendantThreads(sessionId)) {
|
|
339
|
+
seen.add(spawned.thread.id);
|
|
340
|
+
children.push(childFromDbThread(spawned.thread, spawned.parentId, spawned.depth));
|
|
341
|
+
}
|
|
342
|
+
// Older schemas record forks in `forked_from_id` (direct children only).
|
|
343
|
+
for (const thread of db.getThreadsByForkedFromId(sessionId)) {
|
|
344
|
+
if (seen.has(thread.id))
|
|
345
|
+
continue;
|
|
346
|
+
seen.add(thread.id);
|
|
347
|
+
children.push(childFromDbThread(thread, sessionId, 1));
|
|
348
|
+
}
|
|
349
|
+
return children;
|
|
350
|
+
}
|
|
351
|
+
/** Children below `sessionId` among recent rollouts, by their session_meta. */
|
|
352
|
+
function childrenFromFiles(files, sessionId) {
|
|
353
|
+
const candidates = files
|
|
354
|
+
.map((file) => ({ file, meta: readSessionMeta(file.path) }))
|
|
355
|
+
.filter((entry) => !!entry.meta);
|
|
356
|
+
const depthOf = new Map([[sessionId, 0]]);
|
|
357
|
+
const children = [];
|
|
358
|
+
let added = true;
|
|
359
|
+
while (added) {
|
|
360
|
+
added = false;
|
|
361
|
+
for (const { file, meta } of candidates) {
|
|
362
|
+
const id = extractRolloutSessionId(path.basename(file.path));
|
|
363
|
+
if (depthOf.has(id))
|
|
364
|
+
continue;
|
|
365
|
+
const parentId = meta.forked_from_id ?? spawnParentFromSource(meta.source);
|
|
366
|
+
const parentDepth = parentId !== undefined ? depthOf.get(parentId) : undefined;
|
|
367
|
+
if (parentId === undefined || parentDepth === undefined)
|
|
368
|
+
continue;
|
|
369
|
+
depthOf.set(id, parentDepth + 1);
|
|
370
|
+
added = true;
|
|
371
|
+
const spawn = spawnSource(meta.source);
|
|
372
|
+
// `source` is a plain string for top-level sessions but an object
|
|
373
|
+
// (`{ subagent: { thread_spawn: … } }`) for spawned children.
|
|
374
|
+
const description = meta.agent_nickname || (typeof meta.source === 'string' ? meta.source : undefined);
|
|
375
|
+
children.push({
|
|
376
|
+
id,
|
|
377
|
+
parentId,
|
|
378
|
+
depth: parentDepth + 1,
|
|
379
|
+
rolloutPath: file.path,
|
|
380
|
+
description,
|
|
381
|
+
agentType: meta.agent_role ?? nonEmpty(spawn?.agent_role),
|
|
382
|
+
agentNickname: meta.agent_nickname ?? nonEmpty(spawn?.agent_nickname),
|
|
383
|
+
agentPath: meta.agent_path ?? nonEmpty(spawn?.agent_path),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return children;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Every subagent below a session with links and own-record stats. Children
|
|
391
|
+
* whose rollout is gone (or compressed, without `includeCompressed`) are
|
|
392
|
+
* left out, as before.
|
|
393
|
+
*/
|
|
394
|
+
export function discoverCodexSubagents(sessionId, options) {
|
|
395
|
+
const readOptions = { onDiagnostic: options.onDiagnostic };
|
|
396
|
+
let children = options.db ? childrenFromDb(options.db, sessionId) : [];
|
|
397
|
+
if (children.length === 0)
|
|
398
|
+
children = childrenFromFiles(options.fallbackFiles(), sessionId);
|
|
399
|
+
if (children.length === 0)
|
|
400
|
+
return [];
|
|
401
|
+
const pathOf = new Map();
|
|
402
|
+
for (const child of children) {
|
|
403
|
+
pathOf.set(child.id, resolveRolloutPath(child.rolloutPath, { includeCompressed: options.includeCompressed }));
|
|
404
|
+
}
|
|
405
|
+
const linksOf = new Map();
|
|
406
|
+
const claimed = new Map();
|
|
407
|
+
const links = (parentId) => {
|
|
408
|
+
if (!linksOf.has(parentId)) {
|
|
409
|
+
const parentPath = parentId === sessionId ? options.sessionRolloutPath() : (pathOf.get(parentId) ?? null);
|
|
410
|
+
linksOf.set(parentId, parentPath ? readCodexSpawnLinks(parentPath, readOptions) : null);
|
|
411
|
+
}
|
|
412
|
+
return linksOf.get(parentId) ?? null;
|
|
413
|
+
};
|
|
414
|
+
// Started links first, so the agent-path fallback never takes a claimed call.
|
|
415
|
+
const linked = new Map();
|
|
416
|
+
for (const child of children) {
|
|
417
|
+
const callId = links(child.parentId)?.started.get(child.id)?.callId;
|
|
418
|
+
if (!callId)
|
|
419
|
+
continue;
|
|
420
|
+
linked.set(child.id, callId);
|
|
421
|
+
const set = claimed.get(child.parentId) ?? new Set();
|
|
422
|
+
set.add(callId);
|
|
423
|
+
claimed.set(child.parentId, set);
|
|
424
|
+
}
|
|
425
|
+
const results = [];
|
|
426
|
+
for (const child of children) {
|
|
427
|
+
const filePath = pathOf.get(child.id);
|
|
428
|
+
if (!filePath)
|
|
429
|
+
continue;
|
|
430
|
+
const built = cachedSubagentStats(filePath, () => buildCodexSubagentStats(child.id, filePath, child.description, readOptions));
|
|
431
|
+
if (!built)
|
|
432
|
+
continue;
|
|
433
|
+
let spawnToolUseId = linked.get(child.id);
|
|
434
|
+
const parentLinks = links(child.parentId);
|
|
435
|
+
const agentPath = child.agentPath ?? parentLinks?.started.get(child.id)?.agentPath;
|
|
436
|
+
if (!spawnToolUseId && agentPath && parentLinks) {
|
|
437
|
+
const used = claimed.get(child.parentId) ?? new Set();
|
|
438
|
+
spawnToolUseId = parentLinks.spawnedPaths.get(agentPath)?.find((id) => !used.has(id));
|
|
439
|
+
if (spawnToolUseId) {
|
|
440
|
+
used.add(spawnToolUseId);
|
|
441
|
+
claimed.set(child.parentId, used);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
results.push({
|
|
445
|
+
...built,
|
|
446
|
+
...(child.agentType && !built.agentType ? { agentType: child.agentType } : {}),
|
|
447
|
+
filePath,
|
|
448
|
+
...(spawnToolUseId ? { spawnToolUseId } : {}),
|
|
449
|
+
...(child.agentNickname ? { agentNickname: child.agentNickname } : {}),
|
|
450
|
+
...(agentPath ? { agentPath } : {}),
|
|
451
|
+
parentAgentId: child.parentId,
|
|
452
|
+
depth: child.depth,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
return results;
|
|
456
|
+
}
|