wendkeep 0.66.4 → 0.67.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.en.md +78 -5
  3. package/README.md +78 -5
  4. package/docs/en/commands/costs-and-observability.md +21 -7
  5. package/docs/en/commands/maintenance-and-diagnostics.md +13 -1
  6. package/docs/en/commands/operating-profiles.md +65 -10
  7. package/docs/en/commands/sessions-and-import.md +22 -1
  8. package/docs/en/commands/verify.md +5 -3
  9. package/docs/pt-BR/commands/costs-and-observability.md +21 -7
  10. package/docs/pt-BR/commands/maintenance-and-diagnostics.md +12 -1
  11. package/docs/pt-BR/commands/operating-profiles.md +66 -11
  12. package/docs/pt-BR/commands/sessions-and-import.md +20 -0
  13. package/docs/pt-BR/commands/verify.md +6 -3
  14. package/hooks/change-nag.mjs +8 -0
  15. package/hooks/codex-rollout-meta.mjs +112 -0
  16. package/hooks/codex-subagent-graph.mjs +903 -0
  17. package/hooks/harness-doctor.mjs +82 -1
  18. package/hooks/import-sessions.mjs +185 -50
  19. package/hooks/operating-profile-runtime.mjs +36 -2
  20. package/hooks/operating-profile-task-store.mjs +77 -0
  21. package/hooks/session-identity.mjs +40 -5
  22. package/hooks/session-observability-lifecycle.mjs +129 -0
  23. package/hooks/session-observability-state.mjs +241 -0
  24. package/hooks/session-observability-store.mjs +436 -0
  25. package/hooks/session-observability.mjs +647 -21
  26. package/hooks/session-stop.mjs +339 -11
  27. package/hooks/subagent-stop.mjs +266 -12
  28. package/hooks/subagent-usage.mjs +65 -0
  29. package/hooks/token-usage.mjs +81 -4
  30. package/package.json +3 -3
  31. package/packages/harness/src/operating-profile.mjs +127 -0
  32. package/packages/harness/src/sensors-core.mjs +41 -1
  33. package/packages/integrations/src/prompt-content.mjs +123 -0
  34. package/packages/integrations/src/transcripts.mjs +16 -10
  35. package/src/cost.mjs +40 -6
  36. package/src/doctor.mjs +4 -1
  37. package/src/profile.mjs +95 -17
  38. package/src/rebuild-costs.mjs +220 -34
  39. package/src/skills-seed.mjs +38 -2
  40. package/src/sync-defs.mjs +6 -1
