forge-workflow 0.1.0-beta.5 → 0.1.0-beta.7

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 (268) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +48 -0
  3. package/CLAUDE.md +0 -12
  4. package/CODING_STANDARDS.md +72 -0
  5. package/bin/forge.js +12 -1
  6. package/docs/guides/MIGRATION.md +3 -3
  7. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
  8. package/docs/reference/INSTALL.md +4 -0
  9. package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
  10. package/docs/reference/RELEASE.md +4 -4
  11. package/docs/reference/github-accounts.md +134 -0
  12. package/docs/reference/shepherd.md +63 -13
  13. package/lib/adapters/pr-state-adapter.js +15 -2
  14. package/lib/base-remote.js +138 -0
  15. package/lib/beta5-compatibility-evidence.js +1093 -0
  16. package/lib/bun-lockfile-proof.js +413 -0
  17. package/lib/bun-workflow-pins.js +461 -0
  18. package/lib/capabilities/index.js +9 -0
  19. package/lib/capabilities/model.js +141 -0
  20. package/lib/capabilities/probes.js +347 -0
  21. package/lib/codex-skills.js +2 -2
  22. package/lib/commands/_manifest.js +1 -0
  23. package/lib/commands/_registry.js +48 -18
  24. package/lib/commands/clean.js +57 -1
  25. package/lib/commands/doctor.js +37 -6
  26. package/lib/commands/gate.js +197 -27
  27. package/lib/commands/github.js +215 -0
  28. package/lib/commands/hooks.js +54 -6
  29. package/lib/commands/memory.js +66 -2
  30. package/lib/commands/merge.js +720 -73
  31. package/lib/commands/plan.js +33 -2
  32. package/lib/commands/pr.js +2 -0
  33. package/lib/commands/preflight.js +10 -2
  34. package/lib/commands/push.js +120 -9
  35. package/lib/commands/recall.js +95 -61
  36. package/lib/commands/release.js +23 -2
  37. package/lib/commands/remember.js +28 -4
  38. package/lib/commands/serve.js +26 -9
  39. package/lib/commands/setup.js +132 -4
  40. package/lib/commands/shepherd.js +578 -72
  41. package/lib/commands/ship.js +15 -69
  42. package/lib/commands/skill.js +8 -0
  43. package/lib/commands/team.js +47 -8
  44. package/lib/commands/test.js +163 -4
  45. package/lib/commands/validate.js +78 -22
  46. package/lib/commands/worktree.js +155 -19
  47. package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
  48. package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
  49. package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
  50. package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
  51. package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
  52. package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
  53. package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
  54. package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
  55. package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
  56. package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
  57. package/lib/forge-issues.js +78 -0
  58. package/lib/gate-events.js +98 -10
  59. package/lib/github-context.js +308 -0
  60. package/lib/global-flags.js +1 -0
  61. package/lib/hook-renderer.js +29 -1
  62. package/lib/issue-render.js +19 -0
  63. package/lib/kernel/broker.js +723 -31
  64. package/lib/kernel/claim-reconciler.js +238 -0
  65. package/lib/kernel/lease-enforcer.js +9 -4
  66. package/lib/kernel/legacy-claim-repair.js +442 -0
  67. package/lib/kernel/live-claim-projection.js +26 -0
  68. package/lib/kernel/migrations.js +118 -3
  69. package/lib/kernel/readiness-model.js +184 -12
  70. package/lib/kernel/schema.js +49 -1
  71. package/lib/kernel/sqlite-driver.js +3322 -183
  72. package/lib/kernel/taxonomy-validator.js +4 -1
  73. package/lib/kernel/windows-private-acl.js +239 -0
  74. package/lib/memory/hygiene.js +191 -0
  75. package/lib/memory/router.js +94 -27
  76. package/lib/memory/usage-evidence.js +4 -0
  77. package/lib/memory-digest.js +59 -0
  78. package/lib/merge-rules.js +135 -17
  79. package/lib/npm-publish-workflow.js +233 -40
  80. package/lib/package-root.js +2 -0
  81. package/lib/pr-monitor/auto-actions.js +169 -28
  82. package/lib/pr-monitor/differ.js +110 -4
  83. package/lib/pr-monitor/events.js +0 -0
  84. package/lib/pr-monitor/flow-monitor.js +1424 -0
  85. package/lib/pr-monitor/gather.js +251 -44
  86. package/lib/pr-monitor/journal.js +0 -37
  87. package/lib/pr-monitor/monitor.js +117 -10
  88. package/lib/pr-monitor/process-identity.js +117 -0
  89. package/lib/pr-monitor/reconcile-executor.js +1101 -625
  90. package/lib/pr-monitor/reconcile.js +0 -0
  91. package/lib/pr-monitor/render-summary.js +121 -24
  92. package/lib/pr-monitor/review-preflight.js +269 -0
  93. package/lib/pr-monitor/shepherd-lease.js +28 -19
  94. package/lib/pr-monitor/verdict.js +438 -0
  95. package/lib/pr-monitor/watch-lifecycle.js +144 -38
  96. package/lib/pr-monitor/watch-owner.js +1414 -0
  97. package/lib/pr-monitor/watch.js +129 -58
  98. package/lib/pr-shepherd.js +17 -3
  99. package/lib/project-memory.js +145 -3
  100. package/lib/protected-state-authority.js +799 -4
  101. package/lib/protected-state-surfaces.js +181 -3
  102. package/lib/release-readiness.js +2 -3
  103. package/lib/review-adapter.js +65 -0
  104. package/lib/skills-sync.js +65 -32
  105. package/lib/validation/risk-manifest.js +339 -0
  106. package/lib/validation-receipt.js +190 -0
  107. package/lib/workflow/enforce-stage.js +44 -0
  108. package/lib/workflow/plan-authority.js +225 -0
  109. package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  110. package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  111. package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  112. package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  113. package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  114. package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  115. package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  116. package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  117. package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  118. package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  119. package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  120. package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  121. package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  122. package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  123. package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  124. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  125. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  126. package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  127. package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  128. package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  129. package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  130. package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  131. package/node_modules/@forge/contracts/index.js +32 -0
  132. package/node_modules/@forge/contracts/package.json +35 -0
  133. package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  134. package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  135. package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  136. package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  137. package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  138. package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  139. package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  140. package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  141. package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  142. package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  143. package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  144. package/node_modules/@forge/contracts/src/baseline.js +23 -0
  145. package/node_modules/@forge/contracts/src/canonical.js +151 -0
  146. package/node_modules/@forge/contracts/src/definitions.js +176 -0
  147. package/node_modules/@forge/contracts/src/identity.js +42 -0
  148. package/node_modules/@forge/contracts/src/schema.js +72 -0
  149. package/node_modules/@forge/contracts/src/validate.js +305 -0
  150. package/node_modules/@forge/flow/index.js +114 -0
  151. package/node_modules/@forge/flow/package.json +34 -0
  152. package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
  153. package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
  154. package/node_modules/@forge/flow/src/executor.js +279 -0
  155. package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
  156. package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
  157. package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
  158. package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
  159. package/node_modules/@forge/memory/index.js +331 -0
  160. package/node_modules/@forge/memory/package.json +34 -0
  161. package/node_modules/@forge/memory/src/authority-provider.js +67 -0
  162. package/node_modules/@forge/memory/src/backend-registry.js +166 -0
  163. package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
  164. package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
  165. package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
  166. package/package.json +16 -4
  167. package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  168. package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  169. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  170. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  171. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  172. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  173. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  174. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  175. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  176. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  177. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  178. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  179. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  180. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  181. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  182. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  183. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  184. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  185. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  186. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  187. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  188. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  189. package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
  190. package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
  191. package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  192. package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  193. package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  194. package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  195. package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  196. package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  197. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  198. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  199. package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  200. package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  201. package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  202. package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
  203. package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
  204. package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
  205. package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
  206. package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
  207. package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
  208. package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  209. package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  210. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  211. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  212. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  213. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  214. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  215. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  216. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  217. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  218. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  219. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  220. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  221. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  222. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  223. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  224. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  225. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  226. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  227. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  228. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  229. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  230. package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
  231. package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
  232. package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  233. package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  234. package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  235. package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  236. package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  237. package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  238. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  239. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  240. package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  241. package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  242. package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  243. package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
  244. package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
  245. package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
  246. package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
  247. package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
  248. package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
  249. package/scripts/commitlint.js +13 -15
  250. package/scripts/generate-risk-manifest.js +91 -0
  251. package/scripts/github-context-bridge.sh +10 -0
  252. package/scripts/legacy-claim-repair.js +145 -0
  253. package/scripts/lib/behavioral-eval-runtime.js +3 -2
  254. package/scripts/process-tree.js +14 -2
  255. package/scripts/protected-state-check.js +440 -17
  256. package/scripts/sync-agent-skills.js +333 -34
  257. package/scripts/test-full-suite.js +704 -18
  258. package/scripts/test-profile.js +13 -3
  259. package/scripts/test.js +96 -15
  260. package/skills/coverage.json +1 -0
  261. package/skills/review/SKILL.md +2 -0
  262. package/skills/review/evals/scorecard.json +2 -2
  263. package/skills/setup/SKILL.md +18 -0
  264. package/skills/setup/evals/scorecard.json +3 -3
  265. package/skills/shepherd/SKILL.md +19 -2
  266. package/skills/shepherd/evals/scorecard.json +3 -3
  267. package/skills/validate/SKILL.md +3 -0
  268. package/skills/validate/evals/scorecard.json +1 -1
@@ -3,7 +3,10 @@
3
3
  const fs = require('node:fs');
4
4
  const os = require('node:os');
5
5
  const path = require('node:path');
6
- const { randomUUID } = require('node:crypto');
6
+ const { spawn: nodeSpawn } = require('node:child_process');
7
+ const { pathToFileURL } = require('node:url');
8
+ const { createHash, randomUUID } = require('node:crypto');
9
+ const { types: { isProxy } } = require('node:util');
7
10
 
