@wrongstack/core 0.289.0 → 0.291.1

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 (177) hide show
  1. package/dist/boot.d.ts.map +1 -1
  2. package/dist/chronicle/domain-adapter.d.ts.map +1 -1
  3. package/dist/chronicle/index.d.ts +1 -1
  4. package/dist/chronicle/index.d.ts.map +1 -1
  5. package/dist/chronicle/index.js +964 -245
  6. package/dist/chronicle/index.js.map +3 -3
  7. package/dist/chronicle/journal.d.ts +35 -3
  8. package/dist/chronicle/journal.d.ts.map +1 -1
  9. package/dist/chronicle/query.d.ts +8 -5
  10. package/dist/chronicle/query.d.ts.map +1 -1
  11. package/dist/chronicle/tool-adapter.d.ts.map +1 -1
  12. package/dist/coordination/agent-subagent-runner.d.ts +6 -0
  13. package/dist/coordination/agent-subagent-runner.d.ts.map +1 -1
  14. package/dist/coordination/agents/phase6-domain.d.ts.map +1 -1
  15. package/dist/coordination/brain.d.ts +2 -1
  16. package/dist/coordination/brain.d.ts.map +1 -1
  17. package/dist/coordination/collab-debug.d.ts +0 -9
  18. package/dist/coordination/collab-debug.d.ts.map +1 -1
  19. package/dist/coordination/coordinator/error-classifier.d.ts.map +1 -1
  20. package/dist/coordination/dep-watcher.d.ts +2 -1
  21. package/dist/coordination/dep-watcher.d.ts.map +1 -1
  22. package/dist/coordination/director/director-collab.d.ts +1 -1
  23. package/dist/coordination/director/director-collab.d.ts.map +1 -1
  24. package/dist/coordination/director-tools.d.ts.map +1 -1
  25. package/dist/coordination/director.d.ts +19 -1
  26. package/dist/coordination/director.d.ts.map +1 -1
  27. package/dist/coordination/fleet-event-validation.d.ts +0 -7
  28. package/dist/coordination/fleet-event-validation.d.ts.map +1 -1
  29. package/dist/coordination/fleet-spawn.d.ts.map +1 -1
  30. package/dist/coordination/fleet-supervisor.d.ts +1 -1
  31. package/dist/coordination/fleet-supervisor.d.ts.map +1 -1
  32. package/dist/coordination/fleet.d.ts +0 -44
  33. package/dist/coordination/fleet.d.ts.map +1 -1
  34. package/dist/coordination/index.d.ts +2 -2
  35. package/dist/coordination/index.d.ts.map +1 -1
  36. package/dist/coordination/index.js +733 -111
  37. package/dist/coordination/index.js.map +4 -4
  38. package/dist/coordination/knowledge-graph.d.ts +2 -1
  39. package/dist/coordination/knowledge-graph.d.ts.map +1 -1
  40. package/dist/coordination/large-answer-store.d.ts +0 -6
  41. package/dist/coordination/large-answer-store.d.ts.map +1 -1
  42. package/dist/coordination/mail-tools.d.ts.map +1 -1
  43. package/dist/coordination/mailbox-http-router.d.ts.map +1 -1
  44. package/dist/coordination/mailbox-message-codec.d.ts +35 -0
  45. package/dist/coordination/mailbox-message-codec.d.ts.map +1 -1
  46. package/dist/coordination/mailbox-tool.d.ts +1 -1
  47. package/dist/coordination/mailbox-tool.d.ts.map +1 -1
  48. package/dist/coordination/mailbox-types.d.ts +151 -0
  49. package/dist/coordination/mailbox-types.d.ts.map +1 -1
  50. package/dist/coordination/provider-status-tracker.d.ts +27 -2
  51. package/dist/coordination/provider-status-tracker.d.ts.map +1 -1
  52. package/dist/coordination/subagent-budget.d.ts +2 -1
  53. package/dist/coordination/subagent-budget.d.ts.map +1 -1
  54. package/dist/core/agent-response.d.ts +2 -1
  55. package/dist/core/agent-response.d.ts.map +1 -1
  56. package/dist/core/agent-tools.d.ts.map +1 -1
  57. package/dist/core/context.d.ts +51 -0
  58. package/dist/core/context.d.ts.map +1 -1
  59. package/dist/core/fallback-model.d.ts +1 -1
  60. package/dist/core/fallback-model.d.ts.map +1 -1
  61. package/dist/core/fallback-profile-manager.d.ts +1 -1
  62. package/dist/core/fallback-profile-manager.d.ts.map +1 -1
  63. package/dist/core/fleet-pulse.d.ts +2 -1
  64. package/dist/core/fleet-pulse.d.ts.map +1 -1
  65. package/dist/core/iteration-limit.d.ts +2 -1
  66. package/dist/core/iteration-limit.d.ts.map +1 -1
  67. package/dist/core/mailbox-loop.d.ts +11 -1
  68. package/dist/core/mailbox-loop.d.ts.map +1 -1
  69. package/dist/core/model-availability-calendar.d.ts +27 -0
  70. package/dist/core/model-availability-calendar.d.ts.map +1 -0
  71. package/dist/core/system-prompt-builder.d.ts +2 -1
  72. package/dist/core/system-prompt-builder.d.ts.map +1 -1
  73. package/dist/defaults/index.js +1459 -560
  74. package/dist/defaults/index.js.map +4 -4
  75. package/dist/execution/auto-compaction-middleware.d.ts +3 -2
  76. package/dist/execution/auto-compaction-middleware.d.ts.map +1 -1
  77. package/dist/execution/enhance-recovery.d.ts +8 -0
  78. package/dist/execution/enhance-recovery.d.ts.map +1 -1
  79. package/dist/execution/error-handler.d.ts +2 -2
  80. package/dist/execution/error-handler.d.ts.map +1 -1
  81. package/dist/execution/index.js +643 -134
  82. package/dist/execution/index.js.map +4 -4
  83. package/dist/execution/intelligent-compactor.d.ts +0 -16
  84. package/dist/execution/intelligent-compactor.d.ts.map +1 -1
  85. package/dist/execution/one-shot-llm.d.ts.map +1 -1
  86. package/dist/execution/retry-policy.d.ts +0 -13
  87. package/dist/execution/retry-policy.d.ts.map +1 -1
  88. package/dist/execution/skill-loader.d.ts.map +1 -1
  89. package/dist/execution/tool-executor.d.ts.map +1 -1
  90. package/dist/hq/auth-store.d.ts.map +1 -1
  91. package/dist/hq/index.js +39 -1
  92. package/dist/hq/index.js.map +3 -3
  93. package/dist/index.d.ts +32 -30
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +22263 -19999
  96. package/dist/index.js.map +4 -4
  97. package/dist/infrastructure/index.js +59 -5
  98. package/dist/infrastructure/index.js.map +2 -2
  99. package/dist/kernel/events/agent-events.d.ts +2 -0
  100. package/dist/kernel/events/agent-events.d.ts.map +1 -1
  101. package/dist/kernel/events/file-events.d.ts +46 -0
  102. package/dist/kernel/events/file-events.d.ts.map +1 -1
  103. package/dist/kernel/events/memory-events.d.ts +9 -0
  104. package/dist/kernel/events/memory-events.d.ts.map +1 -1
  105. package/dist/kernel/events/provider-events.d.ts +2 -1
  106. package/dist/kernel/events/provider-events.d.ts.map +1 -1
  107. package/dist/kernel/events/tool-events.d.ts +25 -0
  108. package/dist/kernel/events/tool-events.d.ts.map +1 -1
  109. package/dist/mailbox-attach.d.ts.map +1 -1
  110. package/dist/plugins/auto-review-plugin.d.ts +10 -0
  111. package/dist/plugins/auto-review-plugin.d.ts.map +1 -1
  112. package/dist/plugins/chimera-plugin.d.ts +5 -0
  113. package/dist/plugins/chimera-plugin.d.ts.map +1 -1
  114. package/dist/security/index.d.ts +1 -0
  115. package/dist/security/index.d.ts.map +1 -1
  116. package/dist/security/index.js +317 -9
  117. package/dist/security/index.js.map +4 -4
  118. package/dist/security/kanban-boundary.d.ts +10 -0
  119. package/dist/security/kanban-boundary.d.ts.map +1 -0
  120. package/dist/security/permission-policy.d.ts +8 -1
  121. package/dist/security/permission-policy.d.ts.map +1 -1
  122. package/dist/storage/config-loader.d.ts +15 -0
  123. package/dist/storage/config-loader.d.ts.map +1 -1
  124. package/dist/storage/index.d.ts +2 -2
  125. package/dist/storage/index.d.ts.map +1 -1
  126. package/dist/storage/index.js +627 -428
  127. package/dist/storage/index.js.map +4 -4
  128. package/dist/storage/memory-consolidator.d.ts +49 -4
  129. package/dist/storage/memory-consolidator.d.ts.map +1 -1
  130. package/dist/storage/memory-graph-backend.d.ts +11 -0
  131. package/dist/storage/memory-graph-backend.d.ts.map +1 -1
  132. package/dist/storage/provider-config-watcher.d.ts +17 -0
  133. package/dist/storage/provider-config-watcher.d.ts.map +1 -1
  134. package/dist/storage/session-event-bridge.d.ts +2 -2
  135. package/dist/storage/session-event-bridge.d.ts.map +1 -1
  136. package/dist/tools/fallback-manage-tools.d.ts.map +1 -1
  137. package/dist/tools/index.js +166 -26
  138. package/dist/tools/index.js.map +3 -3
  139. package/dist/types/config.d.ts +12 -4
  140. package/dist/types/config.d.ts.map +1 -1
  141. package/dist/types/file-event-record.d.ts +68 -0
  142. package/dist/types/file-event-record.d.ts.map +1 -0
  143. package/dist/types/index.d.ts +1 -0
  144. package/dist/types/index.d.ts.map +1 -1
  145. package/dist/types/index.js +13 -8
  146. package/dist/types/index.js.map +3 -3
  147. package/dist/types/permission.d.ts +38 -0
  148. package/dist/types/permission.d.ts.map +1 -1
  149. package/dist/types/provider.d.ts +1 -1
  150. package/dist/types/provider.d.ts.map +1 -1
  151. package/dist/types/session.d.ts +27 -0
  152. package/dist/types/session.d.ts.map +1 -1
  153. package/dist/types/tool-executor.d.ts +2 -0
  154. package/dist/types/tool-executor.d.ts.map +1 -1
  155. package/dist/utils/config-backup.d.ts +20 -0
  156. package/dist/utils/config-backup.d.ts.map +1 -0
  157. package/dist/utils/index.d.ts +12 -11
  158. package/dist/utils/index.d.ts.map +1 -1
  159. package/dist/utils/index.js +436 -290
  160. package/dist/utils/index.js.map +4 -4
  161. package/dist/utils/message-invariants.d.ts +12 -0
  162. package/dist/utils/message-invariants.d.ts.map +1 -1
  163. package/dist/utils/safe-json.d.ts +1 -1
  164. package/dist/utils/safe-json.d.ts.map +1 -1
  165. package/dist/utils/term.d.ts +6 -0
  166. package/dist/utils/term.d.ts.map +1 -1
  167. package/dist/utils/tool-wire-compact.d.ts +9 -0
  168. package/dist/utils/tool-wire-compact.d.ts.map +1 -1
  169. package/dist/utils/wstack-paths.d.ts +21 -1
  170. package/dist/utils/wstack-paths.d.ts.map +1 -1
  171. package/instructions/agents/ios.md +113 -0
  172. package/instructions/llm/memory-consolidator.md +4 -12
  173. package/instructions/system.md +4 -1
  174. package/package.json +3 -3
  175. package/skills/auto-review/SKILL.md +176 -0
  176. package/skills/mnemosyne/SKILL.md +156 -0
  177. package/skills/mnemosyne/instructions/agent-prompt.md +200 -0
