@wrongstack/core 0.300.0 → 0.302.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 (146) hide show
  1. package/dist/chronicle/index.d.ts +2 -0
  2. package/dist/chronicle/index.js +297 -10
  3. package/dist/chronicle/project-server.js +253 -15
  4. package/dist/chronicle/sqlite-compaction.d.ts +20 -0
  5. package/dist/chronicle/sqlite-journal.d.ts +57 -0
  6. package/dist/coordination/agents/capability-manifest.d.ts +7 -0
  7. package/dist/coordination/agents/index.d.ts +3 -2
  8. package/dist/coordination/agents/index.js +818 -570
  9. package/dist/coordination/agents/project-agent-identity.d.ts +6 -6
  10. package/dist/coordination/agents/role-skills.d.ts +1 -1
  11. package/dist/coordination/agents/types.d.ts +6 -0
  12. package/dist/coordination/director.d.ts +2 -2
  13. package/dist/coordination/index.js +6787 -6447
  14. package/dist/coordination/mail-tools.d.ts +3 -3
  15. package/dist/coordination/mailbox-project-server.js +3 -4
  16. package/dist/coordination/mailbox-types.d.ts +6 -0
  17. package/dist/core/agent-response.d.ts +5 -1
  18. package/dist/core/agent-tools.d.ts +3 -0
  19. package/dist/core/context.d.ts +20 -0
  20. package/dist/core/index.d.ts +8 -7
  21. package/dist/core/index.js +893 -284
  22. package/dist/core/instruction-template.d.ts +6 -3
  23. package/dist/core/next-steps-slot.d.ts +88 -0
  24. package/dist/core/system-prompt-builder.d.ts +5 -3
  25. package/dist/core/system-prompt-memory-skills.d.ts +1 -0
  26. package/dist/core/system-prompt-skill-bodies.d.ts +3 -3
  27. package/dist/defaults/index.js +9797 -8898
  28. package/dist/design/index.js +11 -3
  29. package/dist/execution/auto-compaction-middleware.d.ts +17 -0
  30. package/dist/execution/autonomy-brain.d.ts +8 -2
  31. package/dist/execution/brain-runtime.d.ts +3 -1
  32. package/dist/execution/compaction-core.d.ts +41 -2
  33. package/dist/execution/council-brain.d.ts +37 -2
  34. package/dist/execution/council-orchestrator.d.ts +2 -2
  35. package/dist/execution/index.d.ts +11 -10
  36. package/dist/execution/index.js +5179 -4078
  37. package/dist/execution/tool-executor.d.ts +4 -1
  38. package/dist/execution/topic-shift-advisor.d.ts +53 -0
  39. package/dist/extension/index.js +8 -2
  40. package/dist/extension/registry.d.ts +8 -1
  41. package/dist/goal/index.js +11 -3
  42. package/dist/hooks/index.js +42 -9
  43. package/dist/hooks/runner.d.ts +12 -1
  44. package/dist/hq/index.js +41 -17
  45. package/dist/hq/publisher.d.ts +21 -3
  46. package/dist/index.d.ts +21 -20
  47. package/dist/index.js +10901 -8067
  48. package/dist/infrastructure/index.js +108 -21
  49. package/dist/kernel/events/session-events.d.ts +13 -0
  50. package/dist/kernel/events/tool-events.d.ts +3 -3
  51. package/dist/models/index.d.ts +1 -0
  52. package/dist/models/index.js +17 -0
  53. package/dist/models/provider-credentials.d.ts +54 -0
  54. package/dist/plugin/index.d.ts +1 -0
  55. package/dist/plugin/index.js +2024 -502
  56. package/dist/plugins/auto-review-plugin.d.ts +3 -0
  57. package/dist/plugins/cloud-config-sync-plugin.d.ts +22 -0
  58. package/dist/plugins/review-claim-registry.d.ts +23 -8
  59. package/dist/plugins/review-types.d.ts +2 -0
  60. package/dist/registry/index.js +109 -74
  61. package/dist/registry/tool-registry.d.ts +15 -0
  62. package/dist/security/capabilities.d.ts +2 -0
  63. package/dist/security/index.d.ts +1 -1
  64. package/dist/security/index.js +9 -2
  65. package/dist/security/readonly-permission-policy.d.ts +20 -0
  66. package/dist/session-registry-atomic-file.d.ts +32 -5
  67. package/dist/session-registry-types.d.ts +17 -0
  68. package/dist/session-registry.d.ts +1 -1
  69. package/dist/skills/frontmatter.d.ts +6 -0
  70. package/dist/skills/index.js +22 -5
  71. package/dist/storage/cloud-config-sync/sanitize.d.ts +54 -0
  72. package/dist/storage/cloud-config-sync.d.ts +87 -0
  73. package/dist/storage/index.d.ts +2 -1
  74. package/dist/storage/index.js +854 -66
  75. package/dist/tools/index.d.ts +1 -1
  76. package/dist/tools/index.js +511 -234
  77. package/dist/tools/one-shot-llm-tool.d.ts +1 -0
  78. package/dist/tools/plugin-manager.d.ts +27 -0
  79. package/dist/types/config/mcp-features.d.ts +18 -11
  80. package/dist/types/config/root.d.ts +8 -1
  81. package/dist/types/config/runtime.d.ts +20 -6
  82. package/dist/types/config/skills-fleet-brain.d.ts +12 -4
  83. package/dist/types/config/tools.d.ts +16 -0
  84. package/dist/types/context-window.d.ts +1 -0
  85. package/dist/types/hooks.d.ts +14 -0
  86. package/dist/types/index.d.ts +2 -2
  87. package/dist/types/index.js +1 -0
  88. package/dist/types/multi-agent.d.ts +2 -0
  89. package/dist/types/one-shot-llm.d.ts +16 -0
  90. package/dist/types/provider.d.ts +16 -0
  91. package/dist/types/runtime-capability-manifest.d.ts +168 -0
  92. package/dist/types/skill.d.ts +5 -0
  93. package/dist/types/system-prompt.d.ts +3 -1
  94. package/dist/types/tool-executor.d.ts +8 -1
  95. package/dist/utils/context-breakdown.d.ts +8 -2
  96. package/dist/utils/index.d.ts +1 -1
  97. package/dist/utils/index.js +141 -31
  98. package/dist/utils/regex-guard.d.ts +16 -2
  99. package/dist/utils/sage-output-block.d.ts +7 -10
  100. package/dist/utils/wstack-paths.d.ts +11 -3
  101. package/instructions/agents/browser.md +1 -4
  102. package/instructions/agents/e2e.md +17 -15
  103. package/instructions/agents/ios.md +3 -3
  104. package/instructions/agents/search.md +1 -1
  105. package/instructions/autonomy/goal-preamble.md +4 -4
  106. package/instructions/coordination/director-preamble.md +2 -3
  107. package/instructions/coordination/subagent-baseline.md +3 -1
  108. package/instructions/leader-after-task.md +12 -0
  109. package/instructions/llm/chimera-review.md +1 -1
  110. package/instructions/modes/audit-lite.md +1 -1
  111. package/instructions/sections/tool/common-patterns.md +18 -2
  112. package/instructions/sections/tool/mailbox-compact.md +1 -1
  113. package/instructions/sections/tool/mailbox-full.md +1 -1
  114. package/instructions/system-lite.md +10 -0
  115. package/instructions/system-pro.md +23 -14
  116. package/instructions/system.md +22 -13
  117. package/package.json +3 -3
  118. package/skills/api-design/SKILL.md +3 -0
  119. package/skills/audit-log/SKILL.md +2 -0
  120. package/skills/auto-review/SKILL.md +6 -1
  121. package/skills/bug-hunter/SKILL.md +4 -1
  122. package/skills/chimera/SKILL.md +3 -0
  123. package/skills/data-governance/SKILL.md +3 -0
  124. package/skills/design-system/SKILL.md +5 -2
  125. package/skills/docker-deploy/SKILL.md +2 -0
  126. package/skills/git-flow/SKILL.md +2 -0
  127. package/skills/mailbox-bridge/SKILL.md +3 -0
  128. package/skills/mnemosyne/SKILL.md +2 -0
  129. package/skills/multi-agent/SKILL.md +4 -1
  130. package/skills/node-modern/SKILL.md +4 -1
  131. package/skills/observability/SKILL.md +3 -0
  132. package/skills/output-standards/SKILL.md +2 -0
  133. package/skills/plugin-author/SKILL.md +4 -1
  134. package/skills/prompt-engineering/SKILL.md +3 -1
  135. package/skills/react-modern/SKILL.md +6 -3
  136. package/skills/refactor-planner/SKILL.md +2 -0
  137. package/skills/research-web/SKILL.md +3 -0
  138. package/skills/sdd/SKILL.md +3 -1
  139. package/skills/security-scanner/SKILL.md +3 -0
  140. package/skills/skill-creator/SKILL.md +2 -0
  141. package/skills/tech-stack/SKILL.md +3 -0
  142. package/skills/testing/SKILL.md +4 -1
  143. package/skills/typescript-strict/SKILL.md +4 -1
  144. package/skills/wrongstack-kanban/SKILL.md +6 -3
  145. package/skills/wrongstack-mailbox/SKILL.md +4 -1
  146. package/skills/wrongstack-mailbox-mcp/SKILL.md +10 -8