8
11
  const {
9
12
  ISSUE_COMMAND_SCHEMA_VERSION,
@@ -13,11 +16,21 @@ const {
13
16
  resolveNextCommands,
14
17
  } = require('./issue-command-contract');
15
18
  const { buildReadinessIndex } = require('./readiness-model');
16
- const { buildMemoryProjectionMigration, memoryFtsDdl } = require('./migrations');
17
- const { rankForPriorityLabel } = require('./taxonomy-validator');
19
+ const { buildMemoryProjectionMigration, buildUsageEvidenceMigration, memoryFtsDdl } = require('./migrations');
20
+ const { assertFilesystemSafeForKernel } = require('./fs-class');
21
+ const { isTerminalStatus, rankForPriorityLabel } = require('./taxonomy-validator');
18
22
  const { isLeaseExpired } = require('./lease-enforcer');
23
+ const { isLiveClaim, projectLiveClaims } = require('./live-claim-projection');
24
+ const {
25
+ ClaimRepairError,
26
+ buildClaimRepairPlan,
27
+ publicClaimRepairPreflight,
28
+ verifyClaimRepairBackup,
29
+ } = require('./legacy-claim-repair');
19
30
  const { CONFLICT_SIGNAL, classifyConflictSignal } = require('./conflict-signal');
20
31
  const { normalizeRecallHit } = require('../memory-recall');
32
+ const { getPackageRoot } = require('../package-root');
33
+ const { appendUsageEvidence, rebuildUsageProjection } = require('@forge/memory');
21
34
 
22
35
  const BUILTIN_SQLITE_RUNTIME_ORDER = Object.freeze(['bun:sqlite', 'node:sqlite']);
23
36
  let probeCounter = 0;
@@ -133,6 +146,18 @@ function createDatabase(runtime, databasePath) {
133
146
  throw new Error(`Unsupported builtin SQLite runtime: ${runtime.id}`);
134
147
  }
135
148
 
149
+ function createExistingWatchOwnerDatabase(runtime, databasePath) {
150
+ if (runtime.id === 'bun:sqlite') {
151
+ return new runtime.module.Database(databasePath, { readwrite: true });
152
+ }
153
+ if (runtime.id === 'node:sqlite') {
154
+ const databaseUrl = pathToFileURL(databasePath);
155
+ databaseUrl.searchParams.set('mode', 'rw');
156
+ return new runtime.module.DatabaseSync(databaseUrl);
157
+ }
158
+ throw new Error(`Unsupported builtin SQLite runtime: ${runtime.id}`);
159
+ }
160
+
136
161
  function execSql(_runtime, db, sql) {
137
162
  db.exec(sql);
138
163
  }
@@ -171,6 +196,35 @@ function runParams(runtime, db, sql, params = []) {
171
196
  return db.prepare(sql).run(...params);
172
197
  }
173
198
 
199
+ function createUsageEvidenceAdapter(runtime, db) {
200
+ return {
201
+ exec(sql) { execSql(runtime, db, sql); },
202
+ run(sql, params = []) { return runParams(runtime, db, sql, params); },
203
+ one(sql, params = []) { return allParams(runtime, db, sql, params)[0] || null; },
204
+ transaction(callback) {
205
+ let active = false;
206
+ try {
207
+ execSql(runtime, db, 'BEGIN IMMEDIATE;');
208
+ active = true;
209
+ const result = callback();
210
+ execSql(runtime, db, 'COMMIT;');
211
+ active = false;
212
+ return result;
213
+ } catch (error) {
214
+ if (active) {
215
+ try { execSql(runtime, db, 'ROLLBACK;'); } catch { /* preserve the write failure */ }
216
+ }
217
+ throw error;
218
+ }
219
+ },
220
+ assertUsageWriterEnabled() {
221
+ const row = allParams(runtime, db,
222
+ 'SELECT enabled FROM memory_usage_writer_state WHERE singleton = 1')[0];
223
+ if (!row || Number(row.enabled) !== 1) throw new Error('memory usage evidence writer is disabled');
224
+ },
225
+ };
226
+ }
227
+
174
228
  // A table may not exist on a partially-migrated DB; readiness inputs degrade to empty.
175
229
  // ONLY a missing-table error is tolerated — a locked/corrupt DB or a real SQL
176
230
  // regression must surface, not silently produce wrong readiness/stats/projection.
@@ -206,7 +260,7 @@ function parseLabels(raw) {
206
260
  }
207
261
 
208
262
  function rowToIssueSummary(row, readinessEntry, claimedBy = null, dependencyIds = [], dependentIds = []) {
209
- return {
263
+ const summary = {
210
264
  id: row.id,
211
265
  title: row.title,
212
266
  body: row.body ?? null,
@@ -250,6 +304,11 @@ function rowToIssueSummary(row, readinessEntry, claimedBy = null, dependencyIds
250
304
  close_reason: row.close_reason ?? null,
251
305
  metadata: row.metadata ?? null,
252
306
  };
307
+ if (readinessEntry?.contract_applicable) {
308
+ summary.readiness_state = readinessEntry.state;
309
+ summary.readiness_reasons = readinessEntry.reasons;
310
+ }
311
+ return summary;
253
312
  }
254
313
 
255
314
  function okIssueResponse(command, data, nextCommands) {
@@ -265,30 +324,48 @@ function okIssueResponse(command, data, nextCommands) {
265
324
  };
266
325
  }
267
326
 
327
+ function loadLiveKernelClaimRows(runtime, db, context = {}) {
328
+ const rows = safeAll(
329
+ runtime,
330
+ db,
331
+ `SELECT claim.*, issue.status AS issue_status
332
+ FROM kernel_claims AS claim
333
+ JOIN kernel_issues AS issue ON issue.id = claim.issue_id
334
+ WHERE claim.state = 'active'`,
335
+ );
336
+ const issues = rows.map(row => ({ id: row.issue_id, status: row.issue_status }));
337
+ return projectLiveClaims(rows, issues, context.now || new Date().toISOString())
338
+ .map(row => {
339
+ const claim = { ...row };
340
+ delete claim.issue_status;
341
+ return claim;
342
+ });
343
+ }
344
+
268
345
  // Derive the whole-board readiness read model (D18) from the authority tables.
269
346
  function loadBoardReadiness(runtime, db, context = {}) {
270
347
  const issues = allParams(runtime, db, 'SELECT * FROM kernel_issues');
271
348
  const dependencies = safeAll(runtime, db, 'SELECT * FROM kernel_dependencies');
272
349
  const conflicts = safeAll(runtime, db, 'SELECT * FROM kernel_conflicts');
273
- const claims = safeAll(runtime, db, 'SELECT * FROM kernel_claims');
350
+ const now = context.now || new Date().toISOString();
351
+ const liveClaims = loadLiveKernelClaimRows(runtime, db, { ...context, now });
274
352
  const index = buildReadinessIndex({
275
353
  issues,
276
354
  dependencies,
277
355
  conflicts,
278
- claims,
279
- now: context.now,
356
+ claims: liveClaims,
357
+ now,
280
358
  actor: context.actor,
359
+ contractPolicy: context.contractPolicy,
360
+ isTrustedAdoption: context.isTrustedAdoption,
281
361
  });
282
- // Surface the active lease holder per issue for issue summaries. Filter on state
283
- // only (matches loadActiveKernelClaimRow); the partial-UNIQUE active-lease index
284
- // guarantees at most one active row per issue, so the map is unambiguous.
362
+ // Surface only the shared live-authority projection as claimed_by. The partial
363
+ // UNIQUE active-lease index guarantees at most one live row per issue.
285
364
  // Null-prototype map: issue ids are unconstrained external strings, so a literal `{}`
286
365
  // keyed by them would be a prototype-pollution vector (matches buildReadinessIndex).
287
366
  const claimedById = Object.create(null);
288
- for (const claim of claims) {
289
- if ((claim.state || 'active') === 'active' && claim.issue_id) {
290
- claimedById[claim.issue_id] = claim.actor ?? null;
291
- }
367
+ for (const claim of liveClaims) {
368
+ if (claim.issue_id) claimedById[claim.issue_id] = claim.actor ?? null;
292
369
  }
293
370
  // Per-issue declared dependency edges (the ids each issue depends on, i.e.
294
371
  // blocks_issue_id where issue_id === the dependent). Distinct from readiness'
@@ -318,7 +395,7 @@ function loadBoardReadiness(runtime, db, context = {}) {
318
395
  dependentsById[issueId] = [...new Set(dependentsById[issueId])]
319
396
  .sort((a, b) => String(a).localeCompare(String(b)));
320
397
  }
321
- return { issues, index, claimedById, dependenciesById, dependentsById };
398
+ return { issues, index, liveClaims, claimedById, dependenciesById, dependentsById };
322
399
  }
323
400
 
324
401
  function firstPositional(args = []) {
@@ -435,6 +512,57 @@ function buildRollup(children) {
435
512
  };
436
513
  }
437
514
 
515
+ function runIssueOwnsRead(runtime, db, args, context) {
516
+ // Lease-ownership verification (kernel d71a824b). A claim returning ok:true does
517
+ // not prove the caller won the lease: a duplicate replay also returns ok:true, so
518
+ // a worker must CONFIRM it holds the live lease before mutating a claimed issue.
519
+ // `owned` is true iff the resolving actor holds the shared live-authority claim.
520
+ // loadActiveKernelClaimRow remains state-only for acquisition/reclaim planning;
521
+ // isLiveClaim applies expiry and issue-terminal fencing here. Actor resolution mirrors the mutation
522
+ // path's `context.actor || 'forge'` default so a bare CLI invocation matches its
523
+ // own claims; `now` falls back to the wall clock like the mutation route.
524
+ const id = firstPositional(args);
525
+ const rows = allParams(runtime, db, 'SELECT * FROM kernel_issues WHERE id = ?', [id]);
526
+ if (!rows[0]) {
527
+ return formatIssueCommandError({
528
+ command: 'issue.owns',
529
+ code: 'FORGE_ISSUE_NOT_FOUND',
530
+ message: `Issue ${id ?? '<missing id>'} not found`,
531
+ exitCode: ISSUE_COMMAND_EXIT_CODES.notFound,
532
+ });
533
+ }
534
+ const now = context.now || new Date().toISOString();
535
+ const actor = context.actor || 'forge';
536
+ const claim = loadActiveKernelClaimRow(runtime, db, id);
537
+ const liveClaim = isLiveClaim(claim, rows[0], now) ? claim : null;
538
+ const claimedBy = liveClaim ? (liveClaim.actor ?? null) : null;
539
+ const expired = claim ? isLeaseExpired(claim, now) : false;
540
+ // Ownership is per-SESSION, not just per-actor (kernel d71a824b): two agents
541
+ // sharing one human actor but running as DIFFERENT sessions must not both read
542
+ // OWNED for a lease only one of them holds. When BOTH the caller and the live
543
+ // lease carry a session-id they must match; if either side is session-less (a
544
+ // no-env caller, or a pre-session claim) we fall back to actor-only ownership so
545
+ // historical behavior is preserved byte-for-byte. An empty/whitespace-only
546
+ // session-id counts as session-LESS on BOTH sides — the SAME trim-truthy test the
547
+ // claim-key write uses (buildClaimMutationEvent in lib/kernel/broker.js) — so ''
548
+ // can never count as "present" here and "absent" there.
549
+ const normalizeSession = value => (typeof value === 'string' && value.trim() !== '' ? value : null);
550
+ const contextSession = normalizeSession(context.sessionId);
551
+ const claimSession = claim ? normalizeSession(claim.session_id) : null;
552
+ const sessionMismatch = contextSession !== null
553
+ && claimSession !== null
554
+ && contextSession !== claimSession;
555
+ const owned = Boolean(liveClaim) && claimedBy === actor && !sessionMismatch;
556
+ return okIssueResponse('issue.owns', {
557
+ id,
558
+ actor,
559
+ claimed_by: claimedBy,
560
+ owned,
561
+ expired,
562
+ expires_at: claim ? (claim.expires_at ?? null) : null,
563
+ });
564
+ }
565
+
438
566
  // Read-side of driver.issueOperation: ready/list/show/search/stats as parameterized
439
567
  // SELECTs returning issue-command-contract shapes. Mutations are handled separately
440
568
  // through the broker's guarded-event path (later wave).
@@ -506,55 +634,7 @@ function runIssueReadOperation(runtime, db, operation, args, context) {
506
634
  });
507
635
  }
508
636
  if (operation === 'owns') {
509
- // Lease-ownership verification (kernel d71a824b). A claim returning ok:true does
510
- // not prove the caller won the lease: a duplicate replay also returns ok:true, so
511
- // a worker must CONFIRM it holds the live lease before mutating a claimed issue.
512
- // `owned` is true iff the resolving actor holds the SINGLE active claim AND that
513
- // lease has not expired. read summaries derive `claimed_by` from state='active'
514
- // only (never expiry — see loadActiveKernelClaimRow), so owns re-applies the
515
- // expiry check here: an expired-but-not-yet-reclaimed lease is NOT ownership
516
- // (planClaimAcquisition would supersede it). Actor resolution mirrors the mutation
517
- // path's `context.actor || 'forge'` default so a bare CLI invocation matches its
518
- // own claims; `now` falls back to the wall clock like the mutation route.
519
- const id = firstPositional(args);
520
- const rows = allParams(runtime, db, 'SELECT * FROM kernel_issues WHERE id = ?', [id]);
521
- if (!rows[0]) {
522
- return formatIssueCommandError({
523
- command: 'issue.owns',
524
- code: 'FORGE_ISSUE_NOT_FOUND',
525
- message: `Issue ${id ?? '<missing id>'} not found`,
526
- exitCode: ISSUE_COMMAND_EXIT_CODES.notFound,
527
- });
528
- }
529
- const now = context.now || new Date().toISOString();
530
- const actor = context.actor || 'forge';
531
- const claim = loadActiveKernelClaimRow(runtime, db, id);
532
- const claimedBy = claim ? (claim.actor ?? null) : null;
533
- const expired = claim ? isLeaseExpired(claim, now) : false;
534
- // Ownership is per-SESSION, not just per-actor (kernel d71a824b): two agents
535
- // sharing one human actor but running as DIFFERENT sessions must not both read
536
- // OWNED for a lease only one of them holds. When BOTH the caller and the live
537
- // lease carry a session-id they must match; if either side is session-less (a
538
- // no-env caller, or a pre-session claim) we fall back to actor-only ownership so
539
- // historical behavior is preserved byte-for-byte. An empty/whitespace-only
540
- // session-id counts as session-LESS on BOTH sides — the SAME trim-truthy test the
541
- // claim-key write uses (buildClaimMutationEvent in lib/kernel/broker.js) — so ''
542
- // can never count as "present" here and "absent" there.
543
- const normalizeSession = value => (typeof value === 'string' && value.trim() !== '' ? value : null);
544
- const contextSession = normalizeSession(context.sessionId);
545
- const claimSession = claim ? normalizeSession(claim.session_id) : null;
546
- const sessionMismatch = contextSession !== null
547
- && claimSession !== null
548
- && contextSession !== claimSession;
549
- const owned = Boolean(claim) && !expired && claimedBy === actor && !sessionMismatch;
550
- return okIssueResponse('issue.owns', {
551
- id,
552
- actor,
553
- claimed_by: claimedBy,
554
- owned,
555
- expired,
556
- expires_at: claim ? (claim.expires_at ?? null) : null,
557
- });
637
+ return runIssueOwnsRead(runtime, db, args, context);
558
638
  }
559
639
  if (operation === 'search') {
560
640
  const term = `%${firstPositional(args) || ''}%`;
@@ -568,20 +648,17 @@ function runIssueReadOperation(runtime, db, operation, args, context) {
568
648
  return okIssueResponse('issue.search', { issues: summaries, count: summaries.length });
569
649
  }
570
650
  if (operation === 'stats') {
571
- const { index } = loadBoardReadiness(runtime, db, context);
651
+ const { index, liveClaims } = loadBoardReadiness(runtime, db, context);
572
652
  const statusRows = allParams(runtime, db, 'SELECT status, COUNT(*) AS n FROM kernel_issues GROUP BY status');
573
653
  const counts = {};
574
654
  for (const row of statusRows) {
575
655
  counts[row.status] = Number(row.n);
576
656
  }
577
- const activeClaims = Number(
578
- safeAll(runtime, db, "SELECT COUNT(*) AS n FROM kernel_claims WHERE state = 'active'")[0]?.n || 0,
579
- );
580
657
  return okIssueResponse('issue.stats', {
581
658
  counts,
582
659
  ready_count: index.readyQueue.length,
583
660
  blocked_count: index.blocked.length,
584
- active_claims: activeClaims,
661
+ active_claims: liveClaims.length,
585
662
  });
586
663
  }
587
664
  // KAP-7: derived read query — every issue whose readiness is blocked
@@ -696,24 +773,11 @@ function runIssueReadOperation(runtime, db, operation, args, context) {
696
773
  count: children.length,
697
774
  });
698
775
  }
699
- // Issue 7dc229d4: active lease/claims read for the dashboard live layer. The
700
- // kernel_claims lease row carries the full who/what/where set (actor,
701
- // session_id, worktree_id, expires_at, issue_id) — the CLI previously exposed
702
- // only `claimed_by` (the actor). A lease is LIVE iff state='active' AND it has
703
- // NOT expired at the read `now` (isLeaseExpired; a null expires_at never
704
- // expires). An expired-but-not-yet-reclaimed lease is deliberately excluded:
705
- // planClaimAcquisition would supersede it, so it is not a live presence signal
706
- // (this mirrors the expiry check the `owns` verdict re-applies). Reads that
707
- // derive claimed_by (loadBoardReadiness/loadActiveKernelClaimRow) filter on
708
- // state only; THIS read additionally honors the lease TTL. Sorted by
709
- // claimed_at (then issue_id) for deterministic output. The kernel_claims
710
- // schema has NO `agent` column — the lease's who-dimension is actor +
711
- // session_id + worktree_id — so no agent field is surfaced.
776
+ // Dashboard lease rows consume the same live-authority projection as stats,
777
+ // claimed_by, owns, and readiness. Sort by claimed_at then issue_id for stable output.
712
778
  if (operation === 'claims') {
713
- const nowIso = context.now || new Date().toISOString();
714
- const rows = safeAll(runtime, db, "SELECT * FROM kernel_claims WHERE state = 'active'");
715
- const claims = rows
716
- .filter(row => !isLeaseExpired(row, nowIso))
779
+ const liveClaims = loadLiveKernelClaimRows(runtime, db, context);
780
+ const claims = liveClaims
717
781
  .map(row => ({
718
782
  id: row.id,
719
783
  issue_id: row.issue_id,
@@ -749,11 +813,20 @@ const KERNEL_EVENT_COLUMNS = Object.freeze([
749
813
  'created_at',
750
814
  ]);
751
815
 
816
+ function assertGenericEventNamespace(event) {
817
+ if (!String(event.idempotency_key || '').startsWith('claim.repair:')) return;
818
+ throw new ClaimRepairError(
819
+ 'CLAIM_REPAIR_RECEIPT_RESERVED',
820
+ 'Generic event insertion cannot write the reserved claim-repair receipt namespace',
821
+ );
822
+ }
823
+
752
824
  // Persist one event. The id is supplied by the caller or minted here (event ids are
753
825
  // TEXT, not autoincrement). The event's payload is stored as payload_json: a
754
826
  // pre-serialized payload_json wins, else the payload object is JSON-stringified. The
755
827
  // native UNIQUE(idempotency_key) error is intentionally NOT caught here.
756
828
  function insertKernelEventRow(runtime, db, event) {
829
+ assertGenericEventNamespace(event);
757
830
  const id = event.id || randomUUID();
758
831
  const payloadJson = event.payload_json ?? JSON.stringify(event.payload ?? {});
759
832
  const row = {
@@ -790,13 +863,14 @@ function loadKernelEntityRow(runtime, db, entityType, entityId) {
790
863
  }
791
864
 
792
865
  // Read the full event stream for one entity, oldest first (matches
793
- // idx_kernel_events_entity_created; there is no seq column, so created_at is the
794
- // ordering key).
866
+ // idx_kernel_events_entity_created. SQLite rowid is the authoritative local
867
+ // insertion sequence when timestamps tie; event ids are random and cannot order
868
+ // same-clock approval/rejection decisions.
795
869
  function listKernelEventRows(runtime, db, entityType, entityId) {
796
870
  return allParams(
797
871
  runtime,
798
872
  db,
799
- 'SELECT * FROM kernel_events WHERE entity_type = ? AND entity_id = ? ORDER BY created_at ASC',
873
+ 'SELECT * FROM kernel_events WHERE entity_type = ? AND entity_id = ? ORDER BY created_at ASC, rowid ASC',
800
874
  [entityType, entityId],
801
875
  );
802
876
  }
@@ -1107,6 +1181,67 @@ function updateKernelClaimStateRow(runtime, db, claimId, state) {
1107
1181
  return { id: claimId, state };
1108
1182
  }
1109
1183
 
1184
+ function listActiveKernelClaimRows(runtime, db) {
1185
+ return safeAll(runtime, db, "SELECT * FROM kernel_claims WHERE state = 'active' ORDER BY claimed_at ASC, issue_id ASC");
1186
+ }
1187
+
1188
+ // Reconciliation must never release whichever lease happens to occupy an issue's
1189
+ // active slot after its evidence was gathered. Bind every ownership discriminator
1190
+ // from the observed row so a concurrent replacement makes this a zero-row no-op.
1191
+ function releaseExactKernelClaimRow(runtime, db, claim) {
1192
+ const result = runParams(
1193
+ runtime,
1194
+ db,
1195
+ `UPDATE kernel_claims SET state = 'released'
1196
+ WHERE id = ? AND issue_id = ? AND actor IS ? AND session_id IS ?
1197
+ AND worktree_id IS ? AND state = 'active'`,
1198
+ [claim.id, claim.issue_id, claim.actor ?? null, claim.session_id ?? null, claim.worktree_id ?? null],
1199
+ );
1200
+ return Number(result?.changes || 0) === 1;
1201
+ }
1202
+
1203
+ function releaseExactKernelClaimIfWorktreeMissing(runtime, db, claim, expectedWorktree, isMissing) {
1204
+ if (!expectedWorktree || typeof isMissing !== 'function') return false;
1205
+ execSql(runtime, db, 'BEGIN IMMEDIATE;');
1206
+ try {
1207
+ const rows = safeAll(
1208
+ runtime,
1209
+ db,
1210
+ `SELECT * FROM kernel_worktrees
1211
+ WHERE id = ? AND git_common_dir IS ? AND path = ? AND branch IS ?
1212
+ AND actor IS ? AND issue_id IS ? AND work_folder IS ?
1213
+ AND registered_at IS ? AND state = 'active'`,
1214
+ [
1215
+ expectedWorktree.id,
1216
+ expectedWorktree.git_common_dir ?? null,
1217
+ expectedWorktree.path,
1218
+ expectedWorktree.branch ?? null,
1219
+ expectedWorktree.actor ?? null,
1220
+ expectedWorktree.issue_id ?? null,
1221
+ expectedWorktree.work_folder ?? null,
1222
+ expectedWorktree.registered_at ?? null,
1223
+ ],
1224
+ );
1225
+ const exact = rows.length === 1 && rows[0];
1226
+ // This check deliberately runs under BEGIN IMMEDIATE to close the
1227
+ // worktree-state TOCTOU window. Keep callbacks synchronous, local, and fast.
1228
+ if (!exact || isMissing(exact.path, exact) !== true) {
1229
+ execSql(runtime, db, 'ROLLBACK;');
1230
+ return false;
1231
+ }
1232
+ const released = releaseExactKernelClaimRow(runtime, db, claim);
1233
+ if (!released) {
1234
+ execSql(runtime, db, 'ROLLBACK;');
1235
+ return false;
1236
+ }
1237
+ execSql(runtime, db, 'COMMIT;');
1238
+ return true;
1239
+ } catch (error) {
1240
+ try { execSql(runtime, db, 'ROLLBACK;'); } catch { /* preserve original failure */ }
1241
+ throw error;
1242
+ }
1243
+ }
1244
+
1110
1245
  // --- Worktree-linkage primitives (P0 kernel linkage backbone). The kernel_worktrees
1111
1246
  // table is a plain authority registry (NOT event-sourced): `forge worktree create`
1112
1247
  // writes a row here so the kernel records issue → worktree → work-folder, and
@@ -1216,6 +1351,194 @@ function listWorktreeRows(runtime, db, filter = {}) {
1216
1351
  return safeAll(runtime, db, 'SELECT * FROM kernel_worktrees ORDER BY registered_at DESC');
1217
1352
  }
1218
1353
 
1354
+ const TRACE_MAX_PULL_REQUESTS = 128;
1355
+ const TRACE_MAX_ITERATIONS = 128;
1356
+
1357
+ function resolvePrLinkageRow(runtime, db, input = {}) {
1358
+ let worktree = null;
1359
+ if (input.worktree_id) {
1360
+ worktree = safeAll(
1361
+ runtime,
1362
+ db,
1363
+ "SELECT * FROM kernel_worktrees WHERE id = ? AND state = 'active' LIMIT 1",
1364
+ [input.worktree_id],
1365
+ )[0] || null;
1366
+ } else if (input.branch && input.git_common_dir) {
1367
+ worktree = safeAll(
1368
+ runtime,
1369
+ db,
1370
+ `SELECT * FROM kernel_worktrees
1371
+ WHERE git_common_dir = ? AND branch = ? AND state = 'active'
1372
+ ORDER BY registered_at DESC LIMIT 1`,
1373
+ [input.git_common_dir, input.branch],
1374
+ )[0] || null;
1375
+ }
1376
+ const inferredIssueId = worktree?.issue_id ?? null;
1377
+ const issueId = input.issue_id ?? inferredIssueId;
1378
+ const issue = issueId
1379
+ ? safeAll(runtime, db, 'SELECT id, entity_revision FROM kernel_issues WHERE id = ? LIMIT 1', [issueId])[0] || null
1380
+ : null;
1381
+ return {
1382
+ issue_id: issueId,
1383
+ inferred_issue_id: inferredIssueId,
1384
+ issue_revision: issue?.entity_revision ?? null,
1385
+ worktree_id: worktree?.id ?? null,
1386
+ worktree_matches: Boolean(worktree
1387
+ && worktree.git_common_dir === input.git_common_dir
1388
+ && worktree.branch === input.branch
1389
+ && worktree.issue_id === issueId),
1390
+ };
1391
+ }
1392
+
1393
+ function parseTraceEvent(row) {
1394
+ let payload;
1395
+ try {
1396
+ payload = JSON.parse(row.payload_json || '{}');
1397
+ } catch {
1398
+ payload = {};
1399
+ }
1400
+ return {
1401
+ id: row.id,
1402
+ type: row.event_type,
1403
+ at: row.created_at,
1404
+ issue_id: payload.issue_id ?? null,
1405
+ worktree_id: payload.worktree_id ?? null,
1406
+ issue_revision: payload.issue_revision ?? null,
1407
+ head_sha: payload.head_sha ?? null,
1408
+ work_packet_hash: payload.work_packet_hash ?? null,
1409
+ work_packet_identity: payload.work_packet_identity ?? null,
1410
+ run_receipt_hash: payload.run_receipt_hash ?? null,
1411
+ run_id: payload.run_id ?? null,
1412
+ attempt_id: payload.attempt_id ?? null,
1413
+ risk_manifest_digest: payload.risk_manifest_digest ?? null,
1414
+ gate_receipts: payload.gate_receipts ?? null,
1415
+ url: payload.url ?? null,
1416
+ };
1417
+ }
1418
+
1419
+ function isCompleteTraceIteration(event) {
1420
+ if (!event.type?.startsWith('pr.')) return true;
1421
+ return typeof event.issue_id === 'string' && event.issue_id.length > 0
1422
+ && typeof event.worktree_id === 'string' && event.worktree_id.length > 0
1423
+ && Number.isInteger(event.issue_revision) && event.issue_revision >= 0
1424
+ && /^[0-9a-f]{40}$/.test(event.head_sha)
1425
+ && /^[0-9a-f]{64}$/.test(event.work_packet_hash)
1426
+ && typeof event.work_packet_identity === 'string' && event.work_packet_identity.length > 0
1427
+ && /^[0-9a-f]{64}$/.test(event.run_receipt_hash)
1428
+ && /^[0-9a-f]{64}$/.test(event.risk_manifest_digest)
1429
+ && Array.isArray(event.gate_receipts) && event.gate_receipts.length > 0
1430
+ && event.gate_receipts.every(receipt => typeof receipt === 'string' && receipt.length > 0);
1431
+ }
1432
+
1433
+ function loadPrTraceRow(runtime, db, row, gaps) {
1434
+ let iterations = safeAll(
1435
+ runtime,
1436
+ db,
1437
+ "SELECT * FROM kernel_events WHERE entity_type = 'pr' AND entity_id = ? ORDER BY created_at ASC, rowid ASC LIMIT ?",
1438
+ [row.id, TRACE_MAX_ITERATIONS + 1],
1439
+ );
1440
+ if (iterations.length > TRACE_MAX_ITERATIONS) {
1441
+ iterations = iterations.slice(0, TRACE_MAX_ITERATIONS);
1442
+ gaps.push(`iterations:${row.id}:overflow`);
1443
+ }
1444
+ const parsedIterations = iterations.map(parseTraceEvent);
1445
+ const prIterations = parsedIterations.filter(event => event.type?.startsWith('pr.'));
1446
+ if (prIterations.length === 0) {
1447
+ gaps.push(`iterations:${row.id}:missing`);
1448
+ } else if (prIterations.some(event => !isCompleteTraceIteration(event))) {
1449
+ gaps.push(`iterations:${row.id}:incomplete`);
1450
+ }
1451
+ return {
1452
+ ...row,
1453
+ url: parsedIterations.find(event => event.url)?.url ?? null,
1454
+ iterations: parsedIterations,
1455
+ };
1456
+ }
1457
+
1458
+ function loadSelectedTracePr(runtime, db, target) {
1459
+ if (target.pr_number === undefined || target.pr_number === null) return null;
1460
+ const clauses = ['number = ?'];
1461
+ const params = [Number(target.pr_number)];
1462
+ if (target.repo) {
1463
+ clauses.push('repo = ?');
1464
+ params.push(target.repo);
1465
+ }
1466
+ if (target.git_common_dir) {
1467
+ clauses.push('git_common_dir = ?');
1468
+ params.push(target.git_common_dir);
1469
+ }
1470
+ const matches = safeAll(
1471
+ runtime,
1472
+ db,
1473
+ `SELECT * FROM kernel_pr WHERE ${clauses.join(' AND ')} ORDER BY registered_at DESC LIMIT 2`,
1474
+ params,
1475
+ );
1476
+ if (matches.length > 1) throw new Error('Kernel trace PR target is ambiguous; supply repo and git_common_dir');
1477
+ return matches[0] || null;
1478
+ }
1479
+
1480
+ function appendSelectedTracePr(pullRequests, selectedPr, issueId, gaps) {
1481
+ if (!selectedPr || pullRequests.some(row => row.id === selectedPr.id)) return pullRequests;
1482
+ if (issueId && selectedPr.issue_id !== issueId) {
1483
+ gaps.push(`pull_requests:${selectedPr.id}:unlinked_issue`);
1484
+ }
1485
+ const retained = pullRequests.length >= TRACE_MAX_PULL_REQUESTS
1486
+ ? pullRequests.slice(0, TRACE_MAX_PULL_REQUESTS - 1)
1487
+ : pullRequests;
1488
+ if (retained.length < pullRequests.length && !gaps.includes('pull_requests:overflow')) {
1489
+ gaps.push('pull_requests:overflow');
1490
+ }
1491
+ return [...retained, selectedPr];
1492
+ }
1493
+
1494
+ function loadTraceRows(runtime, db, target = {}) {
1495
+ const gaps = [];
1496
+ const selectedPr = loadSelectedTracePr(runtime, db, target);
1497
+
1498
+ const issueId = target.issue_id ?? selectedPr?.issue_id ?? null;
1499
+ const issue = issueId
1500
+ ? safeAll(runtime, db, 'SELECT * FROM kernel_issues WHERE id = ? LIMIT 1', [issueId])[0] || null
1501
+ : null;
1502
+ let pullRequests = issue
1503
+ ? safeAll(
1504
+ runtime,
1505
+ db,
1506
+ ' SELECT * FROM kernel_pr WHERE issue_id = ? ORDER BY registered_at ASC, number ASC LIMIT ?',
1507
+ [issue.id, TRACE_MAX_PULL_REQUESTS + 1],
1508
+ )
1509
+ : [];
1510
+ if (pullRequests.length > TRACE_MAX_PULL_REQUESTS) {
1511
+ pullRequests = pullRequests.slice(0, TRACE_MAX_PULL_REQUESTS);
1512
+ gaps.push('pull_requests:overflow');
1513
+ }
1514
+ pullRequests = appendSelectedTracePr(pullRequests, selectedPr, issueId, gaps);
1515
+
1516
+ let worktree = null;
1517
+ const issueWorktreeId = pullRequests.find(row => row.issue_id === issueId && row.worktree_id)?.worktree_id;
1518
+ const selectedWorktreeId = !issueId || selectedPr?.issue_id === issueId ? selectedPr?.worktree_id : null;
1519
+ const preferredWorktreeId = selectedWorktreeId || issueWorktreeId;
1520
+ if (preferredWorktreeId) {
1521
+ worktree = safeAll(runtime, db, 'SELECT * FROM kernel_worktrees WHERE id = ? LIMIT 1', [preferredWorktreeId])[0] || null;
1522
+ }
1523
+ if (!worktree && issue) {
1524
+ worktree = safeAll(
1525
+ runtime,
1526
+ db,
1527
+ `SELECT * FROM kernel_worktrees WHERE issue_id = ?
1528
+ ORDER BY CASE state WHEN 'active' THEN 0 ELSE 1 END, registered_at DESC LIMIT 1`,
1529
+ [issue.id],
1530
+ )[0] || null;
1531
+ }
1532
+
1533
+ return {
1534
+ issue,
1535
+ worktree,
1536
+ work_folder: worktree?.work_folder ?? null,
1537
+ gaps,
1538
+ pull_requests: pullRequests.map(row => loadPrTraceRow(runtime, db, row, gaps)),
1539
+ };
1540
+ }
1541
+
1219
1542
  // --- Stage-run registry (f61601ab). kernel_stage_runs records the REAL workflow
1220
1543
  // phase per issue so the dashboard/`show` read the phase instead of guessing it
1221
1544
  // from status+claim (a claimed-open issue with a merged PR would otherwise still
@@ -1374,6 +1697,78 @@ function recordStageTransitionRow(runtime, db, input) {
1374
1697
  }
1375
1698
  }
1376
1699
 
1700
+ const PLAN_SNAPSHOT_METADATA_KEY = 'forge.plan.v1';
1701
+
1702
+ function parseIssueMetadataObject(metadata) {
1703
+ if (metadata == null || metadata === '') return {};
1704
+ const parsed = typeof metadata === 'string' ? JSON.parse(metadata) : metadata;
1705
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1706
+ throw new Error('issue metadata must be a JSON object');
1707
+ }
1708
+ return parsed;
1709
+ }
1710
+
1711
+ function loadPlanSnapshotRow(runtime, db, issueId) {
1712
+ const issue = loadKernelEntityRow(runtime, db, 'issue', issueId);
1713
+ if (!issue) throw new Error(`Issue ${issueId} not found in the kernel`);
1714
+ return parseIssueMetadataObject(issue.metadata)[PLAN_SNAPSHOT_METADATA_KEY] || null;
1715
+ }
1716
+
1717
+ // Persist the plan snapshot and the plan->dev transition as one authority write.
1718
+ // The issue metadata column already exists, so this adds no schema or migration.
1719
+ // A normal issue.update event is appended in the same transaction so revision/CAS
1720
+ // history remains truthful rather than mutating the issue projection out-of-band.
1721
+ function recordPlanSnapshotTransitionRow(runtime, db, input) {
1722
+ const issueId = input && input.issue_id;
1723
+ const snapshot = input && input.snapshot;
1724
+ if (!issueId || !snapshot) {
1725
+ throw new Error('recordPlanSnapshotTransition requires issue_id and snapshot');
1726
+ }
1727
+ const now = input.now || new Date().toISOString();
1728
+ execSql(runtime, db, 'BEGIN IMMEDIATE;');
1729
+ try {
1730
+ const issue = loadKernelEntityRow(runtime, db, 'issue', issueId);
1731
+ if (!issue) throw new Error(`Issue ${issueId} not found in the kernel`);
1732
+ const metadata = parseIssueMetadataObject(issue.metadata);
1733
+ const established = metadata[PLAN_SNAPSHOT_METADATA_KEY];
1734
+ if (established) {
1735
+ if (established.digest === snapshot.digest) {
1736
+ execSql(runtime, db, 'COMMIT;');
1737
+ return { idempotent: true };
1738
+ }
1739
+ throw new Error('Kernel plan snapshot is immutable; reconcile repository drift before retrying');
1740
+ }
1741
+ metadata[PLAN_SNAPSHOT_METADATA_KEY] = snapshot;
1742
+ const event = {
1743
+ id: randomUUID(),
1744
+ entity_type: 'issue',
1745
+ entity_id: issueId,
1746
+ event_type: 'issue.update',
1747
+ idempotency_key: `plan.snapshot:${issueId}:${snapshot.digest}`,
1748
+ expected_revision: Number(issue.entity_revision ?? 0),
1749
+ actor: input.actor || 'forge-plan',
1750
+ origin: 'local',
1751
+ payload: { metadata: JSON.stringify(metadata) },
1752
+ created_at: now,
1753
+ };
1754
+ insertKernelEventRow(runtime, db, event);
1755
+ applyAcceptedIssueEvent(runtime, db, event, {});
1756
+ const transition = {
1757
+ from: recordStageRunRow(runtime, db, { issue_id: issueId, stage: 'plan', action: 'complete', now }),
1758
+ to: recordStageRunRow(runtime, db, { issue_id: issueId, stage: 'dev', action: 'start', now }),
1759
+ };
1760
+ execSql(runtime, db, 'COMMIT;');
1761
+ return transition;
1762
+ } catch (error) {
1763
+ try {
1764
+ execSql(runtime, db, 'ROLLBACK;');
1765
+ } catch {
1766
+ // Preserve the original failure.
1767
+ }
1768
+ throw error;
1769
+ }
1770
+ }
1771
+
1377
1772
  function listStageRunRows(runtime, db, issueId) {
1378
1773
  if (!issueId) return [];
1379
1774
  // Deterministic order: started_at first, then rowid (the implicit INSERT sequence)
@@ -1585,6 +1980,15 @@ function applyAcceptedIssueEvent(runtime, db, event, context = {}) {
1585
1980
  // on this connection), so computeNewlyUnblocked sees the post-close readiness.
1586
1981
  function finalizeIssueMutation(runtime, db, event, issueId, revision, context) {
1587
1982
  const summary = { id: issueId, revision };
1983
+ const issue = loadKernelEntityRow(runtime, db, 'issue', issueId);
1984
+ if (isTerminalStatus(issue?.status)) {
1985
+ const claim = loadActiveKernelClaimRow(runtime, db, issueId);
1986
+ if (claim && !releaseExactKernelClaimRow(runtime, db, claim)) {
1987
+ const error = new Error(`failed to release current claim for terminal issue ${issueId}`);
1988
+ error.code = 'FORGE_CLAIM_TERMINAL_RELEASE_CONFLICT';
1989
+ throw error;
1990
+ }
1991
+ }
1588
1992
  if (event.event_type === 'issue.close') {
1589
1993
  summary.newly_unblocked = computeNewlyUnblocked(runtime, db, issueId, context);
1590
1994
  }
@@ -1888,6 +2292,7 @@ function importIssueRecords(runtime, db, records = {}, options = {}) {
1888
2292
  for (const event of activityEvents) {
1889
2293
  if (!event || event.id == null) { summary.events.skipped += 1; continue; }
1890
2294
  const row = buildImportEventRow(event, now);
2295
+ assertGenericEventNamespace(row);
1891
2296
  const result = runParams(runtime, db, eventSql, IMPORT_EVENT_COLUMNS.map(column => row[column]));
1892
2297
  summary.events[wasInserted(result) ? 'inserted' : 'skipped'] += 1;
1893
2298
  }
@@ -2029,23 +2434,43 @@ function searchMemoryRows(runtime, db, query) {
2029
2434
  ).map(memoryRowToEntry);
2030
2435
  }
2031
2436
 
2032
- // Optional source_agent allow-list → a parameterized `WHERE source_agent IN (...)` clause.
2033
- // Lets the default `recall` view show only human `remember` notes without loading and
2034
- // filtering the whole table in JS. Returns { clause, params }; empty when no filter.
2035
- function memoryAgentFilter(agents) {
2036
- if (!Array.isArray(agents) || agents.length === 0) {
2037
- return { clause: '', params: [] };
2437
+ // Optional source-agent and kind filters become one parameterized WHERE clause. This keeps
2438
+ // typed recall complete even when its oldest match lies outside a broad recent-note window.
2439
+ function memoryReadFilter(options = {}, table = 'kernel_memories') {
2440
+ const predicates = [];
2441
+ const params = [];
2442
+ if (options.includeSuperseded !== true) {
2443
+ predicates.push(`(COALESCE(${table}.scope, ''), ${table}.key) NOT IN (
2444
+ SELECT COALESCE(superseder.scope, ''), superseded.value
2445
+ FROM kernel_memories superseder,
2446
+ json_each(COALESCE(superseder.supersedes_json, '[]')) superseded
2447
+ WHERE superseded.type = 'text'
2448
+ AND superseder.key != superseded.value
2449
+ )`);
2450
+ }
2451
+ if (Array.isArray(options.agents) && options.agents.length > 0) {
2452
+ predicates.push(`${table}.source_agent IN (${options.agents.map(() => '?').join(', ')})`);
2453
+ params.push(...options.agents);
2038
2454
  }
2039
- const placeholders = agents.map(() => '?').join(', ');
2040
- return { clause: ` WHERE source_agent IN (${placeholders})`, params: [...agents] };
2455
+ if (typeof options.kind === 'string' && options.kind.trim()) {
2456
+ predicates.push(`EXISTS (
2457
+ SELECT 1 FROM json_each(${table}.tags_json)
2458
+ WHERE lower(json_each.value) = ?
2459
+ )`);
2460
+ params.push(`type:${options.kind.trim().toLowerCase()}`);
2461
+ }
2462
+ return {
2463
+ clause: predicates.length > 0 ? ` WHERE ${predicates.join(' AND ')}` : '',
2464
+ params,
2465
+ };
2041
2466
  }
2042
2467
 
2043
2468
  // The newest `limit` entries by logical (as-of) timestamp — the default read model for
2044
2469
  // `recall` with no query. rowid breaks ties so same-timestamp rows are still deterministic.
2045
- // An optional `agents` allow-list scopes the view (e.g. to human `remember` notes).
2046
- function recentMemoryRows(runtime, db, limit, agents) {
2470
+ // Optional `agents` and `kind` filters scope the view before the limit is applied.
2471
+ function recentMemoryRows(runtime, db, limit, options = {}) {
2047
2472
  const capped = Number.isInteger(limit) && limit > 0 ? limit : 20;
2048
- const { clause, params } = memoryAgentFilter(agents);
2473
+ const { clause, params } = memoryReadFilter(options);
2049
2474
  return allParams(
2050
2475
  runtime,
2051
2476
  db,
@@ -2054,10 +2479,10 @@ function recentMemoryRows(runtime, db, limit, agents) {
2054
2479
  ).map(memoryRowToEntry);
2055
2480
  }
2056
2481
 
2057
- // Total number of stored memories (optionally scoped by `agents`) — paired with
2482
+ // Total number of stored memories (optionally scoped by `agents` and `kind`) — paired with
2058
2483
  // recentMemoryRows so `recall` can report "showing N of TOTAL" instead of silently truncating.
2059
- function countMemoryRows(runtime, db, agents) {
2060
- const { clause, params } = memoryAgentFilter(agents);
2484
+ function countMemoryRows(runtime, db, options = {}) {
2485
+ const { clause, params } = memoryReadFilter(options);
2061
2486
  const rows = allParams(runtime, db, `SELECT count(*) AS count FROM kernel_memories${clause}`, params);
2062
2487
  return Number((rows[0] || {}).count) || 0;
2063
2488
  }
@@ -2086,21 +2511,26 @@ function buildMemoryFtsMatchOr(query) {
2086
2511
  // BM25 top-N recall over the kernel_memories_fts index (migration 008). Joins the FTS
2087
2512
  // rowid back to the memory row and orders by bm25 (lower = better match). An empty/tokenless
2088
2513
  // query falls back to recent entries so `recall` never returns a bare full dump.
2089
- function searchMemoryRowsRanked(runtime, db, query, limit) {
2514
+ function searchMemoryRowsRanked(runtime, db, query, limit, options = {}) {
2090
2515
  const capped = Number.isInteger(limit) && limit > 0 ? limit : 20;
2091
2516
  const match = buildMemoryFtsMatch(query);
2092
2517
  if (!match) {
2093
- return recentMemoryRows(runtime, db, capped);
2518
+ return recentMemoryRows(runtime, db, capped, options);
2094
2519
  }
2520
+ const { clause, params } = memoryReadFilter({
2521
+ kind: options.kind,
2522
+ includeSuperseded: options.includeSuperseded,
2523
+ }, 'm');
2524
+ const kindPredicate = clause ? clause.replace(/^ WHERE /, ' AND ') : '';
2095
2525
  return allParams(
2096
2526
  runtime,
2097
2527
  db,
2098
2528
  `SELECT m.* FROM kernel_memories m
2099
2529
  JOIN kernel_memories_fts ON kernel_memories_fts.rowid = m.rowid
2100
- WHERE kernel_memories_fts MATCH ?
2530
+ WHERE kernel_memories_fts MATCH ?${kindPredicate}
2101
2531
  ORDER BY bm25(kernel_memories_fts)
2102
2532
  LIMIT ?`,
2103
- [match, capped],
2533
+ [match, ...params, capped],
2104
2534
  ).map(memoryRowToEntry);
2105
2535
  }
2106
2536
 
@@ -2151,6 +2581,14 @@ function searchMemoryRowsRankedScored(runtime, db, query, limit, options = {}) {
2151
2581
  : '';
2152
2582
  const confirmed = confirmedMemorySql('m');
2153
2583
  const supersederConfirmed = confirmedMemorySql('s');
2584
+ const supersessionPredicate = options.includeSuperseded === true
2585
+ ? ''
2586
+ : `AND NOT EXISTS (
2587
+ SELECT 1
2588
+ FROM eligible_superseders superseder
2589
+ WHERE superseder.memory_key = m.key
2590
+ AND (superseder.is_confirmed = 1 OR NOT ${confirmed})
2591
+ )`;
2154
2592
  // Expand eligible supersession edges once. A correlated json_each scan repeated the
2155
2593
  // entire memory table for every FTS candidate and dominated the 1,000-row prompt path.
2156
2594
  // Recall temporarily waits less than the connection default so a real lock cannot outlive
@@ -2181,12 +2619,7 @@ function searchMemoryRowsRankedScored(runtime, db, query, limit, options = {}) {
2181
2619
  AND ${projectMemoryScopeSql('m')}
2182
2620
  AND (${confirmed} OR m.updated_at >= ?)
2183
2621
  ${seenSql}
2184
- AND NOT EXISTS (
2185
- SELECT 1
2186
- FROM eligible_superseders superseder
2187
- WHERE superseder.memory_key = m.key
2188
- AND (superseder.is_confirmed = 1 OR NOT ${confirmed})
2189
- )
2622
+ ${supersessionPredicate}
2190
2623
  ORDER BY bm25(kernel_memories_fts),
2191
2624
  CASE WHEN ${confirmed} THEN 0 ELSE 1 END,
2192
2625
  m.source_agent ASC, m.updated_at DESC, m.key ASC
@@ -2209,77 +2642,2197 @@ function closeDatabase(db) {
2209
2642
  }
2210
2643
  }
2211
2644
 
2212
- function createDriver(runtime, configuredDatabasePath) {
2213
- let db;
2214
- let openedDatabasePath;
2215
- let memorySchemaEnsured = false;
2645
+ const WATCH_OWNER_TABLE = 'kernel_pr_watch_owners';
2646
+ const WATCH_GATE_TABLE = 'kernel_pr_watch_migration_gate';
2647
+ const WATCH_OWNER_SCHEMA_COLUMNS = Object.freeze([
2648
+ { name: 'repo', type: 'TEXT', notnull: 1, pk: 1 },
2649
+ { name: 'pr', type: 'INTEGER', notnull: 1, pk: 2 },
2650
+ { name: 'version', type: 'INTEGER', notnull: 1, pk: 0 },
2651
+ { name: 'generation', type: 'TEXT', notnull: 1, pk: 0 },
2652
+ { name: 'phase', type: 'TEXT', notnull: 1, pk: 0 },
2653
+ { name: 'controller_pid', type: 'INTEGER', notnull: 0, pk: 0 },
2654
+ { name: 'watcher_pid', type: 'INTEGER', notnull: 0, pk: 0 },
2655
+ { name: 'started_at', type: 'TEXT', notnull: 1, pk: 0 },
2656
+ { name: 'updated_at', type: 'TEXT', notnull: 1, pk: 0 },
2657
+ { name: 'heartbeat_at', type: 'TEXT', notnull: 0, pk: 0 },
2658
+ { name: 'terminal_receipt_id', type: 'TEXT', notnull: 0, pk: 0 },
2659
+ { name: 'block_reason', type: 'TEXT', notnull: 0, pk: 0 },
2660
+ { name: 'legacy_evidence_hash', type: 'TEXT', notnull: 0, pk: 0 },
2661
+ ]);
2662
+ const WATCH_GATE_SCHEMA_COLUMNS = Object.freeze([
2663
+ { name: 'singleton', type: 'INTEGER', notnull: 1, pk: 1 },
2664
+ { name: 'state', type: 'TEXT', notnull: 1, pk: 0 },
2665
+ { name: 'snapshot_hash', type: 'TEXT', notnull: 0, pk: 0 },
2666
+ { name: 'conflict_code', type: 'TEXT', notnull: 0, pk: 0 },
2667
+ { name: 'updated_at', type: 'TEXT', notnull: 1, pk: 0 },
2668
+ ]);
2216
2669
 
2217
- // kernel_memories is created by migration 005 through broker.initialize(), but the
2218
- // synchronous project-memory facade writes WITHOUT first running migrations. Lazily
2219
- // ensure the table (idempotent CREATE IF NOT EXISTS, rendered from the same migration)
2220
- // plus a busy_timeout for the second connection the issue backend may hold open.
2221
- function ensureMemorySchema(database, busyTimeoutMs) {
2222
- if (memorySchemaEnsured) return;
2223
- const requestedBusyTimeout = Number(busyTimeoutMs);
2224
- const busyTimeout = Number.isFinite(requestedBusyTimeout) && requestedBusyTimeout >= 0
2225
- ? Math.floor(requestedBusyTimeout)
2226
- : 5_000;
2227
- execSql(runtime, database, `PRAGMA busy_timeout=${busyTimeout};`);
2228
- try {
2229
- for (const statement of buildMemoryProjectionMigration().apply) {
2230
- execSql(runtime, database, statement);
2231
- }
2232
- // FTS5 recall index (migration 008): create the virtual table + sync triggers
2233
- // idempotently so a synchronous memory write stays indexed without a prior
2234
- // broker.initialize(). When the index is NEWLY created, rebuild once to backfill any
2235
- // rows written before it existed (a DB upgraded from before this feature, or rows the
2236
- // insights engine wrote straight to kernel_memories) — the sync triggers keep it
2237
- // current thereafter, so steady-state process starts skip the reindex.
2238
- //
2239
- // Staleness is detected by TABLE EXISTENCE (sqlite_master), never by count(*): on an
2240
- // external-content FTS5 table `count(*)` returns the CONTENT row count, not the
2241
- // indexed-doc count, so it can never reveal an un-backfilled index.
2242
- const ftsDdl = memoryFtsDdl();
2243
- const ftsExisted = Number(queryOne(
2244
- runtime,
2245
- database,
2246
- "SELECT count(*) AS count FROM sqlite_master WHERE type = 'table' AND name = 'kernel_memories_fts'",
2247
- ).count) > 0;
2248
- execSql(runtime, database, ftsDdl.create);
2249
- for (const trigger of ftsDdl.triggers) {
2250
- execSql(runtime, database, trigger);
2251
- }
2252
- if (!ftsExisted) {
2253
- execSql(runtime, database, ftsDdl.rebuild);
2254
- }
2255
- memorySchemaEnsured = true;
2256
- } finally {
2257
- if (busyTimeout !== 5_000) {
2258
- execSql(runtime, database, 'PRAGMA busy_timeout=5000;');
2259
- }
2260
- }
2670
+ function watchOwnerStoreError(code, message, cause) {
2671
+ const error = new Error(message);
2672
+ error.code = code;
2673
+ if (cause) error.cause = cause;
2674
+ return error;
2675
+ }
2676
+
2677
+ function assertWatchOwnerDatabasePath(databasePath, options = {}) {
2678
+ if (typeof databasePath !== 'string' || !databasePath
2679
+ || databasePath === ':memory:' || databasePath.startsWith('file:')) {
2680
+ throw watchOwnerStoreError('AUTHORITY_UNAVAILABLE', 'Watcher authority requires an existing file-backed Kernel database');
2681
+ }
2682
+ const resolved = path.resolve(databasePath);
2683
+ const filesystemDeps = options.watchOwnerFilesystemDeps || {};
2684
+ const authorityFilesystemDeps = {
2685
+ ...filesystemDeps,
2686
+ env: { ...(filesystemDeps.env || process.env), FORGE_KERNEL_ALLOW_UNSAFE_FS: '' },
2687
+ };
2688
+ if (path.basename(resolved) !== 'kernel.sqlite' || path.basename(path.dirname(resolved)) !== 'forge') {
2689
+ throw watchOwnerStoreError('AUTHORITY_UNAVAILABLE', 'Watcher authority database path is outside the canonical forge/kernel.sqlite location');
2690
+ }
2691
+ try {
2692
+ assertFilesystemSafeForKernel(resolved, authorityFilesystemDeps);
2693
+ } catch (error) {
2694
+ throw watchOwnerStoreError('AUTHORITY_UNAVAILABLE', 'Watcher authority database is on a refused filesystem class', error);
2695
+ }
2696
+ let stat;
2697
+ try {
2698
+ stat = fs.statSync(resolved);
2699
+ } catch (error) {
2700
+ throw watchOwnerStoreError('AUTHORITY_UNAVAILABLE', 'Watcher authority database does not exist', error);
2701
+ }
2702
+ if (!stat.isFile()) {
2703
+ throw watchOwnerStoreError('AUTHORITY_UNAVAILABLE', 'Watcher authority database is not a regular file');
2704
+ }
2705
+ let realPath;
2706
+ try {
2707
+ realPath = fs.realpathSync(resolved);
2708
+ } catch (error) {
2709
+ throw watchOwnerStoreError('AUTHORITY_UNAVAILABLE', 'Watcher authority database path cannot be resolved', error);
2710
+ }
2711
+ if (path.basename(realPath) !== 'kernel.sqlite' || path.basename(path.dirname(realPath)) !== 'forge') {
2712
+ throw watchOwnerStoreError('AUTHORITY_UNAVAILABLE', 'Watcher authority database resolves outside the canonical forge/kernel.sqlite location');
2713
+ }
2714
+ try {
2715
+ assertFilesystemSafeForKernel(realPath, authorityFilesystemDeps);
2716
+ } catch (error) {
2717
+ throw watchOwnerStoreError('AUTHORITY_UNAVAILABLE', 'Watcher authority database resolves through a refused filesystem class', error);
2261
2718
  }
2719
+ return { realPath, dev: stat.dev, ino: stat.ino };
2720
+ }
2262
2721
 
2263
- function resolveDatabasePath(config) {
2264
- const brokerDatabasePath = config && config.databasePath;
2265
- if (configuredDatabasePath && brokerDatabasePath && configuredDatabasePath !== brokerDatabasePath) {
2266
- throw new Error([
2267
- 'Kernel SQLite driver databasePath mismatch:',
2268
- `driver is configured for ${configuredDatabasePath}`,
2269
- `but broker config uses ${brokerDatabasePath}`,
2270
- ].join(' '));
2722
+ function sameWatchOwnerDatabaseIdentity(expected, actual) {
2723
+ return expected.realPath === actual.realPath
2724
+ && expected.dev === actual.dev
2725
+ && expected.ino === actual.ino;
2726
+ }
2727
+
2728
+ function openWatchOwnerDatabase(runtime, validated) {
2729
+ let database;
2730
+ let guard;
2731
+ try {
2732
+ // SQLite's filename constructor may create a missing file. Hold a must-exist
2733
+ // descriptor across the open so a path swap after validation fails before any
2734
+ // runtime can create or rebind the authority database.
2735
+ guard = fs.openSync(validated.realPath, 'r+');
2736
+ const guardedStat = fs.fstatSync(guard);
2737
+ if (!sameWatchOwnerDatabaseIdentity(validated, {
2738
+ realPath: validated.realPath, dev: guardedStat.dev, ino: guardedStat.ino,
2739
+ })) {
2740
+ throw new Error('authority file identity changed before open');
2271
2741
  }
2272
- const databasePath = brokerDatabasePath || configuredDatabasePath;
2273
- if (!databasePath) {
2274
- throw new Error('Kernel SQLite driver requires a databasePath or broker config databasePath');
2742
+ database = createExistingWatchOwnerDatabase(runtime, validated.realPath);
2743
+ const main = queryAll(runtime, database, 'PRAGMA database_list;')
2744
+ .find(row => row.name === 'main');
2745
+ if (!main || typeof main.file !== 'string' || !main.file) {
2746
+ throw new Error('opened database did not expose a file-backed main database');
2747
+ }
2748
+ const openedPath = path.resolve(main.file);
2749
+ const openedRealPath = fs.realpathSync(openedPath);
2750
+ const openedStat = fs.statSync(openedRealPath);
2751
+ const actual = { realPath: openedRealPath, dev: openedStat.dev, ino: openedStat.ino };
2752
+ if (!sameWatchOwnerDatabaseIdentity(validated, actual)) {
2753
+ throw new Error('opened database identity differs from the validated authority file');
2754
+ }
2755
+ return database;
2756
+ } catch (error) {
2757
+ closeDatabase(database);
2758
+ throw watchOwnerStoreError('AUTHORITY_UNAVAILABLE', 'Watcher authority database changed while opening', error);
2759
+ } finally {
2760
+ if (guard != null) {
2761
+ try { fs.closeSync(guard); } catch { /* preserve the open/identity result */ }
2275
2762
  }
2276
- return databasePath;
2277
2763
  }
2764
+ }
2278
2765
 
2279
- function getDatabase(config) {
2280
- const databasePath = resolveDatabasePath(config);
2766
+ function watchOwnerBusyTimeout(options = {}) {
2767
+ const requested = Number(options.watchOwnerBusyTimeoutMs);
2768
+ if (!Number.isFinite(requested)) return 1_000;
2769
+ return Math.max(0, Math.min(5_000, Math.floor(requested)));
2770
+ }
2771
+
2772
+ function hasExactWatchSchemaColumns(runtime, db, tableName, expectedColumns) {
2773
+ const columns = queryAll(runtime, db, `PRAGMA table_info('${tableName}')`);
2774
+ return columns.length === expectedColumns.length
2775
+ && columns.every((column, index) => {
2776
+ const expected = expectedColumns[index];
2777
+ return column.name === expected.name
2778
+ && String(column.type).toUpperCase() === expected.type
2779
+ && Number(column.notnull) === expected.notnull
2780
+ && Number(column.pk) === expected.pk;
2781
+ });
2782
+ }
2783
+
2784
+ // Triggers owned by the authority tables are forbidden outright, and so are
2785
+ // triggers on any other table whose body even references an authority table:
2786
+ // quoted or schema-qualified identifiers would otherwise evade write-shape
2787
+ // matching, so a mere mention fails closed.
2788
+ function hasUnexpectedWatchAuthorityTriggers(runtime, db) {
2789
+ const triggers = allParams(
2790
+ runtime,
2791
+ db,
2792
+ "SELECT tbl_name AS tbl_name, sql FROM sqlite_master WHERE type = 'trigger'",
2793
+ );
2794
+ const authorityMention = new RegExp(
2795
+ `(?:${WATCH_OWNER_TABLE}|${WATCH_GATE_TABLE})`,
2796
+ 'i',
2797
+ );
2798
+ return triggers.some(trigger => {
2799
+ const owned = [WATCH_OWNER_TABLE, WATCH_GATE_TABLE].includes(String(trigger.tbl_name).toLowerCase());
2800
+ return owned || (typeof trigger.sql === 'string' && authorityMention.test(trigger.sql));
2801
+ });
2802
+ }
2803
+
2804
+ // Inbound ON DELETE CASCADE foreign keys would make abort/release delete rows
2805
+ // in unrelated tables (or vice versa), so any referencing table fails closed.
2806
+ // Outbound cascades declared on the authority tables themselves are equally
2807
+ // forbidden: deleting a referenced parent must never erase authority rows.
2808
+ function hasForbiddenWatchOwnerForeignKeys(runtime, db) {
2809
+ const tables = allParams(
2810
+ runtime,
2811
+ db,
2812
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT IN (?, ?)",
2813
+ [WATCH_OWNER_TABLE, WATCH_GATE_TABLE],
2814
+ );
2815
+ for (const { name } of tables) {
2816
+ const references = allParams(runtime, db, `PRAGMA foreign_key_list("${name.replace(/"/g, '""')}")`);
2817
+ if (references.some(reference => [WATCH_OWNER_TABLE, WATCH_GATE_TABLE]
2818
+ .includes(String(reference.table).toLowerCase()))) return true;
2819
+ }
2820
+ for (const table of [WATCH_OWNER_TABLE, WATCH_GATE_TABLE]) {
2821
+ if (allParams(runtime, db, `PRAGMA foreign_key_list("${table}")`).length > 0) return true;
2822
+ }
2823
+ return false;
2824
+ }
2825
+
2826
+ // Extra uniqueness constraints (e.g. UNIQUE(repo) ON CONFLICT REPLACE) let a
2827
+ // later insert silently delete an earlier authority row, so anything beyond
2828
+ // the migration's primary-key indexes fails closed.
2829
+ function hasUnexpectedWatchAuthorityIndexes(runtime, db) {
2830
+ for (const table of [WATCH_OWNER_TABLE, WATCH_GATE_TABLE]) {
2831
+ const indexes = allParams(runtime, db, `PRAGMA index_list("${table}")`);
2832
+ if (indexes.some(index => index.origin !== 'pk')) return true;
2833
+ }
2834
+ return false;
2835
+ }
2836
+
2837
+ function assertWatchOwnerSchema(runtime, db) {
2838
+ const rows = allParams(
2839
+ runtime,
2840
+ db,
2841
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name IN (?, ?)",
2842
+ [WATCH_OWNER_TABLE, WATCH_GATE_TABLE],
2843
+ );
2844
+ const tables = new Set(rows.map(row => row.name));
2845
+ if (!tables.has(WATCH_OWNER_TABLE) || !tables.has(WATCH_GATE_TABLE)) {
2846
+ throw watchOwnerStoreError('AUTHORITY_UNAVAILABLE', 'Watcher authority schema is not initialized; run Kernel migrations');
2847
+ }
2848
+ if (!hasExactWatchSchemaColumns(runtime, db, WATCH_OWNER_TABLE, WATCH_OWNER_SCHEMA_COLUMNS)
2849
+ || !hasExactWatchSchemaColumns(runtime, db, WATCH_GATE_TABLE, WATCH_GATE_SCHEMA_COLUMNS)
2850
+ || hasUnexpectedWatchAuthorityTriggers(runtime, db)
2851
+ || hasUnexpectedWatchAuthorityIndexes(runtime, db)) {
2852
+ throw watchOwnerStoreError('AUTHORITY_UNAVAILABLE', 'Watcher authority schema does not match the Kernel migration');
2853
+ }
2854
+ if (hasForbiddenWatchOwnerForeignKeys(runtime, db)) {
2855
+ throw watchOwnerStoreError('AUTHORITY_UNAVAILABLE', 'Watcher authority tables participate in a forbidden foreign key relationship');
2856
+ }
2857
+ }
2858
+
2859
+ const activeWatchOwnerConnections = new WeakSet();
2860
+
2861
+ function runWatchOwnerTransactionOnConnection(runtime, database, options, operation) {
2862
+ if (typeof operation !== 'function'
2863
+ || operation.constructor?.name === 'AsyncFunction') {
2864
+ throw watchOwnerStoreError('INVALID_OPERATION', 'Watcher authority transactions require a synchronous operation');
2865
+ }
2866
+ if (activeWatchOwnerConnections.has(database)) {
2867
+ throw watchOwnerStoreError('INVALID_OPERATION', 'Watcher authority transaction re-entry on the same connection is forbidden');
2868
+ }
2869
+ activeWatchOwnerConnections.add(database);
2870
+ const timeout = watchOwnerBusyTimeout(options);
2871
+ let active = false;
2872
+ let committed = false;
2873
+ try {
2874
+ let previousTimeout;
2875
+ let timeoutInstalled = false;
2876
+ let result;
2877
+ let transactionError;
2878
+ let restorationError;
2879
+ try {
2880
+ previousTimeout = Number(queryOne(runtime, database, 'PRAGMA busy_timeout;').timeout) || 0;
2881
+ execSql(runtime, database, `PRAGMA busy_timeout=${timeout};`);
2882
+ timeoutInstalled = true;
2883
+ execSql(runtime, database, 'PRAGMA foreign_keys=ON;');
2884
+ execSql(runtime, database, 'BEGIN IMMEDIATE;');
2885
+ active = true;
2886
+ assertWatchOwnerSchema(runtime, database);
2887
+ result = operation(database, nestedOperation => runWatchOwnerTransactionOnConnection(
2888
+ runtime, database, options, nestedOperation,
2889
+ ));
2890
+ if (result && typeof result.then === 'function') {
2891
+ throw watchOwnerStoreError('INVALID_OPERATION', 'Watcher authority transactions cannot return a thenable');
2892
+ }
2893
+ execSql(runtime, database, 'COMMIT;');
2894
+ active = false;
2895
+ committed = true;
2896
+ } catch (error) {
2897
+ if (active) rollbackTransaction(runtime, database);
2898
+ transactionError = /database is locked|SQLITE_BUSY/i.test(String(error?.message || ''))
2899
+ ? watchOwnerStoreError('AUTHORITY_UNAVAILABLE', `Watcher authority remained busy after ${timeout}ms`, error)
2900
+ : error;
2901
+ } finally {
2902
+ if (timeoutInstalled) {
2903
+ try {
2904
+ execSql(runtime, database, `PRAGMA busy_timeout=${previousTimeout};`);
2905
+ } catch (error) {
2906
+ restorationError = watchOwnerStoreError('AUTHORITY_UNAVAILABLE', 'Watcher authority timeout restoration failed', error);
2907
+ }
2908
+ }
2909
+ }
2910
+ if (transactionError) throw transactionError;
2911
+ if (restorationError && !committed) throw restorationError;
2912
+ return result;
2913
+ } finally {
2914
+ activeWatchOwnerConnections.delete(database);
2915
+ }
2916
+ }
2917
+
2918
+ function runWatchOwnerTransaction(runtime, databasePath, options, operation) {
2919
+ const validated = assertWatchOwnerDatabasePath(databasePath, options);
2920
+ const database = openWatchOwnerDatabase(runtime, validated);
2921
+ try {
2922
+ return runWatchOwnerTransactionOnConnection(runtime, database, options, operation);
2923
+ } finally {
2924
+ closeDatabase(database);
2925
+ }
2926
+ }
2927
+
2928
+ const WATCH_OWNER_PHASES = new Set(['starting', 'running', 'stop_requested', 'terminal_pending', 'complete', 'blocked']);
2929
+ const WATCH_OWNER_BLOCK_REASONS = new Set([
2930
+ 'legacy_live_pid',
2931
+ 'legacy_conflict',
2932
+ 'legacy_unreadable',
2933
+ 'legacy_lossy',
2934
+ 'legacy_receipt_unverified',
2935
+ ]);
2936
+ const WATCH_OWNER_GENERATION_OPERATIONS = new Set([
2937
+ 'reserveReopened', 'bindRunning', 'heartbeat', 'requestStop', 'recordTerminal',
2938
+ 'completeTerminal', 'abortStarting', 'releaseNonterminal', 'recoverDeadStarting',
2939
+ 'recoverDeadWatcher', 'recheckLegacyBlocked',
2940
+ ]);
2941
+ const WATCH_OWNER_MONOTONIC_OPERATIONS = new Set([
2942
+ 'reserveReopened', 'bindRunning', 'heartbeat', 'requestStop', 'recordTerminal',
2943
+ 'completeTerminal', 'recoverDeadStarting', 'recoverDeadWatcher', 'markLegacyBlocked',
2944
+ 'recheckLegacyBlocked', 'importLegacyStarting', 'importLegacyComplete',
2945
+ ]);
2946
+ const WATCH_OWNER_EVIDENCE_BOUND_OPERATIONS = new Set([
2947
+ 'reserveReopened', 'recordTerminal', 'completeTerminal', 'abortStarting',
2948
+ 'recoverDeadStarting', 'recoverDeadWatcher', 'markLegacyBlocked', 'recheckLegacyBlocked',
2949
+ 'importLegacyStarting', 'importLegacyComplete',
2950
+ ]);
2951
+ const WATCH_OWNER_SNAPSHOT_FIELDS = Object.freeze([
2952
+ 'repo', 'pr', 'version', 'generation', 'phase', 'controller_pid', 'watcher_pid',
2953
+ 'started_at', 'updated_at', 'heartbeat_at', 'terminal_receipt_id', 'block_reason',
2954
+ 'legacy_evidence_hash',
2955
+ ]);
2956
+ const WATCH_GATE_SNAPSHOT_FIELDS = Object.freeze([
2957
+ 'singleton', 'state', 'snapshot_hash', 'conflict_code', 'updated_at',
2958
+ ]);
2959
+ const WATCH_OWNER_MUTATION_FIELDS = Object.freeze([
2960
+ 'repo', 'pr', 'controllerPid', 'watcherPid', 'expectedControllerPid', 'generation',
2961
+ 'terminalReceiptId', 'expectedReceiptId', 'now', 'snapshotHash', 'legacyEvidenceHash',
2962
+ 'blockReason', 'action', 'expectedSnapshot', 'expectedGate',
2963
+ ]);
2964
+ const WATCH_GATE_STATES = new Set(['quarantined', 'conflict', 'complete']);
2965
+ const WATCH_GATE_CONFLICT_CODES = new Set([
2966
+ 'legacy_identity_unmappable',
2967
+ 'legacy_snapshot_changed',
2968
+ 'legacy_owner_conflict',
2969
+ ]);
2970
+ const WATCH_REPOSITORY = /^[a-z0-9_.-]+\/[a-z0-9_.-]+$/;
2971
+ const WATCH_SHA256 = /^[0-9a-f]{64}$/;
2972
+ const WATCH_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
2973
+ const WATCH_OWNER_ENUMERATION_LIMIT = 4_096;
2974
+ const WATCH_OWNER_ENUMERATION_BYTES = 4 * 1024 * 1024;
2975
+
2976
+ function watchUtf8Bytes(value) {
2977
+ return typeof value === 'string' ? Buffer.byteLength(value, 'utf8') : -1;
2978
+ }
2979
+
2980
+ function watchTimestamp(value) {
2981
+ if (typeof value !== 'string' || watchUtf8Bytes(value) !== 24 || !WATCH_TIMESTAMP.test(value)) return false;
2982
+ try {
2983
+ return new Date(value).toISOString() === value;
2984
+ } catch {
2985
+ return false;
2986
+ }
2987
+ }
2988
+
2989
+ function positiveWatchPid(value) {
2990
+ return Number.isSafeInteger(value) && value > 0;
2991
+ }
2992
+
2993
+ function boundedWatchString(value, maxBytes) {
2994
+ const bytes = watchUtf8Bytes(value);
2995
+ return typeof value === 'string' && bytes > 0 && bytes <= maxBytes;
2996
+ }
2997
+
2998
+ function optionalWatchString(value, maxBytes) {
2999
+ return value == null || boundedWatchString(value, maxBytes);
3000
+ }
3001
+
3002
+ function validWatchRepository(value) {
3003
+ return boundedWatchString(value, 256) && WATCH_REPOSITORY.test(value);
3004
+ }
3005
+
3006
+ function validWatchHash(value) {
3007
+ return typeof value === 'string' && WATCH_SHA256.test(value);
3008
+ }
3009
+
3010
+ function validWatchOwnerRow(row) {
3011
+ if (!row || row.version !== 1 || !validWatchRepository(row.repo)
3012
+ || !Number.isSafeInteger(row.pr) || row.pr <= 0
3013
+ || !boundedWatchString(row.generation, 128)
3014
+ || !WATCH_OWNER_PHASES.has(row.phase) || !watchTimestamp(row.started_at)
3015
+ || !watchTimestamp(row.updated_at) || row.updated_at < row.started_at
3016
+ || !optionalWatchString(row.terminal_receipt_id, 256)) return false;
3017
+ const controller = row.controller_pid;
3018
+ const watcher = row.watcher_pid;
3019
+ if ((controller != null && !positiveWatchPid(controller)) || (watcher != null && !positiveWatchPid(watcher))) return false;
3020
+ if (row.heartbeat_at != null && (!watchTimestamp(row.heartbeat_at)
3021
+ || row.heartbeat_at < row.started_at || row.heartbeat_at > row.updated_at)) return false;
3022
+ if (row.legacy_evidence_hash != null && !validWatchHash(row.legacy_evidence_hash)) return false;
3023
+ if (row.phase === 'starting') {
3024
+ return controller != null && watcher == null && row.heartbeat_at == null
3025
+ && row.terminal_receipt_id == null && row.block_reason == null;
3026
+ }
3027
+ if (row.phase === 'running' || row.phase === 'stop_requested') {
3028
+ return controller == null && watcher != null && row.heartbeat_at != null
3029
+ && row.terminal_receipt_id == null && row.block_reason == null;
3030
+ }
3031
+ if (row.phase === 'terminal_pending') {
3032
+ return controller == null && watcher != null && row.heartbeat_at != null
3033
+ && row.terminal_receipt_id != null && row.block_reason == null;
3034
+ }
3035
+ if (row.phase === 'complete') {
3036
+ return controller == null && watcher == null && row.heartbeat_at == null
3037
+ && row.terminal_receipt_id != null && row.block_reason == null;
3038
+ }
3039
+ if (!WATCH_OWNER_BLOCK_REASONS.has(row.block_reason) || row.legacy_evidence_hash == null
3040
+ || controller != null || row.heartbeat_at != null) return false;
3041
+ return row.block_reason === 'legacy_live_pid' ? watcher != null : watcher == null;
3042
+ }
3043
+
3044
+ function validWatchGateRow(row) {
3045
+ if (!row || row.singleton !== 1 || !WATCH_GATE_STATES.has(row.state)
3046
+ || !watchTimestamp(row.updated_at)) return false;
3047
+ if (row.state === 'quarantined') {
3048
+ return (row.snapshot_hash == null || validWatchHash(row.snapshot_hash)) && row.conflict_code == null;
3049
+ }
3050
+ if (!validWatchHash(row.snapshot_hash)) return false;
3051
+ return row.state === 'complete'
3052
+ ? row.conflict_code == null
3053
+ : WATCH_GATE_CONFLICT_CODES.has(row.conflict_code);
3054
+ }
3055
+
3056
+ function copyWatchGateSnapshot(gate) {
3057
+ if (gate === null) return null;
3058
+ try {
3059
+ const copy = Object.fromEntries(WATCH_GATE_SNAPSHOT_FIELDS.map(field => [field, gate[field]]));
3060
+ return validWatchGateRow(copy) ? copy : undefined;
3061
+ } catch {
3062
+ return undefined;
3063
+ }
3064
+ }
3065
+
3066
+ function sameWatchGateSnapshot(current, expected) {
3067
+ if (current == null || expected == null) return current == null && expected == null;
3068
+ return WATCH_GATE_SNAPSHOT_FIELDS.every(field => Object.is(current[field], expected[field]));
3069
+ }
3070
+
3071
+ function readWatchOwnerRow(runtime, database, input) {
3072
+ return allParams(
3073
+ runtime,
3074
+ database,
3075
+ `SELECT * FROM ${WATCH_OWNER_TABLE} WHERE repo = ? AND pr = ?`,
3076
+ [input.repo, input.pr],
3077
+ )[0] || null;
3078
+ }
3079
+
3080
+ function saveWatchOwnerRow(runtime, database, row, insert = false) {
3081
+ const columns = [
3082
+ row.repo, row.pr, row.version, row.generation, row.phase, row.controller_pid,
3083
+ row.watcher_pid, row.started_at, row.updated_at, row.heartbeat_at,
3084
+ row.terminal_receipt_id, row.block_reason, row.legacy_evidence_hash,
3085
+ ];
3086
+ if (insert) {
3087
+ runParams(runtime, database, `INSERT INTO ${WATCH_OWNER_TABLE}
3088
+ (repo, pr, version, generation, phase, controller_pid, watcher_pid, started_at,
3089
+ updated_at, heartbeat_at, terminal_receipt_id, block_reason, legacy_evidence_hash)
3090
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, columns);
3091
+ return;
3092
+ }
3093
+ runParams(runtime, database, `UPDATE ${WATCH_OWNER_TABLE} SET
3094
+ version = ?, generation = ?, phase = ?, controller_pid = ?, watcher_pid = ?,
3095
+ started_at = ?, updated_at = ?, heartbeat_at = ?, terminal_receipt_id = ?,
3096
+ block_reason = ?, legacy_evidence_hash = ? WHERE repo = ? AND pr = ?`, [
3097
+ row.version, row.generation, row.phase, row.controller_pid, row.watcher_pid,
3098
+ row.started_at, row.updated_at, row.heartbeat_at, row.terminal_receipt_id,
3099
+ row.block_reason, row.legacy_evidence_hash, row.repo, row.pr,
3100
+ ]);
3101
+ }
3102
+
3103
+ function deleteWatchOwnerRow(runtime, database, input) {
3104
+ runParams(runtime, database, `DELETE FROM ${WATCH_OWNER_TABLE} WHERE repo = ? AND pr = ?`, [input.repo, input.pr]);
3105
+ }
3106
+
3107
+ function watchOwnerResult(ok, changed, reason, row = null) {
3108
+ return { ok, changed, reason, row };
3109
+ }
3110
+
3111
+ function watchOwnerMismatch(current, input, phases) {
3112
+ if (!current) return 'absent';
3113
+ if (!validWatchOwnerRow(current)) return 'corrupt';
3114
+ if (input.generation != null && current.generation !== input.generation) return 'generation_mismatch';
3115
+ if (phases && ![].concat(phases).includes(current.phase)) return 'phase_mismatch';
3116
+ return null;
3117
+ }
3118
+
3119
+ function watchOwnerUpdatedAtRegresses(operation, current, input) {
3120
+ if (!current || !WATCH_OWNER_MONOTONIC_OPERATIONS.has(operation)) return false;
3121
+ if (operation === 'recheckLegacyBlocked' && input.action === 'release') return false;
3122
+ return [current.updated_at, current.heartbeat_at]
3123
+ .filter(Boolean)
3124
+ .some(timestamp => Date.parse(input.now) < Date.parse(timestamp));
3125
+ }
3126
+
3127
+ function sameWatchOwnerSnapshot(current, expected) {
3128
+ if (current == null || expected == null) return current == null && expected == null;
3129
+ return WATCH_OWNER_SNAPSHOT_FIELDS.every(field => Object.is(current[field], expected[field]));
3130
+ }
3131
+
3132
+ function startingWatchOwnerRow(input, generation = randomUUID()) {
3133
+ return {
3134
+ repo: input.repo,
3135
+ pr: input.pr,
3136
+ version: 1,
3137
+ generation,
3138
+ phase: 'starting',
3139
+ controller_pid: input.controllerPid,
3140
+ watcher_pid: null,
3141
+ started_at: input.now,
3142
+ updated_at: input.now,
3143
+ heartbeat_at: null,
3144
+ terminal_receipt_id: null,
3145
+ block_reason: null,
3146
+ legacy_evidence_hash: null,
3147
+ };
3148
+ }
3149
+
3150
+ function validWatchOwnerIdentity(input) {
3151
+ return validWatchRepository(input?.repo) && Number.isSafeInteger(input.pr) && input.pr > 0;
3152
+ }
3153
+
3154
+ function captureWatchOwnerIdentity(input) {
3155
+ try {
3156
+ return { repo: input?.repo, pr: input?.pr };
3157
+ } catch {
3158
+ return null;
3159
+ }
3160
+ }
3161
+
3162
+ function validWatchOwnerMutationInput(operation, input) {
3163
+ if (!validWatchOwnerIdentity(input)) return false;
3164
+ if (WATCH_OWNER_EVIDENCE_BOUND_OPERATIONS.has(operation)
3165
+ && (!Object.prototype.hasOwnProperty.call(input, 'expectedSnapshot')
3166
+ || input.expectedSnapshot === undefined)) return false;
3167
+ if (!['abortStarting', 'releaseNonterminal'].includes(operation) && !watchTimestamp(input.now)) return false;
3168
+ if (!['controllerPid', 'watcherPid', 'expectedControllerPid'].every(field => (
3169
+ input[field] == null || positiveWatchPid(input[field])
3170
+ ))) return false;
3171
+ if (!['generation', 'terminalReceiptId', 'expectedReceiptId'].every(field => (
3172
+ input[field] == null || boundedWatchString(input[field], field === 'generation' ? 128 : 256)
3173
+ ))) return false;
3174
+ if (WATCH_OWNER_GENERATION_OPERATIONS.has(operation)
3175
+ && !boundedWatchString(input.generation, 128)) return false;
3176
+ if (!['snapshotHash', 'legacyEvidenceHash'].every(field => (
3177
+ input[field] == null || validWatchHash(input[field])
3178
+ ))) return false;
3179
+ if (input.blockReason != null && !WATCH_OWNER_BLOCK_REASONS.has(input.blockReason)) return false;
3180
+ if (operation === 'recheckLegacyBlocked') {
3181
+ return ['release', 'complete'].includes(input.action);
3182
+ }
3183
+ return input.action == null || ['release', 'complete'].includes(input.action);
3184
+ }
3185
+
3186
+ function captureWatchOwnerMutationInput(input) {
3187
+ try {
3188
+ const ownFields = new Set(Object.getOwnPropertyNames(input));
3189
+ const prepared = {};
3190
+ for (const field of WATCH_OWNER_MUTATION_FIELDS) {
3191
+ if (ownFields.has(field)) prepared[field] = input[field];
3192
+ }
3193
+ return prepared;
3194
+ } catch {
3195
+ return null;
3196
+ }
3197
+ }
3198
+
3199
+ function copyWatchOwnerSnapshot(snapshot) {
3200
+ if (snapshot === null) return null;
3201
+ if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) return undefined;
3202
+ try {
3203
+ Object.keys(snapshot);
3204
+ const copy = {};
3205
+ for (const field of WATCH_OWNER_SNAPSHOT_FIELDS) {
3206
+ const value = snapshot[field];
3207
+ if (value !== null && !['number', 'string'].includes(typeof value)) return undefined;
3208
+ copy[field] = value;
3209
+ }
3210
+ return validWatchOwnerRow(copy) ? Object.freeze(copy) : undefined;
3211
+ } catch {
3212
+ return undefined;
3213
+ }
3214
+ }
3215
+
3216
+ function prepareWatchOwnerMutationInput(operation, input) {
3217
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return null;
3218
+ const prepared = captureWatchOwnerMutationInput(input);
3219
+ if (!prepared) return null;
3220
+ if (!validWatchOwnerMutationInput(operation, prepared)) return null;
3221
+ if (Object.prototype.hasOwnProperty.call(prepared, 'expectedSnapshot')) {
3222
+ const expectedSnapshot = copyWatchOwnerSnapshot(prepared.expectedSnapshot);
3223
+ if (expectedSnapshot === undefined) return null;
3224
+ prepared.expectedSnapshot = expectedSnapshot;
3225
+ }
3226
+ if (Object.prototype.hasOwnProperty.call(prepared, 'expectedGate')) {
3227
+ const expectedGate = copyWatchGateSnapshot(prepared.expectedGate);
3228
+ if (expectedGate === undefined) return null;
3229
+ prepared.expectedGate = expectedGate;
3230
+ }
3231
+ return Object.freeze(prepared);
3232
+ }
3233
+
3234
+ function applyWatchOwnerOperation(runtime, databasePath, options, operation, rawInput) {
3235
+ const input = prepareWatchOwnerMutationInput(operation, rawInput);
3236
+ if (!input) return watchOwnerResult(false, false, 'invalid_input');
3237
+ return runWatchOwnerTransaction(runtime, databasePath, options, database => {
3238
+ const current = readWatchOwnerRow(runtime, database, input);
3239
+ if (current && !validWatchOwnerRow(current)) return watchOwnerResult(false, false, 'corrupt', current);
3240
+ if (Object.prototype.hasOwnProperty.call(input, 'expectedSnapshot')
3241
+ && !sameWatchOwnerSnapshot(current, input.expectedSnapshot)) {
3242
+ return watchOwnerResult(false, false, 'stale_evidence', current);
3243
+ }
3244
+ if (Object.prototype.hasOwnProperty.call(input, 'expectedGate')) {
3245
+ const currentGate = allParams(runtime, database,
3246
+ `SELECT * FROM ${WATCH_GATE_TABLE} WHERE singleton = 1`)[0] || null;
3247
+ if (currentGate && !validWatchGateRow(currentGate)) {
3248
+ return watchOwnerResult(false, false, 'corrupt', current);
3249
+ }
3250
+ if (!sameWatchGateSnapshot(currentGate, input.expectedGate)) {
3251
+ return watchOwnerResult(false, false, 'stale_evidence', current);
3252
+ }
3253
+ }
3254
+ if (watchOwnerUpdatedAtRegresses(operation, current, input)) {
3255
+ return watchOwnerResult(false, false, 'stale_evidence', current);
3256
+ }
3257
+ let row;
3258
+ let mismatch;
3259
+ switch (operation) {
3260
+ case 'reserveStarting':
3261
+ if (current) return watchOwnerResult(false, false, 'busy', current);
3262
+ row = startingWatchOwnerRow(input);
3263
+ if (!validWatchOwnerRow(row)) return watchOwnerResult(false, false, 'invalid_transition');
3264
+ saveWatchOwnerRow(runtime, database, row, true);
3265
+ return watchOwnerResult(true, true, 'acquired', row);
3266
+ case 'reserveReopened':
3267
+ mismatch = watchOwnerMismatch(current, input, 'complete');
3268
+ if (mismatch) return watchOwnerResult(false, false, mismatch, current);
3269
+ if (current.terminal_receipt_id !== input.expectedReceiptId) return watchOwnerResult(false, false, 'receipt_mismatch', current);
3270
+ row = { ...startingWatchOwnerRow(input), legacy_evidence_hash: current.legacy_evidence_hash };
3271
+ if (!validWatchOwnerRow(row)) return watchOwnerResult(false, false, 'invalid_transition', current);
3272
+ saveWatchOwnerRow(runtime, database, row);
3273
+ return watchOwnerResult(true, true, 'reopened', row);
3274
+ case 'bindRunning':
3275
+ if (current?.phase === 'running' && current.generation === input.generation
3276
+ && current.watcher_pid === input.watcherPid) return watchOwnerResult(true, false, 'idempotent', current);
3277
+ mismatch = watchOwnerMismatch(current, input, 'starting');
3278
+ if (mismatch) return watchOwnerResult(false, false, mismatch, current);
3279
+ if (current.controller_pid !== input.controllerPid) return watchOwnerResult(false, false, 'controller_pid_mismatch', current);
3280
+ row = { ...current, phase: 'running', controller_pid: null, watcher_pid: input.watcherPid,
3281
+ updated_at: input.now, heartbeat_at: input.now };
3282
+ break;
3283
+ case 'heartbeat':
3284
+ mismatch = watchOwnerMismatch(current, input, ['running', 'stop_requested']);
3285
+ if (mismatch) return watchOwnerResult(false, false, mismatch, current);
3286
+ if (current.watcher_pid !== input.watcherPid) return watchOwnerResult(false, false, 'pid_mismatch', current);
3287
+ if (Date.parse(input.now) < Math.max(Date.parse(current.updated_at), Date.parse(current.heartbeat_at))) {
3288
+ return watchOwnerResult(false, false, 'stale_evidence', current);
3289
+ }
3290
+ row = { ...current, updated_at: input.now, heartbeat_at: input.now };
3291
+ break;
3292
+ case 'requestStop':
3293
+ if (current?.phase === 'stop_requested' && current.generation === input.generation
3294
+ && current.watcher_pid === input.watcherPid) return watchOwnerResult(true, false, 'idempotent', current);
3295
+ mismatch = watchOwnerMismatch(current, input, 'running');
3296
+ if (mismatch) return watchOwnerResult(false, false, mismatch, current);
3297
+ if (current.watcher_pid !== input.watcherPid) return watchOwnerResult(false, false, 'pid_mismatch', current);
3298
+ row = { ...current, phase: 'stop_requested', updated_at: input.now };
3299
+ break;
3300
+ case 'recordTerminal':
3301
+ if (current?.phase === 'terminal_pending' && current.generation === input.generation
3302
+ && current.watcher_pid === input.watcherPid
3303
+ && current.terminal_receipt_id === input.terminalReceiptId) return watchOwnerResult(true, false, 'idempotent', current);
3304
+ mismatch = watchOwnerMismatch(current, input, ['running', 'stop_requested']);
3305
+ if (mismatch) return watchOwnerResult(false, false, mismatch, current);
3306
+ if (current.watcher_pid !== input.watcherPid) return watchOwnerResult(false, false, 'pid_mismatch', current);
3307
+ row = { ...current, phase: 'terminal_pending', terminal_receipt_id: input.terminalReceiptId, updated_at: input.now };
3308
+ break;
3309
+ case 'completeTerminal':
3310
+ if (current?.phase === 'complete' && current.generation === input.generation
3311
+ && current.terminal_receipt_id === input.terminalReceiptId) return watchOwnerResult(true, false, 'idempotent', current);
3312
+ mismatch = watchOwnerMismatch(current, input, 'terminal_pending');
3313
+ if (mismatch) return watchOwnerResult(false, false, mismatch, current);
3314
+ if (current.watcher_pid !== input.watcherPid) return watchOwnerResult(false, false, 'pid_mismatch', current);
3315
+ if (current.terminal_receipt_id !== input.terminalReceiptId) return watchOwnerResult(false, false, 'receipt_mismatch', current);
3316
+ row = { ...current, phase: 'complete', watcher_pid: null, heartbeat_at: null, updated_at: input.now };
3317
+ break;
3318
+ case 'abortStarting':
3319
+ mismatch = watchOwnerMismatch(current, input, 'starting');
3320
+ if (mismatch) return watchOwnerResult(false, false, mismatch, current);
3321
+ if (current.controller_pid !== input.controllerPid) return watchOwnerResult(false, false, 'controller_pid_mismatch', current);
3322
+ deleteWatchOwnerRow(runtime, database, input);
3323
+ return watchOwnerResult(true, true, 'aborted');
3324
+ case 'releaseNonterminal':
3325
+ mismatch = watchOwnerMismatch(current, input, 'stop_requested');
3326
+ if (mismatch) return watchOwnerResult(false, false, mismatch, current);
3327
+ if (current.watcher_pid !== input.watcherPid) return watchOwnerResult(false, false, 'pid_mismatch', current);
3328
+ deleteWatchOwnerRow(runtime, database, input);
3329
+ return watchOwnerResult(true, true, 'released');
3330
+ case 'recoverDeadStarting':
3331
+ mismatch = watchOwnerMismatch(current, input, 'starting');
3332
+ if (mismatch) return watchOwnerResult(false, false, mismatch, current);
3333
+ if (current.controller_pid !== input.expectedControllerPid) return watchOwnerResult(false, false, 'controller_pid_mismatch', current);
3334
+ row = { ...startingWatchOwnerRow(input), legacy_evidence_hash: current.legacy_evidence_hash };
3335
+ break;
3336
+ case 'recoverDeadWatcher':
3337
+ // `stop_requested` is a nonterminal watcher-owned phase exactly like
3338
+ // `running`; a dead watcher in either state recovers into a fresh
3339
+ // `starting` row.
3340
+ mismatch = watchOwnerMismatch(current, input, ['running', 'stop_requested']);
3341
+ if (mismatch) return watchOwnerResult(false, false, mismatch, current);
3342
+ if (current.watcher_pid !== input.watcherPid) return watchOwnerResult(false, false, 'pid_mismatch', current);
3343
+ row = { ...startingWatchOwnerRow(input), legacy_evidence_hash: current.legacy_evidence_hash };
3344
+ break;
3345
+ case 'importLegacyStarting': {
3346
+ const gate = allParams(runtime, database,
3347
+ `SELECT * FROM ${WATCH_GATE_TABLE} WHERE singleton = 1`)[0] || null;
3348
+ if (!validWatchGateRow(gate)) return watchOwnerResult(false, false, gate ? 'corrupt' : 'gate_mismatch', current);
3349
+ if (gate.state !== 'quarantined' || gate.snapshot_hash !== input.snapshotHash) {
3350
+ return watchOwnerResult(false, false, 'gate_mismatch', current);
3351
+ }
3352
+ if (current) {
3353
+ const same = validWatchOwnerRow(current) && current.phase === 'starting'
3354
+ && current.controller_pid === input.controllerPid
3355
+ && current.legacy_evidence_hash === input.legacyEvidenceHash;
3356
+ return watchOwnerResult(same, false, same ? 'idempotent' : 'owner_conflict', current);
3357
+ }
3358
+ row = { ...startingWatchOwnerRow(input), legacy_evidence_hash: input.legacyEvidenceHash };
3359
+ if (!validWatchOwnerRow(row)) return watchOwnerResult(false, false, 'invalid_transition');
3360
+ saveWatchOwnerRow(runtime, database, row, true);
3361
+ return watchOwnerResult(true, true, 'imported', row);
3362
+ }
3363
+ case 'markLegacyBlocked':
3364
+ {
3365
+ const gate = allParams(runtime, database,
3366
+ `SELECT * FROM ${WATCH_GATE_TABLE} WHERE singleton = 1`)[0] || null;
3367
+ if (!validWatchGateRow(gate)) return watchOwnerResult(false, false, gate ? 'corrupt' : 'gate_mismatch', current);
3368
+ if (gate.state !== 'quarantined' || gate.snapshot_hash !== input.snapshotHash) {
3369
+ return watchOwnerResult(false, false, 'gate_mismatch', current);
3370
+ }
3371
+ }
3372
+ if (current) {
3373
+ const same = validWatchOwnerRow(current) && current.phase === 'blocked'
3374
+ && current.controller_pid == null && current.watcher_pid === input.watcherPid
3375
+ && current.terminal_receipt_id === input.terminalReceiptId
3376
+ && current.block_reason === input.blockReason
3377
+ && current.legacy_evidence_hash === input.legacyEvidenceHash;
3378
+ return watchOwnerResult(same, false, same ? 'idempotent' : 'owner_conflict', current);
3379
+ }
3380
+ row = { ...startingWatchOwnerRow(input), phase: 'blocked', controller_pid: null,
3381
+ watcher_pid: input.watcherPid, terminal_receipt_id: input.terminalReceiptId,
3382
+ block_reason: input.blockReason, legacy_evidence_hash: input.legacyEvidenceHash };
3383
+ if (!validWatchOwnerRow(row)) return watchOwnerResult(false, false, 'invalid_transition');
3384
+ saveWatchOwnerRow(runtime, database, row, true);
3385
+ return watchOwnerResult(true, true, 'blocked', row);
3386
+ case 'recheckLegacyBlocked':
3387
+ mismatch = watchOwnerMismatch(current, input, 'blocked');
3388
+ if (mismatch) return watchOwnerResult(false, false, mismatch, current);
3389
+ if (current.legacy_evidence_hash !== input.legacyEvidenceHash) return watchOwnerResult(false, false, 'evidence_mismatch', current);
3390
+ if ((current.block_reason === 'legacy_live_pid' || input.watcherPid != null)
3391
+ && current.watcher_pid !== input.watcherPid) {
3392
+ return watchOwnerResult(false, false, 'pid_mismatch', current);
3393
+ }
3394
+ if (input.action === 'release') {
3395
+ if (current.block_reason !== 'legacy_live_pid') {
3396
+ return watchOwnerResult(false, false, 'invalid_transition', current);
3397
+ }
3398
+ deleteWatchOwnerRow(runtime, database, input);
3399
+ return watchOwnerResult(true, true, 'released');
3400
+ }
3401
+ row = { ...current, phase: 'complete', watcher_pid: null, heartbeat_at: null,
3402
+ terminal_receipt_id: input.terminalReceiptId, block_reason: null, updated_at: input.now };
3403
+ break;
3404
+ case 'importLegacyComplete': {
3405
+ const gate = allParams(runtime, database,
3406
+ `SELECT * FROM ${WATCH_GATE_TABLE} WHERE singleton = 1`)[0] || null;
3407
+ if (!validWatchGateRow(gate)) return watchOwnerResult(false, false, gate ? 'corrupt' : 'gate_mismatch', current);
3408
+ if (gate.state !== 'quarantined' || gate.snapshot_hash !== input.snapshotHash) {
3409
+ return watchOwnerResult(false, false, 'gate_mismatch', current);
3410
+ }
3411
+ if (current) {
3412
+ const same = validWatchOwnerRow(current) && current.phase === 'complete'
3413
+ && current.legacy_evidence_hash === input.legacyEvidenceHash
3414
+ && current.terminal_receipt_id === input.terminalReceiptId;
3415
+ return watchOwnerResult(same, false, same ? 'idempotent' : 'owner_conflict', current);
3416
+ }
3417
+ row = { ...startingWatchOwnerRow(input), phase: 'complete', controller_pid: null,
3418
+ terminal_receipt_id: input.terminalReceiptId, legacy_evidence_hash: input.legacyEvidenceHash };
3419
+ if (!validWatchOwnerRow(row)) return watchOwnerResult(false, false, 'invalid_transition');
3420
+ saveWatchOwnerRow(runtime, database, row, true);
3421
+ return watchOwnerResult(true, true, 'imported', row);
3422
+ }
3423
+ default:
3424
+ throw watchOwnerStoreError('INVALID_OPERATION', 'Unknown watcher authority operation');
3425
+ }
3426
+ if (!validWatchOwnerRow(row)) return watchOwnerResult(false, false, 'invalid_transition', current);
3427
+ saveWatchOwnerRow(runtime, database, row);
3428
+ return watchOwnerResult(true, true, {
3429
+ bindRunning: 'bound',
3430
+ heartbeat: 'heartbeat',
3431
+ requestStop: 'stop_requested',
3432
+ recordTerminal: 'terminal_pending',
3433
+ completeTerminal: 'complete',
3434
+ recoverDeadStarting: 'recovered',
3435
+ recoverDeadWatcher: 'recovered',
3436
+ recheckLegacyBlocked: 'complete',
3437
+ }[operation], row);
3438
+ });
3439
+ }
3440
+
3441
+ function readWatchOwner(runtime, databasePath, options, input) {
3442
+ const identity = captureWatchOwnerIdentity(input);
3443
+ if (!identity || !validWatchOwnerIdentity(identity)) {
3444
+ return watchOwnerResult(false, false, 'invalid_input');
3445
+ }
3446
+ return runWatchOwnerTransaction(runtime, databasePath, options, database => {
3447
+ const row = readWatchOwnerRow(runtime, database, identity);
3448
+ if (!row) return watchOwnerResult(true, false, 'absent');
3449
+ return validWatchOwnerRow(row)
3450
+ ? watchOwnerResult(true, false, 'read', row)
3451
+ : watchOwnerResult(false, false, 'corrupt', row);
3452
+ });
3453
+ }
3454
+
3455
+ function listWatchOwners(runtime, databasePath, options) {
3456
+ return runWatchOwnerTransaction(runtime, databasePath, options, database => {
3457
+ const rows = allParams(runtime, database,
3458
+ `SELECT * FROM ${WATCH_OWNER_TABLE} ORDER BY repo, pr LIMIT ?`,
3459
+ [WATCH_OWNER_ENUMERATION_LIMIT + 1]);
3460
+ if (rows.length > WATCH_OWNER_ENUMERATION_LIMIT) {
3461
+ return { ok: false, changed: false, reason: 'enumeration_overflow', rows: [] };
3462
+ }
3463
+ let bytes = 0;
3464
+ for (const row of rows) {
3465
+ if (!validWatchOwnerRow(row)) return { ok: false, changed: false, reason: 'corrupt', rows: [] };
3466
+ bytes += Buffer.byteLength(JSON.stringify(row), 'utf8');
3467
+ if (bytes > WATCH_OWNER_ENUMERATION_BYTES) {
3468
+ return { ok: false, changed: false, reason: 'enumeration_overflow', rows: [] };
3469
+ }
3470
+ }
3471
+ return { ok: true, changed: false, reason: 'read', rows };
3472
+ });
3473
+ }
3474
+
3475
+ function readWatchGate(runtime, databasePath, options) {
3476
+ return runWatchOwnerTransaction(runtime, databasePath, options, database => {
3477
+ const gate = allParams(runtime, database,
3478
+ `SELECT * FROM ${WATCH_GATE_TABLE} WHERE singleton = 1`)[0] || null;
3479
+ if (!gate) return { ok: false, changed: false, reason: 'absent', gate: null };
3480
+ return validWatchGateRow(gate)
3481
+ ? { ok: true, changed: false, reason: 'read', gate }
3482
+ : { ok: false, changed: false, reason: 'corrupt', gate };
3483
+ });
3484
+ }
3485
+
3486
+ function validWatchGateMutationInput(operation, input) {
3487
+ if (!input || !watchTimestamp(input.now)) return false;
3488
+ if (operation === 'publishQuarantine') return true;
3489
+ if (operation === 'retryConflict') {
3490
+ return validWatchHash(input.expectedSnapshotHash)
3491
+ && WATCH_GATE_CONFLICT_CODES.has(input.expectedConflictCode)
3492
+ && validWatchHash(input.replacementSnapshotHash)
3493
+ && input.replacementSnapshotHash !== input.expectedSnapshotHash;
3494
+ }
3495
+ if (!validWatchHash(input.snapshotHash)) return false;
3496
+ return operation !== 'publishConflict' || WATCH_GATE_CONFLICT_CODES.has(input.conflictCode);
3497
+ }
3498
+
3499
+ function captureWatchGateMutationInput(operation, input) {
3500
+ if (!input) return null;
3501
+ try {
3502
+ const captured = { now: input.now };
3503
+ if (Object.prototype.hasOwnProperty.call(input, 'expectedGate')) {
3504
+ const expectedGate = copyWatchGateSnapshot(input.expectedGate);
3505
+ if (expectedGate === undefined) return null;
3506
+ captured.expectedGate = expectedGate;
3507
+ }
3508
+ if (operation === 'retryConflict') {
3509
+ captured.expectedSnapshotHash = input.expectedSnapshotHash;
3510
+ captured.expectedConflictCode = input.expectedConflictCode;
3511
+ captured.replacementSnapshotHash = input.replacementSnapshotHash;
3512
+ } else {
3513
+ if (operation !== 'publishQuarantine') captured.snapshotHash = input.snapshotHash;
3514
+ if (operation === 'publishConflict') captured.conflictCode = input.conflictCode;
3515
+ }
3516
+ return Object.freeze(captured);
3517
+ } catch {
3518
+ return null;
3519
+ }
3520
+ }
3521
+
3522
+ function applyWatchGateOperation(runtime, databasePath, options, operation, input) {
3523
+ input = captureWatchGateMutationInput(operation, input);
3524
+ if (!validWatchGateMutationInput(operation, input)) {
3525
+ return { ok: false, changed: false, reason: 'invalid_input', gate: null };
3526
+ }
3527
+ return runWatchOwnerTransaction(runtime, databasePath, options, database => {
3528
+ const current = allParams(runtime, database,
3529
+ `SELECT * FROM ${WATCH_GATE_TABLE} WHERE singleton = 1`)[0] || null;
3530
+ if (current && !validWatchGateRow(current)) return { ok: false, changed: false, reason: 'corrupt', gate: current };
3531
+ if (Object.prototype.hasOwnProperty.call(input, 'expectedGate')
3532
+ && !sameWatchGateSnapshot(current, input.expectedGate)) {
3533
+ return { ok: false, changed: false, reason: 'stale_evidence', gate: current };
3534
+ }
3535
+ if (current && input.now < current.updated_at) {
3536
+ return { ok: false, changed: false, reason: 'stale_evidence', gate: current };
3537
+ }
3538
+ if (operation === 'publishQuarantine') {
3539
+ if (current) return { ok: current.state === 'quarantined', changed: false,
3540
+ reason: current.state === 'quarantined' ? 'idempotent' : 'gate_conflict', gate: current };
3541
+ runParams(runtime, database, `INSERT INTO ${WATCH_GATE_TABLE}
3542
+ (singleton, state, snapshot_hash, conflict_code, updated_at) VALUES (1, 'quarantined', NULL, NULL, ?)`, [input.now]);
3543
+ return { ok: true, changed: true, reason: 'quarantined', gate: {
3544
+ singleton: 1, state: 'quarantined', snapshot_hash: null, conflict_code: null, updated_at: input.now,
3545
+ } };
3546
+ }
3547
+ if (!current) {
3548
+ return { ok: false, changed: false, reason: 'absent', gate: null };
3549
+ }
3550
+ if (operation === 'bindSnapshot') {
3551
+ if (current.state !== 'quarantined') return { ok: false, changed: false, reason: 'phase_mismatch', gate: current };
3552
+ if (current.snapshot_hash === input.snapshotHash) return { ok: true, changed: false, reason: 'idempotent', gate: current };
3553
+ if (current.snapshot_hash != null) return { ok: false, changed: false, reason: 'snapshot_mismatch', gate: current };
3554
+ runParams(runtime, database, `UPDATE ${WATCH_GATE_TABLE} SET snapshot_hash = ?, updated_at = ? WHERE singleton = 1`,
3555
+ [input.snapshotHash, input.now]);
3556
+ return { ok: true, changed: true, reason: 'bound', gate: { ...current, snapshot_hash: input.snapshotHash, updated_at: input.now } };
3557
+ }
3558
+ if (operation === 'publishConflict') {
3559
+ if (!WATCH_SHA256.test(input.snapshotHash) || !WATCH_GATE_CONFLICT_CODES.has(input.conflictCode)) {
3560
+ return { ok: false, changed: false, reason: 'invalid_conflict', gate: current };
3561
+ }
3562
+ if (current.state === 'conflict') {
3563
+ const idempotent = current.snapshot_hash === input.snapshotHash
3564
+ && current.conflict_code === input.conflictCode;
3565
+ return { ok: idempotent, changed: false,
3566
+ reason: idempotent ? 'idempotent' : 'conflict_mismatch', gate: current };
3567
+ }
3568
+ if (current.state !== 'quarantined') return { ok: false, changed: false, reason: 'phase_mismatch', gate: current };
3569
+ if (current.snapshot_hash != null && current.snapshot_hash !== input.snapshotHash) {
3570
+ return { ok: false, changed: false, reason: 'snapshot_mismatch', gate: current };
3571
+ }
3572
+ runParams(runtime, database, `UPDATE ${WATCH_GATE_TABLE} SET state = 'conflict', snapshot_hash = ?, conflict_code = ?, updated_at = ? WHERE singleton = 1`,
3573
+ [input.snapshotHash, input.conflictCode, input.now]);
3574
+ return { ok: true, changed: true, reason: 'conflict', gate: { ...current, state: 'conflict', snapshot_hash: input.snapshotHash,
3575
+ conflict_code: input.conflictCode, updated_at: input.now } };
3576
+ }
3577
+ if (operation === 'retryConflict') {
3578
+ if (current.state !== 'conflict') return { ok: false, changed: false, reason: 'phase_mismatch', gate: current };
3579
+ if (current.snapshot_hash !== input.expectedSnapshotHash
3580
+ || current.conflict_code !== input.expectedConflictCode) {
3581
+ return { ok: false, changed: false, reason: 'conflict_mismatch', gate: current };
3582
+ }
3583
+ runParams(runtime, database, `UPDATE ${WATCH_GATE_TABLE}
3584
+ SET state = 'quarantined', snapshot_hash = ?, conflict_code = NULL, updated_at = ? WHERE singleton = 1`,
3585
+ [input.replacementSnapshotHash, input.now]);
3586
+ return { ok: true, changed: true, reason: 'retry_bound', gate: {
3587
+ ...current, state: 'quarantined', snapshot_hash: input.replacementSnapshotHash,
3588
+ conflict_code: null, updated_at: input.now,
3589
+ } };
3590
+ }
3591
+ if (operation === 'completeMigration') {
3592
+ if (current.state === 'complete' && current.snapshot_hash === input.snapshotHash) {
3593
+ return { ok: true, changed: false, reason: 'idempotent', gate: current };
3594
+ }
3595
+ if (current.state !== 'quarantined' || current.snapshot_hash !== input.snapshotHash) {
3596
+ return { ok: false, changed: false, reason: 'snapshot_mismatch', gate: current };
3597
+ }
3598
+ runParams(runtime, database, `UPDATE ${WATCH_GATE_TABLE} SET state = 'complete', updated_at = ? WHERE singleton = 1`, [input.now]);
3599
+ return { ok: true, changed: true, reason: 'complete', gate: { ...current, state: 'complete', updated_at: input.now } };
3600
+ }
3601
+ throw watchOwnerStoreError('INVALID_OPERATION', 'Unknown watcher migration-gate operation');
3602
+ });
3603
+ }
3604
+
3605
+ function rollbackTransaction(runtime, db) {
3606
+ try {
3607
+ execSql(runtime, db, 'ROLLBACK;');
3608
+ } catch {
3609
+ // Preserve the original failure when SQLite has already closed the transaction.
3610
+ }
3611
+ }
3612
+
3613
+ const MONITOR_HASH = /^[0-9a-f]{64}$/;
3614
+ const MONITOR_TARGET = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
3615
+ const MONITOR_SECRET_PATTERNS = [
3616
+ /gh[pousr]_[a-z0-9]{20,}/i,
3617
+ /github_pat_[a-z0-9_]{20,}/i,
3618
+ /sk_(?:live|test)_[a-z0-9]{16,}/i,
3619
+ /sk-[a-z0-9]{16,}/i,
3620
+ /AKIA[0-9A-Z]{16}/i,
3621
+ /(?:api[_-]?key|token|secret|password)\s*[:=]\s*\S{8,}/i,
3622
+ ];
3623
+ const MONITOR_MAX_TARGETS = 32;
3624
+ const MONITOR_MAX_TARGET_LENGTH = 128;
3625
+ const MONITOR_MAX_ENVELOPE_BYTES = 16_384;
3626
+
3627
+ const MONITOR_MAX_ENVELOPE_DEPTH = 8;
3628
+ const MONITOR_MAX_ENVELOPE_ITEMS = 128;
3629
+ const MONITOR_MAX_ENVELOPE_PROPERTIES = 64;
3630
+ const MONITOR_MAX_ENVELOPE_NODES = 1_024;
3631
+ const MONITOR_DEFAULT_READ_LIMIT = 128;
3632
+ const MONITOR_MAX_READ_LIMIT = 4_096;
3633
+
3634
+ function monitorStoreError(code, message, cause) {
3635
+ const error = new Error(message, cause ? { cause } : undefined);
3636
+ error.code = code;
3637
+ return error;
3638
+ }
3639
+
3640
+ function containsMonitorSecret(value) {
3641
+ return MONITOR_SECRET_PATTERNS.some(pattern => pattern.test(value));
3642
+ }
3643
+
3644
+ const MONITOR_PRIVATE_PATH_ROOTS = ['users', 'home', 'root'];
3645
+ const MONITOR_MAX_PRIVATE_SCAN_LENGTH = MONITOR_MAX_ENVELOPE_BYTES;
3646
+
3647
+ function hasNonWhitespacePathSegment(segment) {
3648
+ return Boolean(segment && segment.trim());
3649
+ }
3650
+
3651
+ function containsMonitorPrivatePath(value) {
3652
+ if (typeof value !== 'string') return false;
3653
+ if (value.length > MONITOR_MAX_PRIVATE_SCAN_LENGTH) return true;
3654
+ const normalized = value.replaceAll('\\', '/').toLowerCase();
3655
+ for (const root of MONITOR_PRIVATE_PATH_ROOTS) {
3656
+ const marker = `/${root}/`;
3657
+ let offset = 0;
3658
+ while (true) {
3659
+ const index = normalized.indexOf(marker, offset);
3660
+ if (index < 0) break;
3661
+ const segment = normalized.slice(index + marker.length).split('/')[0];
3662
+ if (hasNonWhitespacePathSegment(segment)) return true;
3663
+ offset = index + marker.length;
3664
+ }
3665
+ }
3666
+ for (let code = 97; code <= 122; code += 1) {
3667
+ const marker = `${String.fromCharCode(code)}:/users/`;
3668
+ const index = normalized.indexOf(marker);
3669
+ if (index >= 0 && hasNonWhitespacePathSegment(
3670
+ normalized.slice(index + marker.length).split('/')[0],
3671
+ )) return true;
3672
+ }
3673
+ return false;
3674
+ }
3675
+
3676
+ function invalidMonitorPlainData() {
3677
+ throw new Error('monitor envelope must contain only bounded plain JSON data');
3678
+ }
3679
+
3680
+ function cloneMonitorArray(value, descriptors, keys, state, depth) {
3681
+ const length = descriptors.length?.value;
3682
+ if (!Number.isInteger(length) || length < 0 || length > MONITOR_MAX_ENVELOPE_ITEMS
3683
+ || keys.length !== length + 1) {
3684
+ invalidMonitorPlainData();
3685
+ }
3686
+ const clone = [];
3687
+ for (let index = 0; index < length; index += 1) {
3688
+ const descriptor = descriptors[String(index)];
3689
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) invalidMonitorPlainData();
3690
+ clone.push(cloneMonitorPlainData(descriptor.value, state, depth + 1));
3691
+ }
3692
+ return clone;
3693
+ }
3694
+
3695
+ function cloneMonitorObject(descriptors, keys, state, depth) {
3696
+ const clone = Object.create(null);
3697
+ for (const key of keys) {
3698
+ const descriptor = descriptors[key];
3699
+ if (!descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) invalidMonitorPlainData();
3700
+ clone[key] = cloneMonitorPlainData(descriptor.value, state, depth + 1);
3701
+ }
3702
+ return clone;
3703
+ }
3704
+
3705
+ function cloneMonitorPlainData(value, state, depth = 0) {
3706
+ const traversal = state || { ancestors: new WeakSet(), nodes: 0 };
3707
+ traversal.nodes += 1;
3708
+ if (traversal.nodes > MONITOR_MAX_ENVELOPE_NODES || depth > MONITOR_MAX_ENVELOPE_DEPTH) {
3709
+ invalidMonitorPlainData();
3710
+ }
3711
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
3712
+ if (typeof value === 'number') {
3713
+ if (!Number.isFinite(value)) invalidMonitorPlainData();
3714
+ return value;
3715
+ }
3716
+ if (typeof value !== 'object' || isProxy(value) || traversal.ancestors.has(value)) {
3717
+ invalidMonitorPlainData();
3718
+ }
3719
+
3720
+ let prototype;
3721
+ let descriptors;
3722
+ try {
3723
+ prototype = Object.getPrototypeOf(value);
3724
+ descriptors = Object.getOwnPropertyDescriptors(value);
3725
+ } catch {
3726
+ invalidMonitorPlainData();
3727
+ }
3728
+ const keys = Reflect.ownKeys(descriptors);
3729
+ if (keys.some(key => typeof key !== 'string' || key === 'toJSON')) invalidMonitorPlainData();
3730
+
3731
+ traversal.ancestors.add(value);
3732
+ try {
3733
+ if (Array.isArray(value)) {
3734
+ if (prototype !== Array.prototype) invalidMonitorPlainData();
3735
+ return cloneMonitorArray(value, descriptors, keys, traversal, depth);
3736
+ }
3737
+
3738
+ if ((prototype !== Object.prototype && prototype !== null)
3739
+ || keys.length > MONITOR_MAX_ENVELOPE_PROPERTIES) {
3740
+ invalidMonitorPlainData();
3741
+ }
3742
+ return cloneMonitorObject(descriptors, keys, traversal, depth);
3743
+ } finally {
3744
+ traversal.ancestors.delete(value);
3745
+ }
3746
+ }
3747
+
3748
+ function containsPrivateMonitorData(value) {
3749
+ if (typeof value === 'string') {
3750
+ return containsMonitorSecret(value) || containsMonitorPrivatePath(value);
3751
+ }
3752
+ if (!value || typeof value !== 'object') return false;
3753
+ return Object.entries(value).some(([key, nestedValue]) => (
3754
+ containsMonitorSecret(key)
3755
+ || containsMonitorPrivatePath(key)
3756
+ || containsPrivateMonitorData(nestedValue)
3757
+ ));
3758
+ }
3759
+
3760
+ function assertMonitorEnvelope(envelope, schemaId) {
3761
+ const plainEnvelope = cloneMonitorPlainData(envelope);
3762
+ const serialized = JSON.stringify(plainEnvelope);
3763
+ const safeEnvelope = JSON.parse(serialized);
3764
+ if (containsPrivateMonitorData(safeEnvelope)) {
3765
+ throw new Error(`private content rejected from ${schemaId}`);
3766
+ }
3767
+ const payload = safeEnvelope?.payload;
3768
+ const validCommon = safeEnvelope?.schema_id === schemaId
3769
+ && MONITOR_HASH.test(safeEnvelope?.content_hash)
3770
+ && typeof safeEnvelope?.created_at === 'string'
3771
+ && payload && typeof payload === 'object' && !Array.isArray(payload);
3772
+ let validPayload = false;
3773
+ if (schemaId === 'forge.memory.monitor-event.v1') {
3774
+ validPayload = typeof payload?.monitor_id === 'string' && payload.monitor_id.length > 0
3775
+ && typeof payload?.event_id === 'string' && payload.event_id.length > 0
3776
+ && Number.isInteger(payload?.sequence) && payload.sequence >= 0;
3777
+ } else if (schemaId === 'forge.memory.delivery-receipt.v1') {
3778
+ validPayload = typeof payload?.event_id === 'string' && payload.event_id.length > 0
3779
+ && Number.isInteger(payload?.attempt) && payload.attempt > 0;
3780
+ } else if (schemaId === 'forge.memory.monitor-receipt.v1') {
3781
+ validPayload = typeof payload?.monitor_id === 'string' && payload.monitor_id.length > 0
3782
+ && Number.isInteger(payload?.last_sequence) && payload.last_sequence >= 0;
3783
+ }
3784
+ if (!validCommon || !validPayload || Buffer.byteLength(serialized, 'utf8') > MONITOR_MAX_ENVELOPE_BYTES) {
3785
+ throw new Error(`invalid ${schemaId.replace('forge.memory.', '').replace('.v1', '').replaceAll('-', ' ')} envelope`);
3786
+ }
3787
+ return safeEnvelope;
3788
+ }
3789
+
3790
+ function assertMonitorTarget(target) {
3791
+ if (typeof target !== 'string' || target.length === 0 || target.length > MONITOR_MAX_TARGET_LENGTH
3792
+ || !MONITOR_TARGET.test(target) || containsMonitorSecret(target) || containsMonitorPrivatePath(target)) {
3793
+ throw new Error('invalid or private monitor delivery target');
3794
+ }
3795
+ return target;
3796
+ }
3797
+
3798
+ function normalizeMonitorTargets(targets) {
3799
+ if (!Array.isArray(targets) || targets.length === 0 || targets.length > MONITOR_MAX_TARGETS) {
3800
+ throw new Error(`monitor delivery targets must contain 1-${MONITOR_MAX_TARGETS} entries`);
3801
+ }
3802
+ return [...new Set(targets.map(assertMonitorTarget))];
3803
+ }
3804
+
3805
+ function compareMonitorTargets(left, right) {
3806
+ if (left < right) return -1;
3807
+ if (left > right) return 1;
3808
+ return 0;
3809
+ }
3810
+
3811
+ function assertMonitorWriterEnabled(runtime, db, config = {}) {
3812
+ if (config.monitorDurabilityEnabled === false) {
3813
+ throw monitorStoreError('MONITOR_UNAVAILABLE', 'Monitor durability writers are disabled; retained evidence remains readable');
3814
+ }
3815
+ let row;
3816
+ try {
3817
+ row = allParams(runtime, db,
3818
+ 'SELECT enabled FROM memory_monitor_writer_state WHERE singleton = 1')[0];
3819
+ } catch (error) {
3820
+ if (/no such table/i.test(String(error?.message || ''))) {
3821
+ throw monitorStoreError('MONITOR_UNAVAILABLE', 'Monitor durability schema is not initialized; run Kernel migrations', error);
3822
+ }
3823
+ throw error;
3824
+ }
3825
+ if (Number(row?.enabled) !== 1) {
3826
+ throw monitorStoreError('MONITOR_UNAVAILABLE', 'Monitor durability writers are disabled; retained evidence remains readable');
3827
+ }
3828
+ }
3829
+
3830
+ function monitorBusyTimeout(config = {}) {
3831
+ const requested = Number(config.monitorBusyTimeoutMs);
3832
+ if (!Number.isFinite(requested)) return 1_000;
3833
+ return Math.max(0, Math.min(5_000, Math.floor(requested)));
3834
+ }
3835
+
3836
+ function runMonitorTransaction(runtime, db, config, operation) {
3837
+ const timeout = monitorBusyTimeout(config);
3838
+ const previousTimeout = Number(queryOne(runtime, db, 'PRAGMA busy_timeout;').timeout) || 0;
3839
+ execSql(runtime, db, `PRAGMA busy_timeout=${timeout};`);
3840
+ let active = false;
3841
+ try {
3842
+ execSql(runtime, db, 'BEGIN IMMEDIATE;');
3843
+ active = true;
3844
+ const result = operation();
3845
+ execSql(runtime, db, 'COMMIT;');
3846
+ active = false;
3847
+ return result;
3848
+ } catch (error) {
3849
+ if (active) rollbackTransaction(runtime, db);
3850
+ if (/database is locked|SQLITE_BUSY/i.test(String(error?.message || ''))) {
3851
+ throw monitorStoreError('MONITOR_UNAVAILABLE', `Monitor durability write remained busy after ${timeout}ms`, error);
3852
+ }
3853
+ throw error;
3854
+ } finally {
3855
+ execSql(runtime, db, `PRAGMA busy_timeout=${previousTimeout};`);
3856
+ }
3857
+ }
3858
+
3859
+ function monitorEventRow(envelope) {
3860
+ const payload = envelope.payload || {};
3861
+ return {
3862
+ event_id: payload.event_id,
3863
+ monitor_id: payload.monitor_id,
3864
+ sequence: payload.sequence,
3865
+ content_hash: envelope.content_hash,
3866
+ envelope_json: JSON.stringify(envelope),
3867
+ artifact_digest: payload.artifact_digest ?? null,
3868
+ created_at: envelope.created_at,
3869
+ };
3870
+ }
3871
+
3872
+ function appendMonitorEventRow(runtime, db, envelope, targets, config) {
3873
+ const row = monitorEventRow(envelope);
3874
+ return runMonitorTransaction(runtime, db, config, () => {
3875
+ assertMonitorWriterEnabled(runtime, db, config);
3876
+ const existingById = allParams(runtime, db,
3877
+ 'SELECT event_id, monitor_id, sequence, content_hash FROM memory_monitor_events WHERE event_id = ?', [row.event_id])[0];
3878
+ const existingBySequence = allParams(runtime, db,
3879
+ 'SELECT event_id, monitor_id, sequence, content_hash FROM memory_monitor_events WHERE monitor_id = ? AND sequence = ?', [row.monitor_id, row.sequence])[0];
3880
+ const existing = existingById || existingBySequence;
3881
+ if (existing) {
3882
+ if (existing.content_hash !== row.content_hash) throw monitorStoreError('MONITOR_EVENT_CONFLICT', 'monitor event conflict: immutable identity has different content_hash');
3883
+ const storedTargets = allParams(runtime, db,
3884
+ 'SELECT target FROM memory_monitor_outbox WHERE event_id = ? ORDER BY target ASC', [existing.event_id])
3885
+ .map(entry => entry.target);
3886
+ const replayTargets = [...targets].sort(compareMonitorTargets);
3887
+ if (storedTargets.length !== replayTargets.length
3888
+ || storedTargets.some((target, index) => target !== replayTargets[index])) {
3889
+ throw monitorStoreError('MONITOR_TARGET_SET_CONFLICT', 'monitor event target set conflict: identical event replay changed delivery targets');
3890
+ }
3891
+ return { idempotent: true, event_id: existing.event_id, monitor_id: existing.monitor_id, sequence: existing.sequence };
3892
+ }
3893
+ const terminalReceipt = allParams(runtime, db,
3894
+ 'SELECT monitor_id FROM memory_monitor_receipts WHERE monitor_id = ?', [row.monitor_id])[0];
3895
+ if (terminalReceipt) throw monitorStoreError('MONITOR_TERMINAL', 'monitor already has a terminal receipt');
3896
+
3897
+ runParams(runtime, db,
3898
+ `INSERT INTO memory_monitor_events
3899
+ (event_id, monitor_id, sequence, content_hash, envelope_json, artifact_digest, created_at)
3900
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
3901
+ [row.event_id, row.monitor_id, row.sequence, row.content_hash, row.envelope_json, row.artifact_digest, row.created_at]);
3902
+ for (const target of targets) {
3903
+ runParams(runtime, db,
3904
+ `INSERT INTO memory_monitor_outbox
3905
+ (id, event_id, target, status, attempts, next_attempt_at, created_at)
3906
+ VALUES (?, ?, ?, 'pending', 0, NULL, ?)`,
3907
+ [randomUUID(), row.event_id, target, row.created_at]);
3908
+ }
3909
+ return { idempotent: false, event_id: row.event_id, monitor_id: row.monitor_id, sequence: row.sequence };
3910
+ });
3911
+ }
3912
+
3913
+ function recordMonitorDeliveryReceiptRow(runtime, db, envelope, config) {
3914
+ const payload = envelope.payload || {};
3915
+ const row = {
3916
+ event_id: payload.event_id,
3917
+ target: payload.target,
3918
+ attempt: payload.attempt,
3919
+ content_hash: envelope.content_hash,
3920
+ envelope_json: JSON.stringify(envelope),
3921
+ acknowledged: payload.acknowledged ? 1 : 0,
3922
+ delivered_at: payload.delivered_at,
3923
+ outcome: payload.outcome,
3924
+ };
3925
+ return runMonitorTransaction(runtime, db, config, () => {
3926
+ assertMonitorWriterEnabled(runtime, db, config);
3927
+ const existing = allParams(runtime, db,
3928
+ `SELECT content_hash FROM memory_monitor_delivery_receipts
3929
+ WHERE event_id = ? AND target = ? AND attempt = ?`, [row.event_id, row.target, row.attempt])[0];
3930
+ if (existing) {
3931
+ if (existing.content_hash !== row.content_hash) throw monitorStoreError('MONITOR_DELIVERY_CONFLICT', 'monitor delivery receipt conflict: attempt has different content_hash');
3932
+ return { idempotent: true, event_id: row.event_id, target: row.target, attempt: row.attempt };
3933
+ }
3934
+ const event = allParams(runtime, db,
3935
+ 'SELECT monitor_id, sequence FROM memory_monitor_events WHERE event_id = ?', [row.event_id])[0];
3936
+ if (!event) throw monitorStoreError('MONITOR_DELIVERY_CONFLICT', 'monitor delivery receipt references an unknown event');
3937
+ const outbox = allParams(runtime, db,
3938
+ 'SELECT id FROM memory_monitor_outbox WHERE event_id = ? AND target = ?', [row.event_id, row.target])[0];
3939
+ if (!outbox) throw monitorStoreError('MONITOR_DELIVERY_CONFLICT', 'monitor delivery receipt references an unplanned target');
3940
+ if (row.acknowledged) {
3941
+ const cursor = allParams(runtime, db,
3942
+ 'SELECT sequence FROM memory_monitor_cursors WHERE monitor_id = ? AND target = ?',
3943
+ [event.monitor_id, row.target])[0];
3944
+ if (cursor && event.sequence < Number(cursor.sequence)) {
3945
+ throw monitorStoreError('MONITOR_STALE_CURSOR', 'stale monitor delivery cursor: acknowledged sequence is behind the durable cursor');
3946
+ }
3947
+ }
3948
+
3949
+ runParams(runtime, db,
3950
+ `INSERT INTO memory_monitor_delivery_receipts
3951
+ (event_id, target, attempt, content_hash, envelope_json, acknowledged, delivered_at, outcome)
3952
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
3953
+ [row.event_id, row.target, row.attempt, row.content_hash, row.envelope_json, row.acknowledged, row.delivered_at, row.outcome]);
3954
+ if (row.acknowledged) {
3955
+ runParams(runtime, db,
3956
+ `INSERT INTO memory_monitor_cursors (monitor_id, target, sequence, updated_at)
3957
+ VALUES (?, ?, ?, ?)
3958
+ ON CONFLICT(monitor_id, target) DO UPDATE SET
3959
+ sequence = excluded.sequence,
3960
+ updated_at = excluded.updated_at
3961
+ WHERE excluded.sequence > memory_monitor_cursors.sequence`,
3962
+ [event.monitor_id, row.target, event.sequence, row.delivered_at]);
3963
+ runParams(runtime, db,
3964
+ "UPDATE memory_monitor_outbox SET status = 'acknowledged', attempts = attempts + 1, next_attempt_at = NULL WHERE id = ?",
3965
+ [outbox.id]);
3966
+ }
3967
+ return { idempotent: false, event_id: row.event_id, target: row.target, attempt: row.attempt, acknowledged: Boolean(row.acknowledged) };
3968
+ });
3969
+ }
3970
+
3971
+ function recordMonitorTerminalReceiptRow(runtime, db, envelope, config) {
3972
+ const payload = envelope.payload || {};
3973
+ const row = {
3974
+ monitor_id: payload.monitor_id,
3975
+ content_hash: envelope.content_hash,
3976
+ envelope_json: JSON.stringify(envelope),
3977
+ owner_run_id: payload.owner_run_id,
3978
+ terminal_state: payload.terminal_state,
3979
+ last_sequence: payload.last_sequence,
3980
+ evidence_digest: payload.evidence_digest,
3981
+ undelivered_cursor: payload.undelivered_cursor ?? null,
3982
+ created_at: envelope.created_at,
3983
+ };
3984
+ return runMonitorTransaction(runtime, db, config, () => {
3985
+ assertMonitorWriterEnabled(runtime, db, config);
3986
+ const existing = allParams(runtime, db,
3987
+ 'SELECT content_hash FROM memory_monitor_receipts WHERE monitor_id = ?', [row.monitor_id])[0];
3988
+ if (existing) {
3989
+ if (existing.content_hash !== row.content_hash) throw monitorStoreError('MONITOR_RECEIPT_CONFLICT', 'monitor receipt conflict: terminal evidence has different content_hash');
3990
+ return { idempotent: true, monitor_id: row.monitor_id };
3991
+ }
3992
+ const eventState = allParams(runtime, db,
3993
+ 'SELECT COUNT(*) AS event_count, MAX(sequence) AS max_sequence FROM memory_monitor_events WHERE monitor_id = ?',
3994
+ [row.monitor_id])[0];
3995
+ const expectedLastSequence = Number(eventState.event_count) === 0 ? 0 : Number(eventState.max_sequence);
3996
+ if (row.last_sequence !== expectedLastSequence) {
3997
+ throw monitorStoreError('MONITOR_STALE_TERMINAL', 'stale monitor terminal sequence: last_sequence does not match durable events');
3998
+ }
3999
+ runParams(runtime, db,
4000
+ `INSERT INTO memory_monitor_receipts
4001
+ (monitor_id, content_hash, envelope_json, owner_run_id, terminal_state, last_sequence, evidence_digest, undelivered_cursor, created_at)
4002
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4003
+ [row.monitor_id, row.content_hash, row.envelope_json, row.owner_run_id, row.terminal_state, row.last_sequence, row.evidence_digest, row.undelivered_cursor, row.created_at]);
4004
+ return { idempotent: false, monitor_id: row.monitor_id };
4005
+ });
4006
+ }
4007
+
4008
+ function monitorReadLimit(options = {}, fallback = MONITOR_DEFAULT_READ_LIMIT) {
4009
+ const requested = options.limit ?? fallback;
4010
+ if (!Number.isSafeInteger(requested) || requested < 1 || requested > MONITOR_MAX_READ_LIMIT) {
4011
+ throw new TypeError(`monitor read limit must be an integer from 1 to ${MONITOR_MAX_READ_LIMIT}`);
4012
+ }
4013
+ return requested;
4014
+ }
4015
+
4016
+ function getMonitorEventRow(runtime, db, eventId) {
4017
+ return allParams(
4018
+ runtime,
4019
+ db,
4020
+ `SELECT event_id, monitor_id, sequence, content_hash, envelope_json, artifact_digest, created_at
4021
+ FROM memory_monitor_events WHERE event_id = ? LIMIT 1`,
4022
+ [eventId],
4023
+ )[0] || null;
4024
+ }
4025
+
4026
+ function listMonitorEventRows(runtime, db, monitorId) {
4027
+ return allParams(
4028
+ runtime,
4029
+ db,
4030
+ `SELECT event_id, monitor_id, sequence, content_hash, envelope_json, artifact_digest, created_at
4031
+ FROM memory_monitor_events WHERE monitor_id = ? ORDER BY sequence ASC, event_id ASC`,
4032
+ [monitorId],
4033
+ );
4034
+ }
4035
+
4036
+ function readMonitorEventTailRows(runtime, db, monitorId, options = {}) {
4037
+ const limit = monitorReadLimit(options);
4038
+ const selected = allParams(
4039
+ runtime,
4040
+ db,
4041
+ `SELECT event_id, monitor_id, sequence, content_hash, envelope_json, artifact_digest, created_at
4042
+ FROM memory_monitor_events WHERE monitor_id = ?
4043
+ ORDER BY sequence DESC, event_id DESC LIMIT ?`,
4044
+ [monitorId, limit + 1],
4045
+ );
4046
+ const overflow = selected.length > limit;
4047
+ const events = selected.slice(0, limit).reverse();
4048
+ return {
4049
+ events,
4050
+ overflow,
4051
+ truncated_before_sequence: overflow && events.length > 0 ? Number(events[0].sequence) : null,
4052
+ };
4053
+ }
4054
+
4055
+ function readMonitorDeliveryStateRows(runtime, db, monitorId, options = {}) {
4056
+ const limit = monitorReadLimit(options);
4057
+ const cursorRows = allParams(
4058
+ runtime,
4059
+ db,
4060
+ `SELECT monitor_id, target, sequence, updated_at FROM memory_monitor_cursors
4061
+ WHERE monitor_id = ? ORDER BY target ASC LIMIT ?`,
4062
+ [monitorId, limit + 1],
4063
+ );
4064
+ const outboxRows = allParams(
4065
+ runtime,
4066
+ db,
4067
+ `SELECT o.id AS outbox_id, o.event_id, e.monitor_id, e.sequence, o.target,
4068
+ o.status, o.attempts, o.next_attempt_at, o.created_at
4069
+ FROM memory_monitor_outbox o
4070
+ JOIN memory_monitor_events e ON e.event_id = o.event_id
4071
+ WHERE e.monitor_id = ?
4072
+ ORDER BY e.sequence ASC, o.target ASC, o.id ASC LIMIT ?`,
4073
+ [monitorId, limit + 1],
4074
+ );
4075
+ const terminalReceipt = allParams(
4076
+ runtime,
4077
+ db,
4078
+ `SELECT monitor_id, content_hash, envelope_json, owner_run_id, terminal_state,
4079
+ last_sequence, evidence_digest, undelivered_cursor, created_at
4080
+ FROM memory_monitor_receipts WHERE monitor_id = ? LIMIT 1`,
4081
+ [monitorId],
4082
+ )[0] || null;
4083
+ return {
4084
+ cursors: cursorRows.slice(0, limit),
4085
+ outbox: outboxRows.slice(0, limit),
4086
+ terminal_receipt: terminalReceipt,
4087
+ overflow: {
4088
+ cursors: cursorRows.length > limit,
4089
+ outbox: outboxRows.length > limit,
4090
+ },
4091
+ };
4092
+ }
4093
+
4094
+ const CLAIM_REPAIR_INDEX_NAMES = Object.freeze([
4095
+ 'idx_kernel_claims_active_lease',
4096
+ 'idx_kernel_claims_actor_state',
4097
+ 'idx_kernel_claims_issue_state',
4098
+ ]);
4099
+ const CLAIM_REPAIR_AUTHORITY_ROWID_TABLES = new Set([
4100
+ 'kernel_events',
4101
+ 'kernel_memories',
4102
+ 'kernel_stage_runs',
4103
+ 'kernel_worktrees',
4104
+ ]);
4105
+
4106
+ function quoteSqlIdentifier(value) {
4107
+ return `"${String(value).replaceAll('"', '""')}"`;
4108
+ }
4109
+
4110
+ function loadCompleteAuthoritySnapshot(runtime, db, guardedRead) {
4111
+ const schema = guardedRead(
4112
+ 'authority_schema',
4113
+ () => allParams(
4114
+ runtime,
4115
+ db,
4116
+ `SELECT type, name, tbl_name, sql FROM sqlite_master
4117
+ WHERE name NOT LIKE 'sqlite_%' ORDER BY type ASC, name ASC`,
4118
+ ),
4119
+ );
4120
+ const tables = schema
4121
+ .filter(row => row.type === 'table')
4122
+ .map(row => ({
4123
+ name: row.name,
4124
+ rows: guardedRead(
4125
+ `authority_table:${row.name}`,
4126
+ () => allParams(
4127
+ runtime,
4128
+ db,
4129
+ `SELECT ${CLAIM_REPAIR_AUTHORITY_ROWID_TABLES.has(row.name) ? 'rowid AS __forge_rowid, ' : ''}* FROM ${quoteSqlIdentifier(row.name)}`,
4130
+ ),
4131
+ ),
4132
+ }));
4133
+ return { schema, tables };
4134
+ }
4135
+
4136
+ function loadLegacyClaimRepairSnapshot(runtime, db) {
4137
+ const readErrors = [];
4138
+ const guardedRead = (label, read, fallback = []) => {
4139
+ try {
4140
+ return read();
4141
+ } catch {
4142
+ readErrors.push(label);
4143
+ return fallback;
4144
+ }
4145
+ };
4146
+ const integrityRows = guardedRead('integrity_check', () => queryAll(runtime, db, 'PRAGMA integrity_check;'));
4147
+ const foreignKeyState = guardedRead('foreign_keys', () => queryOne(runtime, db, 'PRAGMA foreign_keys;'), {});
4148
+ const foreignKeyFaults = guardedRead('foreign_key_check', () => queryAll(runtime, db, 'PRAGMA foreign_key_check;'));
4149
+ const claimColumns = guardedRead('claim_columns', () => queryAll(runtime, db, "PRAGMA table_info('kernel_claims');"));
4150
+ const issueColumns = guardedRead('issue_columns', () => queryAll(runtime, db, "PRAGMA table_info('kernel_issues');"));
4151
+ const claimForeignKeys = guardedRead('claim_foreign_keys', () => queryAll(runtime, db, "PRAGMA foreign_key_list('kernel_claims');"));
4152
+ const indexList = guardedRead('claim_indexes', () => queryAll(runtime, db, "PRAGMA index_list('kernel_claims');"));
4153
+ const indexSqlRows = guardedRead(
4154
+ 'claim_index_sql',
4155
+ () => allParams(runtime, db, "SELECT name, sql FROM sqlite_master WHERE type = 'index' AND tbl_name = 'kernel_claims'"),
4156
+ );
4157
+ const indexSql = new Map(indexSqlRows.map(row => [row.name, row.sql]));
4158
+ const indexByName = new Map(indexList.map(row => [row.name, row]));
4159
+ const claimIndexes = CLAIM_REPAIR_INDEX_NAMES.map(name => {
4160
+ const index = indexByName.get(name) || {};
4161
+ const columns = guardedRead(
4162
+ `index_columns:${name}`,
4163
+ () => queryAll(runtime, db, `PRAGMA index_info('${name}');`),
4164
+ );
4165
+ return {
4166
+ name,
4167
+ columns: columns.map(row => row.name),
4168
+ unique: Number(index.unique) === 1,
4169
+ partial: Number(index.partial) === 1,
4170
+ sql: indexSql.get(name) || '',
4171
+ };
4172
+ });
4173
+ const issues = guardedRead(
4174
+ 'issues',
4175
+ () => allParams(runtime, db, 'SELECT id, status, type FROM kernel_issues ORDER BY id ASC'),
4176
+ );
4177
+ const claims = guardedRead(
4178
+ 'claims',
4179
+ () => allParams(
4180
+ runtime,
4181
+ db,
4182
+ `SELECT id, issue_id, actor, state, session_id, worktree_id, claimed_at, expires_at
4183
+ FROM kernel_claims ORDER BY id ASC`,
4184
+ ),
4185
+ );
4186
+ const authority = loadCompleteAuthoritySnapshot(runtime, db, guardedRead);
4187
+ return {
4188
+ integrity: integrityRows.length > 0
4189
+ && integrityRows.every(row => String(row.integrity_check || '').toLowerCase() === 'ok')
4190
+ ? 'ok'
4191
+ : 'failed',
4192
+ foreign_keys_enabled: Number(foreignKeyState.foreign_keys) === 1,
4193
+ foreign_key_faults: foreignKeyFaults.length,
4194
+ claim_columns: claimColumns.map(row => row.name),
4195
+ issue_columns: issueColumns.map(row => row.name),
4196
+ claim_foreign_keys: claimForeignKeys.map(row => ({ table: row.table, from: row.from, to: row.to })),
4197
+ claim_indexes: claimIndexes,
4198
+ issues,
4199
+ claims,
4200
+ authority_schema: authority.schema,
4201
+ authority_tables: authority.tables,
4202
+ read_errors: readErrors,
4203
+ };
4204
+ }
4205
+
4206
+ function preflightLegacyClaimRepairRow(runtime, db, input = {}) {
4207
+ const snapshot = loadLegacyClaimRepairSnapshot(runtime, db);
4208
+ return publicClaimRepairPreflight(buildClaimRepairPlan(snapshot, input));
4209
+ }
4210
+
4211
+ function assertClaimRepairApplyInput(input = {}) {
4212
+ const approvedDigest = input.approvedDigest;
4213
+ const proof = input.backupProof;
4214
+ if (!/^[0-9a-f]{64}$/.test(String(approvedDigest || ''))) {
4215
+ throw new ClaimRepairError('CLAIM_REPAIR_APPROVAL_REQUIRED', 'Apply requires the exact approved preflight digest');
4216
+ }
4217
+ if (proof?.schema_version !== 'forge.claim-repair.backup-proof.v1'
4218
+ || proof.integrity !== 'ok'
4219
+ || proof.plan_digest !== approvedDigest
4220
+ || proof.restore_digest !== approvedDigest
4221
+ || !/^[0-9a-f]{64}$/.test(String(proof.backup_sha256 || ''))) {
4222
+ throw new ClaimRepairError(
4223
+ 'CLAIM_REPAIR_BACKUP_PROOF_REQUIRED',
4224
+ 'Apply requires a verified separate backup restored from the exact approved snapshot',
4225
+ );
4226
+ }
4227
+ if (typeof input.actor !== 'string' || input.actor.trim() === '') {
4228
+ throw new ClaimRepairError('CLAIM_REPAIR_ACTOR_REQUIRED', 'Apply requires an explicit operator actor');
4229
+ }
4230
+ }
4231
+
4232
+ function parseStoredClaimRepairReceipt(row, approvedDigest, backupProof = null) {
4233
+ if (!row) return null;
4234
+ let receipt;
4235
+ try {
4236
+ receipt = JSON.parse(row.payload_json);
4237
+ } catch {
4238
+ throw new ClaimRepairError('CLAIM_REPAIR_RECEIPT_INVALID', 'Stored claim repair receipt is malformed');
4239
+ }
4240
+ if (receipt?.schema_version !== 'forge.claim-repair.receipt.v1'
4241
+ || row.id !== receipt.receipt_id
4242
+ || row.entity_type !== 'claim_repair'
4243
+ || row.entity_id !== 'legacy_claims'
4244
+ || row.event_type !== 'claim.repair'
4245
+ || row.origin !== 'forge.claim-repair'
4246
+ || receipt.approved_digest !== approvedDigest
4247
+ || (backupProof && receipt.backup_sha256 !== backupProof.backup_sha256)
4248
+ || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(receipt.recovery_ref || ''))
4249
+ || !/^[0-9a-f]{64}$/.test(String(receipt.backup_sha256 || ''))
4250
+ || !/^[0-9a-f]{64}$/.test(String(receipt.after_digest || ''))) {
4251
+ throw new ClaimRepairError('CLAIM_REPAIR_RECEIPT_INVALID', 'Stored claim repair receipt does not match the approved proof');
4252
+ }
4253
+ return receipt;
4254
+ }
4255
+
4256
+ function claimRepairRecoveryPath(backupPath, recoveryReference) {
4257
+ return `${backupPath}.forge-recovery-${recoveryReference}`;
4258
+ }
4259
+
4260
+ function attachClaimRepairRecoveryPath(receipt, backupPath) {
4261
+ if (!receipt || typeof backupPath !== 'string' || !path.isAbsolute(backupPath)) return receipt;
4262
+ return {
4263
+ ...receipt,
4264
+ recovery_path: claimRepairRecoveryPath(backupPath, receipt.recovery_ref),
4265
+ };
4266
+ }
4267
+
4268
+ function replayStoredClaimRepairReceipt(runtime, db, approvedDigest, observedAt, backupPath) {
4269
+ if (!/^[0-9a-f]{64}$/.test(String(approvedDigest || ''))) return null;
4270
+ const existingRow = allParams(
4271
+ runtime,
4272
+ db,
4273
+ 'SELECT * FROM kernel_events WHERE idempotency_key = ? LIMIT 1',
4274
+ [`claim.repair:${approvedDigest}`],
4275
+ )[0];
4276
+ const receipt = parseStoredClaimRepairReceipt(existingRow, approvedDigest);
4277
+ if (receipt && receipt.observed_at !== observedAt) {
4278
+ throw new ClaimRepairError('CLAIM_REPAIR_RECEIPT_INVALID', 'Stored claim repair receipt does not match the fixed observation time');
4279
+ }
4280
+ return receipt
4281
+ ? attachClaimRepairRecoveryPath({ ...receipt, replayed: true }, backupPath)
4282
+ : null;
4283
+ }
4284
+
4285
+ function claimRepairSourceIdentities(databasePath) {
4286
+ return [databasePath, `${databasePath}-wal`, `${databasePath}-shm`, `${databasePath}-journal`]
4287
+ .filter(sourcePath => fs.existsSync(sourcePath))
4288
+ .map(sourcePath => {
4289
+ const stat = fs.statSync(sourcePath, { bigint: true });
4290
+ return { device: stat.dev, inode: stat.ino };
4291
+ });
4292
+ }
4293
+
4294
+ function assertNoClaimRepairBackupSidecars(backupPath) {
4295
+ for (const suffix of ['-wal', '-shm', '-journal']) {
4296
+ if (fs.existsSync(`${backupPath}${suffix}`)) throw new Error('backup sidecar exists');
4297
+ }
4298
+ }
4299
+
4300
+ function hashOpenFileDescriptor(fileDescriptor) {
4301
+ const hash = createHash('sha256');
4302
+ const buffer = Buffer.allocUnsafe(64 * 1024);
4303
+ let offset = 0;
4304
+ for (;;) {
4305
+ const bytesRead = fs.readSync(fileDescriptor, buffer, 0, buffer.length, offset);
4306
+ if (bytesRead === 0) break;
4307
+ hash.update(buffer.subarray(0, bytesRead));
4308
+ offset += bytesRead;
4309
+ }
4310
+ return hash.digest('hex');
4311
+ }
4312
+
4313
+ function copyOpenFileDescriptor(sourceDescriptor, destinationDescriptor) {
4314
+ const buffer = Buffer.allocUnsafe(64 * 1024);
4315
+ let offset = 0;
4316
+ for (;;) {
4317
+ const bytesRead = fs.readSync(sourceDescriptor, buffer, 0, buffer.length, offset);
4318
+ if (bytesRead === 0) break;
4319
+ let written = 0;
4320
+ while (written < bytesRead) {
4321
+ const bytesWritten = fs.writeSync(
4322
+ destinationDescriptor,
4323
+ buffer,
4324
+ written,
4325
+ bytesRead - written,
4326
+ offset + written,
4327
+ );
4328
+ if (bytesWritten <= 0) throw new Error('recovery copy write made no progress');
4329
+ written += bytesWritten;
4330
+ }
4331
+ offset += bytesRead;
4332
+ }
4333
+ fs.fsyncSync(destinationDescriptor);
4334
+ }
4335
+
4336
+ function syncClaimRepairRecoveryDirectory(recoveryPath, options = {}) {
4337
+ const platform = options.platform || process.platform;
4338
+ if (platform === 'win32') return;
4339
+ const fsApi = options.fsApi || fs;
4340
+ let directoryDescriptor;
4341
+ try {
4342
+ directoryDescriptor = fsApi.openSync(path.dirname(recoveryPath), 'r');
4343
+ fsApi.fsyncSync(directoryDescriptor);
4344
+ } finally {
4345
+ if (directoryDescriptor !== undefined) fsApi.closeSync(directoryDescriptor);
4346
+ }
4347
+ }
4348
+
4349
+ async function verifyClaimRepairRecovery(receipt, backupPath, databasePath, options = {}) {
4350
+ const recoveryPath = claimRepairRecoveryPath(backupPath, receipt.recovery_ref);
4351
+ let fileDescriptor;
4352
+ try {
4353
+ await (options.hardenPath || hardenBackupPermissions)(recoveryPath);
4354
+ const sourceIdentities = claimRepairSourceIdentities(databasePath);
4355
+ const backupIdentity = fs.existsSync(backupPath) ? fs.statSync(backupPath, { bigint: true }) : null;
4356
+ fileDescriptor = fs.openSync(recoveryPath, 'r');
4357
+ const before = fs.fstatSync(fileDescriptor, { bigint: true });
4358
+ const digest = hashOpenFileDescriptor(fileDescriptor);
4359
+ const after = fs.fstatSync(fileDescriptor, { bigint: true });
4360
+ const named = fs.statSync(recoveryPath, { bigint: true });
4361
+ if (digest !== receipt.backup_sha256
4362
+ || before.size !== after.size
4363
+ || before.mtimeNs !== after.mtimeNs
4364
+ || named.dev !== before.dev
4365
+ || named.ino !== before.ino
4366
+ || (backupIdentity && backupIdentity.dev === before.dev && backupIdentity.ino === before.ino)
4367
+ || sourceIdentities.some(source => source.device === before.dev && source.inode === before.ino)) {
4368
+ throw new Error('recovery copy changed');
4369
+ }
4370
+ return recoveryPath;
4371
+ } catch {
4372
+ throw new ClaimRepairError(
4373
+ 'CLAIM_REPAIR_RECOVERY_INVALID',
4374
+ 'Retained claim-repair recovery copy is missing, unsafe, or changed',
4375
+ );
4376
+ } finally {
4377
+ if (fileDescriptor !== undefined) fs.closeSync(fileDescriptor);
4378
+ }
4379
+ }
4380
+
4381
+ async function openClaimRepairBackupFence(databasePath, backupPath, backupProof, recoveryReference, options = {}) {
4382
+ let fileDescriptor;
4383
+ let recoveryFileDescriptor;
4384
+ let recoveryPath;
4385
+ try {
4386
+ const hardenPath = options.hardenPath || hardenBackupPermissions;
4387
+ const hardenPaths = options.hardenPaths;
4388
+ await hardenPath(backupPath);
4389
+ assertSafeBackupDestination(databasePath, backupPath);
4390
+ assertNoClaimRepairBackupSidecars(backupPath);
4391
+ const sourceIdentities = claimRepairSourceIdentities(databasePath);
4392
+ fileDescriptor = fs.openSync(backupPath, 'r');
4393
+ const before = fs.fstatSync(fileDescriptor, { bigint: true });
4394
+ if (sourceIdentities.some(source => source.device === before.dev && source.inode === before.ino)) {
4395
+ throw new Error('pinned backup aliases source');
4396
+ }
4397
+ const digest = hashOpenFileDescriptor(fileDescriptor);
4398
+ const after = fs.fstatSync(fileDescriptor, { bigint: true });
4399
+ const named = fs.statSync(backupPath, { bigint: true });
4400
+ if (digest !== backupProof.backup_sha256
4401
+ || before.size !== after.size
4402
+ || before.mtimeNs !== after.mtimeNs
4403
+ || before.dev !== named.dev
4404
+ || before.ino !== named.ino) {
4405
+ throw new Error('backup identity or content changed');
4406
+ }
4407
+ recoveryPath = claimRepairRecoveryPath(backupPath, recoveryReference);
4408
+ recoveryFileDescriptor = fs.openSync(recoveryPath, 'wx+', 0o600);
4409
+ // The backup was hardened before opening its descriptor; harden the retained
4410
+ // copy in the same Windows PowerShell pass before copying any bytes.
4411
+ if (typeof hardenPaths === 'function') await hardenPaths([backupPath, recoveryPath]);
4412
+ else {
4413
+ await hardenPath(backupPath);
4414
+ await hardenPath(recoveryPath);
4415
+ }
4416
+ copyOpenFileDescriptor(fileDescriptor, recoveryFileDescriptor);
4417
+ syncClaimRepairRecoveryDirectory(recoveryPath);
4418
+ const sourceAfterCopy = fs.fstatSync(fileDescriptor, { bigint: true });
4419
+ const sourceDigestAfterCopy = hashOpenFileDescriptor(fileDescriptor);
4420
+ const namedAfterCopy = fs.statSync(backupPath, { bigint: true });
4421
+ const recoveryBefore = fs.fstatSync(recoveryFileDescriptor, { bigint: true });
4422
+ const recoveryDigest = hashOpenFileDescriptor(recoveryFileDescriptor);
4423
+ const recoveryAfter = fs.fstatSync(recoveryFileDescriptor, { bigint: true });
4424
+ const recoveryNamed = fs.statSync(recoveryPath, { bigint: true });
4425
+ if (sourceDigestAfterCopy !== backupProof.backup_sha256
4426
+ || before.size !== sourceAfterCopy.size
4427
+ || before.mtimeNs !== sourceAfterCopy.mtimeNs
4428
+ || namedAfterCopy.dev !== before.dev
4429
+ || namedAfterCopy.ino !== before.ino
4430
+ || recoveryDigest !== backupProof.backup_sha256
4431
+ || recoveryBefore.size !== recoveryAfter.size
4432
+ || recoveryBefore.mtimeNs !== recoveryAfter.mtimeNs
4433
+ || recoveryNamed.dev !== recoveryBefore.dev
4434
+ || recoveryNamed.ino !== recoveryBefore.ino
4435
+ || (recoveryBefore.dev === before.dev && recoveryBefore.ino === before.ino)) {
4436
+ throw new Error('recovery copy does not preserve independent verified bytes');
4437
+ }
4438
+ return {
4439
+ databasePath,
4440
+ backupPath,
4441
+ backupSha256: backupProof.backup_sha256,
4442
+ fileDescriptor,
4443
+ device: before.dev,
4444
+ inode: before.ino,
4445
+ recoveryFileDescriptor,
4446
+ recoveryDevice: recoveryBefore.dev,
4447
+ recoveryInode: recoveryBefore.ino,
4448
+ recoverySize: recoveryBefore.size,
4449
+ recoveryMtimeNs: recoveryBefore.mtimeNs,
4450
+ recoveryPath,
4451
+ };
4452
+ } catch {
4453
+ if (fileDescriptor !== undefined) fs.closeSync(fileDescriptor);
4454
+ if (recoveryFileDescriptor !== undefined) fs.closeSync(recoveryFileDescriptor);
4455
+ if (recoveryPath) {
4456
+ try { fs.rmSync(recoveryPath); } catch { /* cleanup only */ }
4457
+ }
4458
+ throw new ClaimRepairError(
4459
+ 'CLAIM_REPAIR_BACKUP_DRIFT',
4460
+ 'Verified claim-repair backup changed before receipt commit',
4461
+ );
4462
+ }
4463
+ }
4464
+
4465
+ async function assertClaimRepairBackupFenceCurrent(fence, options = {}) {
4466
+ try {
4467
+ await (options.hardenPath || hardenBackupPermissions)(fence.backupPath);
4468
+ assertSafeBackupDestination(fence.databasePath, fence.backupPath);
4469
+ if (!options.sidecarsBlocked) assertNoClaimRepairBackupSidecars(fence.backupPath);
4470
+ const before = fs.fstatSync(fence.fileDescriptor, { bigint: true });
4471
+ const digest = hashOpenFileDescriptor(fence.fileDescriptor);
4472
+ const after = fs.fstatSync(fence.fileDescriptor, { bigint: true });
4473
+ const named = fs.statSync(fence.backupPath, { bigint: true });
4474
+ const recoveryBefore = fs.fstatSync(fence.recoveryFileDescriptor, { bigint: true });
4475
+ const recoveryDigest = hashOpenFileDescriptor(fence.recoveryFileDescriptor);
4476
+ const recoveryAfter = fs.fstatSync(fence.recoveryFileDescriptor, { bigint: true });
4477
+ const recoveryNamed = fs.statSync(fence.recoveryPath, { bigint: true });
4478
+ if (digest !== fence.backupSha256
4479
+ || before.size !== after.size
4480
+ || before.mtimeNs !== after.mtimeNs
4481
+ || before.dev !== fence.device
4482
+ || before.ino !== fence.inode
4483
+ || named.dev !== fence.device
4484
+ || named.ino !== fence.inode
4485
+ || recoveryDigest !== fence.backupSha256
4486
+ || recoveryBefore.size !== recoveryAfter.size
4487
+ || recoveryBefore.size !== fence.recoverySize
4488
+ || recoveryBefore.mtimeNs !== recoveryAfter.mtimeNs
4489
+ || recoveryBefore.mtimeNs !== fence.recoveryMtimeNs
4490
+ || recoveryBefore.dev !== fence.recoveryDevice
4491
+ || recoveryBefore.ino !== fence.recoveryInode
4492
+ || recoveryNamed.dev !== fence.recoveryDevice
4493
+ || recoveryNamed.ino !== fence.recoveryInode
4494
+ || (recoveryNamed.dev === fence.device && recoveryNamed.ino === fence.inode)) {
4495
+ throw new Error('backup fence changed');
4496
+ }
4497
+ } catch {
4498
+ throw new ClaimRepairError(
4499
+ 'CLAIM_REPAIR_BACKUP_DRIFT',
4500
+ 'Verified claim-repair backup changed before receipt commit',
4501
+ );
4502
+ }
4503
+ }
4504
+
4505
+ function assertClaimRepairBackupSidecarsRemainAbsent(backupPath) {
4506
+ assertNoClaimRepairBackupSidecars(backupPath);
4507
+ const blockers = [];
4508
+ try {
4509
+ for (const suffix of ['-wal', '-shm', '-journal']) {
4510
+ const sidecarPath = `${backupPath}${suffix}`;
4511
+ fs.mkdirSync(sidecarPath);
4512
+ const stat = fs.statSync(sidecarPath, { bigint: true });
4513
+ blockers.push({ path: sidecarPath, device: stat.dev, inode: stat.ino });
4514
+ }
4515
+ return blockers;
4516
+ } catch (error) {
4517
+ for (const blocker of blockers) {
4518
+ try { fs.rmdirSync(blocker.path); } catch { /* cleanup only */ }
4519
+ }
4520
+ throw error;
4521
+ }
4522
+ }
4523
+
4524
+ function assertClaimRepairBackupSidecarBlockersCurrent(blockers = []) {
4525
+ for (const blocker of blockers) {
4526
+ const stat = fs.statSync(blocker.path, { bigint: true });
4527
+ if (!stat.isDirectory() || stat.dev !== blocker.device || stat.ino !== blocker.inode) {
4528
+ throw new Error('backup sidecar blocker changed');
4529
+ }
4530
+ }
4531
+ }
4532
+
4533
+ function removeClaimRepairBackupSidecarBlockers(blockers = []) {
4534
+ for (const blocker of blockers) {
4535
+ try {
4536
+ const stat = fs.statSync(blocker.path, { bigint: true });
4537
+ if (stat.isDirectory() && stat.dev === blocker.device && stat.ino === blocker.inode) {
4538
+ fs.rmdirSync(blocker.path);
4539
+ }
4540
+ } catch { /* cleanup cannot change the committed receipt */ }
4541
+ }
4542
+ }
4543
+
4544
+ function closeClaimRepairBackupFence(fence, options = {}) {
4545
+ if (!fence) return;
4546
+ try { fs.closeSync(fence.fileDescriptor); } catch { /* descriptor cleanup cannot change a committed receipt */ }
4547
+ try { fs.closeSync(fence.recoveryFileDescriptor); } catch { /* descriptor cleanup cannot change a committed receipt */ }
4548
+ if (options.removeRecovery) {
4549
+ try {
4550
+ const recovery = fs.statSync(fence.recoveryPath, { bigint: true });
4551
+ if (recovery.dev === fence.recoveryDevice && recovery.ino === fence.recoveryInode) {
4552
+ fs.rmSync(fence.recoveryPath);
4553
+ }
4554
+ } catch { /* rolled-back cleanup cannot change authority */ }
4555
+ }
4556
+ }
4557
+
4558
+ function injectClaimRepairFault(driverOptions, phase) {
4559
+ if (typeof driverOptions.claimRepairFaultInjector === 'function') {
4560
+ driverOptions.claimRepairFaultInjector(phase);
4561
+ }
4562
+ }
4563
+
4564
+ function assertClaimRepairReplayInput(input = {}) {
4565
+ if (!/^[0-9a-f]{64}$/.test(String(input.approvedDigest || ''))) {
4566
+ throw new ClaimRepairError('CLAIM_REPAIR_APPROVAL_REQUIRED', 'Apply requires the exact approved preflight digest');
4567
+ }
4568
+ if (typeof input.actor !== 'string' || input.actor.trim() === '') {
4569
+ throw new ClaimRepairError('CLAIM_REPAIR_ACTOR_REQUIRED', 'Apply requires an explicit operator actor');
4570
+ }
4571
+ if (typeof input.observedAt !== 'string'
4572
+ || !Number.isFinite(Date.parse(input.observedAt))
4573
+ || new Date(Date.parse(input.observedAt)).toISOString() !== input.observedAt) {
4574
+ throw new ClaimRepairError('CLAIM_REPAIR_INVALID_TIME', 'Apply requires a fixed canonical UTC observation time');
4575
+ }
4576
+ }
4577
+
4578
+ function updateExactClaimRepairAction(runtime, db, action) {
4579
+ const claim = action.claim;
4580
+ const result = runParams(
4581
+ runtime,
4582
+ db,
4583
+ `UPDATE kernel_claims SET state = ?
4584
+ WHERE id = ? AND issue_id = ? AND actor = ? AND state = 'active'
4585
+ AND session_id IS ? AND worktree_id IS ? AND claimed_at = ? AND expires_at IS ?
4586
+ AND EXISTS (
4587
+ SELECT 1 FROM kernel_issues WHERE id = ? AND status = ?
4588
+ )`,
4589
+ [
4590
+ action.to_state,
4591
+ claim.id,
4592
+ claim.issue_id,
4593
+ claim.actor,
4594
+ claim.session_id,
4595
+ claim.worktree_id,
4596
+ claim.claimed_at,
4597
+ claim.expires_at,
4598
+ claim.issue_id,
4599
+ action.issue_status,
4600
+ ],
4601
+ );
4602
+ if (Number(result?.changes || 0) !== 1) {
4603
+ throw new ClaimRepairError(
4604
+ 'CLAIM_REPAIR_CAS_CONFLICT',
4605
+ 'Claim repair exact-row compare-and-swap rejected concurrent drift',
4606
+ );
4607
+ }
4608
+ }
4609
+
4610
+ const claimRepairApplyQueues = new Map();
4611
+
4612
+ function claimRepairApplyQueueKey(databasePath) {
4613
+ const canonicalPath = fs.realpathSync.native(databasePath);
4614
+ return process.platform === 'win32' ? canonicalPath.toLowerCase() : canonicalPath;
4615
+ }
4616
+
4617
+ async function withClaimRepairApplyQueue(databasePath, operation, queue = claimRepairApplyQueues) {
4618
+ const key = claimRepairApplyQueueKey(databasePath);
4619
+ const previous = queue.get(key) || Promise.resolve();
4620
+ let release;
4621
+ const gate = new Promise(resolve => { release = resolve; });
4622
+ const tail = previous.then(() => gate);
4623
+ queue.set(key, tail);
4624
+ await previous;
4625
+ try {
4626
+ return await operation();
4627
+ } finally {
4628
+ release();
4629
+ if (queue.get(key) === tail) queue.delete(key);
4630
+ }
4631
+ }
4632
+
4633
+ async function applyLegacyClaimRepairRow(runtime, db, input = {}, driverOptions = {}) {
4634
+ assertClaimRepairApplyInput(input);
4635
+ const idempotencyKey = `claim.repair:${input.approvedDigest}`;
4636
+ const hardenPath = driverOptions.hardenPath || hardenBackupPermissions;
4637
+ const hardenPaths = driverOptions.hardenPaths
4638
+ || (driverOptions.hardenPath ? driverOptions.hardenPath.batch : hardenBackupPermissionsBatch);
4639
+ let backupFence;
4640
+ let sidecarBlockers = [];
4641
+ let committed = false;
4642
+ let rolledBack = false;
4643
+ execSql(runtime, db, 'BEGIN IMMEDIATE;');
4644
+ try {
4645
+ const existingRow = allParams(
4646
+ runtime,
4647
+ db,
4648
+ 'SELECT * FROM kernel_events WHERE idempotency_key = ? LIMIT 1',
4649
+ [idempotencyKey],
4650
+ )[0];
4651
+ const existingReceipt = parseStoredClaimRepairReceipt(existingRow, input.approvedDigest, input.backupProof);
4652
+ if (existingReceipt) {
4653
+ await verifyClaimRepairRecovery(existingReceipt, input.backupPath, input.databasePath, { hardenPath });
4654
+ execSql(runtime, db, 'COMMIT;');
4655
+ return attachClaimRepairRecoveryPath({ ...existingReceipt, replayed: true }, input.backupPath);
4656
+ }
4657
+ const plan = buildClaimRepairPlan(loadLegacyClaimRepairSnapshot(runtime, db), {
4658
+ observedAt: input.observedAt,
4659
+ });
4660
+ if (plan.digest !== input.approvedDigest) {
4661
+ throw new ClaimRepairError(
4662
+ 'CLAIM_REPAIR_DIGEST_DRIFT',
4663
+ 'Live claim authority changed after approval; generate and approve a new fixed-time preflight',
4664
+ );
4665
+ }
4666
+
4667
+ for (const action of plan.actions) updateExactClaimRepairAction(runtime, db, action);
4668
+ injectClaimRepairFault(driverOptions, 'after-mutations');
4669
+ const afterPlan = buildClaimRepairPlan(loadLegacyClaimRepairSnapshot(runtime, db), {
4670
+ observedAt: input.observedAt,
4671
+ });
4672
+ if (afterPlan.digest !== plan.afterDigest) {
4673
+ throw new ClaimRepairError('CLAIM_REPAIR_POSTCONDITION_FAILED', 'Claim repair postcondition digest did not match the approved plan');
4674
+ }
4675
+ injectClaimRepairFault(driverOptions, 'before-backup-commit-check');
4676
+ const recoveryReference = randomUUID();
4677
+ backupFence = await openClaimRepairBackupFence(
4678
+ input.databasePath,
4679
+ input.backupPath,
4680
+ input.backupProof,
4681
+ recoveryReference,
4682
+ { hardenPath, hardenPaths },
4683
+ );
4684
+ injectClaimRepairFault(driverOptions, 'after-backup-fence');
4685
+
4686
+ const receiptId = randomUUID();
4687
+ const receipt = {
4688
+ schema_version: 'forge.claim-repair.receipt.v1',
4689
+ receipt_id: receiptId,
4690
+ observed_at: input.observedAt,
4691
+ approved_digest: input.approvedDigest,
4692
+ after_digest: afterPlan.digest,
4693
+ backup_sha256: input.backupProof.backup_sha256,
4694
+ recovery_ref: recoveryReference,
4695
+ mutations: {
4696
+ released: plan.actions.filter(action => action.to_state === 'released').length,
4697
+ reclaimable: plan.actions.filter(action => action.to_state === 'reclaimable').length,
4698
+ total: plan.actions.length,
4699
+ },
4700
+ replayed: false,
4701
+ };
4702
+ runParams(
4703
+ runtime,
4704
+ db,
4705
+ `INSERT INTO kernel_events (
4706
+ id, entity_type, entity_id, event_type, idempotency_key,
4707
+ expected_revision, actor, origin, payload_json, created_at
4708
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4709
+ [
4710
+ receiptId,
4711
+ 'claim_repair',
4712
+ 'legacy_claims',
4713
+ 'claim.repair',
4714
+ idempotencyKey,
4715
+ 0,
4716
+ input.actor,
4717
+ 'forge.claim-repair',
4718
+ JSON.stringify(receipt),
4719
+ new Date().toISOString(),
4720
+ ],
4721
+ );
4722
+ injectClaimRepairFault(driverOptions, 'after-receipt-before-commit');
4723
+ await assertClaimRepairBackupFenceCurrent(backupFence, { hardenPath });
4724
+ sidecarBlockers = assertClaimRepairBackupSidecarsRemainAbsent(input.backupPath);
4725
+ await assertClaimRepairBackupFenceCurrent(backupFence, { hardenPath, sidecarsBlocked: true });
4726
+ execSql(runtime, db, 'COMMIT;');
4727
+ committed = true;
4728
+ injectClaimRepairFault(driverOptions, 'after-commit-before-backup-check');
4729
+ try {
4730
+ await assertClaimRepairBackupFenceCurrent(backupFence, { hardenPath, sidecarsBlocked: true });
4731
+ assertClaimRepairBackupSidecarBlockersCurrent(sidecarBlockers);
4732
+ } catch {
4733
+ throw new ClaimRepairError(
4734
+ 'CLAIM_REPAIR_BACKUP_POSTCOMMIT_DRIFT',
4735
+ 'Claim repair committed, but the named backup changed; preserve the verified recovery path',
4736
+ { recovery_path: backupFence.recoveryPath },
4737
+ );
4738
+ }
4739
+ injectClaimRepairFault(driverOptions, 'after-postcommit-backup-check');
4740
+ return attachClaimRepairRecoveryPath(receipt, input.backupPath);
4741
+ } catch (error) {
4742
+ if (!committed) {
4743
+ try {
4744
+ execSql(runtime, db, 'ROLLBACK;');
4745
+ rolledBack = true;
4746
+ } catch { /* preserve the original failure and recovery copy */ }
4747
+ }
4748
+ throw error;
4749
+ } finally {
4750
+ removeClaimRepairBackupSidecarBlockers(sidecarBlockers);
4751
+ closeClaimRepairBackupFence(backupFence, { removeRecovery: rolledBack });
4752
+ }
4753
+ }
4754
+
4755
+ function createDriver(runtime, configuredDatabasePath, driverOptions = {}) {
4756
+ let db;
4757
+ let openedDatabasePath;
4758
+ let memorySchemaEnsured = false;
4759
+ let usageEvidenceSchemaEnsured = false;
4760
+
4761
+ // kernel_memories is created by migration 005 through broker.initialize(), but the
4762
+ // synchronous project-memory facade writes WITHOUT first running migrations. Lazily
4763
+ // ensure the table (idempotent CREATE IF NOT EXISTS, rendered from the same migration)
4764
+ // plus a busy_timeout for the second connection the issue backend may hold open.
4765
+ function ensureMemorySchema(database, busyTimeoutMs) {
4766
+ if (memorySchemaEnsured) return;
4767
+ const requestedBusyTimeout = Number(busyTimeoutMs);
4768
+ const busyTimeout = Number.isFinite(requestedBusyTimeout) && requestedBusyTimeout >= 0
4769
+ ? Math.floor(requestedBusyTimeout)
4770
+ : 5_000;
4771
+ execSql(runtime, database, `PRAGMA busy_timeout=${busyTimeout};`);
4772
+ try {
4773
+ for (const statement of buildMemoryProjectionMigration().apply) {
4774
+ execSql(runtime, database, statement);
4775
+ }
4776
+ // FTS5 recall index (migration 008): create the virtual table + sync triggers
4777
+ // idempotently so a synchronous memory write stays indexed without a prior
4778
+ // broker.initialize(). When the index is NEWLY created, rebuild once to backfill any
4779
+ // rows written before it existed (a DB upgraded from before this feature, or rows the
4780
+ // insights engine wrote straight to kernel_memories) — the sync triggers keep it
4781
+ // current thereafter, so steady-state process starts skip the reindex.
4782
+ //
4783
+ // Staleness is detected by TABLE EXISTENCE (sqlite_master), never by count(*): on an
4784
+ // external-content FTS5 table `count(*)` returns the CONTENT row count, not the
4785
+ // indexed-doc count, so it can never reveal an un-backfilled index.
4786
+ const ftsDdl = memoryFtsDdl();
4787
+ const ftsExisted = Number(queryOne(
4788
+ runtime,
4789
+ database,
4790
+ "SELECT count(*) AS count FROM sqlite_master WHERE type = 'table' AND name = 'kernel_memories_fts'",
4791
+ ).count) > 0;
4792
+ execSql(runtime, database, ftsDdl.create);
4793
+ for (const trigger of ftsDdl.triggers) {
4794
+ execSql(runtime, database, trigger);
4795
+ }
4796
+ if (!ftsExisted) {
4797
+ execSql(runtime, database, ftsDdl.rebuild);
4798
+ }
4799
+ memorySchemaEnsured = true;
4800
+ } finally {
4801
+ if (busyTimeout !== 5_000) {
4802
+ execSql(runtime, database, 'PRAGMA busy_timeout=5000;');
4803
+ }
4804
+ }
4805
+ }
4806
+
4807
+ function ensureUsageEvidenceSchema(database) {
4808
+ if (usageEvidenceSchemaEnsured) return;
4809
+ for (const statement of buildUsageEvidenceMigration().apply) {
4810
+ execSql(runtime, database, statement);
4811
+ }
4812
+ usageEvidenceSchemaEnsured = true;
4813
+ }
4814
+
4815
+ function resolveDatabasePath(config) {
4816
+ const brokerDatabasePath = config && config.databasePath;
4817
+ if (configuredDatabasePath && brokerDatabasePath && configuredDatabasePath !== brokerDatabasePath) {
4818
+ throw new Error([
4819
+ 'Kernel SQLite driver databasePath mismatch:',
4820
+ `driver is configured for ${configuredDatabasePath}`,
4821
+ `but broker config uses ${brokerDatabasePath}`,
4822
+ ].join(' '));
4823
+ }
4824
+ const databasePath = brokerDatabasePath || configuredDatabasePath;
4825
+ if (!databasePath) {
4826
+ throw new Error('Kernel SQLite driver requires a databasePath or broker config databasePath');
4827
+ }
4828
+ return databasePath;
4829
+ }
4830
+
4831
+ function getDatabase(config) {
4832
+ const databasePath = resolveDatabasePath(config);
2281
4833
  if (!db) {
2282
4834
  db = createDatabase(runtime, databasePath);
4835
+ execSql(runtime, db, 'PRAGMA foreign_keys=ON;');
2283
4836
  openedDatabasePath = databasePath;
2284
4837
  } else if (openedDatabasePath !== databasePath) {
2285
4838
  throw new Error(`Kernel SQLite driver is already open for ${openedDatabasePath}`);
@@ -2287,7 +4840,7 @@ function createDriver(runtime, configuredDatabasePath) {
2287
4840
  return db;
2288
4841
  }
2289
4842
 
2290
- return {
4843
+ const driver = {
2291
4844
  runtime: {
2292
4845
  id: runtime.id,
2293
4846
  databaseClassName: runtime.databaseClassName,
@@ -2295,12 +4848,91 @@ function createDriver(runtime, configuredDatabasePath) {
2295
4848
  experimental: runtime.experimental,
2296
4849
  },
2297
4850
  databasePath: configuredDatabasePath,
4851
+ forkConnection(config = {}) {
4852
+ const databasePath = resolveDatabasePath(config);
4853
+ if (databasePath === ':memory:') {
4854
+ const fork = Object.create(driver);
4855
+ Object.defineProperty(fork, 'close', { value() {} });
4856
+ Object.defineProperty(fork, 'transactionQueueKey', { value: driver });
4857
+ return fork;
4858
+ }
4859
+ return createDriver(runtime, databasePath, driverOptions);
4860
+ },
2298
4861
  async exec(statement, config) {
2299
4862
  execSql(runtime, getDatabase(config), statement);
2300
4863
  },
2301
4864
  async queryAll(statement, config) {
2302
4865
  return queryAll(runtime, getDatabase(config), statement);
2303
4866
  },
4867
+ watchOwnerRead(input, config = {}) {
4868
+ return readWatchOwner(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, input);
4869
+ },
4870
+ watchOwnerList(config = {}) {
4871
+ return listWatchOwners(runtime, resolveDatabasePath(config), { ...driverOptions, ...config });
4872
+ },
4873
+ watchOwnerReserveStarting(input, config = {}) {
4874
+ return applyWatchOwnerOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'reserveStarting', input);
4875
+ },
4876
+ watchOwnerReserveReopened(input, config = {}) {
4877
+ return applyWatchOwnerOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'reserveReopened', input);
4878
+ },
4879
+ watchOwnerBindRunning(input, config = {}) {
4880
+ return applyWatchOwnerOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'bindRunning', input);
4881
+ },
4882
+ watchOwnerHeartbeat(input, config = {}) {
4883
+ return applyWatchOwnerOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'heartbeat', input);
4884
+ },
4885
+ watchOwnerRequestStop(input, config = {}) {
4886
+ return applyWatchOwnerOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'requestStop', input);
4887
+ },
4888
+ watchOwnerRecordTerminal(input, config = {}) {
4889
+ return applyWatchOwnerOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'recordTerminal', input);
4890
+ },
4891
+ watchOwnerCompleteTerminal(input, config = {}) {
4892
+ return applyWatchOwnerOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'completeTerminal', input);
4893
+ },
4894
+ watchOwnerAbortStarting(input, config = {}) {
4895
+ return applyWatchOwnerOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'abortStarting', input);
4896
+ },
4897
+ watchOwnerReleaseNonterminal(input, config = {}) {
4898
+ return applyWatchOwnerOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'releaseNonterminal', input);
4899
+ },
4900
+ watchOwnerRecoverDeadStarting(input, config = {}) {
4901
+ return applyWatchOwnerOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'recoverDeadStarting', input);
4902
+ },
4903
+ watchOwnerRecoverDeadWatcher(input, config = {}) {
4904
+ return applyWatchOwnerOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'recoverDeadWatcher', input);
4905
+ },
4906
+ watchOwnerMarkLegacyBlocked(input, config = {}) {
4907
+ return applyWatchOwnerOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'markLegacyBlocked', input);
4908
+ },
4909
+ watchOwnerRecheckLegacyBlocked(input, config = {}) {
4910
+ return applyWatchOwnerOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'recheckLegacyBlocked', input);
4911
+ },
4912
+ watchOwnerImportLegacyStarting(input, config = {}) {
4913
+ return applyWatchOwnerOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'importLegacyStarting', input);
4914
+ },
4915
+ watchOwnerImportLegacyComplete(input, config = {}) {
4916
+ return applyWatchOwnerOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'importLegacyComplete', input);
4917
+ },
4918
+ watchGateRead(_input = {}, config = {}) {
4919
+ return readWatchGate(runtime, resolveDatabasePath(config), { ...driverOptions, ...config });
4920
+ },
4921
+ watchGatePublishQuarantine(input, config = {}) {
4922
+ return applyWatchGateOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'publishQuarantine', input);
4923
+ },
4924
+ watchGateBindSnapshot(input, config = {}) {
4925
+ return applyWatchGateOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'bindSnapshot', input);
4926
+ },
4927
+ watchGatePublishConflict(input, config = {}) {
4928
+ return applyWatchGateOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'publishConflict', input);
4929
+ },
4930
+ watchGateRetryConflict(input, config = {}) {
4931
+ return applyWatchGateOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'retryConflict', input);
4932
+ },
4933
+ watchGateCompleteMigration(input, config = {}) {
4934
+ return applyWatchGateOperation(runtime, resolveDatabasePath(config), { ...driverOptions, ...config }, 'completeMigration', input);
4935
+ },
2304
4936
  async issueOperation(operation, args = [], context = {}, config = {}) {
2305
4937
  const database = getDatabase(config);
2306
4938
  const READ_OPERATIONS = new Set(['ready', 'list', 'show', 'search', 'stats', 'blocked', 'stale', 'orphans', 'lint', 'children', 'owns', 'claims']);
@@ -2396,7 +5028,22 @@ function createDriver(runtime, configuredDatabasePath) {
2396
5028
  registeredAt,
2397
5029
  ],
2398
5030
  );
2399
- return { ok: true, id };
5031
+ const persisted = allParams(
5032
+ runtime,
5033
+ getDatabase(config),
5034
+ 'SELECT * FROM kernel_pr WHERE git_common_dir = ? AND repo = ? AND number = ? LIMIT 1',
5035
+ [row.git_common_dir, row.repo, row.number],
5036
+ )[0];
5037
+ if (!persisted) {
5038
+ throw new Error(`Kernel SQLite driver upsertPr: no kernel_pr row after upsert for ${row.repo}#${row.number}`);
5039
+ }
5040
+ return { ok: true, ...persisted };
5041
+ },
5042
+ async resolvePrLinkage(input, _context = {}, config = {}) {
5043
+ return resolvePrLinkageRow(runtime, getDatabase(config), input);
5044
+ },
5045
+ async readTrace(target, _context = {}, config = {}) {
5046
+ return loadTraceRows(runtime, getDatabase(config), target);
2400
5047
  },
2401
5048
  // The ONE verdict authority WRITE (design §1.2 rule 2) — FRESHEST-HEAD-SHA
2402
5049
  // PRECEDENCE enforced in the WHERE so a verdict computed against a SUPERSEDED head is
@@ -2462,6 +5109,11 @@ function createDriver(runtime, configuredDatabasePath) {
2462
5109
  async loadKernelEventByIdempotencyKey(idempotencyKey, _context = {}, config = {}) {
2463
5110
  return loadKernelEventByIdempotencyKeyRow(runtime, getDatabase(config), idempotencyKey);
2464
5111
  },
5112
+ async loadPrEventsByRunId(runId, _context = {}, config = {}) {
5113
+ return allParams(runtime, getDatabase(config),
5114
+ "SELECT * FROM kernel_events WHERE entity_type = 'pr' AND json_extract(payload_json, '$.run_id') = ? ORDER BY created_at ASC, rowid ASC",
5115
+ [runId]);
5116
+ },
2465
5117
  async insertKernelConflict(conflict, _context = {}, config = {}) {
2466
5118
  return insertKernelConflictRow(runtime, getDatabase(config), conflict);
2467
5119
  },
@@ -2504,6 +5156,69 @@ function createDriver(runtime, configuredDatabasePath) {
2504
5156
  async updateKernelClaimState(claimId, state, _context = {}, config = {}) {
2505
5157
  return updateKernelClaimStateRow(runtime, getDatabase(config), claimId, state);
2506
5158
  },
5159
+ async listActiveClaims(_context = {}, config = {}) {
5160
+ return listActiveKernelClaimRows(runtime, getDatabase(config));
5161
+ },
5162
+ async releaseExactClaim(claim, _evidence = {}, config = {}) {
5163
+ return releaseExactKernelClaimRow(runtime, getDatabase(config), claim);
5164
+ },
5165
+ async releaseExactClaimIfWorktreeMissing(claim, worktree, isMissing, _evidence = {}, config = {}) {
5166
+ return releaseExactKernelClaimIfWorktreeMissing(
5167
+ runtime, getDatabase(config), claim, worktree, isMissing,
5168
+ );
5169
+ },
5170
+ // Explicit operator-only legacy data repair. This surface is never called by
5171
+ // broker initialization or normal claim acquisition: dry-run is immutable,
5172
+ // while apply requires a separately verified backup and approved exact digest.
5173
+ async preflightLegacyClaimRepair(input = {}, config = {}) {
5174
+ return preflightLegacyClaimRepairRow(runtime, getDatabase(config), input);
5175
+ },
5176
+ async applyLegacyClaimRepair(input = {}, config = {}) {
5177
+ assertClaimRepairReplayInput(input);
5178
+ const databasePath = resolveDatabasePath(config);
5179
+ return withClaimRepairApplyQueue(databasePath, async () => {
5180
+ const database = createDatabase(runtime, databasePath);
5181
+ const hardenPath = driverOptions.hardenPath || hardenBackupPermissions;
5182
+ const hardenPaths = driverOptions.hardenPaths
5183
+ || (driverOptions.hardenPath ? driverOptions.hardenPath.batch : hardenBackupPermissionsBatch);
5184
+ try {
5185
+ execSql(runtime, database, 'PRAGMA foreign_keys=ON;');
5186
+ const replayed = replayStoredClaimRepairReceipt(
5187
+ runtime,
5188
+ database,
5189
+ input.approvedDigest,
5190
+ input.observedAt,
5191
+ input.backupPath,
5192
+ );
5193
+ if (replayed) {
5194
+ await verifyClaimRepairRecovery(replayed, input.backupPath, databasePath, { hardenPath });
5195
+ return replayed;
5196
+ }
5197
+ if (!input.backupPath) {
5198
+ throw new ClaimRepairError(
5199
+ 'CLAIM_REPAIR_BACKUP_PROOF_REQUIRED',
5200
+ 'Apply requires the path to a separately verified SQLite backup',
5201
+ );
5202
+ }
5203
+ assertSafeBackupDestination(databasePath, input.backupPath);
5204
+ const backupProof = await verifyClaimRepairBackup({
5205
+ backupPath: input.backupPath,
5206
+ observedAt: input.observedAt,
5207
+ openDriver: restorePath => createDriver(runtime, restorePath),
5208
+ hardenPath,
5209
+ hardenPaths,
5210
+ });
5211
+ return await applyLegacyClaimRepairRow(
5212
+ runtime,
5213
+ database,
5214
+ { ...input, databasePath, backupProof },
5215
+ driverOptions,
5216
+ );
5217
+ } finally {
5218
+ closeDatabase(database);
5219
+ }
5220
+ }, driverOptions.claimRepairApplyQueue);
5221
+ },
2507
5222
  // commitGuardedAccept invokes this (typeof-guarded) INSIDE its BEGIN IMMEDIATE
2508
5223
  // transaction to apply an accepted issue event to the authority tables. The
2509
5224
  // returned summary ({id, revision, comment_id?}) flows back through
@@ -2551,6 +5266,12 @@ function createDriver(runtime, configuredDatabasePath) {
2551
5266
  recordStageTransition(input, config = {}) {
2552
5267
  return recordStageTransitionRow(runtime, getDatabase(config), input);
2553
5268
  },
5269
+ recordPlanSnapshotTransition(input, config = {}) {
5270
+ return recordPlanSnapshotTransitionRow(runtime, getDatabase(config), input);
5271
+ },
5272
+ loadPlanSnapshot(filter = {}, config = {}) {
5273
+ return loadPlanSnapshotRow(runtime, getDatabase(config), filter.issue_id);
5274
+ },
2554
5275
  listStageRuns(filter = {}, config = {}) {
2555
5276
  return listStageRunRows(runtime, getDatabase(config), filter.issue_id);
2556
5277
  },
@@ -2577,7 +5298,7 @@ function createDriver(runtime, configuredDatabasePath) {
2577
5298
  searchMemoriesRanked(query, limit, config = {}) {
2578
5299
  const database = getDatabase(config);
2579
5300
  ensureMemorySchema(database);
2580
- return searchMemoryRowsRanked(runtime, database, query, limit);
5301
+ return searchMemoryRowsRanked(runtime, database, query, limit, config);
2581
5302
  },
2582
5303
  // Relevance-only BM25 recall that also returns the raw bm25 `score` per entry, so a
2583
5304
  // caller can apply a relevance floor. A no-match/empty query returns [] (no recency
@@ -2592,27 +5313,96 @@ function createDriver(runtime, configuredDatabasePath) {
2592
5313
  recentMemories(limit, options = {}, config = {}) {
2593
5314
  const database = getDatabase(config);
2594
5315
  ensureMemorySchema(database);
2595
- return recentMemoryRows(runtime, database, limit, options.agents);
5316
+ return recentMemoryRows(runtime, database, limit, options);
2596
5317
  },
2597
- // Total stored memories (optionally scoped by `options.agents`) — lets recall report
5318
+ // Total stored memories (optionally scoped by `options.agents` and `options.kind`) — lets recall report
2598
5319
  // "showing N of TOTAL".
2599
5320
  countMemories(options = {}, config = {}) {
2600
5321
  const database = getDatabase(config);
2601
5322
  ensureMemorySchema(database);
2602
- return countMemoryRows(runtime, database, options.agents);
5323
+ return countMemoryRows(runtime, database, options);
5324
+ },
5325
+ appendUsageEvidence(event, config = {}) {
5326
+ const database = getDatabase(config);
5327
+ ensureUsageEvidenceSchema(database);
5328
+ return appendUsageEvidence(createUsageEvidenceAdapter(runtime, database), event);
5329
+ },
5330
+ rebuildUsageProjection(config = {}) {
5331
+ const database = getDatabase(config);
5332
+ ensureUsageEvidenceSchema(database);
5333
+ return rebuildUsageProjection(createUsageEvidenceAdapter(runtime, database));
5334
+ },
5335
+ loadUsageProjection(memoryId, config = {}) {
5336
+ const database = getDatabase(config);
5337
+ ensureUsageEvidenceSchema(database);
5338
+ return allParams(runtime, database,
5339
+ 'SELECT scope, memory_id, last_used_at, use_count FROM memory_usage_projection WHERE memory_id = ?', [memoryId])[0] || null;
5340
+ },
5341
+ loadUsageProjections(memoryIds = [], config = {}) {
5342
+ const database = getDatabase(config);
5343
+ ensureUsageEvidenceSchema(database);
5344
+ const ids = [...new Set(Array.isArray(memoryIds) ? memoryIds : [])];
5345
+ if (ids.length === 0) return [];
5346
+ return allParams(runtime, database,
5347
+ `SELECT scope, memory_id, last_used_at, use_count FROM memory_usage_projection WHERE memory_id IN (${ids.map(() => '?').join(', ')})`, ids);
2603
5348
  },
2604
5349
  listMemories(config = {}) {
2605
5350
  const database = getDatabase(config);
2606
5351
  ensureMemorySchema(database);
2607
5352
  return listMemoryRows(runtime, database);
2608
5353
  },
5354
+ // --- Memory-owned monitor durability. Public callers use @forge/memory, while
5355
+ // these primitives independently reject malformed/private data as defense in depth.
5356
+ async appendMonitorEvent(envelope, targets = [], config = {}) {
5357
+ const database = getDatabase(config);
5358
+ const safeEnvelope = assertMonitorEnvelope(envelope, 'forge.memory.monitor-event.v1');
5359
+ return appendMonitorEventRow(runtime, database, safeEnvelope, normalizeMonitorTargets(targets), config);
5360
+ },
5361
+ async recordMonitorDeliveryReceipt(envelope, config = {}) {
5362
+ const database = getDatabase(config);
5363
+ const safeEnvelope = assertMonitorEnvelope(envelope, 'forge.memory.delivery-receipt.v1');
5364
+ assertMonitorTarget(safeEnvelope.payload.target);
5365
+ return recordMonitorDeliveryReceiptRow(runtime, database, safeEnvelope, config);
5366
+ },
5367
+ async recordMonitorTerminalReceipt(envelope, config = {}) {
5368
+ const database = getDatabase(config);
5369
+ const safeEnvelope = assertMonitorEnvelope(envelope, 'forge.memory.monitor-receipt.v1');
5370
+ return recordMonitorTerminalReceiptRow(runtime, database, safeEnvelope, config);
5371
+ },
5372
+ async getMonitorEvent(eventId, config = {}) {
5373
+ if (typeof eventId !== 'string' || !eventId || eventId.length > 255) {
5374
+ throw new TypeError('eventId must be a bounded non-empty string');
5375
+ }
5376
+ return getMonitorEventRow(runtime, getDatabase(config), eventId);
5377
+ },
5378
+ async readMonitorEventTail(monitorId, options = {}, config = {}) {
5379
+ return readMonitorEventTailRows(runtime, getDatabase(config), monitorId, options);
5380
+ },
5381
+ async readMonitorDeliveryState(monitorId, options = {}, config = {}) {
5382
+ return readMonitorDeliveryStateRows(runtime, getDatabase(config), monitorId, options);
5383
+ },
5384
+ async listMonitorEvents(monitorId, config = {}) {
5385
+ return listMonitorEventRows(runtime, getDatabase(config), monitorId);
5386
+ },
5387
+ async backup(destinationPath, config = {}, backupOptions = {}) {
5388
+ const databasePath = resolveDatabasePath(config);
5389
+ return createSafeBackup(
5390
+ runtime,
5391
+ getDatabase(config),
5392
+ databasePath,
5393
+ destinationPath,
5394
+ { ...driverOptions, ...backupOptions },
5395
+ );
5396
+ },
2609
5397
  close() {
2610
5398
  closeDatabase(db);
2611
5399
  db = null;
2612
5400
  openedDatabasePath = null;
2613
5401
  memorySchemaEnsured = false;
5402
+ usageEvidenceSchemaEnsured = false;
2614
5403
  },
2615
5404
  };
5405
+ return driver;
2616
5406
  }
2617
5407
 
2618
5408
  function assertCapability(runtime, capability, detail) {
@@ -2719,13 +5509,354 @@ async function createBackup(runtime, db, backupPath) {
2719
5509
  if (typeof db.serialize !== 'function') {
2720
5510
  throw new Error('bun:sqlite Database.serialize() is unavailable');
2721
5511
  }
2722
- fs.writeFileSync(backupPath, db.serialize());
5512
+ fs.writeFileSync(backupPath, db.serialize(), { mode: 0o600 });
2723
5513
  return;
2724
5514
  }
2725
5515
 
2726
5516
  throw new Error(`Unsupported builtin SQLite runtime: ${runtime.id}`);
2727
5517
  }
2728
5518
 
5519
+ function comparableFilePath(filePath) {
5520
+ const resolved = path.resolve(filePath);
5521
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
5522
+ }
5523
+
5524
+ function assertSafeBackupDestination(databasePath, backupPath) {
5525
+ if (!backupPath || databasePath === ':memory:' || String(databasePath).startsWith('file:')) {
5526
+ throw new Error('SQLite backup requires distinct file-backed source and destination paths');
5527
+ }
5528
+ const source = comparableFilePath(databasePath);
5529
+ const destination = comparableFilePath(backupPath);
5530
+ if ([source, `${source}-wal`, `${source}-shm`, `${source}-journal`].includes(destination)) {
5531
+ throw new Error('Backup destination must not alias the live SQLite database or its sidecars');
5532
+ }
5533
+ if (fs.existsSync(backupPath)) {
5534
+ const destinationStat = fs.statSync(backupPath, { bigint: true });
5535
+ for (const sourcePath of [databasePath, `${databasePath}-wal`, `${databasePath}-shm`, `${databasePath}-journal`]) {
5536
+ if (!fs.existsSync(sourcePath)) continue;
5537
+ const sourceStat = fs.statSync(sourcePath, { bigint: true });
5538
+ if (sourceStat.dev === destinationStat.dev && sourceStat.ino === destinationStat.ino) {
5539
+ throw new Error('Backup destination must not alias the live SQLite database or its sidecars');
5540
+ }
5541
+ }
5542
+ }
5543
+ }
5544
+
5545
+ function verifyBackupFile(runtime, backupPath) {
5546
+ const backupDb = createDatabase(runtime, backupPath);
5547
+ try {
5548
+ const row = queryOne(runtime, backupDb, 'PRAGMA integrity_check;');
5549
+ if (String(row.integrity_check || '').toLowerCase() !== 'ok') {
5550
+ throw new Error('SQLite backup integrity verification failed');
5551
+ }
5552
+ } finally {
5553
+ closeDatabase(backupDb);
5554
+ }
5555
+ }
5556
+
5557
+ async function hardenBackupPermissions(filePath, options = {}) {
5558
+ const platform = options.platform || process.platform;
5559
+ const fsApi = options.fsApi || fs;
5560
+ const effectiveUserId = options.effectiveUserId ?? (typeof process.geteuid === 'function' ? process.geteuid() : null);
5561
+ try {
5562
+ if (platform === 'win32') {
5563
+ const aclSecurer = options.aclSecurer || secureWindowsPathAcl;
5564
+ await aclSecurer(filePath);
5565
+ return;
5566
+ }
5567
+ const initial = fsApi.statSync(filePath);
5568
+ if (effectiveUserId !== null && Number(initial.uid) !== Number(effectiveUserId)) {
5569
+ throw new Error('path is not owned by the current effective user');
5570
+ }
5571
+ const ownerMode = typeof initial.isDirectory === 'function' && initial.isDirectory() ? 0o700 : 0o600;
5572
+ fsApi.chmodSync(filePath, ownerMode);
5573
+ if ((Number(fsApi.statSync(filePath).mode) & 0o077) !== 0) {
5574
+ throw new Error(`mode remains broader than ${ownerMode.toString(8)}`);
5575
+ }
5576
+ } catch (error) {
5577
+ throw new ClaimRepairError(
5578
+ 'CLAIM_REPAIR_BACKUP_PERMISSIONS',
5579
+ 'SQLite backup could not be secured with owner-only permissions',
5580
+ { cause: error.message || String(error) },
5581
+ );
5582
+ }
5583
+ }
5584
+
5585
+ async function hardenBackupPermissionsBatch(filePaths, options = {}) {
5586
+ if (!Array.isArray(filePaths) || filePaths.length === 0) return;
5587
+ const paths = [...new Set(filePaths)];
5588
+ const platform = options.platform || process.platform;
5589
+ if (platform === 'win32' && !options.aclSecurer && !options.fsApi) {
5590
+ try {
5591
+ // One owner readback serves the whole batch after each DACL is secured.
5592
+ await secureWindowsPathsAcl(paths);
5593
+ return;
5594
+ } catch (error) {
5595
+ throw new ClaimRepairError(
5596
+ 'CLAIM_REPAIR_BACKUP_PERMISSIONS',
5597
+ 'SQLite backup could not be secured with owner-only permissions',
5598
+ { cause: error.message || String(error) },
5599
+ );
5600
+ }
5601
+ }
5602
+ for (const filePath of paths) await hardenBackupPermissions(filePath, options);
5603
+ }
5604
+
5605
+ const WINDOWS_PRIVATE_ACL_SCRIPT_PATH = path.join(__dirname, 'windows-private-acl.js');
5606
+ const WINDOWS_PRIVATE_ACL_SCRIPT_RELATIVE_PATH = path.join('lib', 'kernel', 'windows-private-acl.js');
5607
+ const WINDOWS_PRIVATE_ACL_SCRIPT_SHA256 = '97f44c740bb843fcca0ea158e21d201b4e34b81eb1636a6a9e9410803a31e349';
5608
+
5609
+ function resolveWindowsSystemExecutable(name, environment = process.env) {
5610
+ const systemRoot = environment.SystemRoot;
5611
+ if (typeof systemRoot !== 'string' || !path.win32.isAbsolute(systemRoot)) {
5612
+ throw new Error('Windows backup hardening requires an absolute SystemRoot');
5613
+ }
5614
+ return path.win32.join(systemRoot, 'System32', name);
5615
+ }
5616
+
5617
+ function resolveWindowsCscriptPath(environment = process.env) {
5618
+ return resolveWindowsSystemExecutable('cscript.exe', environment);
5619
+ }
5620
+
5621
+ function windowsAclScriptPath(fsApi = fs, packageRoot = getPackageRoot()) {
5622
+ const scriptPath = path.join(packageRoot, WINDOWS_PRIVATE_ACL_SCRIPT_RELATIVE_PATH);
5623
+ const script = fsApi.readFileSync(scriptPath);
5624
+ const digest = createHash('sha256').update(script).digest('hex');
5625
+ if (digest !== WINDOWS_PRIVATE_ACL_SCRIPT_SHA256) {
5626
+ throw new Error('Windows ACL hardening script integrity check failed');
5627
+ }
5628
+ return scriptPath;
5629
+ }
5630
+
5631
+ async function runWindowsProcess(command, args, options = {}) {
5632
+ const runtime = options.runtime || (process.versions.bun ? 'bun' : 'node');
5633
+ const timeout = options.timeout;
5634
+ if (!Number.isFinite(timeout) || timeout <= 0) throw new Error('Windows ACL hardening subprocess timed out');
5635
+ const setTimer = options.setTimer || setTimeout;
5636
+ const clearTimer = options.clearTimer || clearTimeout;
5637
+ let timedOut = false;
5638
+ let exitCode;
5639
+ let stdout;
5640
+
5641
+ if (runtime === 'bun') {
5642
+ const spawn = options.bunSpawn || globalThis.Bun.spawn;
5643
+ const child = spawn([command, ...args], {
5644
+ cwd: options.cwd,
5645
+ env: options.env,
5646
+ stderr: 'ignore',
5647
+ stdout: options.captureStdout ? 'pipe' : 'ignore',
5648
+ windowsHide: false,
5649
+ });
5650
+ const timer = setTimer(() => {
5651
+ timedOut = true;
5652
+ child.kill('SIGKILL');
5653
+ }, timeout);
5654
+ try {
5655
+ const output = options.captureStdout ? new Response(child.stdout).text() : Promise.resolve('');
5656
+ [exitCode, stdout] = await Promise.all([child.exited, output]);
5657
+ } finally {
5658
+ clearTimer(timer);
5659
+ }
5660
+ } else {
5661
+ const spawn = options.nodeSpawn || nodeSpawn;
5662
+ const child = spawn(command, args, {
5663
+ cwd: options.cwd,
5664
+ env: options.env,
5665
+ stdio: ['ignore', options.captureStdout ? 'pipe' : 'ignore', 'ignore'],
5666
+ windowsHide: false,
5667
+ });
5668
+ const chunks = [];
5669
+ if (options.captureStdout) child.stdout.on('data', chunk => chunks.push(Buffer.from(chunk)));
5670
+ const timer = setTimer(() => {
5671
+ timedOut = true;
5672
+ child.kill('SIGKILL');
5673
+ }, timeout);
5674
+ try {
5675
+ exitCode = await new Promise((resolve, reject) => {
5676
+ child.once('error', reject);
5677
+ child.once('close', resolve);
5678
+ });
5679
+ stdout = options.captureStdout ? Buffer.concat(chunks).toString('utf8') : '';
5680
+ } finally {
5681
+ clearTimer(timer);
5682
+ }
5683
+ }
5684
+
5685
+ if (timedOut) throw new Error('Windows ACL hardening subprocess timed out');
5686
+ if (exitCode !== 0) throw new Error(`Windows ACL hardening subprocess failed (exit ${exitCode})`);
5687
+ return stdout;
5688
+ }
5689
+
5690
+ const WHOAMI_SID_CSV_ERROR = 'Windows ACL hardening could not resolve the current SID';
5691
+
5692
+ function readQuotedCsvCharacter(input, index, state) {
5693
+ const character = input[index];
5694
+ if (character === '\r' || character === '\n') {
5695
+ throw new Error(WHOAMI_SID_CSV_ERROR);
5696
+ }
5697
+ if (character !== '"') {
5698
+ state.field += character;
5699
+ return index;
5700
+ }
5701
+ if (input[index + 1] === '"') {
5702
+ state.field += '"';
5703
+ return index + 1;
5704
+ }
5705
+ state.inQuotes = false;
5706
+ state.closedQuote = true;
5707
+ return index;
5708
+ }
5709
+
5710
+ function readUnquotedCsvCharacter(character, state) {
5711
+ if (character === '"') {
5712
+ if (state.field !== '') {
5713
+ throw new Error(WHOAMI_SID_CSV_ERROR);
5714
+ }
5715
+ state.inQuotes = true;
5716
+ return;
5717
+ }
5718
+ if (character === ',') {
5719
+ state.fields.push(state.field);
5720
+ state.field = '';
5721
+ return;
5722
+ }
5723
+ if (character === '\r' || character === '\n') {
5724
+ throw new Error(WHOAMI_SID_CSV_ERROR);
5725
+ }
5726
+ state.field += character;
5727
+ }
5728
+
5729
+ function parseWhoamiCsvRecord(stdout) {
5730
+ const input = String(stdout).replace(/\r?\n$/, '');
5731
+ const state = { fields: [], field: '', inQuotes: false, closedQuote: false };
5732
+ for (let index = 0; index < input.length; index += 1) {
5733
+ const character = input[index];
5734
+ if (state.inQuotes) {
5735
+ index = readQuotedCsvCharacter(input, index, state);
5736
+ } else if (state.closedQuote) {
5737
+ if (character !== ',') {
5738
+ throw new Error(WHOAMI_SID_CSV_ERROR);
5739
+ }
5740
+ state.fields.push(state.field);
5741
+ state.field = '';
5742
+ state.closedQuote = false;
5743
+ } else {
5744
+ readUnquotedCsvCharacter(character, state);
5745
+ }
5746
+ }
5747
+ if (state.inQuotes) throw new Error(WHOAMI_SID_CSV_ERROR);
5748
+ state.fields.push(state.field);
5749
+ return state.fields;
5750
+ }
5751
+
5752
+ function parseWhoamiSid(stdout) {
5753
+ const fields = parseWhoamiCsvRecord(stdout);
5754
+ const sid = fields[1];
5755
+ if (fields.length !== 2 || !/^S-1-[0-9]+(?:-[0-9]+)+$/.test(sid || '')) {
5756
+ throw new Error('Windows ACL hardening could not resolve the current SID');
5757
+ }
5758
+ return sid;
5759
+ }
5760
+
5761
+ async function secureWindowsPathsAcl(filePaths, options = {}) {
5762
+ if (!Array.isArray(filePaths) || filePaths.length === 0 || filePaths.length > 128) {
5763
+ throw new Error('Windows ACL hardening requires one to 128 targets');
5764
+ }
5765
+ for (const filePath of filePaths) {
5766
+ if (typeof filePath !== 'string' || !filePath || filePath.includes('\0')) {
5767
+ throw new Error('Windows ACL hardening requires valid target paths');
5768
+ }
5769
+ }
5770
+ const targetPaths = [...new Set(filePaths.map(filePath => path.win32.resolve(filePath)))];
5771
+ const environment = options.environment || process.env;
5772
+ const cscriptPath = resolveWindowsCscriptPath(environment);
5773
+ const whoamiPath = resolveWindowsSystemExecutable('whoami.exe', environment);
5774
+ const systemDirectory = path.win32.dirname(cscriptPath);
5775
+ const scriptPath = windowsAclScriptPath(options.fsApi || fs, options.packageRoot || getPackageRoot());
5776
+ const now = options.now || Date.now;
5777
+ const deadline = now() + (options.timeout || 15_000);
5778
+ const remainingTimeout = () => {
5779
+ const remaining = deadline - now();
5780
+ if (remaining <= 0) throw new Error('Windows ACL hardening subprocess timed out');
5781
+ return remaining;
5782
+ };
5783
+ const processOptions = {
5784
+ bunSpawn: options.bunSpawn,
5785
+ clearTimer: options.clearTimer,
5786
+ cwd: systemDirectory,
5787
+ nodeSpawn: options.nodeSpawn,
5788
+ runtime: options.runtime,
5789
+ setTimer: options.setTimer,
5790
+ };
5791
+ const sid = parseWhoamiSid(await runWindowsProcess(whoamiPath, ['/user', '/fo', 'csv', '/nh'], {
5792
+ ...processOptions,
5793
+ captureStdout: true,
5794
+ env: { SystemRoot: environment.SystemRoot },
5795
+ timeout: remainingTimeout(),
5796
+ }));
5797
+ const childEnvironment = {
5798
+ FORGE_PRIVATE_ACL_COUNT: String(targetPaths.length),
5799
+ FORGE_PRIVATE_ACL_SID: sid,
5800
+ SystemRoot: environment.SystemRoot,
5801
+ };
5802
+ targetPaths.forEach((filePath, index) => {
5803
+ childEnvironment[`FORGE_PRIVATE_ACL_TARGET_${index}`] = filePath;
5804
+ });
5805
+ await runWindowsProcess(cscriptPath, ['//B', '//Nologo', '//E:JScript', '//T:14', scriptPath], {
5806
+ ...processOptions,
5807
+ captureStdout: false,
5808
+ env: childEnvironment,
5809
+ timeout: remainingTimeout(),
5810
+ });
5811
+ }
5812
+
5813
+ async function secureWindowsPathAcl(filePath) {
5814
+ await secureWindowsPathsAcl([filePath]);
5815
+ }
5816
+
5817
+ async function createPrivateBackupDirectory(backupPath, options = {}) {
5818
+ const directory = fs.mkdtempSync(path.join(path.dirname(backupPath), '.forge-sqlite-backup-'));
5819
+ try {
5820
+ if (process.platform === 'win32') await (options.hardenPath || secureWindowsPathAcl)(directory);
5821
+ else {
5822
+ fs.chmodSync(directory, 0o700);
5823
+ if ((Number(fs.statSync(directory).mode) & 0o077) !== 0) throw new Error('mode remains broader than 0700');
5824
+ }
5825
+ } catch (error) {
5826
+ fs.rmSync(directory, { recursive: true, force: true });
5827
+ throw new ClaimRepairError(
5828
+ 'CLAIM_REPAIR_BACKUP_PERMISSIONS',
5829
+ 'SQLite backup workspace could not be secured with owner-only permissions',
5830
+ { cause: error.message || String(error) },
5831
+ );
5832
+ }
5833
+ return directory;
5834
+ }
5835
+
5836
+ async function createSafeBackup(runtime, db, databasePath, backupPath, options = {}) {
5837
+ assertSafeBackupDestination(databasePath, backupPath);
5838
+ ensureFileBackedDatabaseDirectory(backupPath);
5839
+ const hardenPath = options.hardenPath || hardenBackupPermissions;
5840
+ const tempDirectory = await createPrivateBackupDirectory(backupPath, { hardenPath });
5841
+ const tempPath = path.join(tempDirectory, `${path.basename(backupPath)}.${process.pid}.${randomUUID()}.tmp`);
5842
+ const backupWriter = options.backupWriter || createBackup;
5843
+ const backupVerifier = options.backupVerifier || verifyBackupFile;
5844
+ const backupReplacer = options.backupReplacer || (options.noReplace
5845
+ ? ((source, destination) => (process.platform === 'win32'
5846
+ ? fs.linkSync(source, destination)
5847
+ : fs.copyFileSync(source, destination, fs.constants.COPYFILE_EXCL)))
5848
+ : ((source, destination) => fs.renameSync(source, destination)));
5849
+ try {
5850
+ await backupWriter(runtime, db, tempPath);
5851
+ await hardenPath(tempPath);
5852
+ await backupVerifier(runtime, tempPath);
5853
+ await backupReplacer(tempPath, backupPath);
5854
+ await hardenPath(backupPath);
5855
+ } finally {
5856
+ fs.rmSync(tempDirectory, { recursive: true, force: true });
5857
+ }
5858
+ }
5859
+
2729
5860
  async function validateBackup(runtime, db, backupPath) {
2730
5861
  const tableName = createProbeTableName('forge_backup_probe');
2731
5862
  try {
@@ -2800,7 +5931,7 @@ async function validateBuiltinSQLiteRuntimeDriver(options = {}, deps = {}) {
2800
5931
 
2801
5932
  function createBuiltinSQLiteDriver(options = {}, deps = {}) {
2802
5933
  const runtime = options.runtime || selectBuiltinSQLiteRuntime(deps);
2803
- return createDriver(runtime, options.databasePath);
5934
+ return createDriver(runtime, options.databasePath, options);
2804
5935
  }
2805
5936
 
2806
5937
  module.exports = {
@@ -2808,7 +5939,15 @@ module.exports = {
2808
5939
  CONFLICT_SIGNAL,
2809
5940
  classifyConflictSignal,
2810
5941
  createBuiltinSQLiteDriver,
5942
+ hardenBackupPermissions,
5943
+ hardenBackupPermissionsBatch,
2811
5944
  requireSqliteRuntimeModule,
5945
+ resolveWindowsCscriptPath,
5946
+ secureWindowsPathsAcl,
2812
5947
  selectBuiltinSQLiteRuntime,
5948
+ syncClaimRepairRecoveryDirectory,
2813
5949
  validateBuiltinSQLiteRuntimeDriver,
5950
+ WINDOWS_PRIVATE_ACL_SCRIPT_PATH,
5951
+ WINDOWS_PRIVATE_ACL_SCRIPT_SHA256,
5952
+ _runWindowsProcess: runWindowsProcess,
2814
5953
  };