@@ -235,10 +235,10 @@ async function scanProject(root, excluded, maxHashBytes, onError) {
235
235
  }
236
236
  async function fingerprint(filePath, maxHashBytes) {
237
237
  try {
238
- const stat3 = await fsp.stat(filePath);
239
- if (!stat3.isFile()) return void 0;
240
- const base = { size: stat3.size, mtimeMs: stat3.mtimeMs };
241
- if (stat3.size <= maxHashBytes) {
238
+ const stat5 = await fsp.stat(filePath);
239
+ if (!stat5.isFile()) return void 0;
240
+ const base = { size: stat5.size, mtimeMs: stat5.mtimeMs };
241
+ if (stat5.size <= maxHashBytes) {
242
242
  base.hash = createHash2("sha256").update(await fsp.readFile(filePath)).digest("hex");
243
243
  }
244
244
  return base;
@@ -271,6 +271,7 @@ import * as fs3 from "node:fs/promises";
271
271
  import * as path4 from "node:path";
272
272
 
273
273
  // src/utils/atomic-write.ts
274
+ import { randomBytes } from "node:crypto";
274
275
  import * as fs2 from "node:fs/promises";
275
276
  import { watch as watchDir } from "node:fs";
276
277
  import * as path3 from "node:path";
@@ -378,6 +379,50 @@ var FsError = class extends WrongStackError {
378
379
  };
379
380
 
380
381
  // src/utils/atomic-write.ts
382
+ async function atomicWrite(targetPath, content, opts = {}) {
383
+ const dir = path3.dirname(targetPath);
384
+ await fs2.mkdir(dir, { recursive: true });
385
+ const tmp = path3.join(dir, `.${path3.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
386
+ try {
387
+ if (typeof content === "string") {
388
+ await fs2.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
389
+ } else {
390
+ await fs2.writeFile(tmp, content, { flag: "wx" });
391
+ }
392
+ try {
393
+ const fh = await fs2.open(tmp, "r+");
394
+ try {
395
+ await fh.sync();
396
+ } finally {
397
+ await fh.close();
398
+ }
399
+ } catch {
400
+ }
401
+ let mode;
402
+ try {
403
+ const stat5 = await fs2.stat(targetPath);
404
+ mode = stat5.mode & 511;
405
+ } catch {
406
+ mode = opts.mode;
407
+ }
408
+ if (mode !== void 0) {
409
+ await fs2.chmod(tmp, mode);
410
+ }
411
+ await renameWithRetry(tmp, targetPath);
412
+ if (mode !== void 0 && process.platform === "win32") {
413
+ try {
414
+ await fs2.chmod(targetPath, mode);
415
+ } catch {
416
+ }
417
+ }
418
+ } catch (err) {
419
+ try {
420
+ await fs2.unlink(tmp);
421
+ } catch {
422
+ }
423
+ throw err;
424
+ }
425
+ }
381
426
  async function ensureDir(dir) {
382
427
  await fs2.mkdir(dir, { recursive: true });
383
428
  }
@@ -409,8 +454,8 @@ async function withFileLock(targetPath, fn, opts = {}) {
409
454
  }
410
455
  if (code !== "EEXIST" && code !== "EPERM") throw err;
411
456
  try {
412
- const stat3 = await fs2.stat(lockPath);
413
- if (Date.now() - stat3.mtimeMs > staleMs) {
457
+ const stat5 = await fs2.stat(lockPath);
458
+ if (Date.now() - stat5.mtimeMs > staleMs) {
414
459
  await fs2.unlink(lockPath);
415
460
  continue;
416
461
  }
@@ -486,50 +531,78 @@ async function waitForLockRelease(lockPath, remainingMs) {
486
531
  );
487
532
  });
488
533
  }
534
+ var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
535
+ async function renameWithRetry(from, to) {
536
+ if (process.platform !== "win32") {
537
+ await fs2.rename(from, to);
538
+ return;
539
+ }
540
+ const delays = [10, 25, 60, 120, 250];
541
+ let lastErr;
542
+ for (let i = 0; i <= delays.length; i++) {
543
+ try {
544
+ await fs2.rename(from, to);
545
+ return;
546
+ } catch (err) {
547
+ lastErr = err;
548
+ const code = err?.code;
549
+ if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
550
+ throw err;
551
+ }
552
+ await new Promise((resolve5) => setTimeout(resolve5, delays[i]));
553
+ }
554
+ }
555
+ throw lastErr;
556
+ }
489
557
 
490
558
  // src/chronicle/types.ts
491
559
  var CHRONICLE_SCHEMA_VERSION = 1;
492
560
 
493
561
  // src/chronicle/journal.ts
494
562
  var GENESIS_HASH = "0".repeat(64);
563
+ var DEFAULT_MAX_PARTITION_BYTES = 100 * 1024 * 1024;
564
+ var DEFAULT_ROTATION_WINDOW_MS = 60 * 60 * 1e3;
565
+ var RETENTION_CHECKPOINT_VERSION = 1;
495
566
  var ChronicleJournal = class {
496
- filePath;
567
+ basePath;
497
568
  now;
498
569
  monotonicNow;
499
570
  idFactory;
500
571
  maxPending;
501
572
  batchWindowMs;
573
+ maxPartitionSizeBytes;
574
+ rotationWindowMs;
575
+ retentionDays;
576
+ autoPurgeIntervalMs;
502
577
  pending = [];
503
578
  drainPromise;
504
579
  drainScheduled = false;
505
580
  drainTimer;
506
- counters = {
507
- acceptedEvents: 0,
508
- persistedEvents: 0,
509
- rejectedEvents: 0,
510
- failedEvents: 0,
511
- batches: 0,
512
- maxObservedPending: 0,
513
- largestBatch: 0
514
- };
581
+ counters = { acceptedEvents: 0, persistedEvents: 0, rejectedEvents: 0, failedEvents: 0, batches: 0, maxObservedPending: 0, largestBatch: 0, partitionRolls: 0 };
515
582
  lastBatchDurationMs;
583
+ partitionIndex = 0;
584
+ partitionStartedAt;
585
+ lastSequence = 0;
586
+ lastHash = GENESIS_HASH;
587
+ lastAutoPurgeAt = 0;
516
588
  constructor(options) {
517
- this.filePath = path4.resolve(options.filePath);
589
+ this.basePath = path4.resolve(options.filePath);
518
590
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
519
591
  this.monotonicNow = options.monotonicNow ?? (() => process.hrtime.bigint());
520
592
  this.idFactory = options.idFactory ?? randomUUID2;
521
593
  this.maxPending = Math.max(1, options.maxPending ?? 1e5);
522
594
  this.batchWindowMs = Math.max(0, options.batchWindowMs ?? 5);
595
+ this.maxPartitionSizeBytes = options.maxPartitionSizeBytes ?? DEFAULT_MAX_PARTITION_BYTES;
596
+ this.rotationWindowMs = options.rotationWindowMs ?? DEFAULT_ROTATION_WINDOW_MS;
597
+ this.retentionDays = options.retentionDays && Number.isFinite(options.retentionDays) && options.retentionDays > 0 ? options.retentionDays : 0;
598
+ this.autoPurgeIntervalMs = Math.max(0, options.autoPurgeIntervalMs ?? 36e5);
599
+ this.partitionStartedAt = Date.now();
523
600
  }
524
601
  get path() {
525
- return this.filePath;
602
+ return this.partitionIndex === 0 ? this.basePath : rotatedPath(this.basePath, this.partitionIndex);
526
603
  }
527
604
  stats() {
528
- return {
529
- ...this.counters,
530
- pendingEvents: this.pending.length,
531
- ...this.lastBatchDurationMs !== void 0 ? { lastBatchDurationMs: this.lastBatchDurationMs } : {}
532
- };
605
+ return { ...this.counters, pendingEvents: this.pending.length, ...this.lastBatchDurationMs !== void 0 ? { lastBatchDurationMs: this.lastBatchDurationMs } : {} };
533
606
  }
534
607
  append(input) {
535
608
  if (this.pending.length >= this.maxPending) {
@@ -546,9 +619,11 @@ var ChronicleJournal = class {
546
619
  }
547
620
  async readAll() {
548
621
  await this.flush();
549
- return readEntries(this.filePath);
622
+ const files = await collectPartitions(this.basePath);
623
+ const entries = [];
624
+ for (const file of files) entries.push(...await readEntriesStrict(file));
625
+ return entries;
550
626
  }
551
- /** Wait until every append accepted by this process has settled. */
552
627
  async flush() {
553
628
  if (this.drainTimer) {
554
629
  clearTimeout(this.drainTimer);
@@ -562,33 +637,108 @@ var ChronicleJournal = class {
562
637
  }
563
638
  async verify() {
564
639
  await this.flush();
565
- let entries;
566
- try {
567
- entries = await readEntries(this.filePath, true);
568
- } catch (error) {
569
- return { ok: false, entries: 0, brokenAt: 0, reason: errorMessage(error) };
640
+ const files = await collectPartitions(this.basePath);
641
+ const checkpointResult = await readRetentionCheckpoint(this.basePath);
642
+ if (checkpointResult.error) return { ok: false, entries: 0, brokenAt: 0, reason: checkpointResult.error };
643
+ return verifyPartitionFiles(files, checkpointResult.checkpoint);
644
+ }
645
+ async purge(options) {
646
+ await this.flush();
647
+ if (!Number.isFinite(options.retentionDays) || options.retentionDays <= 0) {
648
+ throw new TypeError("Chronicle retentionDays must be a positive finite number");
570
649
  }
571
- let expectedPrevious = GENESIS_HASH;
572
- for (let index = 0; index < entries.length; index++) {
573
- const entry = entries[index];
574
- if (entry.sequence !== index + 1) {
575
- return { ok: false, entries: entries.length, brokenAt: index, reason: `sequence ${entry.sequence} is not ${index + 1}` };
650
+ await this.refreshStateFromDisk();
651
+ const cutoff = Date.now() - options.retentionDays * 864e5;
652
+ const activePath = path4.resolve(this.path);
653
+ const errors = [];
654
+ let dc = 0, db = 0, sc = 0;
655
+ const suppliedFiles = options.files === void 0 ? await collectJournalPartitions(this.basePath) : [...options.files];
656
+ const eligible = /* @__PURE__ */ new Set();
657
+ for (const suppliedPath of new Set(suppliedFiles)) {
658
+ const file = path4.resolve(suppliedPath);
659
+ if (!isJournalPartition(file, path4.dirname(this.basePath), this.basePath)) {
660
+ errors.push({ file: suppliedPath, reason: "not a Chronicle journal partition in this journal directory" });
661
+ sc++;
662
+ continue;
576
663
  }
577
- if (entry.previousHash !== expectedPrevious) {
578
- return { ok: false, entries: entries.length, brokenAt: index, reason: "previous hash mismatch" };
664
+ if (file === activePath) {
665
+ sc++;
666
+ continue;
579
667
  }
580
- const { hash: recordedHash, ...content } = entry;
581
- if (hashValue(content) !== recordedHash) {
582
- return { ok: false, entries: entries.length, brokenAt: index, reason: "entry hash mismatch" };
668
+ let mtimeMs;
669
+ try {
670
+ const fileStat = await fs3.lstat(file);
671
+ if (!fileStat.isFile()) {
672
+ sc++;
673
+ continue;
674
+ }
675
+ mtimeMs = fileStat.mtimeMs;
676
+ } catch (error) {
677
+ if (isNotFound(error)) continue;
678
+ errors.push({ file, reason: errorMessage(error) });
679
+ sc++;
680
+ continue;
681
+ }
682
+ if (mtimeMs > cutoff) {
683
+ sc++;
684
+ continue;
583
685
  }
584
- expectedPrevious = recordedHash;
686
+ eligible.add(file);
687
+ }
688
+ const candidates = [];
689
+ const allPartitions = await collectJournalPartitions(this.basePath);
690
+ for (const family of groupPartitionsByFamily(allPartitions).values()) {
691
+ for (const file of family) {
692
+ if (file === activePath || !eligible.has(file)) break;
693
+ candidates.push(file);
694
+ eligible.delete(file);
695
+ }
696
+ }
697
+ sc += eligible.size;
698
+ if (!options.dryRun) {
699
+ for (const file of candidates) {
700
+ try {
701
+ const familyBase = partitionFamilyBase(file);
702
+ let deletedBytes;
703
+ await withFileLock(familyBase, async () => {
704
+ const fileStat = await fs3.lstat(file);
705
+ if (!fileStat.isFile() || fileStat.mtimeMs > cutoff) return;
706
+ const entries = await readEntriesStrict(file);
707
+ const checkpointResult = await readRetentionCheckpoint(familyBase);
708
+ if (checkpointResult.error) throw new Error(checkpointResult.error);
709
+ const checkpoint = checkpointResult.checkpoint;
710
+ const nextCheckpoint = verifyRetainedPrefix(entries, checkpoint);
711
+ if (!nextCheckpoint) throw new Error("partition does not extend the trusted Chronicle chain");
712
+ if (nextCheckpoint.sequence > (checkpoint?.sequence ?? 0)) {
713
+ await writeRetentionCheckpoint(familyBase, nextCheckpoint);
714
+ }
715
+ deletedBytes = fileStat.size;
716
+ await fs3.unlink(file);
717
+ });
718
+ if (deletedBytes === void 0) {
719
+ sc++;
720
+ break;
721
+ }
722
+ db += deletedBytes;
723
+ dc++;
724
+ } catch (error) {
725
+ errors.push({ file, reason: errorMessage(error) });
726
+ sc++;
727
+ break;
728
+ }
729
+ }
730
+ }
731
+ return { deletedCount: dc, deletedBytes: db, skippedCount: sc, errors, ...options.dryRun ? { candidates } : {} };
732
+ }
733
+ async maybeAutoPurge() {
734
+ if (this.retentionDays <= 0) return;
735
+ const n = Date.now();
736
+ if (n - this.lastAutoPurgeAt < this.autoPurgeIntervalMs) return;
737
+ this.lastAutoPurgeAt = n;
738
+ try {
739
+ await this.purge({ retentionDays: this.retentionDays });
740
+ } catch {
585
741
  }
586
- return {
587
- ok: true,
588
- entries: entries.length,
589
- lastSequence: entries.at(-1)?.sequence ?? 0,
590
- lastHash: entries.at(-1)?.hash ?? GENESIS_HASH
591
- };
592
742
  }
593
743
  scheduleDrain() {
594
744
  if (this.drainScheduled || this.drainPromise) return;
@@ -608,45 +758,241 @@ var ChronicleJournal = class {
608
758
  if (this.pending.length > 0) this.scheduleDrain();
609
759
  });
610
760
  }
761
+ async refreshStateFromDisk() {
762
+ const files = await collectPartitions(this.basePath);
763
+ const latest = files[files.length - 1] ?? this.basePath;
764
+ this.partitionIndex = partitionIndex(latest, this.basePath);
765
+ const entry = await readLastEntry(latest);
766
+ const checkpointResult = await readRetentionCheckpoint(this.basePath);
767
+ if (checkpointResult.error) throw new Error(checkpointResult.error);
768
+ const checkpoint = checkpointResult.checkpoint;
769
+ this.lastSequence = entry?.sequence ?? checkpoint?.sequence ?? 0;
770
+ this.lastHash = entry?.hash ?? checkpoint?.hash ?? GENESIS_HASH;
771
+ try {
772
+ this.partitionStartedAt = (await fs3.stat(latest)).birthtimeMs;
773
+ } catch {
774
+ this.partitionStartedAt = Date.now();
775
+ }
776
+ }
777
+ async checkRotation() {
778
+ if (this.partitionIndex === 0 && this.lastSequence === 0) return;
779
+ if (Number.isFinite(this.rotationWindowMs) && Date.now() - this.partitionStartedAt >= this.rotationWindowMs) {
780
+ this.rotate();
781
+ return;
782
+ }
783
+ if (Number.isFinite(this.maxPartitionSizeBytes)) {
784
+ try {
785
+ if ((await fs3.stat(this.path)).size >= this.maxPartitionSizeBytes) this.rotate();
786
+ } catch {
787
+ }
788
+ }
789
+ }
790
+ rotate() {
791
+ this.partitionIndex++;
792
+ this.partitionStartedAt = Date.now();
793
+ this.counters.partitionRolls++;
794
+ }
611
795
  async persistBatch(batch) {
612
796
  const started = performance.now();
613
797
  this.counters.batches++;
614
798
  this.counters.largestBatch = Math.max(this.counters.largestBatch, batch.length);
615
799
  try {
616
- await ensureDir(path4.dirname(this.filePath));
800
+ await ensureDir(path4.dirname(this.basePath));
617
801
  let recorded = [];
618
- await withFileLock(this.filePath, async () => {
619
- let previous = await readLastEntry(this.filePath);
802
+ await withFileLock(this.basePath, async () => {
803
+ await this.refreshStateFromDisk();
804
+ await this.checkRotation();
805
+ const cp = this.path;
806
+ let prev = this.lastSequence > 0 ? { sequence: this.lastSequence, hash: this.lastHash } : void 0;
620
807
  recorded = batch.map(({ input }) => {
621
808
  const instant = this.now().toISOString();
622
- const normalizedInput = removeUndefined(input);
623
- const unhashed = {
624
- ...normalizedInput,
625
- occurredAt: input.occurredAt ?? instant,
626
- monotonicNs: input.monotonicNs ?? this.monotonicNow().toString(),
627
- schemaVersion: CHRONICLE_SCHEMA_VERSION,
628
- eventId: this.idFactory(),
629
- observedAt: instant,
630
- persistedAt: instant,
631
- sequence: (previous?.sequence ?? 0) + 1,
632
- previousHash: previous?.hash ?? GENESIS_HASH
633
- };
634
- const event = { ...unhashed, hash: hashValue(unhashed) };
635
- previous = event;
809
+ const ni = removeUndefined(input);
810
+ const uh = { ...ni, occurredAt: input.occurredAt ?? instant, monotonicNs: input.monotonicNs ?? this.monotonicNow().toString(), schemaVersion: CHRONICLE_SCHEMA_VERSION, eventId: this.idFactory(), observedAt: instant, persistedAt: instant, sequence: (prev?.sequence ?? 0) + 1, previousHash: prev?.hash ?? GENESIS_HASH };
811
+ const event = { ...uh, hash: hashValue(uh) };
812
+ prev = event;
636
813
  return event;
637
814
  });
638
- await fs3.appendFile(this.filePath, recorded.map((event) => JSON.stringify(event)).join("\n") + "\n", "utf8");
815
+ await fs3.appendFile(cp, recorded.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf8");
816
+ });
817
+ const last = recorded[recorded.length - 1];
818
+ this.lastSequence = last.sequence;
819
+ this.lastHash = last.hash;
820
+ batch.forEach((item, i) => {
821
+ item.resolve(recorded[i]);
639
822
  });
640
- batch.forEach((item, index) => item.resolve(recorded[index]));
641
823
  this.counters.persistedEvents += batch.length;
824
+ void this.maybeAutoPurge();
642
825
  } catch (error) {
643
826
  this.counters.failedEvents += batch.length;
644
- batch.forEach((item) => item.reject(error));
827
+ batch.forEach((item) => {
828
+ item.reject(error);
829
+ });
645
830
  } finally {
646
831
  this.lastBatchDurationMs = performance.now() - started;
647
832
  }
648
833
  }
649
834
  };
835
+ function rotatedPath(basePath, index) {
836
+ const dir = path4.dirname(basePath);
837
+ const ext = path4.extname(basePath);
838
+ const base = path4.basename(basePath, ext);
839
+ return path4.join(dir, `${base}.${String(index).padStart(5, "0")}${ext}`);
840
+ }
841
+ async function collectPartitions(basePath) {
842
+ const dir = path4.dirname(basePath);
843
+ const ext = path4.extname(basePath);
844
+ const base = path4.basename(basePath, ext);
845
+ const pattern = new RegExp(`^${escapeRegex(base)}(?:\\.\\d{5})?${escapeRegex(ext)}$`);
846
+ const result = [];
847
+ try {
848
+ const entries = await fs3.readdir(dir, { withFileTypes: true });
849
+ for (const entry of entries) if (entry.isFile() && pattern.test(entry.name)) result.push(path4.join(dir, entry.name));
850
+ } catch {
851
+ }
852
+ const baseFile = path4.join(dir, base + ext);
853
+ const rotated = result.filter((file) => file !== baseFile).sort((left, right) => parseIndex(left, base, ext) - parseIndex(right, base, ext));
854
+ return [baseFile, ...rotated];
855
+ }
856
+ async function collectJournalPartitions(basePath) {
857
+ const directory = path4.dirname(basePath);
858
+ const result = [];
859
+ try {
860
+ const entries = await fs3.readdir(directory, { withFileTypes: true });
861
+ for (const entry of entries) {
862
+ const file = path4.join(directory, entry.name);
863
+ if (entry.isFile() && isJournalPartition(file, directory, basePath)) result.push(file);
864
+ }
865
+ } catch {
866
+ }
867
+ return result.sort(compareJournalPartitions);
868
+ }
869
+ function isJournalPartition(filePath, directory, basePath) {
870
+ if (path4.dirname(path4.resolve(filePath)) !== path4.resolve(directory)) return false;
871
+ const fileName = path4.basename(filePath);
872
+ if (!basePath) return false;
873
+ const baseName = path4.basename(basePath);
874
+ const dailyFamily = /^\d{4}-\d{2}-\d{2}\.events(?:\.\d{5})?\.jsonl$/;
875
+ if (/^\d{4}-\d{2}-\d{2}\.events\.jsonl$/.test(baseName)) return dailyFamily.test(fileName);
876
+ return partitionFamilyBase(path4.resolve(filePath)) === partitionFamilyBase(path4.resolve(basePath));
877
+ }
878
+ function compareJournalPartitions(left, right) {
879
+ const pattern = /^(.*\.events)(?:\.(\d{5}))?\.jsonl$/;
880
+ const leftMatch = pattern.exec(path4.basename(left));
881
+ const rightMatch = pattern.exec(path4.basename(right));
882
+ const familyOrder = (leftMatch?.[1] ?? left).localeCompare(rightMatch?.[1] ?? right);
883
+ return familyOrder || Number(leftMatch?.[2] ?? 0) - Number(rightMatch?.[2] ?? 0);
884
+ }
885
+ function groupPartitionsByFamily(files) {
886
+ const groups = /* @__PURE__ */ new Map();
887
+ for (const file of files) {
888
+ const family = partitionFamilyBase(file);
889
+ const group = groups.get(family) ?? [];
890
+ group.push(file);
891
+ groups.set(family, group);
892
+ }
893
+ return groups;
894
+ }
895
+ function partitionFamilyBase(filePath) {
896
+ return filePath.replace(/\.\d{5}(?=\.jsonl$)/, "");
897
+ }
898
+ function retentionCheckpointPath(basePath) {
899
+ return `${partitionFamilyBase(basePath)}.retention.json`;
900
+ }
901
+ function verifyRetainedPrefix(entries, checkpoint) {
902
+ if (entries.length === 0) return void 0;
903
+ let sequence = checkpoint?.sequence ?? 0;
904
+ let hash3 = checkpoint?.hash ?? GENESIS_HASH;
905
+ let advanced = false;
906
+ for (const entry of entries) {
907
+ if (entry.sequence <= sequence) continue;
908
+ if (entry.sequence !== sequence + 1 || entry.previousHash !== hash3) return void 0;
909
+ const { hash: recordedHash, ...content } = entry;
910
+ if (hashValue(content) !== recordedHash) return void 0;
911
+ sequence = entry.sequence;
912
+ hash3 = recordedHash;
913
+ advanced = true;
914
+ }
915
+ if (!advanced) return checkpoint;
916
+ return { version: RETENTION_CHECKPOINT_VERSION, sequence, hash: hash3 };
917
+ }
918
+ async function verifyPartitionFiles(files, checkpoint) {
919
+ const checkpointSequence = checkpoint?.sequence ?? 0;
920
+ let previousHash = checkpoint?.hash ?? GENESIS_HASH;
921
+ let entries = 0;
922
+ let lastSequence = checkpointSequence;
923
+ let coveredPrevious;
924
+ for (const file of files) {
925
+ let chunk;
926
+ try {
927
+ chunk = await readEntriesStrict(file);
928
+ } catch (error) {
929
+ return { ok: false, entries, brokenAt: entries, reason: errorMessage(error) };
930
+ }
931
+ for (const entry of chunk) {
932
+ const { hash: recordedHash, ...content } = entry;
933
+ if (entry.sequence <= checkpointSequence) {
934
+ if (hashValue(content) !== recordedHash) return { ok: false, entries, brokenAt: entries, reason: "entry hash mismatch" };
935
+ if (coveredPrevious && entry.sequence !== coveredPrevious.sequence + 1) {
936
+ return { ok: false, entries, brokenAt: entries, reason: `sequence ${entry.sequence} is not ${coveredPrevious.sequence + 1}` };
937
+ }
938
+ if (coveredPrevious && entry.previousHash !== coveredPrevious.hash) {
939
+ return { ok: false, entries, brokenAt: entries, reason: "previous hash mismatch" };
940
+ }
941
+ if (entry.sequence === checkpointSequence && recordedHash !== checkpoint?.hash) {
942
+ return { ok: false, entries, brokenAt: entries, reason: "retention checkpoint hash mismatch" };
943
+ }
944
+ coveredPrevious = entry;
945
+ continue;
946
+ }
947
+ const index = entries++;
948
+ if (entry.sequence !== lastSequence + 1) return { ok: false, entries, brokenAt: index, reason: `sequence ${entry.sequence} is not ${lastSequence + 1}` };
949
+ if (entry.previousHash !== previousHash) return { ok: false, entries, brokenAt: index, reason: "previous hash mismatch" };
950
+ if (hashValue(content) !== recordedHash) return { ok: false, entries, brokenAt: index, reason: "entry hash mismatch" };
951
+ previousHash = recordedHash;
952
+ lastSequence = entry.sequence;
953
+ }
954
+ }
955
+ if (coveredPrevious && (coveredPrevious.sequence !== checkpointSequence || coveredPrevious.hash !== checkpoint?.hash)) {
956
+ return { ok: false, entries, brokenAt: entries, reason: "retention checkpoint hash mismatch" };
957
+ }
958
+ return { ok: true, entries, lastSequence, lastHash: previousHash };
959
+ }
960
+ async function readRetentionCheckpoint(basePath) {
961
+ const checkpointPath = retentionCheckpointPath(basePath);
962
+ let raw;
963
+ try {
964
+ raw = await fs3.readFile(checkpointPath, "utf8");
965
+ } catch (error) {
966
+ return isNotFound(error) ? {} : { error: `cannot read retention checkpoint: ${errorMessage(error)}` };
967
+ }
968
+ try {
969
+ const parsed = JSON.parse(raw);
970
+ if (parsed.version !== RETENTION_CHECKPOINT_VERSION || !Number.isSafeInteger(parsed.sequence) || (parsed.sequence ?? -1) < 0 || !isHash(parsed.hash)) {
971
+ return { error: "invalid Chronicle retention checkpoint" };
972
+ }
973
+ return { checkpoint: parsed };
974
+ } catch {
975
+ return { error: "invalid Chronicle retention checkpoint JSON" };
976
+ }
977
+ }
978
+ async function writeRetentionCheckpoint(basePath, checkpoint) {
979
+ await atomicWrite(retentionCheckpointPath(basePath), `${JSON.stringify(checkpoint)}
980
+ `, { mode: 384 });
981
+ }
982
+ function isHash(value) {
983
+ return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
984
+ }
985
+ function parseIndex(filePath, base, ext) {
986
+ const suffix = path4.basename(filePath).slice(base.length + 1, -ext.length);
987
+ return suffix ? parseInt(suffix, 10) : 0;
988
+ }
989
+ function partitionIndex(filePath, basePath) {
990
+ const ext = path4.extname(basePath);
991
+ return parseIndex(filePath, path4.basename(basePath, ext), ext);
992
+ }
993
+ function escapeRegex(text2) {
994
+ return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
995
+ }
650
996
  async function readLastEntry(filePath) {
651
997
  let handle;
652
998
  try {
@@ -657,22 +1003,20 @@ async function readLastEntry(filePath) {
657
1003
  }
658
1004
  try {
659
1005
  const size = (await handle.stat()).size;
660
- const chunkSize = 64 * 1024;
661
- let position = size;
662
- let suffix = "";
1006
+ let position = size, suffix = "";
663
1007
  while (position > 0) {
664
- const length = Math.min(chunkSize, position);
1008
+ const length = Math.min(65536, position);
665
1009
  position -= length;
666
- const buffer = Buffer.allocUnsafe(length);
667
- await handle.read(buffer, 0, length, position);
668
- suffix = buffer.toString("utf8") + suffix;
1010
+ const buf = Buffer.allocUnsafe(length);
1011
+ await handle.read(buf, 0, length, position);
1012
+ suffix = buf.toString("utf8") + suffix;
669
1013
  const lines = suffix.split("\n");
670
- const first = position === 0 ? 0 : 1;
671
- for (let index = lines.length - 1; index >= first; index--) {
672
- const line = lines[index].trim();
673
- if (!line) continue;
1014
+ const start = position === 0 ? 0 : 1;
1015
+ for (let i = lines.length - 1; i >= start; i--) {
1016
+ const trimmed = lines[i].trim();
1017
+ if (!trimmed) continue;
674
1018
  try {
675
- return JSON.parse(line);
1019
+ return JSON.parse(trimmed);
676
1020
  } catch {
677
1021
  }
678
1022
  }
@@ -683,7 +1027,7 @@ async function readLastEntry(filePath) {
683
1027
  await handle.close();
684
1028
  }
685
1029
  }
686
- async function readEntries(filePath, strict = false) {
1030
+ async function readEntriesStrict(filePath) {
687
1031
  let raw;
688
1032
  try {
689
1033
  raw = await fs3.readFile(filePath, "utf8");
@@ -692,12 +1036,14 @@ async function readEntries(filePath, strict = false) {
692
1036
  throw error;
693
1037
  }
694
1038
  const entries = [];
695
- for (const [index, line] of raw.split("\n").entries()) {
696
- if (!line.trim()) continue;
1039
+ const lines = raw.split("\n");
1040
+ for (let i = 0; i < lines.length; i++) {
1041
+ const trimmed = lines[i].trim();
1042
+ if (!trimmed) continue;
697
1043
  try {
698
- entries.push(JSON.parse(line));
699
- } catch (error) {
700
- if (strict) throw new Error(`invalid JSON at line ${index + 1}: ${errorMessage(error)}`);
1044
+ entries.push(JSON.parse(trimmed));
1045
+ } catch {
1046
+ throw new Error(`invalid JSON at line ${i + 1} in ${path4.basename(filePath)}`);
701
1047
  }
702
1048
  }
703
1049
  return entries;
@@ -708,16 +1054,14 @@ function hashValue(value) {
708
1054
  function stableStringify(value) {
709
1055
  if (value === null || typeof value !== "object") return JSON.stringify(value);
710
1056
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
711
- const object = value;
712
- return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
1057
+ const obj = value;
1058
+ return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
713
1059
  }
714
1060
  function removeUndefined(value) {
715
1061
  if (Array.isArray(value)) return value.map((item) => item === void 0 ? null : removeUndefined(item));
716
1062
  if (value === null || typeof value !== "object") return value;
717
1063
  const result = {};
718
- for (const [key, item] of Object.entries(value)) {
719
- if (item !== void 0) result[key] = removeUndefined(item);
720
- }
1064
+ for (const [key, item] of Object.entries(value)) if (item !== void 0) result[key] = removeUndefined(item);
721
1065
  return result;
722
1066
  }
723
1067
  function isNotFound(error) {
@@ -748,7 +1092,9 @@ function wireProviderAttemptsToChronicle(options) {
748
1092
  durationNs: millisecondsToNanoseconds(event.durationMs)
749
1093
  }))
750
1094
  ];
751
- return () => unsubs.forEach((unsubscribe) => unsubscribe());
1095
+ return () => unsubs.forEach((unsubscribe) => {
1096
+ unsubscribe();
1097
+ });
752
1098
  }
753
1099
  function persist(options, event, base) {
754
1100
  const context = typeof options.context === "function" ? options.context() : options.context;
@@ -803,6 +1149,25 @@ function wireToolsToChronicle(options) {
803
1149
  }
804
1150
  });
805
1151
  }),
1152
+ options.events.on("permission.evaluated", (event) => {
1153
+ persist2(options, event, {
1154
+ eventType: "permission.evaluated",
1155
+ outcome: event.effectiveDecision === "deny" ? "denied" : "success",
1156
+ attributes: {
1157
+ toolName: event.name,
1158
+ inputHash: event.inputHash,
1159
+ policyDecision: event.policyDecision,
1160
+ effectiveDecision: event.effectiveDecision,
1161
+ decisionSource: event.decisionSource,
1162
+ reason: event.reason ? options.scrubber.scrub(event.reason) : void 0,
1163
+ riskTier: event.riskTier,
1164
+ yoloEnabled: event.yoloEnabled,
1165
+ boundaryDecision: event.boundaryDecision,
1166
+ boundaryReason: event.boundaryReason ? options.scrubber.scrub(event.boundaryReason) : void 0,
1167
+ capabilityDowngraded: event.capabilityDowngraded
1168
+ }
1169
+ });
1170
+ }),
806
1171
  options.events.on("tool.executed", (event) => {
807
1172
  const output = options.scrubber.scrub(event.output ?? "");
808
1173
  persist2(options, event, {
@@ -853,7 +1218,9 @@ function wireToolsToChronicle(options) {
853
1218
  });
854
1219
  })
855
1220
  ];
856
- return () => unsubs.forEach((unsubscribe) => unsubscribe());
1221
+ return () => unsubs.forEach((unsubscribe) => {
1222
+ unsubscribe();
1223
+ });
857
1224
  }
858
1225
  function persist2(options, event, fields) {
859
1226
  const context = typeof options.context === "function" ? options.context() : options.context;
@@ -958,7 +1325,9 @@ function wireProcessesToChronicle(options) {
958
1325
  }
959
1326
  }))
960
1327
  ];
961
- return () => unsubs.forEach((unsubscribe) => unsubscribe());
1328
+ return () => unsubs.forEach((unsubscribe) => {
1329
+ unsubscribe();
1330
+ });
962
1331
  }
963
1332
  function persist3(options, event, fields) {
964
1333
  const context = typeof options.context === "function" ? options.context() : options.context;
@@ -1086,7 +1455,9 @@ function wireDecisionsToChronicle(options) {
1086
1455
  options.events.on("brain.human_answered", (e) => write("decision.human_answered", e.at, e.sessionId, e.id, { resolver: "human", optionId: e.optionId, denied: e.deny ?? false, answerHash: hash(e.text) }, e.deny ? "denied" : "success")),
1087
1456
  options.events.on("brain.outcome", (e) => write("decision.outcome_observed", e.at, e.sessionId, e.requestId, { observedOutcome: e.outcome, detailHash: hash(e.detail) }, e.outcome))
1088
1457
  ];
1089
- return () => offs.forEach((off) => off());
1458
+ return () => offs.forEach((off) => {
1459
+ off();
1460
+ });
1090
1461
  }
1091
1462
  function hash(value) {
1092
1463
  return value ? createHash6("sha256").update(value).digest("hex") : void 0;
@@ -1116,6 +1487,9 @@ var CODING_SIGNAL = [
1116
1487
  ];
1117
1488
  var SENSITIVE_KEY = /(content|text|prompt|question|rationale|reason|detail|summary|description|context|input|output|error|message|secret|token|password|key)$/i;
1118
1489
  var PRESERVE_STRING_KEY = /(^|_)(id|status|state|kind|type|source|model|provider|phase|risk|fallback|path|name|role|sha|branch|mode)$/i;
1490
+ var TRUNCATED_ARRAYS = /* @__PURE__ */ new Set(["recentMail", "recentTools", "recentCommands"]);
1491
+ var TRUNCATED_ARRAY_MAX = 5;
1492
+ var DEFAULT_ARRAY_MAX = 20;
1119
1493
  function wireDomainEventsToChronicle(options) {
1120
1494
  return options.events.onAny((eventName, payload) => {
1121
1495
  if (SPECIALIZED.some((pattern) => pattern.test(eventName))) return;
@@ -1197,8 +1571,9 @@ function sanitize(value, key = "", depth = 0, seen = /* @__PURE__ */ new WeakSet
1197
1571
  if (depth >= 5) return { hash: digest(safeString(value)), depthLimited: true };
1198
1572
  seen.add(value);
1199
1573
  if (Array.isArray(value)) {
1200
- const items = value.slice(0, 100).map((item) => sanitize(item, key, depth + 1, seen));
1201
- return value.length > 100 ? { items, total: value.length, truncated: true } : items;
1574
+ const cap = TRUNCATED_ARRAYS.has(key) ? TRUNCATED_ARRAY_MAX : DEFAULT_ARRAY_MAX;
1575
+ const items = value.slice(0, cap).map((item) => sanitize(item, key, depth + 1, seen));
1576
+ return value.length > cap ? { items, total: value.length, truncated: true } : items;
1202
1577
  }
1203
1578
  const output = {};
1204
1579
  const entries = Object.entries(value);
@@ -1291,7 +1666,9 @@ function wireProviderStreamsToChronicle(options) {
1291
1666
  ];
1292
1667
  return () => {
1293
1668
  for (const state of [...states.values()]) flush(state.sessionId, state.agentId, "failure");
1294
- offs.forEach((off) => off());
1669
+ offs.forEach((off) => {
1670
+ off();
1671
+ });
1295
1672
  };
1296
1673
  }
1297
1674
 
@@ -1527,7 +1904,9 @@ function wireRollupsToChronicle(options) {
1527
1904
  return () => {
1528
1905
  clearInterval(timer);
1529
1906
  for (const key of [...buckets.keys()]) flush(key);
1530
- offs.forEach((off) => off());
1907
+ offs.forEach((off) => {
1908
+ off();
1909
+ });
1531
1910
  };
1532
1911
  }
1533
1912
  function text(value) {
@@ -1542,94 +1921,166 @@ function safeDigest(value) {
1542
1921
  }
1543
1922
 
1544
1923
  // src/chronicle/query.ts
1924
+ import { createHash as createHash11 } from "node:crypto";
1925
+ import { createReadStream } from "node:fs";
1545
1926
  import * as fs4 from "node:fs/promises";
1546
1927
  import * as path5 from "node:path";
1928
+ import { createInterface } from "node:readline";
1929
+ var MAX_CURSOR_SNAPSHOT_ENTRIES = 1e4;
1930
+ function streamLines(filePath, maxBytes) {
1931
+ const stream = createReadStream(filePath, {
1932
+ encoding: "utf8",
1933
+ highWaterMark: 256 * 1024,
1934
+ ...maxBytes !== void 0 ? { end: maxBytes - 1 } : {}
1935
+ });
1936
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
1937
+ return rl[Symbol.asyncIterator]();
1938
+ }
1547
1939
  var ChronicleQueryEngine = class _ChronicleQueryEngine {
1548
- constructor(events, diagnostics) {
1549
- this.events = events;
1940
+ constructor(files, diagnostics) {
1550
1941
  this.diagnostics = diagnostics;
1942
+ this.partitionFiles = files;
1551
1943
  }
1552
- events;
1553
1944
  diagnostics;
1945
+ partitionFiles;
1554
1946
  static async fromDirectory(directory) {
1555
- return _ChronicleQueryEngine.fromFiles(await findPartitions(path5.resolve(directory)));
1947
+ const files = await findPartitions(path5.resolve(directory));
1948
+ return new _ChronicleQueryEngine(files, { sourceFiles: files.length, invalidLines: 0 });
1556
1949
  }
1557
1950
  static async fromFiles(files) {
1558
- const events = [];
1951
+ return new _ChronicleQueryEngine(files, { sourceFiles: files.length, invalidLines: 0 });
1952
+ }
1953
+ /** Stream all partitions, filter, and return the requested page + summary. */
1954
+ async query(query = {}) {
1955
+ const order = query.order ?? "desc";
1956
+ const limit = Math.max(1, Math.min(query.limit ?? 100, 1e4));
1957
+ const queryHash = hashQuery(query);
1958
+ const cursor = decodeCursor(query.cursor, order, queryHash);
1959
+ const snapshotFiles = cursor ? await resolveSnapshotFiles(this.partitionFiles, cursor.snapshot) : await captureSnapshot(this.partitionFiles);
1960
+ const files = order === "asc" ? snapshotFiles : snapshotFiles.slice().reverse();
1961
+ const summaryAcc = createSummaryAccumulator();
1962
+ const orderedCandidates = [];
1963
+ const pageOrder = (left, right) => compareEvents(left, right) * (order === "asc" ? 1 : -1);
1964
+ let totalCount = 0;
1965
+ let remainingCount = 0;
1966
+ let scannedEvents = 0;
1559
1967
  let invalidLines = 0;
1560
- for (const file of files) {
1561
- let raw;
1968
+ for (const snapshotFile of files) {
1562
1969
  try {
1563
- raw = await fs4.readFile(file, "utf8");
1564
- } catch {
1565
- continue;
1566
- }
1567
- for (const line of raw.split("\n")) {
1568
- if (!line.trim()) continue;
1569
- try {
1570
- const parsed = JSON.parse(line);
1571
- if (isChronicleEvent(parsed)) events.push(parsed);
1572
- else invalidLines++;
1573
- } catch {
1574
- invalidLines++;
1970
+ if (snapshotFile.size === 0) continue;
1971
+ const lines = order === "asc" ? streamLines(snapshotFile.file, snapshotFile.size) : reverseLines(snapshotFile.file, snapshotFile.size);
1972
+ for await (const line of lines) {
1973
+ if (!line.trim()) continue;
1974
+ let event;
1975
+ try {
1976
+ event = JSON.parse(line);
1977
+ if (!isChronicleEvent(event)) {
1978
+ invalidLines++;
1979
+ continue;
1980
+ }
1981
+ } catch {
1982
+ invalidLines++;
1983
+ continue;
1984
+ }
1985
+ scannedEvents++;
1986
+ if (!matches(event, query)) continue;
1987
+ totalCount++;
1988
+ updateSummary(summaryAcc, event);
1989
+ if (cursor && compareEventToKey(event, cursor.after) * (order === "asc" ? 1 : -1) <= 0) {
1990
+ continue;
1991
+ }
1992
+ remainingCount++;
1993
+ const insertionIndex = findInsertionIndex(orderedCandidates, event, pageOrder);
1994
+ if (insertionIndex < limit) {
1995
+ orderedCandidates.splice(insertionIndex, 0, event);
1996
+ if (orderedCandidates.length > limit) orderedCandidates.pop();
1997
+ }
1575
1998
  }
1999
+ } catch {
1576
2000
  }
1577
2001
  }
1578
- return new _ChronicleQueryEngine(events, { sourceFiles: files.length, invalidLines });
1579
- }
1580
- query(query = {}) {
1581
- const order = query.order ?? "desc";
1582
- const found = this.events.filter((event) => matches(event, query));
1583
- found.sort((a, b) => compareEvents(a, b) * (order === "asc" ? 1 : -1));
1584
- const offset = decodeCursor(query.cursor);
1585
- const limit = Math.max(1, Math.min(query.limit ?? 100, 1e4));
1586
- const events = found.slice(offset, offset + limit);
1587
- const nextOffset = offset + events.length;
2002
+ const pageEvents = orderedCandidates;
2003
+ const lastEvent = pageEvents.at(-1);
1588
2004
  return {
1589
- events,
1590
- total: found.length,
1591
- summary: summarize(found),
1592
- ...nextOffset < found.length ? { nextCursor: encodeCursor(nextOffset) } : {},
1593
- scannedEvents: this.events.length,
1594
- sourceFiles: this.diagnostics.sourceFiles,
1595
- invalidLines: this.diagnostics.invalidLines
2005
+ events: pageEvents,
2006
+ total: totalCount,
2007
+ summary: finalizeSummary(summaryAcc),
2008
+ ...lastEvent && pageEvents.length < remainingCount ? { nextCursor: encodeCursor({
2009
+ version: 1,
2010
+ order,
2011
+ queryHash,
2012
+ after: orderKey(lastEvent),
2013
+ snapshot: snapshotFiles.map(({ id, size }) => ({ id, size }))
2014
+ }) } : {},
2015
+ scannedEvents,
2016
+ sourceFiles: snapshotFiles.length,
2017
+ invalidLines
1596
2018
  };
1597
2019
  }
1598
- facet(field, query = {}, limit = 100) {
2020
+ /** Stream all partitions and compute facet value counts. */
2021
+ async facet(field, query = {}, limit = 100) {
1599
2022
  const counts = /* @__PURE__ */ new Map();
1600
- for (const event of this.events) {
1601
- if (!matches(event, query)) continue;
1602
- const value = facetValue(event, field);
1603
- if (value !== void 0) counts.set(value, (counts.get(value) ?? 0) + 1);
2023
+ let invalidLines = 0;
2024
+ for (const file of this.partitionFiles) {
2025
+ try {
2026
+ for await (const line of streamLines(file)) {
2027
+ if (!line.trim()) continue;
2028
+ let event;
2029
+ try {
2030
+ event = JSON.parse(line);
2031
+ if (!isChronicleEvent(event)) {
2032
+ invalidLines++;
2033
+ continue;
2034
+ }
2035
+ } catch {
2036
+ invalidLines++;
2037
+ continue;
2038
+ }
2039
+ if (!matches(event, query)) continue;
2040
+ const value = facetValue(event, field);
2041
+ if (value !== void 0) counts.set(value, (counts.get(value) ?? 0) + 1);
2042
+ }
2043
+ } catch {
2044
+ }
1604
2045
  }
2046
+ this.diagnostics.invalidLines = invalidLines;
1605
2047
  return [...counts].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value)).slice(0, Math.max(0, limit));
1606
2048
  }
1607
2049
  /** Expand explicit and typed correlation edges; temporal proximity alone never creates causality. */
1608
- graph(seed, hops = 2, maxNodes = 1e3) {
1609
- const seeds = this.events.filter((event) => matches(event, seed));
1610
- const byKey = /* @__PURE__ */ new Map();
1611
- for (const event of this.events) for (const key of relationKeys(event)) {
1612
- const values = byKey.get(key.key) ?? [];
1613
- values.push(event);
1614
- byKey.set(key.key, values);
2050
+ async graph(seed = {}, hops = 2, maxNodes = 1e3) {
2051
+ const nodeLimit = Math.max(0, Math.floor(maxNodes));
2052
+ const selected = /* @__PURE__ */ new Map();
2053
+ let seedCount = 0;
2054
+ for await (const event of streamEvents(this.partitionFiles)) {
2055
+ if (!matches(event, seed)) continue;
2056
+ seedCount++;
2057
+ if (selected.size < nodeLimit) selected.set(event.eventId, event);
1615
2058
  }
1616
- const selected = new Map(seeds.slice(0, maxNodes).map((event) => [event.eventId, event]));
1617
2059
  let frontier = [...selected.values()];
1618
- for (let depth = 0; depth < Math.max(0, Math.min(hops, 10)) && frontier.length > 0 && selected.size < maxNodes; depth++) {
2060
+ const depthLimit = Math.max(0, Math.min(hops, 10));
2061
+ for (let depth = 0; depth < depthLimit && frontier.length > 0 && selected.size < nodeLimit; depth++) {
2062
+ const frontierKeys = new Set(frontier.flatMap((event) => relationKeys(event).map((relation) => relation.key)));
1619
2063
  const next = [];
1620
- for (const event of frontier) for (const relation of relationKeys(event)) for (const candidate of byKey.get(relation.key) ?? []) {
1621
- if (!selected.has(candidate.eventId) && selected.size < maxNodes) {
1622
- selected.set(candidate.eventId, candidate);
1623
- next.push(candidate);
1624
- }
2064
+ for await (const event of streamEvents(this.partitionFiles)) {
2065
+ if (selected.has(event.eventId)) continue;
2066
+ if (!relationKeys(event).some((relation) => frontierKeys.has(relation.key))) continue;
2067
+ selected.set(event.eventId, event);
2068
+ next.push(event);
2069
+ if (selected.size >= nodeLimit) break;
1625
2070
  }
1626
2071
  frontier = next;
1627
2072
  }
1628
2073
  const nodes = [...selected.values()].sort(compareEvents);
2074
+ const byKey = /* @__PURE__ */ new Map();
2075
+ for (const node of nodes) for (const relation of relationKeys(node)) {
2076
+ const related = byKey.get(relation.key) ?? [];
2077
+ related.push(node);
2078
+ byKey.set(relation.key, related);
2079
+ }
1629
2080
  const edges = [];
1630
2081
  const seen = /* @__PURE__ */ new Set();
1631
2082
  for (const node of nodes) for (const relation of relationKeys(node)) for (const candidate of byKey.get(relation.key) ?? []) {
1632
- if (!selected.has(candidate.eventId) || candidate.eventId === node.eventId) continue;
2083
+ if (candidate.eventId === node.eventId) continue;
1633
2084
  const [from, to] = compareEvents(node, candidate) <= 0 ? [node, candidate] : [candidate, node];
1634
2085
  const id = `${from.eventId}:${to.eventId}:${relation.kind}`;
1635
2086
  if (!seen.has(id)) {
@@ -1637,87 +2088,241 @@ var ChronicleQueryEngine = class _ChronicleQueryEngine {
1637
2088
  edges.push({ from: from.eventId, to: to.eventId, kind: relation.kind, confidence: relation.confidence });
1638
2089
  }
1639
2090
  }
1640
- return { nodes, edges, truncated: seeds.length > maxNodes || selected.size >= maxNodes };
2091
+ return { nodes, edges, truncated: seedCount > nodeLimit || selected.size >= nodeLimit };
1641
2092
  }
1642
2093
  };
1643
- function summarize(events) {
1644
- const ofType = (type) => events.filter((event) => event.eventType === type);
1645
- const unique = (values) => new Set(values.filter((value) => Boolean(value))).size;
1646
- const numberAt = (event, path6) => {
1647
- const value = readPath(event.attributes ?? {}, path6);
1648
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
2094
+ async function* streamEvents(files) {
2095
+ for (const file of files) {
2096
+ try {
2097
+ for await (const line of streamLines(file)) {
2098
+ if (!line.trim()) continue;
2099
+ try {
2100
+ const event = JSON.parse(line);
2101
+ if (isChronicleEvent(event)) yield event;
2102
+ } catch {
2103
+ }
2104
+ }
2105
+ } catch {
2106
+ }
2107
+ }
2108
+ }
2109
+ async function* reverseLines(filePath, maxBytes) {
2110
+ const CHUNK = 64 * 1024;
2111
+ const NEWLINE = 10;
2112
+ let handle;
2113
+ try {
2114
+ handle = await fs4.open(filePath, "r");
2115
+ } catch {
2116
+ return;
2117
+ }
2118
+ try {
2119
+ const fileSize = (await handle.stat()).size;
2120
+ const size = Math.min(fileSize, maxBytes ?? fileSize);
2121
+ let position = size;
2122
+ let suffix = Buffer.alloc(0);
2123
+ while (position > 0) {
2124
+ const length = Math.min(CHUNK, position);
2125
+ position -= length;
2126
+ const buffer = Buffer.allocUnsafe(length);
2127
+ await handle.read(buffer, 0, length, position);
2128
+ const data = suffix.length === 0 ? buffer : Buffer.concat([buffer, suffix]);
2129
+ let lineEnd = data.length;
2130
+ let firstNewline = -1;
2131
+ for (let index = data.length - 1; index >= 0; index--) {
2132
+ if (data[index] !== NEWLINE) continue;
2133
+ const trimmed2 = data.subarray(index + 1, lineEnd).toString("utf8").trim();
2134
+ if (trimmed2) yield trimmed2;
2135
+ lineEnd = index;
2136
+ firstNewline = index;
2137
+ }
2138
+ suffix = firstNewline >= 0 ? Buffer.from(data.subarray(0, firstNewline)) : data;
2139
+ }
2140
+ const trimmed = suffix.toString("utf8").trim();
2141
+ if (trimmed) yield trimmed;
2142
+ } finally {
2143
+ await handle.close();
2144
+ }
2145
+ }
2146
+ function createSummaryAccumulator() {
2147
+ return {
2148
+ logicalRequestIds: /* @__PURE__ */ new Set(),
2149
+ modelAttempts: 0,
2150
+ completedAttempts: 0,
2151
+ failedAttempts: 0,
2152
+ scheduledRetries: 0,
2153
+ fallbacks: 0,
2154
+ providers: /* @__PURE__ */ new Set(),
2155
+ models: /* @__PURE__ */ new Set(),
2156
+ inputTokens: 0,
2157
+ outputTokens: 0,
2158
+ cacheReadTokens: 0,
2159
+ cacheWriteTokens: 0,
2160
+ costByScope: /* @__PURE__ */ new Map(),
2161
+ providerDurations: [],
2162
+ toolCalls: 0,
2163
+ completedTools: 0,
2164
+ failedTools: 0,
2165
+ toolDurations: [],
2166
+ processes: 0,
2167
+ failedProcesses: 0,
2168
+ fileEvents: 0,
2169
+ uniqueFiles: /* @__PURE__ */ new Set(),
2170
+ agentEvents: 0,
2171
+ uniqueAgents: /* @__PURE__ */ new Set(),
2172
+ decisions: 0,
2173
+ escalations: 0,
2174
+ failures: 0,
2175
+ cancellations: 0,
2176
+ families: { llm: 0, agent: 0, tool: 0, file: 0, memory: 0, task: 0, decision: 0, runtime: 0 },
2177
+ failuresByFamily: { llm: 0, agent: 0, tool: 0, file: 0, memory: 0, task: 0, decision: 0, runtime: 0 }
1649
2178
  };
1650
- const completedAttempts = ofType("provider.attempt.completed");
1651
- const failedAttempts = ofType("provider.attempt.failed");
1652
- const completedTools = ofType("tool.executed");
1653
- const failedTools = ofType("tool.failed");
1654
- const providerDurations = [...completedAttempts, ...failedAttempts].map(durationMs).filter((value) => value > 0).sort((a, b) => a - b);
1655
- const toolDurations = [...completedTools, ...failedTools].map(durationMs).filter((value) => value > 0);
1656
- const files = events.filter((event) => event.resource?.kind === "file" || event.eventType.startsWith("file."));
1657
- const failedProcesses = ofType("process.completed").filter((event) => event.outcome === "failure").length;
1658
- const latestAccounted = latestByScope(ofType("token.accounted"));
1659
- const families = familyCounts(events);
1660
- const terminalFailures = events.filter(isTerminalFailure);
2179
+ }
2180
+ function updateSummary(acc, event) {
2181
+ const family = signalFamily(event);
2182
+ acc.families[family]++;
2183
+ if (isTerminalFailure(event)) acc.failuresByFamily[family]++;
2184
+ if (event.correlation.logicalRequestId) acc.logicalRequestIds.add(event.correlation.logicalRequestId);
2185
+ if (event.runtime?.providerId) acc.providers.add(event.runtime.providerId);
2186
+ if (event.runtime?.modelId) acc.models.add(event.runtime.modelId);
2187
+ if (event.eventType === "provider.attempt.started") acc.modelAttempts++;
2188
+ else if (event.eventType === "provider.attempt.completed") {
2189
+ acc.completedAttempts++;
2190
+ acc.inputTokens += numberAt(event, "usage.input");
2191
+ acc.outputTokens += numberAt(event, "usage.output");
2192
+ acc.cacheReadTokens += numberAt(event, "usage.cacheRead");
2193
+ acc.cacheWriteTokens += numberAt(event, "usage.cacheWrite");
2194
+ const dur = durationMs(event);
2195
+ if (dur > 0) acc.providerDurations.push(dur);
2196
+ } else if (event.eventType === "provider.attempt.failed") {
2197
+ acc.failedAttempts++;
2198
+ if (event.attributes?.retryScheduled === true) acc.scheduledRetries++;
2199
+ const dur = durationMs(event);
2200
+ if (dur > 0) acc.providerDurations.push(dur);
2201
+ } else if (event.eventType === "provider.fallback") acc.fallbacks++;
2202
+ else if (event.eventType === "tool.started") acc.toolCalls++;
2203
+ else if (event.eventType === "tool.executed") {
2204
+ acc.completedTools++;
2205
+ const dur = durationMs(event);
2206
+ if (dur > 0) acc.toolDurations.push(dur);
2207
+ } else if (event.eventType === "tool.failed") {
2208
+ acc.failedTools++;
2209
+ const dur = durationMs(event);
2210
+ if (dur > 0) acc.toolDurations.push(dur);
2211
+ } else if (event.eventType === "process.started") acc.processes++;
2212
+ else if (event.eventType === "process.completed" && event.outcome === "failure") acc.failedProcesses++;
2213
+ else if (event.eventType === "decision.requested") acc.decisions++;
2214
+ else if (event.eventType === "decision.escalated") acc.escalations++;
2215
+ if (event.eventType === "token.accounted") {
2216
+ const cost = readPath(event.attributes ?? {}, "cost.total");
2217
+ if (typeof cost === "number" && Number.isFinite(cost)) {
2218
+ const key = scopeKey(event);
2219
+ const existing = acc.costByScope.get(key);
2220
+ if (!existing || compareEvents(event, existing.event) > 0) {
2221
+ acc.costByScope.set(key, { cost, event });
2222
+ }
2223
+ }
2224
+ }
2225
+ if (event.resource?.kind === "file" || event.eventType.startsWith("file.")) {
2226
+ acc.fileEvents++;
2227
+ if (event.resource?.path) acc.uniqueFiles.add(event.resource.path);
2228
+ }
2229
+ if (family === "agent") acc.agentEvents++;
2230
+ if (event.scope.agentId) acc.uniqueAgents.add(event.scope.agentId);
2231
+ if (isTerminalFailure(event)) acc.failures++;
2232
+ if (event.outcome === "cancelled" || event.outcome === "abandoned") acc.cancellations++;
2233
+ }
2234
+ function finalizeSummary(acc) {
2235
+ const sortedProviderDurations = acc.providerDurations.slice().sort((a, b) => a - b);
2236
+ const sortedToolDurations = acc.toolDurations.slice().sort((a, b) => a - b);
2237
+ const totalCost = [...acc.costByScope.values()].reduce((sum, entry) => sum + entry.cost, 0);
1661
2238
  return {
1662
- logicalRequests: unique(events.map((event) => event.correlation.logicalRequestId)),
1663
- modelAttempts: ofType("provider.attempt.started").length,
1664
- completedAttempts: completedAttempts.length,
1665
- failedAttempts: failedAttempts.length,
1666
- scheduledRetries: failedAttempts.filter((event) => event.attributes?.retryScheduled === true).length,
1667
- fallbacks: ofType("provider.fallback").length,
1668
- providers: unique(events.map((event) => event.runtime?.providerId)),
1669
- models: unique(events.map((event) => event.runtime?.modelId)),
1670
- inputTokens: completedAttempts.reduce((sum, event) => sum + numberAt(event, "usage.input"), 0),
1671
- outputTokens: completedAttempts.reduce((sum, event) => sum + numberAt(event, "usage.output"), 0),
1672
- cacheReadTokens: completedAttempts.reduce((sum, event) => sum + numberAt(event, "usage.cacheRead"), 0),
1673
- cacheWriteTokens: completedAttempts.reduce((sum, event) => sum + numberAt(event, "usage.cacheWrite"), 0),
1674
- estimatedCostUsd: latestAccounted.reduce((sum, event) => sum + numberAt(event, "cost.total"), 0),
1675
- providerAvgDurationMs: average(providerDurations),
1676
- providerP95DurationMs: percentile(providerDurations, 0.95),
1677
- toolCalls: ofType("tool.started").length,
1678
- completedTools: completedTools.length,
1679
- failedTools: failedTools.length,
1680
- toolAvgDurationMs: average(toolDurations),
1681
- processes: ofType("process.started").length,
1682
- failedProcesses,
1683
- fileEvents: files.length,
1684
- uniqueFiles: unique(files.map((event) => event.resource?.path)),
1685
- agentEvents: families.agent,
1686
- uniqueAgents: unique(events.map((event) => event.scope.agentId)),
1687
- decisions: ofType("decision.requested").length,
1688
- escalations: ofType("decision.escalated").length,
1689
- failures: terminalFailures.length,
1690
- cancellations: events.filter((event) => event.outcome === "cancelled" || event.outcome === "abandoned").length,
1691
- families,
1692
- failuresByFamily: familyCounts(terminalFailures)
2239
+ logicalRequests: acc.logicalRequestIds.size,
2240
+ modelAttempts: acc.modelAttempts,
2241
+ completedAttempts: acc.completedAttempts,
2242
+ failedAttempts: acc.failedAttempts,
2243
+ scheduledRetries: acc.scheduledRetries,
2244
+ fallbacks: acc.fallbacks,
2245
+ providers: acc.providers.size,
2246
+ models: acc.models.size,
2247
+ inputTokens: acc.inputTokens,
2248
+ outputTokens: acc.outputTokens,
2249
+ cacheReadTokens: acc.cacheReadTokens,
2250
+ cacheWriteTokens: acc.cacheWriteTokens,
2251
+ estimatedCostUsd: totalCost,
2252
+ providerAvgDurationMs: average(sortedProviderDurations),
2253
+ providerP95DurationMs: percentile(sortedProviderDurations, 0.95),
2254
+ toolCalls: acc.toolCalls,
2255
+ completedTools: acc.completedTools,
2256
+ failedTools: acc.failedTools,
2257
+ toolAvgDurationMs: average(sortedToolDurations),
2258
+ processes: acc.processes,
2259
+ failedProcesses: acc.failedProcesses,
2260
+ fileEvents: acc.fileEvents,
2261
+ uniqueFiles: acc.uniqueFiles.size,
2262
+ agentEvents: acc.agentEvents,
2263
+ uniqueAgents: acc.uniqueAgents.size,
2264
+ decisions: acc.decisions,
2265
+ escalations: acc.escalations,
2266
+ failures: acc.failures,
2267
+ cancellations: acc.cancellations,
2268
+ families: acc.families,
2269
+ failuresByFamily: acc.failuresByFamily
2270
+ };
2271
+ }
2272
+ Object.defineProperty(ChronicleQueryEngine.prototype, "partitionFiles", {
2273
+ get() {
2274
+ throw new Error("ChronicleQueryEngine no longer loads events on construction. Use async query().");
2275
+ },
2276
+ set(_val) {
2277
+ Object.defineProperty(this, "partitionFiles", { value: _val, writable: false, configurable: false });
2278
+ }
2279
+ });
2280
+ async function findPartitions(root) {
2281
+ const result = [];
2282
+ const partitionName = /^(.*\.events)(?:\.(\d{5}))?\.jsonl$/;
2283
+ const visit = async (directory) => {
2284
+ let entries;
2285
+ try {
2286
+ entries = await fs4.readdir(directory, { withFileTypes: true });
2287
+ } catch {
2288
+ return;
2289
+ }
2290
+ for (const entry of entries) {
2291
+ const full = path5.join(directory, entry.name);
2292
+ if (entry.isDirectory()) await visit(full);
2293
+ else if (entry.isFile() && partitionName.test(entry.name)) result.push(full);
2294
+ }
1693
2295
  };
2296
+ await visit(root);
2297
+ return result.sort((left, right) => {
2298
+ const leftMatch = partitionName.exec(path5.basename(left));
2299
+ const rightMatch = partitionName.exec(path5.basename(right));
2300
+ const leftGroup = path5.join(path5.dirname(left), leftMatch?.[1] ?? left);
2301
+ const rightGroup = path5.join(path5.dirname(right), rightMatch?.[1] ?? right);
2302
+ const groupOrder = leftGroup.localeCompare(rightGroup);
2303
+ if (groupOrder !== 0) return groupOrder;
2304
+ return Number(leftMatch?.[2] ?? 0) - Number(rightMatch?.[2] ?? 0);
2305
+ });
2306
+ }
2307
+ function numberAt(event, dotPath) {
2308
+ const value = readPath(event.attributes ?? {}, dotPath);
2309
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1694
2310
  }
1695
2311
  function durationMs(event) {
1696
2312
  const value = Number(event.durationNs ?? 0) / 1e6;
1697
2313
  return Number.isFinite(value) ? value : 0;
1698
2314
  }
1699
2315
  function average(values) {
1700
- return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
2316
+ return values.length ? values.reduce((sum, v) => sum + v, 0) / values.length : 0;
1701
2317
  }
1702
2318
  function percentile(sorted, quantile) {
1703
2319
  return sorted.length ? sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * quantile) - 1))] : 0;
1704
2320
  }
1705
- function latestByScope(events) {
1706
- const latest = /* @__PURE__ */ new Map();
1707
- for (const event of events) {
1708
- const key = `${event.scope.projectId ?? ""}\0${event.scope.sessionId ?? ""}\0${event.scope.agentId ?? ""}`;
1709
- const current = latest.get(key);
1710
- if (!current || compareEvents(current, event) < 0) latest.set(key, event);
1711
- }
1712
- return [...latest.values()];
1713
- }
1714
- function familyCounts(events) {
1715
- const counts = { llm: 0, agent: 0, tool: 0, file: 0, memory: 0, task: 0, decision: 0, runtime: 0 };
1716
- for (const event of events) counts[signalFamily(event)]++;
1717
- return counts;
2321
+ function scopeKey(event) {
2322
+ return `${event.scope.projectId ?? ""}\0${event.scope.sessionId ?? ""}\0${event.scope.agentId ?? ""}`;
1718
2323
  }
1719
2324
  function signalFamily(event) {
1720
- if (event.eventType.startsWith("decision.") || event.eventType.startsWith("brain.")) return "decision";
2325
+ if (/^(?:decision|brain|permission)\./.test(event.eventType)) return "decision";
1721
2326
  if (event.resource?.kind === "file" || event.resource?.kind === "symbol" || /^(?:file|worktree)\./.test(event.eventType)) return "file";
1722
2327
  if (/^(?:provider|token|context|ctx|compaction)\./.test(event.eventType)) return "llm";
1723
2328
  if (/^(?:agent|subagent|delegate|fleet|concurrency)\./.test(event.eventType)) return "agent";
@@ -1747,24 +2352,6 @@ function relationKeys(event) {
1747
2352
  add("parent_span", event.correlation.spanId, "explicit");
1748
2353
  return result;
1749
2354
  }
1750
- async function findPartitions(root) {
1751
- const result = [];
1752
- const visit = async (directory) => {
1753
- let entries;
1754
- try {
1755
- entries = await fs4.readdir(directory, { withFileTypes: true });
1756
- } catch {
1757
- return;
1758
- }
1759
- for (const entry of entries) {
1760
- const full = path5.join(directory, entry.name);
1761
- if (entry.isDirectory()) await visit(full);
1762
- else if (entry.isFile() && entry.name.endsWith(".events.jsonl")) result.push(full);
1763
- }
1764
- };
1765
- await visit(root);
1766
- return result.sort();
1767
- }
1768
2355
  function matches(event, query) {
1769
2356
  if (query.eventId && event.eventId !== query.eventId) return false;
1770
2357
  if (query.eventTypes && !query.eventTypes.includes(event.eventType)) return false;
@@ -1791,7 +2378,8 @@ function normalize2(value) {
1791
2378
  return value?.replaceAll("\\", "/").toLocaleLowerCase();
1792
2379
  }
1793
2380
  function lineContains(event, line) {
1794
- const start = event.resource?.lineStart, end = event.resource?.lineEnd ?? start;
2381
+ const start = event.resource?.lineStart;
2382
+ const end = event.resource?.lineEnd ?? start;
1795
2383
  return start !== void 0 && end !== void 0 && line >= start && line <= end;
1796
2384
  }
1797
2385
  function objectContains(actual, expected) {
@@ -1803,19 +2391,150 @@ function readPath(value, key) {
1803
2391
  function deepEqual(left, right) {
1804
2392
  return JSON.stringify(left) === JSON.stringify(right);
1805
2393
  }
2394
+ function findInsertionIndex(events, event, compare) {
2395
+ let low = 0;
2396
+ let high = events.length;
2397
+ while (low < high) {
2398
+ const middle = low + high >>> 1;
2399
+ if (compare(events[middle], event) <= 0) low = middle + 1;
2400
+ else high = middle;
2401
+ }
2402
+ return low;
2403
+ }
1806
2404
  function compareEvents(a, b) {
1807
- return (a.occurredAt ?? a.observedAt).localeCompare(b.occurredAt ?? b.observedAt) || a.persistedAt.localeCompare(b.persistedAt) || a.sequence - b.sequence;
2405
+ return compareEventToKey(a, orderKey(b));
2406
+ }
2407
+ function compareEventToKey(event, key) {
2408
+ return (event.occurredAt ?? event.observedAt).localeCompare(key.occurredAt) || event.persistedAt.localeCompare(key.persistedAt) || event.sequence - key.sequence || event.eventId.localeCompare(key.eventId);
2409
+ }
2410
+ function orderKey(event) {
2411
+ return {
2412
+ occurredAt: event.occurredAt ?? event.observedAt,
2413
+ persistedAt: event.persistedAt,
2414
+ sequence: event.sequence,
2415
+ eventId: event.eventId
2416
+ };
2417
+ }
2418
+ function hashQuery(query) {
2419
+ const { cursor: _cursor, limit: _limit, order: _order, ...filters } = query;
2420
+ return createHash11("sha256").update(stableStringify2(filters), "utf8").digest("base64url");
2421
+ }
2422
+ function encodeCursor(cursor) {
2423
+ return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
2424
+ }
2425
+ function decodeCursor(encoded, order, queryHash) {
2426
+ if (!encoded) return void 0;
2427
+ if (encoded.length > 1e6) throw new Error("Invalid Chronicle cursor");
2428
+ try {
2429
+ const parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
2430
+ if (!isCursor(parsed) || parsed.order !== order || parsed.queryHash !== queryHash) {
2431
+ throw new Error("cursor does not match the query");
2432
+ }
2433
+ return parsed;
2434
+ } catch (error) {
2435
+ throw new Error(`Invalid Chronicle cursor: ${error instanceof Error ? error.message : String(error)}`);
2436
+ }
1808
2437
  }
1809
- function encodeCursor(offset) {
1810
- return Buffer.from(String(offset)).toString("base64url");
2438
+ function isCursor(value) {
2439
+ if (!value || typeof value !== "object") return false;
2440
+ const cursor = value;
2441
+ const after = cursor.after;
2442
+ if (cursor.version !== 1 || cursor.order !== "asc" && cursor.order !== "desc" || typeof cursor.queryHash !== "string" || !after || typeof after.occurredAt !== "string" || typeof after.persistedAt !== "string" || !Number.isSafeInteger(after.sequence) || typeof after.eventId !== "string" || !Array.isArray(cursor.snapshot) || cursor.snapshot.length > MAX_CURSOR_SNAPSHOT_ENTRIES) return false;
2443
+ const snapshotIds = /* @__PURE__ */ new Set();
2444
+ for (const entry of cursor.snapshot) {
2445
+ if (!entry || typeof entry.id !== "string" || !entry.id || !Number.isSafeInteger(entry.size) || entry.size < 0 || snapshotIds.has(entry.id)) return false;
2446
+ snapshotIds.add(entry.id);
2447
+ }
2448
+ return true;
2449
+ }
2450
+ async function captureSnapshot(files) {
2451
+ if (files.length > MAX_CURSOR_SNAPSHOT_ENTRIES) {
2452
+ throw new Error("Chronicle snapshot contains too many partitions");
2453
+ }
2454
+ return Promise.all(files.map(async (file) => {
2455
+ let size = 0;
2456
+ try {
2457
+ size = (await fs4.stat(file)).size;
2458
+ } catch {
2459
+ }
2460
+ return { file, id: fileId(file), size };
2461
+ }));
1811
2462
  }
1812
- function decodeCursor(cursor) {
1813
- if (!cursor) return 0;
1814
- const value = Number(Buffer.from(cursor, "base64url").toString("utf8"));
1815
- return Number.isSafeInteger(value) && value >= 0 ? value : 0;
2463
+ async function resolveSnapshotFiles(files, snapshot) {
2464
+ const currentFiles = new Map(files.map((file) => [fileId(file), file]));
2465
+ return Promise.all(snapshot.map(async (entry) => {
2466
+ const file = currentFiles.get(entry.id);
2467
+ if (!file) throw new Error("Chronicle cursor snapshot has expired");
2468
+ let currentSize;
2469
+ try {
2470
+ currentSize = (await fs4.stat(file)).size;
2471
+ } catch {
2472
+ throw new Error("Chronicle cursor snapshot has expired");
2473
+ }
2474
+ if (currentSize < entry.size) throw new Error("Chronicle cursor snapshot has expired");
2475
+ return { file, ...entry };
2476
+ }));
2477
+ }
2478
+ function fileId(file) {
2479
+ return createHash11("sha256").update(path5.resolve(file), "utf8").digest("base64url");
2480
+ }
2481
+ function stableStringify2(value) {
2482
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
2483
+ if (Array.isArray(value)) return `[${value.map(stableStringify2).join(",")}]`;
2484
+ const object = value;
2485
+ return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify2(object[key])}`).join(",")}}`;
1816
2486
  }
1817
2487
  function isChronicleEvent(value) {
1818
- return Boolean(value && typeof value.eventId === "string" && typeof value.eventType === "string" && typeof value.occurredAt === "string");
2488
+ if (!isRecord(value) || !isRecord(value.scope) || !isRecord(value.correlation)) return false;
2489
+ return value.schemaVersion === 1 && typeof value.eventId === "string" && typeof value.eventType === "string" && typeof value.occurredAt === "string" && typeof value.observedAt === "string" && typeof value.persistedAt === "string" && Number.isSafeInteger(value.sequence) && value.sequence >= 0 && typeof value.previousHash === "string" && typeof value.hash === "string" && typeof value.scope.installationId === "string" && typeof value.scope.machineId === "string" && optionalStrings(value.scope, [
2490
+ "projectId",
2491
+ "repositoryId",
2492
+ "workspaceId",
2493
+ "worktreeId",
2494
+ "sessionId",
2495
+ "turnId",
2496
+ "iterationId",
2497
+ "agentId",
2498
+ "goalId",
2499
+ "planId",
2500
+ "taskId",
2501
+ "kanbanBoardId"
2502
+ ]) && typeof value.correlation.traceId === "string" && typeof value.correlation.spanId === "string" && optionalStrings(value.correlation, [
2503
+ "parentSpanId",
2504
+ "logicalRequestId",
2505
+ "attemptId",
2506
+ "toolCallId"
2507
+ ]) && isRuntime(value.runtime) && isResource(value.resource) && (value.attributes === void 0 || isRecord(value.attributes)) && (value.tags === void 0 || isStringRecord(value.tags));
2508
+ }
2509
+ function isRuntime(value) {
2510
+ return value === void 0 || isRecord(value) && optionalStrings(value, ["providerId", "modelId", "modelRevision"]) && optionalFiniteNumbers(value, ["processId", "parentProcessId"]);
2511
+ }
2512
+ function isResource(value) {
2513
+ if (value === void 0) return true;
2514
+ if (!isRecord(value) || ![
2515
+ "file",
2516
+ "symbol",
2517
+ "memory",
2518
+ "task",
2519
+ "kanban",
2520
+ "process",
2521
+ "network",
2522
+ "artifact",
2523
+ "other"
2524
+ ].includes(String(value.kind)) || typeof value.id !== "string") return false;
2525
+ return optionalStrings(value, ["path", "contentHashBefore", "contentHashAfter"]) && optionalFiniteNumbers(value, ["lineStart", "lineEnd"]);
2526
+ }
2527
+ function optionalStrings(value, keys) {
2528
+ return keys.every((key) => value[key] === void 0 || typeof value[key] === "string");
2529
+ }
2530
+ function optionalFiniteNumbers(value, keys) {
2531
+ return keys.every((key) => value[key] === void 0 || typeof value[key] === "number" && Number.isFinite(value[key]));
2532
+ }
2533
+ function isStringRecord(value) {
2534
+ return isRecord(value) && Object.values(value).every((entry) => typeof entry === "string");
2535
+ }
2536
+ function isRecord(value) {
2537
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
1819
2538
  }
1820
2539
  function facetValue(event, field) {
1821
2540
  const values = {