@@ -7,6 +7,8 @@ export { ChronicleProjectServerClient, ChronicleRemoteJournal, isChronicleProjec
7
7
  export { chronicleProjectServerEndpoint, chronicleProjectServerKey, chronicleProjectServerMetadataPath, } from './project-server-endpoint.js';
8
8
  export { CHRONICLE_PROJECT_SERVER_MAX_FRAME_CHARS, CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION, type ChronicleMetricsRequest, type ChronicleMetricsResponse, type ChronicleMetricsView, type ChronicleProjectServerHealth, type ChronicleProjectServerInfo, type ChronicleServerOperationName, type ChronicleServerOperations, } from './project-server-protocol.js';
9
9
  export { ChronicleJournal, GENESIS_HASH, type ChronicleJournalOptions, type ChronicleJournalStats, type ChroniclePurgeOptions, type ChroniclePurgeResult } from './journal.js';
10
+ export { ChronicleCompactionBusyError, compactChronicleSqlite, type ChronicleSqliteCompactionOptions, type ChronicleSqliteCompactionResult, } from './sqlite-compaction.js';
11
+ export { ChronicleStorageQuotaError } from './sqlite-journal.js';
10
12
  export { wireProviderAttemptsToChronicle, type ChronicleProviderAdapterOptions, } from './provider-adapter.js';
11
13
  export { wireToolsToChronicle, type ChronicleToolAdapterOptions } from './tool-adapter.js';
12
14
  export { wireProcessesToChronicle, type ChronicleProcessAdapterOptions, } from './process-adapter.js';
@@ -557,6 +557,9 @@ function canonicalProjectRoot(absRoot) {
557
557
  const commonDirFile = path4.join(gitDir, "commondir");
558
558
  if (!fs2.statSync(commonDirFile).isFile()) return checkoutRoot;
559
559
  const commonDir = path4.resolve(gitDir, fs2.readFileSync(commonDirFile, "utf8").trim());
560
+ const worktreesDir = path4.dirname(gitDir);
561
+ if (path4.basename(worktreesDir).toLowerCase() !== "worktrees") return checkoutRoot;
562
+ if (path4.resolve(worktreesDir, "..") !== commonDir) return checkoutRoot;
560
563
  if (path4.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
561
564
  return path4.dirname(commonDir);
562
565
  } catch {
@@ -579,7 +582,7 @@ function safeProfileName(name) {
579
582
  const safe = (name ?? "").replace(/[/\\:]/g, "_").replace(/\.\./g, "_").trim();
580
583
  return safe || "default";
581
584
  }
582
- function activeProfileName(globalRoot) {
585
+ function bootstrapProfileName(globalRoot) {
583
586
  try {
584
587
  const parsed = JSON.parse(fs2.readFileSync(path4.join(globalRoot, "config.json"), "utf8"));
585
588
  return safeProfileName(
@@ -597,7 +600,7 @@ function wstackGlobalRoot() {
597
600
  function resolveWstackPaths(opts) {
598
601
  const globalRoot = opts.globalRoot ?? (opts.userHome ? path4.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
599
602
  const homeDir = opts.userHome ?? os2.homedir();
600
- const profileName = safeProfileName(opts.profileName ?? activeProfileName(globalRoot));
603
+ const profileName = safeProfileName(opts.profileName ?? bootstrapProfileName(globalRoot));
601
604
  const profileDir = path4.join(globalRoot, "profiles", profileName);
602
605
  const hash3 = projectHash(opts.projectRoot);
603
606
  const slug = projectSlug(opts.projectRoot);
@@ -667,7 +670,12 @@ function resolveWstackPaths(opts) {
667
670
  projectRequirementIntakes: path4.join(projectDir, "requirement-intakes"),
668
671
  syncConfig: path4.join(profileDir, "sync.json"),
669
672
  configHistoryDir: path4.join(globalRoot, "config-history"),
670
- projectStatus: (projectHash2) => path4.join(globalRoot, "projects", projectHash2, "status.json")
673
+ projectStatus: (statusProjectSlug) => {
674
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,39})-[a-f0-9]{6}$/.test(statusProjectSlug)) {
675
+ throw new Error(`Invalid project slug: ${statusProjectSlug}`);
676
+ }
677
+ return path4.join(globalRoot, "projects", statusProjectSlug, "status.json");
678
+ }
671
679
  };
672
680
  }
673
681
 
@@ -3099,9 +3107,24 @@ async function importLegacyChronicleJournal(journal, directory) {
3099
3107
 
3100
3108
  // src/chronicle/sqlite-journal.ts
3101
3109
  import { randomUUID as randomUUID3 } from "node:crypto";
3110
+ import * as fs8 from "node:fs";
3102
3111
  import { createRequire as createRequire2 } from "node:module";
3103
3112
  import * as path11 from "node:path";
3104
3113
 
3114
+ // src/utils/pid.ts
3115
+ function isPidAlive(pid) {
3116
+ if (!Number.isInteger(pid) || pid <= 0) return false;
3117
+ if (pid === process.pid) return true;
3118
+ try {
3119
+ process.kill(pid, 0);
3120
+ return true;
3121
+ } catch (err) {
3122
+ const code = err.code;
3123
+ if (code === "EPERM") return true;
3124
+ return false;
3125
+ }
3126
+ }
3127
+
3105
3128
  // src/chronicle/sqlite-query.ts
3106
3129
  var MAX_LIMIT = 1e4;
3107
3130
  function encodeCursor2(cursor) {
@@ -3344,6 +3367,25 @@ var CHRONICLE_SQLITE_FILE = "chronicle.sqlite";
3344
3367
  var LEGACY_JSONL_MIGRATION_KEY = "legacy-jsonl-v1";
3345
3368
  var LEGACY_JSONL_QUARANTINE_KEY = "legacy-jsonl-v1:quarantine";
3346
3369
  var SCHEMA_VERSION2 = 1;
3370
+ var SQLITE_FIXED_OVERHEAD_BYTES = 16 * 1024 * 1024;
3371
+ var MIN_SQLITE_PAGE_BUDGET_BYTES = 64 * 1024;
3372
+ var MAX_ROLLBACK_JOURNAL_RESERVE_BYTES = 64 * 1024 * 1024;
3373
+ var ChronicleStorageQuotaError = class extends Error {
3374
+ currentBytes;
3375
+ batchBytes;
3376
+ maxBytes;
3377
+ path;
3378
+ constructor(details) {
3379
+ super(
3380
+ `Chronicle SQLite quota exceeded at ${details.path}: ${details.currentBytes} live bytes + ${details.batchBytes} batch bytes exceeds ${details.maxBytes}; lower chronicle retentionDays/maxEvents to shed data (run chronicle compact to return the freed pages to the filesystem)`
3381
+ );
3382
+ this.name = "ChronicleStorageQuotaError";
3383
+ this.currentBytes = details.currentBytes;
3384
+ this.batchBytes = details.batchBytes;
3385
+ this.maxBytes = details.maxBytes;
3386
+ this.path = details.path;
3387
+ }
3388
+ };
3347
3389
  var Ctor2;
3348
3390
  function loadDatabaseSync2() {
3349
3391
  if (Ctor2) return Ctor2;
@@ -3366,6 +3408,14 @@ var ChronicleSqliteJournal = class {
3366
3408
  now;
3367
3409
  monotonicNow;
3368
3410
  idFactory;
3411
+ retentionDays;
3412
+ maxEvents;
3413
+ maxBytes;
3414
+ retentionCheckIntervalMs;
3415
+ nextRetentionCheckAt = 0;
3416
+ retainedEventCount = 0;
3417
+ /** Resolved lazily by `pageSizeBytes()`; constant for an open database. */
3418
+ cachedPageSize;
3369
3419
  /**
3370
3420
  * Cached chain head per day. Cleared wholesale on any write failure so the
3371
3421
  * next append rebuilds from the database rather than trusting a counter that
@@ -3387,10 +3437,36 @@ var ChronicleSqliteJournal = class {
3387
3437
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
3388
3438
  this.monotonicNow = options.monotonicNow ?? (() => process.hrtime.bigint());
3389
3439
  this.idFactory = options.idFactory ?? (() => randomUUID3());
3440
+ if (options.retentionDays !== void 0 && (!Number.isFinite(options.retentionDays) || options.retentionDays <= 0)) {
3441
+ throw new RangeError("retentionDays must be a positive finite number");
3442
+ }
3443
+ if (options.retentionCheckIntervalMs !== void 0 && (!Number.isFinite(options.retentionCheckIntervalMs) || options.retentionCheckIntervalMs <= 0)) {
3444
+ throw new RangeError("retentionCheckIntervalMs must be a positive finite number");
3445
+ }
3446
+ if (options.maxEvents !== void 0 && (!Number.isInteger(options.maxEvents) || options.maxEvents < 1)) {
3447
+ throw new RangeError("maxEvents must be a positive integer");
3448
+ }
3449
+ if (options.maxBytes !== void 0 && (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1)) {
3450
+ throw new RangeError("maxBytes must be a positive safe integer");
3451
+ }
3452
+ const minimumQuotaBytes = SQLITE_FIXED_OVERHEAD_BYTES + 2 * MIN_SQLITE_PAGE_BUDGET_BYTES;
3453
+ if (options.maxBytes !== void 0 && options.maxBytes < minimumQuotaBytes) {
3454
+ throw new RangeError(`maxBytes must be at least ${minimumQuotaBytes}`);
3455
+ }
3456
+ this.retentionDays = options.retentionDays;
3457
+ this.maxEvents = options.maxEvents;
3458
+ this.maxBytes = options.maxBytes;
3459
+ this.retentionCheckIntervalMs = options.retentionCheckIntervalMs ?? 60 * 60 * 1e3;
3390
3460
  const Database = loadDatabaseSync2();
3391
3461
  this.db = new Database(this.dbPath);
3392
- this.db.exec("PRAGMA journal_mode = WAL");
3462
+ this.db.exec(`PRAGMA journal_mode = ${this.maxBytes === void 0 ? "WAL" : "DELETE"}`);
3393
3463
  this.ensureSchema();
3464
+ this.configureByteQuota();
3465
+ if (this.maxEvents !== void 0) {
3466
+ const row = this.db.prepare("SELECT COUNT(*) AS count FROM events").get();
3467
+ this.retainedEventCount = row.count;
3468
+ this.enforceEventLimitAtStartup();
3469
+ }
3394
3470
  }
3395
3471
  close() {
3396
3472
  this.db.close();
@@ -3449,8 +3525,10 @@ var ChronicleSqliteJournal = class {
3449
3525
  events.push(event);
3450
3526
  previous = { sequence: event.sequence, hash: event.hash };
3451
3527
  }
3528
+ let retainedCountAfterCommit = this.retainedEventCount;
3452
3529
  try {
3453
3530
  this.db.exec("BEGIN IMMEDIATE");
3531
+ this.assertWithinByteQuota(events);
3454
3532
  const insert = this.db.prepare(
3455
3533
  `INSERT INTO events (
3456
3534
  day, sequence, event_id, hash, previous_hash, occurred_at, event_type, outcome,
@@ -3482,6 +3560,8 @@ var ChronicleSqliteJournal = class {
3482
3560
  JSON.stringify(event)
3483
3561
  );
3484
3562
  }
3563
+ retainedCountAfterCommit = this.enforceEventLimitWithinTransaction();
3564
+ this.assertActualAllocationWithinQuota();
3485
3565
  this.db.exec("COMMIT");
3486
3566
  } catch (error) {
3487
3567
  try {
@@ -3491,11 +3571,13 @@ var ChronicleSqliteJournal = class {
3491
3571
  this.anchors.clear();
3492
3572
  this.counters.failedEvents += inputs.length;
3493
3573
  this.lastBatchDurationMs = performance.now() - started;
3494
- throw error;
3574
+ throw this.normalizeQuotaError(error);
3495
3575
  }
3496
3576
  this.anchors.set(day, previous);
3497
3577
  this.counters.persistedEvents += events.length;
3578
+ this.retainedEventCount = retainedCountAfterCommit;
3498
3579
  this.lastBatchDurationMs = performance.now() - started;
3580
+ await this.enforceRetentionIfDue();
3499
3581
  return events;
3500
3582
  }
3501
3583
  async readAll() {
@@ -3556,6 +3638,145 @@ var ChronicleSqliteJournal = class {
3556
3638
  }
3557
3639
  return { ok: true, entries, lastSequence, lastHash };
3558
3640
  }
3641
+ async enforceRetentionIfDue() {
3642
+ if (this.retentionDays === void 0) return;
3643
+ const now = this.now();
3644
+ if (now.getTime() < this.nextRetentionCheckAt) return;
3645
+ this.nextRetentionCheckAt = now.getTime() + this.retentionCheckIntervalMs;
3646
+ try {
3647
+ await this.purge({ retentionDays: this.retentionDays });
3648
+ } catch {
3649
+ }
3650
+ }
3651
+ configureByteQuota() {
3652
+ if (this.maxBytes === void 0) {
3653
+ this.db.exec("PRAGMA max_page_count = 2147483646");
3654
+ return;
3655
+ }
3656
+ const halfSplit = Math.floor((this.maxBytes - SQLITE_FIXED_OVERHEAD_BYTES) / 2);
3657
+ const journalReserve = Math.min(MAX_ROLLBACK_JOURNAL_RESERVE_BYTES, halfSplit);
3658
+ const mainBudget = this.maxBytes - SQLITE_FIXED_OVERHEAD_BYTES - journalReserve;
3659
+ if (mainBudget < MIN_SQLITE_PAGE_BUDGET_BYTES) {
3660
+ throw new Error(
3661
+ `maxBytes must be at least ${SQLITE_FIXED_OVERHEAD_BYTES + 2 * MIN_SQLITE_PAGE_BUDGET_BYTES}`
3662
+ );
3663
+ }
3664
+ const maxPages = Math.max(1, Math.floor(mainBudget / this.pageSizeBytes()));
3665
+ this.db.exec(`PRAGMA max_page_count = ${maxPages}`);
3666
+ }
3667
+ /**
3668
+ * Bytes the journal actually occupies: live pages in the main database plus
3669
+ * whatever the rollback/WAL sidecars currently hold.
3670
+ *
3671
+ * The quota MUST be measured this way rather than by `statSync` on the main
3672
+ * database file. SQLite never returns freed pages to the filesystem — it
3673
+ * parks them on the freelist and reuses them — so a database that once grew
3674
+ * large keeps that size forever, even after retention and `maxEvents` have
3675
+ * evicted almost everything. Measuring the file instead wedged the journal
3676
+ * permanently: a 3.9 GB file whose live data was 243 MB failed a 512 MB
3677
+ * quota on EVERY append, and the only escape (`chronicle compact`) refuses
3678
+ * to run while the daemon is up — and the daemon respawns on demand. Live
3679
+ * pages shrink when retention deletes rows, so the quota can recover on its
3680
+ * own; allocated size cannot.
3681
+ *
3682
+ * The sidecars keep their on-disk size: they are transient and genuinely
3683
+ * bounded by `configureByteQuota`'s half-of-budget split.
3684
+ */
3685
+ aggregateLiveBytes() {
3686
+ let total = 0;
3687
+ for (const file of [`${this.dbPath}-journal`, `${this.dbPath}-wal`, `${this.dbPath}-shm`]) {
3688
+ try {
3689
+ total += fs8.statSync(file).size;
3690
+ } catch (error) {
3691
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
3692
+ }
3693
+ }
3694
+ return total + this.mainDatabaseLiveBytes();
3695
+ }
3696
+ /** Live (non-freelist) pages of the main database, in bytes. */
3697
+ mainDatabaseLiveBytes() {
3698
+ const pageCount = Number(
3699
+ this.db.prepare("PRAGMA page_count").get().page_count
3700
+ );
3701
+ const freelist = Number(
3702
+ this.db.prepare("PRAGMA freelist_count").get().freelist_count
3703
+ );
3704
+ return Math.max(0, pageCount - freelist) * this.pageSizeBytes();
3705
+ }
3706
+ /** Page size never changes for an open database; resolve it once. */
3707
+ pageSizeBytes() {
3708
+ if (this.cachedPageSize === void 0) {
3709
+ this.cachedPageSize = Number(
3710
+ this.db.prepare("PRAGMA page_size").get().page_size
3711
+ );
3712
+ }
3713
+ return this.cachedPageSize;
3714
+ }
3715
+ normalizeQuotaError(error) {
3716
+ if (this.maxBytes === void 0 || !(error instanceof Error)) return error;
3717
+ const sqliteError = error;
3718
+ const quotaExhausted = sqliteError.code === "SQLITE_FULL" || sqliteError.code === 13 || sqliteError.errcode === 13 || /database or disk is full/i.test(sqliteError.message);
3719
+ if (!quotaExhausted) return error;
3720
+ return new ChronicleStorageQuotaError({
3721
+ currentBytes: this.aggregateLiveBytes(),
3722
+ batchBytes: 0,
3723
+ maxBytes: this.maxBytes,
3724
+ path: this.dbPath
3725
+ });
3726
+ }
3727
+ assertActualAllocationWithinQuota() {
3728
+ if (this.maxBytes === void 0) return;
3729
+ const currentBytes = this.aggregateLiveBytes();
3730
+ if (currentBytes <= this.maxBytes) return;
3731
+ throw new ChronicleStorageQuotaError({
3732
+ currentBytes,
3733
+ batchBytes: 0,
3734
+ maxBytes: this.maxBytes,
3735
+ path: this.dbPath
3736
+ });
3737
+ }
3738
+ assertWithinByteQuota(events) {
3739
+ if (this.maxBytes === void 0) return;
3740
+ const currentBytes = this.aggregateLiveBytes();
3741
+ const batchBytes = Buffer.byteLength(JSON.stringify(events), "utf8");
3742
+ if (currentBytes + batchBytes <= this.maxBytes) return;
3743
+ throw new ChronicleStorageQuotaError({
3744
+ currentBytes,
3745
+ batchBytes,
3746
+ maxBytes: this.maxBytes,
3747
+ path: this.dbPath
3748
+ });
3749
+ }
3750
+ enforceEventLimitWithinTransaction() {
3751
+ const row = this.db.prepare("SELECT COUNT(*) AS count FROM events").get();
3752
+ if (this.maxEvents === void 0 || row.count <= this.maxEvents) return row.count;
3753
+ const excess = row.count - this.maxEvents;
3754
+ const boundary = this.db.prepare("SELECT day, sequence, hash FROM events ORDER BY day, sequence LIMIT 1 OFFSET ?").get(excess - 1);
3755
+ if (!boundary) return row.count;
3756
+ this.db.prepare(
3757
+ `INSERT INTO chain_checkpoint (day, sequence, hash) VALUES (?, ?, ?)
3758
+ ON CONFLICT(day) DO UPDATE SET sequence = excluded.sequence, hash = excluded.hash`
3759
+ ).run(boundary.day, boundary.sequence, boundary.hash);
3760
+ this.db.prepare("DELETE FROM events WHERE day < ? OR (day = ? AND sequence <= ?)").run(boundary.day, boundary.day, boundary.sequence);
3761
+ this.db.prepare("DELETE FROM chain_checkpoint WHERE day < ?").run(boundary.day);
3762
+ this.anchors.clear();
3763
+ return this.maxEvents;
3764
+ }
3765
+ enforceEventLimitAtStartup() {
3766
+ if (this.maxEvents === void 0 || this.retainedEventCount <= this.maxEvents) return;
3767
+ try {
3768
+ this.db.exec("BEGIN IMMEDIATE");
3769
+ const retainedCount = this.enforceEventLimitWithinTransaction();
3770
+ this.db.exec("COMMIT");
3771
+ this.retainedEventCount = retainedCount;
3772
+ } catch (error) {
3773
+ try {
3774
+ this.db.exec("ROLLBACK");
3775
+ } catch {
3776
+ }
3777
+ throw error;
3778
+ }
3779
+ }
3559
3780
  /**
3560
3781
  * Drop events older than the retention window.
3561
3782
  *
@@ -3601,6 +3822,7 @@ var ChronicleSqliteJournal = class {
3601
3822
  };
3602
3823
  }
3603
3824
  this.anchors.clear();
3825
+ this.retainedEventCount = Math.max(0, this.retainedEventCount - count);
3604
3826
  return { ...empty, deletedCount: count };
3605
3827
  }
3606
3828
  /**
@@ -3632,7 +3854,9 @@ var ChronicleSqliteJournal = class {
3632
3854
  ON CONFLICT(day) DO UPDATE SET sequence = excluded.sequence, hash = excluded.hash`
3633
3855
  );
3634
3856
  this.db.exec("BEGIN IMMEDIATE");
3857
+ let retainedCountAfterCommit = this.retainedEventCount;
3635
3858
  try {
3859
+ this.assertWithinByteQuota([]);
3636
3860
  await load({
3637
3861
  insert: (day, event) => {
3638
3862
  const row = projectEvent(event);
@@ -3662,16 +3886,19 @@ var ChronicleSqliteJournal = class {
3662
3886
  checkpoint.run(day, sequence, hash3);
3663
3887
  }
3664
3888
  });
3889
+ retainedCountAfterCommit = this.enforceEventLimitWithinTransaction();
3890
+ this.assertActualAllocationWithinQuota();
3665
3891
  this.db.exec("COMMIT");
3666
3892
  } catch (error) {
3667
3893
  try {
3668
3894
  this.db.exec("ROLLBACK");
3669
3895
  } catch {
3670
3896
  }
3671
- throw error;
3897
+ throw this.normalizeQuotaError(error);
3672
3898
  } finally {
3673
3899
  this.anchors.clear();
3674
3900
  }
3901
+ this.retainedEventCount = retainedCountAfterCommit;
3675
3902
  }
3676
3903
  /**
3677
3904
  * A read engine over this journal's own connection.
@@ -3827,7 +4054,7 @@ function projectEvent(event) {
3827
4054
 
3828
4055
  // src/chronicle/project-server-client.ts
3829
4056
  import { spawn } from "node:child_process";
3830
- import * as fs8 from "node:fs";
4057
+ import * as fs9 from "node:fs";
3831
4058
  import * as net from "node:net";
3832
4059
  import * as path13 from "node:path";
3833
4060
  import { fileURLToPath } from "node:url";
@@ -3876,14 +4103,14 @@ var DEFAULT_CALL_TIMEOUT_MS = 3e4;
3876
4103
  var APPEND_CALL_TIMEOUT_MS = 6e4;
3877
4104
  var DEFAULT_BATCH_WINDOW_MS = 5;
3878
4105
  var DEFAULT_MAX_PENDING = 1e5;
3879
- function resolveChronicleDaemonAvailability(moduleUrl = import.meta.url, exists = fs8.existsSync) {
4106
+ function resolveChronicleDaemonAvailability(moduleUrl = import.meta.url, exists = fs9.existsSync) {
3880
4107
  if (process.env["WRONGSTACK_CHRONICLE_INLINE"] || process.env["WRONGSTACK_CHRONICLE_SERVER"] === "0") {
3881
4108
  return { kind: "inline-requested" };
3882
4109
  }
3883
4110
  const url = locateChronicleProjectServer(moduleUrl, exists);
3884
4111
  return url ? { kind: "available", url } : { kind: "missing-build" };
3885
4112
  }
3886
- function resolveChronicleProjectServerUrl(moduleUrl = import.meta.url, exists = fs8.existsSync) {
4113
+ function resolveChronicleProjectServerUrl(moduleUrl = import.meta.url, exists = fs9.existsSync) {
3887
4114
  const availability = resolveChronicleDaemonAvailability(moduleUrl, exists);
3888
4115
  return availability.kind === "available" ? availability.url : null;
3889
4116
  }
@@ -4052,7 +4279,7 @@ var ChronicleProjectServerClient = class {
4052
4279
  currentAuthToken() {
4053
4280
  if (this.authToken === void 0) {
4054
4281
  try {
4055
- const raw = fs8.readFileSync(
4282
+ const raw = fs9.readFileSync(
4056
4283
  chronicleProjectServerMetadataPath(this.options.projectDir),
4057
4284
  "utf8"
4058
4285
  );
@@ -4596,6 +4823,63 @@ var InlineChronicleProjectAccess = class {
4596
4823
  }
4597
4824
  };
4598
4825
 
4826
+ // src/chronicle/sqlite-compaction.ts
4827
+ import { existsSync as existsSync2, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
4828
+ import { createRequire as createRequire3 } from "node:module";
4829
+ var require2 = createRequire3(import.meta.url);
4830
+ var ChronicleCompactionBusyError = class extends Error {
4831
+ constructor(pid) {
4832
+ super(`Chronicle project server is still running (pid ${pid}); stop it before compacting`);
4833
+ this.name = "ChronicleCompactionBusyError";
4834
+ }
4835
+ };
4836
+ function chronicleSqliteAllocatedBytes(dbPath) {
4837
+ return [dbPath, `${dbPath}-wal`, `${dbPath}-shm`].reduce((total, file) => {
4838
+ try {
4839
+ return total + statSync3(file).size;
4840
+ } catch (error) {
4841
+ if (error.code === "ENOENT") return total;
4842
+ throw error;
4843
+ }
4844
+ }, 0);
4845
+ }
4846
+ function compactChronicleSqlite(options) {
4847
+ assertProjectServerStopped(options.endpointMetadataPath);
4848
+ const beforeBytes = chronicleSqliteAllocatedBytes(options.dbPath);
4849
+ if (!existsSync2(options.dbPath)) {
4850
+ return { beforeBytes, afterBytes: beforeBytes, reclaimedBytes: 0 };
4851
+ }
4852
+ const Database = withSqliteExperimentalWarningSuppressed(
4853
+ () => require2("node:sqlite").DatabaseSync
4854
+ );
4855
+ const db = new Database(options.dbPath);
4856
+ try {
4857
+ db.exec("PRAGMA busy_timeout = 1");
4858
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
4859
+ db.exec("VACUUM");
4860
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
4861
+ } finally {
4862
+ db.close();
4863
+ }
4864
+ const afterBytes = chronicleSqliteAllocatedBytes(options.dbPath);
4865
+ return {
4866
+ beforeBytes,
4867
+ afterBytes,
4868
+ reclaimedBytes: Math.max(0, beforeBytes - afterBytes)
4869
+ };
4870
+ }
4871
+ function assertProjectServerStopped(endpointMetadataPath) {
4872
+ if (!endpointMetadataPath || !existsSync2(endpointMetadataPath)) return;
4873
+ try {
4874
+ const metadata = JSON.parse(readFileSync3(endpointMetadataPath, "utf8"));
4875
+ if (typeof metadata.pid === "number" && isPidAlive(metadata.pid)) {
4876
+ throw new ChronicleCompactionBusyError(metadata.pid);
4877
+ }
4878
+ } catch (error) {
4879
+ if (error instanceof ChronicleCompactionBusyError) throw error;
4880
+ }
4881
+ }
4882
+
4599
4883
  // src/chronicle/provider-adapter.ts
4600
4884
  function wireProviderAttemptsToChronicle(options) {
4601
4885
  const seenToolManifests = /* @__PURE__ */ new Set();
@@ -5753,17 +6037,20 @@ export {
5753
6037
  CHRONICLE_PROJECT_SERVER_MAX_FRAME_CHARS,
5754
6038
  CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION,
5755
6039
  CHRONICLE_SCHEMA_VERSION,
6040
+ ChronicleCompactionBusyError,
5756
6041
  ChronicleJournal,
5757
6042
  ChronicleMetricsStore,
5758
6043
  ChroniclePartitionRangeCache,
5759
6044
  ChronicleProjectServerClient,
5760
6045
  ChronicleQueryEngine,
5761
6046
  ChronicleRemoteJournal,
6047
+ ChronicleStorageQuotaError,
5762
6048
  GENESIS_HASH,
5763
6049
  childChronicleContext,
5764
6050
  chronicleProjectServerEndpoint,
5765
6051
  chronicleProjectServerKey,
5766
6052
  chronicleProjectServerMetadataPath,
6053
+ compactChronicleSqlite,
5767
6054
  createChronicleContext,
5768
6055
  createChronicleEventJournal,
5769
6056
  createChronicleProjectAccess,