@@ -0,0 +1,903 @@
1
+ import { createHash } from "node:crypto";
2
+ import { closeSync, openSync, readSync, readdirSync, statSync } from "node:fs";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { StringDecoder } from "node:string_decoder";
5
+
6
+ import { readCodexRolloutMeta } from "./codex-rollout-meta.mjs";
7
+ import {
8
+ addUsage,
9
+ costBreakdown,
10
+ emptyTokenUsage,
11
+ normalizeCodexUsage,
12
+ priceForModel,
13
+ } from "./token-usage.mjs";
14
+
15
+ export const CODEX_SUBAGENT_GRAPH_LIMITS = Object.freeze({
16
+ maxGraphNodes: 4096,
17
+ maxFallbackDays: 31,
18
+ maxFallbackCandidates: 20_000,
19
+ maxLiveUncachedBytes: 512 * 1024 * 1024,
20
+ });
21
+
22
+ const READ_CHUNK_BYTES = 64 * 1024;
23
+ const CACHE_VERSION = 1;
24
+ const DIAGNOSTIC_ORDER = [
25
+ "PARENT_META_INVALID",
26
+ "CHILD_MISSING",
27
+ "CHILD_META_INVALID",
28
+ "ROOT_MISMATCH",
29
+ "LEGACY_CHAIN_UNPROVEN",
30
+ "DUPLICATE_ROLLOUT_ID",
31
+ "GRAPH_LIMIT_EXCEEDED",
32
+ "FALLBACK_LIMIT_EXCEEDED",
33
+ "LIVE_BYTE_BUDGET_EXCEEDED",
34
+ "LIVE_DEADLINE_EXCEEDED",
35
+ "SOURCE_CHANGED_DURING_SCAN",
36
+ "CACHE_INVALID",
37
+ ];
38
+
39
+ const sha256 = (value) => createHash("sha256").update(value).digest("hex");
40
+ const stableHash = (value) => sha256(JSON.stringify(value));
41
+ const round4 = (value) => Math.round((Number(value) || 0) * 10_000) / 10_000;
42
+
43
+ function tokenTotal(usage) {
44
+ const explicit = Number(usage?.total);
45
+ if (Number.isFinite(explicit) && explicit > 0) return explicit;
46
+ return (
47
+ (Number(usage?.input) || 0) +
48
+ (Number(usage?.cached) || 0) +
49
+ (Number(usage?.cacheWrite) || 0) +
50
+ (Number(usage?.output) || 0)
51
+ );
52
+ }
53
+
54
+ function normalizedLimits(overrides = {}) {
55
+ const result = { ...CODEX_SUBAGENT_GRAPH_LIMITS };
56
+ for (const key of Object.keys(result)) {
57
+ const value = Number(overrides?.[key]);
58
+ if (Number.isSafeInteger(value) && value > 0) result[key] = value;
59
+ }
60
+ return result;
61
+ }
62
+
63
+ function makeClock(now) {
64
+ if (typeof now === "function") {
65
+ return (phase) => {
66
+ const value = Number(now(phase));
67
+ return Number.isFinite(value) ? value : Date.now();
68
+ };
69
+ }
70
+ if (Number.isFinite(Number(now))) return () => Number(now);
71
+ return () => Date.now();
72
+ }
73
+
74
+ function safeStat(path) {
75
+ try {
76
+ const stat = statSync(path);
77
+ if (!stat.isFile()) return null;
78
+ return { size: stat.size, mtimeMs: stat.mtimeMs };
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ function sameStat(left, right) {
85
+ return Boolean(
86
+ left &&
87
+ right &&
88
+ Number(left.size) === Number(right.size) &&
89
+ Number(left.mtimeMs) === Number(right.mtimeMs),
90
+ );
91
+ }
92
+
93
+ function cacheKey(rolloutId, stat) {
94
+ return `${rolloutId}\u0000${stat.size}\u0000${stat.mtimeMs}`;
95
+ }
96
+
97
+ function validCachedSummary(value, rolloutId) {
98
+ return Boolean(
99
+ value &&
100
+ typeof value === "object" &&
101
+ value.rolloutId === rolloutId &&
102
+ Array.isArray(value.activities) &&
103
+ Array.isArray(value.tools) &&
104
+ Array.isArray(value.modelRows) &&
105
+ value.totals &&
106
+ typeof value.totals === "object",
107
+ );
108
+ }
109
+
110
+ function normalizeInputCache(cache, diagnostic) {
111
+ if (cache == null) return {};
112
+ if (
113
+ !cache ||
114
+ typeof cache !== "object" ||
115
+ cache.version !== CACHE_VERSION ||
116
+ !cache.entries ||
117
+ typeof cache.entries !== "object" ||
118
+ Array.isArray(cache.entries)
119
+ ) {
120
+ diagnostic("CACHE_INVALID");
121
+ return {};
122
+ }
123
+ return cache.entries;
124
+ }
125
+
126
+ function parseDateParts(value) {
127
+ const match = String(value || "").match(/^(\d{4})-(\d{2})-(\d{2})/);
128
+ if (!match) return null;
129
+ const date = new Date(
130
+ Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])),
131
+ );
132
+ if (!Number.isFinite(date.getTime())) return null;
133
+ return {
134
+ date,
135
+ year: match[1],
136
+ month: match[2],
137
+ day: match[3],
138
+ key: `${match[1]}-${match[2]}-${match[3]}`,
139
+ };
140
+ }
141
+
142
+ function rolloutLocation(path) {
143
+ const normalized = String(path || "").replace(/\\/g, "/");
144
+ const match = normalized.match(/^(.*)\/(\d{4})\/(\d{2})\/(\d{2})\/[^/]+$/);
145
+ if (!match) return null;
146
+ return {
147
+ base: match[1],
148
+ date: parseDateParts(`${match[2]}-${match[3]}-${match[4]}`),
149
+ };
150
+ }
151
+
152
+ function dayDir(base, dateParts) {
153
+ return join(base, dateParts.year, dateParts.month, dateParts.day);
154
+ }
155
+
156
+ function addDays(date, amount) {
157
+ return new Date(date.getTime() + amount * 86_400_000);
158
+ }
159
+
160
+ function partsFromDate(date) {
161
+ const pad = (value) => String(value).padStart(2, "0");
162
+ return parseDateParts(
163
+ `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`,
164
+ );
165
+ }
166
+
167
+ function listJsonl(dir) {
168
+ try {
169
+ return readdirSync(dir)
170
+ .filter((name) => name.toLowerCase().endsWith(".jsonl"))
171
+ .sort()
172
+ .map((name) => join(dir, name));
173
+ } catch {
174
+ return [];
175
+ }
176
+ }
177
+
178
+ function idMatchesFilename(path, rolloutId) {
179
+ const name = basename(path).toLowerCase();
180
+ return (
181
+ name.endsWith(`-${String(rolloutId).toLowerCase()}.jsonl`) ||
182
+ name === `${String(rolloutId).toLowerCase()}.jsonl`
183
+ );
184
+ }
185
+
186
+ function normalizeActivity(event, parentRolloutId) {
187
+ const payload = event?.payload || {};
188
+ if (event?.type !== "event_msg" || payload.type !== "sub_agent_activity")
189
+ return null;
190
+ const childId = String(
191
+ payload.agent_thread_id || payload.agentThreadId || "",
192
+ ).trim();
193
+ const kind = String(payload.kind || "")
194
+ .trim()
195
+ .toLowerCase();
196
+ if (!childId || !["started", "interacted", "interrupted"].includes(kind))
197
+ return null;
198
+ return {
199
+ parentRolloutId,
200
+ childId,
201
+ kind,
202
+ timestamp: event.timestamp || payload.timestamp || "",
203
+ agentPath: String(payload.agent_path || payload.agentPath || ""),
204
+ transcriptPath: "",
205
+ source: "transcript",
206
+ };
207
+ }
208
+
209
+ function normalizeSignal(signal) {
210
+ if (!signal || typeof signal !== "object") return null;
211
+ const childId = String(
212
+ signal.rolloutId ||
213
+ signal.rollout_id ||
214
+ signal.agentThreadId ||
215
+ signal.agent_thread_id ||
216
+ "",
217
+ ).trim();
218
+ const parentRolloutId = String(
219
+ signal.parentRolloutId ||
220
+ signal.parent_rollout_id ||
221
+ signal.parentThreadId ||
222
+ signal.parent_thread_id ||
223
+ "",
224
+ ).trim();
225
+ if (!childId) return null;
226
+ const rawKind = String(
227
+ signal.kind || signal.eventKind || signal.event_kind || "started",
228
+ ).toLowerCase();
229
+ const kind = ["interacted", "interrupted"].includes(rawKind)
230
+ ? rawKind
231
+ : "started";
232
+ return {
233
+ parentRolloutId,
234
+ childId,
235
+ kind,
236
+ timestamp: signal.timestamp || signal.startedAt || signal.started_at || "",
237
+ agentPath: String(signal.agentPath || signal.agent_path || ""),
238
+ transcriptPath: String(
239
+ signal.transcriptPath || signal.transcript_path || signal.path || "",
240
+ ),
241
+ source: "signal",
242
+ };
243
+ }
244
+
245
+ function modelRowCost(row) {
246
+ return costBreakdown(row.usage, priceForModel(row.model))?.total || 0;
247
+ }
248
+
249
+ function emptyScanSummary(rolloutId) {
250
+ return {
251
+ rolloutId,
252
+ activities: [],
253
+ malformedLines: 0,
254
+ effort: "",
255
+ calls: 0,
256
+ tools: [],
257
+ totals: emptyTokenUsage(),
258
+ modelRows: [],
259
+ cost: 0,
260
+ };
261
+ }
262
+
263
+ function parseRolloutStream(path, rolloutId, shouldContinue) {
264
+ const summary = emptyScanSummary(rolloutId);
265
+ const tools = new Set();
266
+ const byModel = new Map();
267
+ let currentModel = "unknown";
268
+ let currentProvider = "unknown";
269
+ let fd;
270
+ let carry = "";
271
+ const decoder = new StringDecoder("utf8");
272
+
273
+ const parseLine = (line) => {
274
+ if (!line) return;
275
+ let event;
276
+ try {
277
+ event = JSON.parse(line);
278
+ } catch {
279
+ summary.malformedLines += 1;
280
+ return;
281
+ }
282
+ const payload = event?.payload || {};
283
+ if (event?.type === "session_meta") {
284
+ currentModel = String(payload.model || currentModel || "unknown");
285
+ currentProvider = String(
286
+ payload.model_provider || currentProvider || "unknown",
287
+ );
288
+ return;
289
+ }
290
+ if (event?.type === "turn_context") {
291
+ currentModel = String(payload.model || currentModel || "unknown");
292
+ currentProvider = String(
293
+ payload.model_provider || currentProvider || "unknown",
294
+ );
295
+ summary.effort = String(
296
+ payload.effort ||
297
+ payload.reasoning_effort ||
298
+ payload.collaboration_mode?.settings?.reasoning_effort ||
299
+ summary.effort ||
300
+ "",
301
+ );
302
+ return;
303
+ }
304
+ const subagentActivity = normalizeActivity(event, rolloutId);
305
+ if (subagentActivity) {
306
+ summary.activities.push(subagentActivity);
307
+ return;
308
+ }
309
+ if (event?.type === "response_item" && payload.type === "function_call") {
310
+ tools.add(String(payload.name || "function_call"));
311
+ return;
312
+ }
313
+ if (
314
+ event?.type === "response_item" &&
315
+ payload.type === "tool_search_call"
316
+ ) {
317
+ tools.add("tool_search");
318
+ return;
319
+ }
320
+ if (event?.type === "response_item" && payload.type === "web_search_call") {
321
+ tools.add("web_search");
322
+ return;
323
+ }
324
+ if (event?.type !== "event_msg" || payload.type !== "token_count") return;
325
+ const info = payload.info || {};
326
+ if (!info.last_token_usage) return;
327
+ const usage = normalizeCodexUsage(info.last_token_usage);
328
+ const model = String(
329
+ info.model || payload.model || currentModel || "unknown",
330
+ );
331
+ const provider = String(
332
+ info.model_provider ||
333
+ payload.model_provider ||
334
+ currentProvider ||
335
+ "unknown",
336
+ );
337
+ const key = `${provider}\u0000${model}\u0000${summary.effort}`;
338
+ const row = byModel.get(key) || {
339
+ provider,
340
+ model,
341
+ effort: summary.effort,
342
+ calls: 0,
343
+ usage: emptyTokenUsage(),
344
+ };
345
+ row.calls += 1;
346
+ addUsage(row.usage, usage);
347
+ byModel.set(key, row);
348
+ summary.calls += 1;
349
+ addUsage(summary.totals, usage);
350
+ };
351
+
352
+ try {
353
+ fd = openSync(path, "r");
354
+ const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES);
355
+ while (true) {
356
+ if (!shouldContinue("chunk")) return { ok: false, summary };
357
+ const bytesRead = readSync(fd, buffer, 0, buffer.length, null);
358
+ if (bytesRead === 0) break;
359
+ carry += decoder.write(buffer.subarray(0, bytesRead));
360
+ let newlineAt;
361
+ while ((newlineAt = carry.indexOf("\n")) !== -1) {
362
+ const line = carry.slice(0, newlineAt).replace(/\r$/, "");
363
+ carry = carry.slice(newlineAt + 1);
364
+ parseLine(line);
365
+ }
366
+ }
367
+ carry += decoder.end();
368
+ if (carry) parseLine(carry.replace(/\r$/, ""));
369
+ } catch {
370
+ return { ok: false, summary };
371
+ } finally {
372
+ if (fd !== undefined) {
373
+ try {
374
+ closeSync(fd);
375
+ } catch {
376
+ /* already closed */
377
+ }
378
+ }
379
+ }
380
+
381
+ summary.tools = [...tools].sort();
382
+ summary.modelRows = [...byModel.values()]
383
+ .sort((left, right) =>
384
+ `${left.provider}\u0000${left.model}\u0000${left.effort}`.localeCompare(
385
+ `${right.provider}\u0000${right.model}\u0000${right.effort}`,
386
+ ),
387
+ )
388
+ .map((row) => {
389
+ const cost = round4(modelRowCost(row));
390
+ return {
391
+ ...row,
392
+ tokens: tokenTotal(row.usage),
393
+ cost,
394
+ costs: { model: cost },
395
+ };
396
+ });
397
+ summary.cost = round4(
398
+ summary.modelRows.reduce((total, row) => total + row.cost, 0),
399
+ );
400
+ return { ok: true, summary };
401
+ }
402
+
403
+ function fileDigest(path, shouldContinue) {
404
+ let fd;
405
+ const hash = createHash("sha256");
406
+ try {
407
+ fd = openSync(path, "r");
408
+ const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES);
409
+ while (true) {
410
+ if (!shouldContinue("duplicate-hash")) return null;
411
+ const bytesRead = readSync(fd, buffer, 0, buffer.length, null);
412
+ if (bytesRead === 0) break;
413
+ hash.update(buffer.subarray(0, bytesRead));
414
+ }
415
+ return hash.digest("hex");
416
+ } catch {
417
+ return null;
418
+ } finally {
419
+ if (fd !== undefined) {
420
+ try {
421
+ closeSync(fd);
422
+ } catch {
423
+ /* already closed */
424
+ }
425
+ }
426
+ }
427
+ }
428
+
429
+ function sourceSubagent(meta) {
430
+ return meta?.source?.subagent || null;
431
+ }
432
+
433
+ function immediateParent(meta) {
434
+ return String(
435
+ meta?.parent_thread_id ||
436
+ sourceSubagent(meta)?.thread_spawn?.parent_thread_id ||
437
+ "",
438
+ ).trim();
439
+ }
440
+
441
+ function makeAgent(meta, summary, status, fallbackDepth) {
442
+ const spawn = sourceSubagent(meta)?.thread_spawn || {};
443
+ const calls = Number(summary.calls) || 0;
444
+ const tokens = tokenTotal(summary.totals);
445
+ return {
446
+ id: String(meta.id),
447
+ parentId: immediateParent(meta),
448
+ depth: Number(spawn.depth) || fallbackDepth,
449
+ agentType: String(spawn.agent_nickname || "codex-subagent"),
450
+ workflow: null,
451
+ status: status || "started",
452
+ model:
453
+ calls > 0 ? String(summary.modelRows[0]?.model || "unknown") : "unknown",
454
+ effort: calls > 0 ? String(summary.effort || "") : "",
455
+ tools: summary.tools.length,
456
+ toolNames: [...summary.tools],
457
+ calls,
458
+ tokens,
459
+ cost: round4(summary.cost),
460
+ modelRows: summary.modelRows.map((row) => ({ ...row })),
461
+ };
462
+ }
463
+
464
+ function aggregateAgents(agents) {
465
+ const usage = emptyTokenUsage();
466
+ const tools = new Set();
467
+ const byModel = new Map();
468
+ let calls = 0;
469
+ let cost = 0;
470
+ for (const agent of agents) {
471
+ calls += agent.calls || 0;
472
+ cost += agent.cost || 0;
473
+ for (const name of agent.toolNames || []) tools.add(name);
474
+ for (const row of agent.modelRows || []) {
475
+ addUsage(usage, row.usage || {});
476
+ const key = `${row.provider || "unknown"}\u0000${row.model || "unknown"}\u0000${row.effort || ""}`;
477
+ const current = byModel.get(key) || {
478
+ provider: row.provider || "unknown",
479
+ model: row.model || "unknown",
480
+ effort: row.effort || "",
481
+ calls: 0,
482
+ usage: emptyTokenUsage(),
483
+ cost: 0,
484
+ };
485
+ current.calls += row.calls || 0;
486
+ addUsage(current.usage, row.usage || {});
487
+ current.cost += row.cost || row.costs?.model || 0;
488
+ byModel.set(key, current);
489
+ }
490
+ }
491
+ const modelRows = [...byModel.values()]
492
+ .sort((left, right) =>
493
+ `${left.provider}\u0000${left.model}\u0000${left.effort}`.localeCompare(
494
+ `${right.provider}\u0000${right.model}\u0000${right.effort}`,
495
+ ),
496
+ )
497
+ .map((row) => ({
498
+ ...row,
499
+ tokens: tokenTotal(row.usage),
500
+ cost: round4(row.cost),
501
+ costs: { model: round4(row.cost) },
502
+ source: "subagent",
503
+ }));
504
+ return {
505
+ count: agents.length,
506
+ calls,
507
+ tokens: tokenTotal(usage),
508
+ cost: round4(cost),
509
+ wasted: 0,
510
+ usage,
511
+ tools: [...tools].sort(),
512
+ modelRows,
513
+ };
514
+ }
515
+
516
+ /**
517
+ * Compose a deterministic Codex subagent graph from registered top-level rollouts.
518
+ * Transcripts are authoritative; cache is a caller-owned, discardable optimization.
519
+ */
520
+ export function composeCodexSubagentGraph({
521
+ rootPaths = [],
522
+ canonicalSessionId = "",
523
+ signals = [],
524
+ cache = null,
525
+ mode = "live",
526
+ limits: limitOverrides = {},
527
+ deadlineAt = Number.POSITIVE_INFINITY,
528
+ now,
529
+ } = {}) {
530
+ const live = mode !== "offline";
531
+ const limits = normalizedLimits(limitOverrides);
532
+ const clock = makeClock(now);
533
+ const diagnosticCounts = new Map();
534
+ const diagnostic = (code, count = 1) => {
535
+ if (!DIAGNOSTIC_ORDER.includes(code)) return;
536
+ diagnosticCounts.set(code, (diagnosticCounts.get(code) || 0) + count);
537
+ };
538
+ let aborted = false;
539
+ const shouldContinue = (phase) => {
540
+ if (aborted) return false;
541
+ if (live && clock(phase) >= Number(deadlineAt)) {
542
+ diagnostic("LIVE_DEADLINE_EXCEEDED");
543
+ aborted = true;
544
+ return false;
545
+ }
546
+ return true;
547
+ };
548
+
549
+ const inputCache = normalizeInputCache(cache, diagnostic);
550
+ const outputCache = {};
551
+ const stats = {
552
+ parsedRollouts: 0,
553
+ cacheHits: 0,
554
+ fallbackDays: 0,
555
+ fallbackCandidates: 0,
556
+ uncachedBytes: 0,
557
+ };
558
+ const sourceByPath = new Map();
559
+ const statuses = new Map();
560
+ const statusRank = { started: 1, interacted: 2, interrupted: 3 };
561
+ const updateStatus = (activityItem) => {
562
+ const current = statuses.get(activityItem.childId) || "started";
563
+ if ((statusRank[activityItem.kind] || 0) >= (statusRank[current] || 0)) {
564
+ statuses.set(activityItem.childId, activityItem.kind);
565
+ }
566
+ };
567
+ const graphEvidence = new Set();
568
+ const queue = [];
569
+ const queuedEdges = new Set();
570
+ const enqueue = (item) => {
571
+ if (!item?.childId) return;
572
+ updateStatus(item);
573
+ if (item.kind !== "started") return;
574
+ const key = `${item.parentRolloutId}\u0000started\u0000${item.childId}`;
575
+ graphEvidence.add(key);
576
+ if (queuedEdges.has(key)) return;
577
+ queuedEdges.add(key);
578
+ queue.push(item);
579
+ };
580
+
581
+ const roots = [
582
+ ...new Set(
583
+ (Array.isArray(rootPaths) ? rootPaths : [rootPaths])
584
+ .filter((value) => typeof value === "string" && value)
585
+ .map((value) => resolve(value)),
586
+ ),
587
+ ].sort();
588
+ if (roots.length === 0) diagnostic("PARENT_META_INVALID");
589
+ const rootLocations = roots.map(rolloutLocation).filter(Boolean);
590
+ const bases = [...new Set(rootLocations.map((item) => item.base))].sort();
591
+ const rootDates = rootLocations.map((item) => item.date).filter(Boolean);
592
+ const rootRecords = [];
593
+ const rootIds = new Set();
594
+ const acceptedParents = new Set();
595
+ const depthById = new Map();
596
+
597
+ const addSource = (path, rolloutId, stat) => {
598
+ sourceByPath.set(resolve(path), {
599
+ path: resolve(path),
600
+ rolloutId,
601
+ ...stat,
602
+ });
603
+ };
604
+
605
+ const scanWithCache = (path, rolloutId, stat, role) => {
606
+ const key = cacheKey(rolloutId, stat);
607
+ const cached = inputCache[key];
608
+ if (cached !== undefined) {
609
+ if (validCachedSummary(cached, rolloutId)) {
610
+ stats.cacheHits += 1;
611
+ outputCache[key] = cached;
612
+ return cached;
613
+ }
614
+ diagnostic("CACHE_INVALID");
615
+ }
616
+ if (live && stats.uncachedBytes + stat.size > limits.maxLiveUncachedBytes) {
617
+ diagnostic("LIVE_BYTE_BUDGET_EXCEEDED");
618
+ aborted = true;
619
+ return null;
620
+ }
621
+ stats.uncachedBytes += stat.size;
622
+ const parsed = parseRolloutStream(path, rolloutId, shouldContinue);
623
+ if (!parsed.ok) {
624
+ if (!aborted)
625
+ diagnostic(
626
+ role === "root" ? "PARENT_META_INVALID" : "CHILD_META_INVALID",
627
+ );
628
+ return null;
629
+ }
630
+ stats.parsedRollouts += 1;
631
+ outputCache[key] = parsed.summary;
632
+ if (parsed.summary.malformedLines > 0) {
633
+ diagnostic(
634
+ role === "root" ? "PARENT_META_INVALID" : "CHILD_META_INVALID",
635
+ );
636
+ }
637
+ return parsed.summary;
638
+ };
639
+
640
+ if (shouldContinue("start")) {
641
+ for (const path of roots) {
642
+ if (!shouldContinue("root")) break;
643
+ const stat = safeStat(path);
644
+ const metaResult = readCodexRolloutMeta(path);
645
+ if (!stat || !metaResult.ok || !metaResult.meta?.id) {
646
+ diagnostic("PARENT_META_INVALID");
647
+ continue;
648
+ }
649
+ const meta = metaResult.meta;
650
+ const rolloutId = String(meta.id);
651
+ addSource(path, rolloutId, stat);
652
+ if (sourceSubagent(meta)) {
653
+ diagnostic("PARENT_META_INVALID");
654
+ continue;
655
+ }
656
+ if (rootIds.has(rolloutId)) {
657
+ diagnostic("DUPLICATE_ROLLOUT_ID");
658
+ continue;
659
+ }
660
+ if (rootIds.size >= limits.maxGraphNodes) {
661
+ diagnostic("GRAPH_LIMIT_EXCEEDED");
662
+ aborted = true;
663
+ break;
664
+ }
665
+ rootIds.add(rolloutId);
666
+ acceptedParents.add(rolloutId);
667
+ depthById.set(rolloutId, 0);
668
+ const summary = scanWithCache(path, rolloutId, stat, "root");
669
+ rootRecords.push({ path, rolloutId, stat, meta, summary });
670
+ for (const item of summary?.activities || []) enqueue(item);
671
+ }
672
+ }
673
+
674
+ const normalizedSignals = (Array.isArray(signals) ? signals : [signals])
675
+ .map(normalizeSignal)
676
+ .filter(Boolean)
677
+ .sort((left, right) =>
678
+ `${left.timestamp}\u0000${left.parentRolloutId}\u0000${left.childId}\u0000${left.kind}`.localeCompare(
679
+ `${right.timestamp}\u0000${right.parentRolloutId}\u0000${right.childId}\u0000${right.kind}`,
680
+ ),
681
+ );
682
+ for (const item of normalizedSignals) enqueue(item);
683
+
684
+ const directCandidates = (item) => {
685
+ const candidates = new Set();
686
+ if (item.transcriptPath && safeStat(item.transcriptPath))
687
+ candidates.add(resolve(item.transcriptPath));
688
+ if (
689
+ item.agentPath &&
690
+ item.agentPath.toLowerCase().endsWith(".jsonl") &&
691
+ safeStat(item.agentPath)
692
+ ) {
693
+ candidates.add(resolve(item.agentPath));
694
+ }
695
+ const date = parseDateParts(item.timestamp);
696
+ if (date) {
697
+ for (const base of bases) {
698
+ for (const path of listJsonl(dayDir(base, date))) {
699
+ if (idMatchesFilename(path, item.childId))
700
+ candidates.add(resolve(path));
701
+ }
702
+ }
703
+ }
704
+ return [...candidates].sort();
705
+ };
706
+
707
+ const fallbackCandidates = (item) => {
708
+ if (!rootDates.length || !bases.length) return [];
709
+ const start = new Date(
710
+ Math.min(...rootDates.map((entry) => entry.date.getTime())),
711
+ );
712
+ const latestRoot = new Date(
713
+ Math.max(...rootDates.map((entry) => entry.date.getTime())),
714
+ );
715
+ const hinted = parseDateParts(item.timestamp)?.date;
716
+ const end = addDays(hinted && hinted > latestRoot ? hinted : latestRoot, 1);
717
+ const span = Math.floor((end.getTime() - start.getTime()) / 86_400_000) + 1;
718
+ const daysToScan = Math.min(span, limits.maxFallbackDays);
719
+ if (span > limits.maxFallbackDays) diagnostic("FALLBACK_LIMIT_EXCEEDED");
720
+ const found = [];
721
+ for (let offset = 0; offset < daysToScan; offset += 1) {
722
+ if (!shouldContinue("fallback-day")) break;
723
+ stats.fallbackDays += 1;
724
+ const date = partsFromDate(addDays(start, offset));
725
+ for (const base of bases) {
726
+ for (const path of listJsonl(dayDir(base, date))) {
727
+ const resolved = resolve(path);
728
+ if (roots.includes(resolved)) continue;
729
+ if (stats.fallbackCandidates >= limits.maxFallbackCandidates) {
730
+ diagnostic("FALLBACK_LIMIT_EXCEEDED");
731
+ return found;
732
+ }
733
+ stats.fallbackCandidates += 1;
734
+ const metaResult = readCodexRolloutMeta(resolved);
735
+ if (
736
+ metaResult.ok &&
737
+ String(metaResult.meta?.id || "") === item.childId
738
+ )
739
+ found.push(resolved);
740
+ }
741
+ }
742
+ }
743
+ return [...new Set(found)].sort();
744
+ };
745
+
746
+ const agents = [];
747
+ const attempted = new Set();
748
+ while (queue.length > 0 && !aborted) {
749
+ if (!shouldContinue("node")) break;
750
+ const item = queue.shift();
751
+ if (
752
+ rootIds.has(item.childId) ||
753
+ acceptedParents.has(item.childId) ||
754
+ attempted.has(item.childId)
755
+ )
756
+ continue;
757
+ attempted.add(item.childId);
758
+ let candidates = directCandidates(item);
759
+ if (!candidates.length) candidates = fallbackCandidates(item);
760
+ if (!candidates.length) {
761
+ diagnostic("CHILD_MISSING");
762
+ continue;
763
+ }
764
+
765
+ const valid = [];
766
+ for (const path of candidates) {
767
+ const stat = safeStat(path);
768
+ const metaResult = readCodexRolloutMeta(path);
769
+ if (
770
+ !stat ||
771
+ !metaResult.ok ||
772
+ String(metaResult.meta?.id || "") !== item.childId
773
+ ) {
774
+ diagnostic("CHILD_META_INVALID");
775
+ continue;
776
+ }
777
+ addSource(path, item.childId, stat);
778
+ valid.push({ path, stat, meta: metaResult.meta });
779
+ }
780
+ if (!valid.length) continue;
781
+ if (valid.length > 1) {
782
+ const hashes = new Set(
783
+ valid
784
+ .map((candidate) => fileDigest(candidate.path, shouldContinue))
785
+ .filter(Boolean),
786
+ );
787
+ if (hashes.size !== 1 || hashes.size === 0) {
788
+ if (!aborted) diagnostic("DUPLICATE_ROLLOUT_ID");
789
+ continue;
790
+ }
791
+ }
792
+ const candidate = valid[0];
793
+ const meta = candidate.meta;
794
+ if (!sourceSubagent(meta)) {
795
+ diagnostic("CHILD_META_INVALID");
796
+ continue;
797
+ }
798
+ const parentId = immediateParent(meta);
799
+ const sessionRoot = String(meta.session_id || "").trim();
800
+ const compatibleRoots = new Set([
801
+ String(canonicalSessionId || ""),
802
+ ...rootIds,
803
+ ]);
804
+ if (sessionRoot && !compatibleRoots.has(sessionRoot)) {
805
+ diagnostic("ROOT_MISMATCH");
806
+ continue;
807
+ }
808
+ const effectiveParentId =
809
+ item.parentRolloutId ||
810
+ (item.source === "signal" && acceptedParents.has(parentId)
811
+ ? parentId
812
+ : "");
813
+ if (
814
+ !acceptedParents.has(effectiveParentId) ||
815
+ parentId !== effectiveParentId
816
+ ) {
817
+ diagnostic(sessionRoot ? "ROOT_MISMATCH" : "LEGACY_CHAIN_UNPROVEN");
818
+ continue;
819
+ }
820
+ if (!sessionRoot && (!parentId || !acceptedParents.has(parentId))) {
821
+ diagnostic("LEGACY_CHAIN_UNPROVEN");
822
+ continue;
823
+ }
824
+ if (rootIds.size + agents.length >= limits.maxGraphNodes) {
825
+ diagnostic("GRAPH_LIMIT_EXCEEDED");
826
+ aborted = true;
827
+ break;
828
+ }
829
+
830
+ const summary = scanWithCache(
831
+ candidate.path,
832
+ item.childId,
833
+ candidate.stat,
834
+ "child",
835
+ );
836
+ if (!summary) continue;
837
+ const depth = (depthById.get(parentId) || 0) + 1;
838
+ const agent = makeAgent(meta, summary, statuses.get(item.childId), depth);
839
+ agents.push(agent);
840
+ acceptedParents.add(item.childId);
841
+ depthById.set(item.childId, agent.depth);
842
+ for (const activityItem of summary.activities) enqueue(activityItem);
843
+ }
844
+
845
+ agents.sort((left, right) => left.id.localeCompare(right.id));
846
+ const aggregate = aggregateAgents(agents);
847
+ if (!aborted && sourceByPath.size > 0 && shouldContinue("before-recheck")) {
848
+ for (const source of sourceByPath.values()) {
849
+ const current = safeStat(source.path);
850
+ if (!sameStat(current, source)) diagnostic("SOURCE_CHANGED_DURING_SCAN");
851
+ }
852
+ }
853
+
854
+ const rootStats = rootRecords
855
+ .map((record) => ({
856
+ rolloutId: record.rolloutId,
857
+ size: record.stat.size,
858
+ mtimeMs: record.stat.mtimeMs,
859
+ }))
860
+ .sort((left, right) => left.rolloutId.localeCompare(right.rolloutId));
861
+ const sourceManifest = [...sourceByPath.values()].sort((left, right) =>
862
+ `${left.rolloutId}\u0000${left.path}`.localeCompare(
863
+ `${right.rolloutId}\u0000${right.path}`,
864
+ ),
865
+ );
866
+ const sourceHashInput = sourceManifest
867
+ .map((source) => ({
868
+ rolloutId: source.rolloutId,
869
+ size: source.size,
870
+ mtimeMs: source.mtimeMs,
871
+ }))
872
+ .sort((left, right) =>
873
+ `${left.rolloutId}\u0000${left.size}\u0000${left.mtimeMs}`.localeCompare(
874
+ `${right.rolloutId}\u0000${right.size}\u0000${right.mtimeMs}`,
875
+ ),
876
+ );
877
+ const diagnostics = DIAGNOSTIC_ORDER.filter((code) =>
878
+ diagnosticCounts.has(code),
879
+ ).map((code) => ({ code, count: diagnosticCounts.get(code) }));
880
+ const state =
881
+ diagnostics.length > 0
882
+ ? "degraded"
883
+ : agents.length > 0
884
+ ? "complete"
885
+ : "none";
886
+
887
+ return {
888
+ state,
889
+ frontier: {
890
+ rootsStatHash: stableHash(rootStats),
891
+ graphCursor: stableHash([...graphEvidence].sort()),
892
+ sourceManifestHash: stableHash(sourceHashInput),
893
+ },
894
+ subagents: agents,
895
+ descendantIds: agents.map((agent) => agent.id),
896
+ workflows: [],
897
+ aggregate,
898
+ diagnostics,
899
+ sourceManifest,
900
+ cache: { version: CACHE_VERSION, entries: outputCache },
901
+ stats,
902
+ };
903
+ }