@ornexus/neocortex 4.60.66 → 4.60.68

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 (236) hide show
  1. package/dist/release-source.json +1 -1
  2. package/dist/sbom.cdx.json +1598 -1640
  3. package/install.ps1 +1 -1
  4. package/install.sh +2 -1
  5. package/package.json +10 -7
  6. package/packages/client/dist/adapters/adapter-registry.js +108 -1
  7. package/packages/client/dist/adapters/antigravity-adapter.js +77 -2
  8. package/packages/client/dist/adapters/claude-code-adapter.js +79 -3
  9. package/packages/client/dist/adapters/codex-adapter.js +80 -2
  10. package/packages/client/dist/adapters/cursor-adapter.js +115 -4
  11. package/packages/client/dist/adapters/gemini-adapter.js +71 -2
  12. package/packages/client/dist/adapters/grok-adapter.js +42 -2
  13. package/packages/client/dist/adapters/index.js +22 -1
  14. package/packages/client/dist/adapters/platform-detector.js +212 -1
  15. package/packages/client/dist/adapters/target-adapter.js +12 -0
  16. package/packages/client/dist/adapters/vscode-adapter.js +72 -2
  17. package/packages/client/dist/agent/refresh-stubs.js +417 -2
  18. package/packages/client/dist/agent/update-agent-yaml.js +102 -1
  19. package/packages/client/dist/agent/update-description.js +251 -1
  20. package/packages/client/dist/cache/crypto-utils.js +78 -1
  21. package/packages/client/dist/cache/encrypted-cache.js +108 -1
  22. package/packages/client/dist/cache/in-memory-asset-cache.js +73 -1
  23. package/packages/client/dist/cache/index.js +13 -1
  24. package/packages/client/dist/cache/protected-pi-boundary.js +44 -1
  25. package/packages/client/dist/checkpoint/checkpoint-client-reader.js +127 -2
  26. package/packages/client/dist/checkpoint/index.js +11 -1
  27. package/packages/client/dist/checkpoint/shared-checkpoint-types.js +32 -1
  28. package/packages/client/dist/cli.js +303 -3
  29. package/packages/client/dist/commands/activate.js +394 -8
  30. package/packages/client/dist/commands/architecture-answers.js +274 -7
  31. package/packages/client/dist/commands/architecture-memory-terminal.js +253 -4
  32. package/packages/client/dist/commands/architecture-non-ui-continuation.js +100 -1
  33. package/packages/client/dist/commands/architecture-root-binding.js +51 -1
  34. package/packages/client/dist/commands/cache-status.js +112 -2
  35. package/packages/client/dist/commands/config.js +88 -1
  36. package/packages/client/dist/commands/doctor.d.ts +135 -0
  37. package/packages/client/dist/commands/doctor.js +635 -0
  38. package/packages/client/dist/commands/invoke.d.ts +18 -0
  39. package/packages/client/dist/commands/invoke.js +7140 -142
  40. package/packages/client/dist/commands/legacy-epic-terminal.js +71 -1
  41. package/packages/client/dist/commands/legacy-story-branch.js +96 -1
  42. package/packages/client/dist/commands/legacy-story-pr.js +276 -1
  43. package/packages/client/dist/commands/mutation-qa-checkpoint.js +285 -5
  44. package/packages/client/dist/commands/mutation-qa-gate.js +54 -1
  45. package/packages/client/dist/commands/mutation-qa-git.js +41 -1
  46. package/packages/client/dist/commands/mutation-qa-policy.js +102 -1
  47. package/packages/client/dist/commands/mutation-qa-run.js +219 -3
  48. package/packages/client/dist/commands/mutation-qa-verdict.js +149 -1
  49. package/packages/client/dist/commands/mutation-qa.js +220 -1
  50. package/packages/client/dist/commands/native-agent.js +136 -2
  51. package/packages/client/dist/commands/planning-diagnosis.js +43 -1
  52. package/packages/client/dist/commands/planning-generation.js +53 -1
  53. package/packages/client/dist/commands/planning-memory.js +40 -1
  54. package/packages/client/dist/commands/planning-research.js +42 -1
  55. package/packages/client/dist/commands/private-session.js +75 -1
  56. package/packages/client/dist/commands/property-qa.js +139 -1
  57. package/packages/client/dist/commands/qa-control.js +16 -1
  58. package/packages/client/dist/commands/qa-scan-browser-launch.js +55 -1
  59. package/packages/client/dist/commands/qa-scan-browser-route.js +28 -1
  60. package/packages/client/dist/commands/qa-scan-capture.js +663 -5
  61. package/packages/client/dist/commands/qa-scan-discovery.js +376 -3
  62. package/packages/client/dist/commands/qa-scan-review.js +826 -5
  63. package/packages/client/dist/commands/qa-synthetic-backend-capture.js +266 -1
  64. package/packages/client/dist/commands/qa-synthetic-backend-review.js +297 -1
  65. package/packages/client/dist/commands/qa-synthetic-backend.js +340 -2
  66. package/packages/client/dist/commands/qa-synthetic-broker-port.js +85 -1
  67. package/packages/client/dist/commands/qa-synthetic-browser.js +39 -1
  68. package/packages/client/dist/commands/refactor-characterization.js +671 -21
  69. package/packages/client/dist/commands/refactor-commit.js +399 -13
  70. package/packages/client/dist/commands/refactor-control.js +142 -3
  71. package/packages/client/dist/commands/refactor-cpp-assertions.js +93 -3
  72. package/packages/client/dist/commands/refactor-review.js +283 -5
  73. package/packages/client/dist/commands/refactor-root.js +588 -7
  74. package/packages/client/dist/commands/refresh-memory.js +156 -3
  75. package/packages/client/dist/commands/shared-architecture-non-ui.js +24 -1
  76. package/packages/client/dist/commands/story-checkpoint.js +171 -1
  77. package/packages/client/dist/config/profile-directory.js +13 -1
  78. package/packages/client/dist/config/resolver-selection.js +278 -1
  79. package/packages/client/dist/config/secure-config.js +281 -12
  80. package/packages/client/dist/config.js +223 -2
  81. package/packages/client/dist/constants.js +25 -1
  82. package/packages/client/dist/context/context-collector.js +222 -2
  83. package/packages/client/dist/context/context-sanitizer.js +145 -1
  84. package/packages/client/dist/continuity/autonomous-governance.js +285 -1
  85. package/packages/client/dist/continuity/continuity-client-state-store.js +1587 -1
  86. package/packages/client/dist/continuity/invoke-hooks.js +129 -1
  87. package/packages/client/dist/continuity/migrations/001-initial-schema.js +12 -2
  88. package/packages/client/dist/continuity/migrations/002-processing-output-summary-indexes.js +12 -2
  89. package/packages/client/dist/continuity/migrations/003-headless-execution-attempts.js +12 -2
  90. package/packages/client/dist/continuity/migrations/004-outbox-delivery-state.js +12 -2
  91. package/packages/client/dist/continuity/migrations/005-bounded-headless-queue.js +12 -2
  92. package/packages/client/dist/continuity/migrations/006-native-agent-dispatches.js +10 -2
  93. package/packages/client/dist/continuity/native-agent-store.js +69 -1
  94. package/packages/client/dist/continuity/native-agent-types.js +90 -1
  95. package/packages/client/dist/continuity/outbox.js +117 -1
  96. package/packages/client/dist/continuity/runner-bootstrap-policy.js +172 -1
  97. package/packages/client/dist/continuity/sqlite-store.js +2536 -81
  98. package/packages/client/dist/effects/invoke-effect-gateways.js +62 -1
  99. package/packages/client/dist/errors/error-messages.js +228 -2
  100. package/packages/client/dist/errors/frontmatter-diagnostics.js +125 -1
  101. package/packages/client/dist/evidence/collector.js +347 -1
  102. package/packages/client/dist/evidence/index.js +1 -1
  103. package/packages/client/dist/evidence/types.js +2 -0
  104. package/packages/client/dist/fs/atomic-state-replace.js +152 -1
  105. package/packages/client/dist/generated/command-registry.generated.js +4076 -1
  106. package/packages/client/dist/generated/public-projections.generated.js +149 -1
  107. package/packages/client/dist/git/direct-yolo-c01-retry.js +138 -4
  108. package/packages/client/dist/git/direct-yolo-checkpoint.js +1234 -4
  109. package/packages/client/dist/git/git-lifecycle-step-ids.js +14 -1
  110. package/packages/client/dist/git/git-lifecycle-store.js +1938 -9
  111. package/packages/client/dist/git/legacy-story-branch-checkpoint.js +259 -9
  112. package/packages/client/dist/git/legacy-story-branch-setup.js +348 -4
  113. package/packages/client/dist/git/legacy-story-pr.js +3538 -70
  114. package/packages/client/dist/git/secure-filesystem-boundary.js +182 -5
  115. package/packages/client/dist/graph-retrieval/pre-command-hook.js +284 -1
  116. package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.js +237 -1
  117. package/packages/client/dist/i18n/first-run.js +60 -2
  118. package/packages/client/dist/index.js +47 -1
  119. package/packages/client/dist/invocation/invoke-input.js +24 -1
  120. package/packages/client/dist/license/index.js +5 -1
  121. package/packages/client/dist/license/license-client.js +304 -1
  122. package/packages/client/dist/machine/fingerprint.js +163 -2
  123. package/packages/client/dist/machine/index.js +5 -1
  124. package/packages/client/dist/memory/project-memory-path-scope.js +62 -1
  125. package/packages/client/dist/memory/project-memory-writer.js +1007 -40
  126. package/packages/client/dist/memory/shared-project-memory-types.js +253 -2
  127. package/packages/client/dist/migrations/project-identity-capability.js +50 -1
  128. package/packages/client/dist/migrations/project-identity.js +493 -7
  129. package/packages/client/dist/migrations/state-json.js +344 -4
  130. package/packages/client/dist/policy/architecture-policy.js +184 -2
  131. package/packages/client/dist/policy/index.js +7 -1
  132. package/packages/client/dist/policy/shared-policy-types.js +38 -0
  133. package/packages/client/dist/policy/subagent-approval-policy.js +55 -1
  134. package/packages/client/dist/private-execution/clean-baseline.js +279 -2
  135. package/packages/client/dist/private-execution/codex-executor.js +574 -4
  136. package/packages/client/dist/private-execution/codex-host-runtime.js +137 -1
  137. package/packages/client/dist/private-execution/codex-lifecycle-guard.js +97 -1
  138. package/packages/client/dist/private-execution/codex-scoped-permissions.js +128 -1
  139. package/packages/client/dist/private-execution/delivery.js +410 -13
  140. package/packages/client/dist/private-execution/diagnosis-root-review.js +294 -2
  141. package/packages/client/dist/private-execution/frozen-handoff.js +19 -6
  142. package/packages/client/dist/private-execution/implementation-validation.js +106 -2
  143. package/packages/client/dist/private-execution/isolated-admission.js +1432 -23
  144. package/packages/client/dist/private-execution/isolated-session.js +725 -5
  145. package/packages/client/dist/private-execution/isolated-workspace.js +122 -1
  146. package/packages/client/dist/private-execution/pact-consumer-runtime.js +409 -3
  147. package/packages/client/dist/private-execution/pact-test-source.js +258 -1
  148. package/packages/client/dist/private-execution/pact-toolchain.js +290 -3
  149. package/packages/client/dist/private-execution/pact-write-set.js +172 -1
  150. package/packages/client/dist/private-execution/planning-diagnosis-checkpoint.js +406 -5
  151. package/packages/client/dist/private-execution/planning-generation-checkpoint.js +462 -5
  152. package/packages/client/dist/private-execution/planning-memory-checkpoint.js +139 -3
  153. package/packages/client/dist/private-execution/planning-research-checkpoint.js +325 -4
  154. package/packages/client/dist/private-execution/private-report-store.js +42 -1
  155. package/packages/client/dist/private-execution/property-test-candidate.js +103 -1
  156. package/packages/client/dist/private-execution/property-test-choices.js +147 -3
  157. package/packages/client/dist/private-execution/property-test-plan.js +271 -7
  158. package/packages/client/dist/private-execution/property-test-review.js +258 -4
  159. package/packages/client/dist/private-execution/property-test-runtime.js +372 -9
  160. package/packages/client/dist/private-execution/property-test-source.js +139 -1
  161. package/packages/client/dist/private-execution/root-checkpoint.js +80 -1
  162. package/packages/client/dist/private-execution/root-memory-git-evidence.js +135 -3
  163. package/packages/client/dist/private-execution/root-memory-recovery.js +93 -1
  164. package/packages/client/dist/private-execution/root-memory-request.js +29 -1
  165. package/packages/client/dist/private-execution/root-memory-scope.js +178 -1
  166. package/packages/client/dist/private-execution/root-memory-writer.js +102 -1
  167. package/packages/client/dist/private-execution/session-lease.js +63 -1
  168. package/packages/client/dist/private-execution/shared-private-admission-types.js +50 -1
  169. package/packages/client/dist/private-execution/shared-project-validation.js +66 -1
  170. package/packages/client/dist/private-execution/shared-root-memory-types.js +61 -1
  171. package/packages/client/dist/private-execution/ux-source-validation.js +372 -1
  172. package/packages/client/dist/private-execution/ux-write-set.js +191 -2
  173. package/packages/client/dist/resilience/circuit-breaker.js +170 -1
  174. package/packages/client/dist/resilience/degradation-manager.js +164 -1
  175. package/packages/client/dist/resilience/freshness-indicator.js +100 -1
  176. package/packages/client/dist/resilience/index.js +8 -1
  177. package/packages/client/dist/resilience/recovery-detector.js +74 -1
  178. package/packages/client/dist/resolvers/asset-resolver.js +13 -0
  179. package/packages/client/dist/resolvers/local-resolver.js +255 -8
  180. package/packages/client/dist/resolvers/remote-resolver.js +233 -1
  181. package/packages/client/dist/runner/binary.js +404 -1
  182. package/packages/client/dist/runner/cli.js +1571 -17
  183. package/packages/client/dist/runner/headless/adapters.js +49 -1
  184. package/packages/client/dist/runner/headless/claude-code-adapter.js +79 -1
  185. package/packages/client/dist/runner/headless/continuous-story-workspace.js +1628 -11
  186. package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.js +69 -1
  187. package/packages/client/dist/runner/headless/epic-merge-candidate-qa.js +110 -2
  188. package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.js +1236 -8
  189. package/packages/client/dist/runner/headless/epic-terminal-git-runner.js +318 -2
  190. package/packages/client/dist/runner/headless/fake-adapter.js +62 -1
  191. package/packages/client/dist/runner/headless/managed-profile.js +132 -1
  192. package/packages/client/dist/runner/headless/opencode-adapter.js +145 -2
  193. package/packages/client/dist/runner/headless/prepared-story-bundle.js +43 -1
  194. package/packages/client/dist/runner/headless/resolver.js +231 -1
  195. package/packages/client/dist/runner/headless/run-yolo.js +885 -1
  196. package/packages/client/dist/runner/headless/run-yoloop.d.ts +4 -1
  197. package/packages/client/dist/runner/headless/run-yoloop.js +2574 -2
  198. package/packages/client/dist/runner/headless/types.js +574 -2
  199. package/packages/client/dist/runner/local-console.js +396 -13
  200. package/packages/client/dist/runner/portal-sync-worker.js +1487 -1
  201. package/packages/client/dist/runner/project-identity.js +106 -1
  202. package/packages/client/dist/runner/scheduler.js +995 -7
  203. package/packages/client/dist/runner-cli.js +18 -2
  204. package/packages/client/dist/state/invocation-shape.js +13 -1
  205. package/packages/client/dist/state/project-state-snapshot.js +1677 -1
  206. package/packages/client/dist/state/state-json-repair.js +785 -3
  207. package/packages/client/dist/state/structural-dag-projection.js +118 -1
  208. package/packages/client/dist/telemetry/index.js +5 -1
  209. package/packages/client/dist/telemetry/offline-queue.js +131 -1
  210. package/packages/client/dist/tier/index.js +5 -1
  211. package/packages/client/dist/tier/tier-aware-client.js +405 -1
  212. package/packages/client/dist/types/index.js +38 -1
  213. package/packages/client/dist/version.js +27 -1
  214. package/packages/client/dist/yolo/change-evidence-collector.js +126 -1
  215. package/packages/client/dist/yolo/invoke-envelope-capability.js +62 -1
  216. package/packages/client/dist/yolo/spec-envelope-collector.js +185 -5
  217. package/packages/client/dist/yoloop/discovery-hook.js +300 -2
  218. package/packages/client/dist/yoloop/index.js +8 -1
  219. package/packages/client/dist/yoloop/invoke-hooks.js +669 -12
  220. package/packages/client/dist/yoloop/shared-discover-epics.js +1007 -1
  221. package/packages/client/dist/yoloop/shared-yoloop-types.js +33 -1
  222. package/packages/client/dist/yoloop/target-normalization.js +180 -1
  223. package/packages/client/dist/yoloop/yoloop-client-state-store.js +1723 -1
  224. package/packages/shared/dist/compatibility/index.js +476 -0
  225. package/packages/shared/dist/yoloop/release-blocked.js +653 -0
  226. package/targets-stubs/antigravity/gemini.md +1 -1
  227. package/targets-stubs/antigravity/skill/SKILL.md +3 -2
  228. package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
  229. package/targets-stubs/claude-code/neocortex-root.md +5 -5
  230. package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
  231. package/targets-stubs/claude-code/neocortex.md +8 -9
  232. package/targets-stubs/codex/AGENTS.md +3 -3
  233. package/targets-stubs/cursor/agent.md +4 -3
  234. package/targets-stubs/gemini-cli/agent.md +5 -4
  235. package/targets-stubs/opencode/neocortex-root.md +3 -3
  236. package/targets-stubs/vscode/neocortex.agent.md +5 -4
@@ -1,31 +1,1186 @@
1
- import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";import{chmodSync as z,closeSync as ge,existsSync as te,mkdirSync as ae,openSync as Ae,readSync as xe}from"node:fs";import{dirname as re,join as se}from"node:path";import{INITIAL_CONTINUITY_SQLITE_MIGRATION as Re}from"./migrations/001-initial-schema.js";import{PROCESSING_OUTPUT_SUMMARY_SQLITE_MIGRATION as Oe}from"./migrations/002-processing-output-summary-indexes.js";import{HEADLESS_EXECUTION_ATTEMPTS_SQLITE_MIGRATION as Ne}from"./migrations/003-headless-execution-attempts.js";import{OUTBOX_DELIVERY_STATE_SQLITE_MIGRATION as Te}from"./migrations/004-outbox-delivery-state.js";import{BOUNDED_HEADLESS_QUEUE_SQLITE_MIGRATION as Le}from"./migrations/005-bounded-headless-queue.js";import{NATIVE_AGENT_DISPATCHES_SQLITE_MIGRATION as ve}from"./migrations/006-native-agent-dispatches.js";import{NATIVE_AGENT_TERMINAL_STATES as oe,isNativeAgentDispatchReceiptV1 as X,isNativeAgentStateTransitionAllowed as je,isNativeDispatchBindingV1 as Me,isNativeHostAttestationV1 as ke}from"./native-agent-types.js";import{CONTINUITY_QUEUE_REASON_CODES as k,CONTINUITY_RECEIPT_COMPACTION_EVENT_TYPE as W,CONTINUITY_RECEIPT_RETENTION_REASON_CODES as F,DEFAULT_CONTINUITY_QUEUE_POLICY as De,computeContinuityQueueRetryAt as Ue,computeContinuityOutboxFailureDecision as we,normalizeContinuityQueuePolicy as B,normalizeContinuityQueuePriority as qe,normalizeContinuityQueueScope as Pe,normalizeContinuityRetentionPolicy as He}from"./outbox.js";const ne=".neocortex/continuity",We="continuity.sqlite",kt=6,Fe=5e3,O="processing_output_summary",Be=2e3,de=20,$e=100,ie="Install the optional Enterprise continuity SQLite adapter with `npm install --workspace packages/client better-sqlite3 --include=optional` or reinstall Neocortex with optional dependencies enabled.",Ye=Object.freeze(["discover_more","create_epic","run_yoloop","run_yolo","run_verifier","run_review_gate","compact_context","wait_external","backoff","waiting_human","complete","abort","scheduled","running","overlap_skipped","missed_run_catchup","completed","aborted","cancelled","blocked","draft","planned"]),ce=20,Ke=new Set(Ye),Ve=/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,6})?)?(?:Z|[+-]\d{2}:?\d{2})?$/;function le(a){return typeof a=="string"&&Ke.has(a)}function ue(a){if(typeof a!="string")return;const e=a.trim();if(!(e.length===0||e.length>40)&&Ve.test(e))return e}function Ge(a){if(!Array.isArray(a))return[];const e=[];for(const t of a){if(!E(t)||!le(t.stage))continue;const r=ue(t.at);r&&e.push({stage:t.stage,at:r})}return e.slice(-ce)}function Qe(a){if(!(typeof a!="number"||!Number.isFinite(a)||a<0))return Math.floor(a)}class i extends Error{code;reasonCode;guidance;mutationBlocked;cause;constructor(e,t,r={}){super(t),this.name="ContinuitySqliteStoreError",this.code=e,this.reasonCode=e,this.guidance=r.guidance,this.cause=r.cause,this.mutationBlocked=r.mutationBlocked??e==="sqlite-integrity-failed"}}function $(a){const e=Object.freeze({schemaVersion:1,storyId:a.story_id,graphRevision:a.graph_revision,dispatchKey:a.dispatch_key,profile:a.profile,parentHandle:a.parent_handle,handle:a.handle,topology:a.topology,state:a.state,requestedAt:a.requested_at,observedAt:a.observed_at,terminalAt:a.terminal_at,freshness:a.freshness,observationSource:a.observation_source,reasonCode:a.reason_code,revision:a.revision,publicSafe:!0});if(!X(e))throw new i("sqlite-integrity-failed","Native-agent receipt failed its closed public contract.",{mutationBlocked:!0});return e}const Je=[Re,Oe,Ne,Te,Le,ve],ze=Object.freeze(["continuity_migrations","jobs","job_leases","job_events","job_receipts","goal_campaigns","goal_ledger_items","goal_verifiers","goal_review_gates","loop_jobs","artifacts","state_snapshots","headless_execution_attempts","runner_config","outbox","native_agent_dispatches","native_agent_observations"]),_e=/(raw[_-]?(?:logs?|payloads?)|std(?:out|err)|stack(?:[_-]?trace)?|transcript|objective(?:[_-]?(?:text|body|prompt))|prompt(?:[_-]?(?:body|text))?|private[_-]?(?:urls?|prompts?)|secret|token|password|protected(?:[_-]?(?:field|body|bodies|internals))?|workflow(?:[_-]?body)?|step(?:[_-]?body)?|customer(?:[_-]?(?:pii|data))?|\bpii\b|authorization|api[_-]?key|license[_-]?key|argv|args|executable(?:[_-]?path)?|binary(?:[_-]?path)?|shell|cwd|command[_-]?path|vendor(?:[_-]?(?:payload|corpus|response|output)))/i,me=/(https?:\/\/\S+|(?:^|\s)(?:\/(?:home|Users|media|mnt|tmp|var|etc|private)\/\S+|[A-Za-z]:\\\S+)|ghp_[A-Za-z0-9_]+|sk-[A-Za-z0-9_-]+|-----BEGIN [A-Z ]*PRIVATE KEY-----|authorization\s*:|password\s*=|token\s*=|api[_-]?key\s*=|license[_-]?key\s*=|raw[_ -]?(?:logs?|outputs?)\s*[:=]|transcript|stack\s*trace|stderr:|stdout:|system prompt|prompt[_ -]?(?:body|text)|workflow body|step body|\bargv\b|(?:bash|sh|zsh|powershell|pwsh)\s+-c\b|(?:\.{0,2}\/|[A-Za-z]:\\|\/)[^\s]*(?:\.exe|\.bat|\.cmd|\.ps1|\.sh|\/bin\/[^\s]+)|\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b|PROTECTED_|PRIVATE_INTERNALS|CUSTOMER_PII|\bPII\b|VENDOR_CORPUS|P180[_-]?SYNTHETIC[_-]?CONTINUITY|P185[_-]?SYNTHETIC[_-]?CONTINUITY|P187[_-]?SYNTHETIC[_-]?(?:CONTINUITY|RUNNER|WATCH)|P190[_-]?SYNTHETIC[_-]?(?:HEADLESS|CONTINUITY)|P192[_-]?SYNTHETIC[_-]?(?:PORTAL|RUNNER|SYNC))/i,Xe=new Set(["unsafe_key","raw_log","prompt_body","private_url","local_absolute_path","secret","protected_field","unbounded_text","unbounded_number","unknown_required_operation","missing"]),Ze=/(corrupt|malformed|not a database|file is not a database|database disk image is malformed)/i,et=/(foreign key constraint failed|SQLITE_CONSTRAINT_FOREIGNKEY)/i,Z="p185:v1:",tt=new Set(["schemaVersion","contractVersion","eventType","executionId","jobId","operationId","idempotencyKey","goalId","loopId","campaignId","itemId","ledgerItemIds","verifierId","verifierIds","gateId","reviewGateIds","runnerId","leaseId","status","reasonCode","reasonCodes","runtimeId","modelId","runtimeSource","headlessRuntime","headlessModel","headlessRuntimeReasonCode","originRuntime","originModel","targetRef","attemptId","attemptStatus","directiveHash","timeoutMs","leaseExpiresAt","heartbeatAt","notBeforeAt","maxAttempts","counts","hashes","relativePaths","artifactRefs","evidenceRefs","validationRefs","registryRefs","storyIds","epicIds","debtIds","iteration","currentStage","stageHistory","durationMs","retryCount","attemptCount","queuePriority","queueScope","queueCap","queueDepth","queueAvailable","queueBackpressure","nextTickAt","nextAttemptAt","nextRetryAt","deadLetterReasonCode","deliveryCursor","lastAttemptAt","sentAt","ackedAt","lastErrorReason","lastErrorSummary","deadLetterSummary","deadLetteredAt","processingOutputSource","publicSummary","redactionEvidence","selectorKind","targetId","targetJobId","relatedJobIds","serverAccessToLocalhost","serverHeldStream","initialCursor","targetResolutionState","targetKind","relatedJobCount","snapshotStatus","snapshotGeneratedAt","renderedSummaryCount","cursorState","currentCursor","nextCursor","cursorAdvanced","eventId","latestEventId","watchStatus","stopReason","syncVersion","transportKind","envelopeId","workspaceId","workspaceLabel","projectId","projectLabel","snapshotId","syncCursor","previousSyncCursor","nextSyncCursor","commandId","receiptId","acknowledgedAt","observedAt","generatedAt","syncStatus","commandStatus","receiptStatus","actorType","actorId","tenantId","commandKind","auditStatus","redactionCount","autonomousGovernance","governanceVersion","governanceEvidence","governanceSource","publicSafe"]),at=new Set(["foreground_invoke","scheduled_tick","runner_tick","manual_tick","headless_execution","jobs_command","invoke_watch","portal_render"]);function rt(a){return import(a)}function st(a){return`sha256:${M("sha256").update(a).digest("hex")}`}function E(a){return typeof a=="object"&&a!==null&&!Array.isArray(a)}function ot(a){return a instanceof Error?a.message:String(a)}function nt(a){return E(a)&&typeof a.code=="string"?a.code:void 0}function m(a,e){if(a instanceof i)return a;const t=ot(a),r=nt(a);return r==="SQLITE_CORRUPT"||r==="SQLITE_NOTADB"||Ze.test(t)?new i("sqlite-integrity-failed","Continuity SQLite integrity check failed; durable mutation is blocked until the store is repaired or rebuilt.",{cause:a,mutationBlocked:!0}):r==="SQLITE_CONSTRAINT_FOREIGNKEY"||et.test(t)?new i("sqlite-foreign-key-failed","Continuity SQLite foreign-key validation failed; mutation was rejected.",{cause:a}):new i(e,t,{cause:a})}function g(a,e){if(typeof a=="string"){if(/\.(?:redactionEvidence|redactionSummary)\[\d+\]\.field$/.test(e)&&/^[A-Za-z0-9_.:[\]-]{1,120}$/.test(a)||/\.(?:redactionEvidence|redactionSummary)\[\d+\]\.reason$/.test(e)&&Xe.has(a))return;if(me.test(a))throw new i("sqlite-public-safety-rejected",`Continuity SQLite metadata rejected unsafe value at ${e}.`);return}if(!(a===null||typeof a=="number"||typeof a=="boolean")){if(Array.isArray(a)){a.forEach((t,r)=>g(t,`${e}[${r}]`));return}if(E(a)){for(const[t,r]of Object.entries(a)){if(_e.test(t))throw new i("sqlite-public-safety-rejected",`Continuity SQLite metadata rejected unsafe key at ${e}.${t}.`);g(r,`${e}.${t}`)}return}throw new i("sqlite-public-safety-rejected",`Continuity SQLite metadata rejected unsupported value at ${e}.`)}}function b(a,e){if(a!==void 0&&me.test(a))throw new i("sqlite-public-safety-rejected",`Continuity SQLite text rejected unsafe value at ${e}.`)}function Y(a,e,t=240){if(a.trim().length===0)throw new i("sqlite-public-safety-rejected",`Continuity SQLite processing summary text is required at ${e}.`);if(b(a,e),a.length>t)throw new i("sqlite-public-safety-rejected",`Continuity SQLite processing summary text exceeded the public bound at ${e}.`)}function K(a){const e={};if(!a)return e;for(const[t,r]of Object.entries(a))if(tt.has(t)&&!_e.test(t))try{g(r,`metadata.${t}`),e[t]=r}catch{}return e}function dt(a){const e={...K(a.metadata),schemaVersion:1,contractVersion:"continuity-v1",eventType:O,executionId:a.executionId,jobId:a.jobId,operationId:a.executionId,publicSummary:a.publicSummary};return a.idempotencyKey&&(e.idempotencyKey=a.idempotencyKey),a.goalId&&(e.goalId=a.goalId),a.loopId&&(e.loopId=a.loopId),a.reasonCode&&(e.reasonCode=a.reasonCode),a.source&&at.has(a.source)&&(e.processingOutputSource=a.source),K(e)}function ye(a){const e=Math.floor(a??de);return Number.isFinite(e)?Math.max(1,Math.min($e,e)):de}function it(a){if(a.eventId&&a.eventId.trim().length>0)return a.eventId;const e=a.executionId.replace(/[^a-z0-9_.:-]+/gi,"-").replace(/^-+|-+$/g,"");return e.length>0?`event-processing-summary-${e.slice(0,160)}`:`event-processing-summary-${M("sha256").update(a.executionId).digest("hex").slice(0,32)}`}function ct(a){return`${Z}${J.from(JSON.stringify([a.created_at,a.event_id]),"utf8").toString("base64url")}`}function lt(a){if(a?.startsWith(Z))try{const e=a.slice(Z.length),t=JSON.parse(J.from(e,"base64url").toString("utf8"));if(!Array.isArray(t)||t.length!==2)return;const[r,s]=t;return typeof r!="string"||typeof s!="string"?void 0:{createdAt:r,eventId:s}}catch{return}}function D(a,e){if(!e)return;const t=e.trim().replace(/^@/,"");if(a==="job"&&/^job-J\d{3,}$/i.test(t))return`job-J${t.match(/\d+$/)?.[0]??""}`;if(a==="goal"&&/^goal-G\d{3,}$/i.test(t))return`goal-G${t.match(/\d+$/)?.[0]??""}`;if(a==="loop"&&/^loop-L\d{3,}$/i.test(t))return`loop-L${t.match(/\d+$/)?.[0]??""}`}function he(a){return a?q(a.map(e=>D("job",e)).filter(e=>!!e)):[]}function pe(a){const e=a.target;if(!e){const r=D("job",a.jobId);return r?{sql:" AND job_id = ?",params:[r]}:{sql:"",params:[]}}const t=e.selectorKind;if(t==="job"){const r=D("job",e.jobId??e.targetId??a.jobId);return r?{sql:" AND job_id = ?",params:[r]}:{sql:" AND 1 = 0",params:[]}}if(t==="goal"){const r=D("goal",e.goalId??e.targetId);if(!r)return{sql:" AND 1 = 0",params:[]};const s=he(e.relatedJobIds);return{sql:` AND (job_id = ? OR json_extract(metadata_json, '$.goalId') = ?${s.length>0?` OR job_id IN (${s.map(()=>"?").join(", ")})`:""})`,params:[r,r,...s]}}if(t==="loop"){const r=D("loop",e.loopId??e.targetId);if(!r)return{sql:" AND 1 = 0",params:[]};const s=he(e.relatedJobIds);return{sql:` AND (job_id = ? OR json_extract(metadata_json, '$.loopId') = ?${s.length>0?` OR job_id IN (${s.map(()=>"?").join(", ")})`:""})`,params:[r,r,...s]}}return{sql:" AND 1 = 0",params:[]}}function be(a){const e={event_id:a.event_id,job_id:a.job_id,execution_id:a.operation_id,public_summary:a.summary,reason_code:a.reason_code,created_at:a.created_at,metadata_json:a.metadata_json};return{...e,cursor:ct(e)}}function S(a){return a?(g(a,"metadata"),JSON.stringify(a)):null}function ut(a,e,t){const r=d=>`sha256:${M("sha256").update(d.map(o=>[o.id,o.status,o.reasonCode].join(":")).sort().join(`
2
- `)).digest("hex")}`,s=r(e),n=r(t);return{receipts:s,outputs:n,aggregate:`sha256:${M("sha256").update(`${a}:${s}:${n}`).digest("hex")}`}}function _t(a){if(!a)return{};try{const e=JSON.parse(a);return E(e)?{...e}:{}}catch{return{}}}function c(a,e,t,r=!1){if(a===void 0){if(r)throw new i("sqlite-public-safety-rejected",`Continuity SQLite text is required at ${e}.`);return}if(b(a,e),a.trim().length===0||a.length>t)throw new i("sqlite-public-safety-rejected",`Continuity SQLite text exceeded the public bound at ${e}.`)}function N(a,e){if(a!==void 0&&(!Number.isFinite(a)||a<0||Math.floor(a)!==a))throw new i("sqlite-public-safety-rejected",`Continuity SQLite numeric value rejected at ${e}.`)}function v(a){c(a.outboxId,"outbox.outboxId",160,!0),c(a.jobId,"outbox.jobId",160),c(a.operationId,"outbox.operationId",160),c(a.idempotencyKey,"outbox.idempotencyKey",160),c(a.type,"outbox.type",80),c(a.status,"outbox.status",40),c(a.deliveryCursor,"outbox.deliveryCursor",200),c(a.timestamp,"outbox.timestamp",80),c(a.reasonCode,"outbox.reasonCode",100),c(a.summary,"outbox.summary",240)}function Ie(a){return a==="pending"||a==="sent"||a==="acked"||a==="failed"||a==="dead_letter"}function mt(a){return typeof a=="string"&&Ie(a)?a:"failed"}function h(a){return typeof a=="string"&&a.length>0?a:void 0}function yt(a){if(typeof a!="string"||a.trim().length===0)return{};try{const e=JSON.parse(a);return E(e)?(g(e,"outbox.payload"),e):{}}catch{return{}}}function ht(a){return{outboxId:String(a.outbox_id??""),...h(a.job_id)?{jobId:h(a.job_id)}:{},operationId:String(a.operation_id??""),...h(a.idempotency_key)?{idempotencyKey:h(a.idempotency_key)}:{},type:String(a.type??""),status:mt(a.status),payload:yt(a.payload_json),retryCount:typeof a.retry_count=="number"?a.retry_count:0,attemptCount:typeof a.attempt_count=="number"?a.attempt_count:0,...h(a.delivery_cursor)?{deliveryCursor:h(a.delivery_cursor)}:{},...h(a.next_attempt_at)?{nextAttemptAt:h(a.next_attempt_at)}:{},...h(a.last_attempt_at)?{lastAttemptAt:h(a.last_attempt_at)}:{},...h(a.sent_at)?{sentAt:h(a.sent_at)}:{},...h(a.acked_at)?{ackedAt:h(a.acked_at)}:{},...h(a.last_error_code)?{lastErrorCode:h(a.last_error_code)}:{},...h(a.last_error_reason)?{lastErrorReason:h(a.last_error_reason)}:{},...h(a.last_error_summary)?{lastErrorSummary:h(a.last_error_summary)}:{},...h(a.dead_letter_summary)?{deadLetterSummary:h(a.dead_letter_summary)}:{},...h(a.dead_lettered_at)?{deadLetteredAt:h(a.dead_lettered_at)}:{},createdAt:String(a.created_at??""),updatedAt:String(a.updated_at??""),publicSafe:!0}}function C(a){return{...a,publicSafe:!0}}function T(a,e){return a===void 0?null:(g(a,e),JSON.stringify(a))}function U(a){if(!a)return;const e=q(a.map(t=>t.replace(/\\/g,"/"))).filter(t=>t.length>0).slice(0,50);for(const[t,r]of e.entries()){c(r,`relativePaths[${t}]`,240,!0);const s=r.split("/").filter(Boolean);if(r.startsWith("/")||r.startsWith("~")||r.includes("://")||/^[A-Za-z]:\//.test(r)||s.includes(".."))throw new i("sqlite-public-safety-rejected",`Continuity SQLite rejected non-relative path at relativePaths[${t}].`)}return e}function V(a){const e={...K(a.metadata),schemaVersion:1,contractVersion:"continuity-v1",attemptId:a.attemptId,executionId:a.executionId,jobId:a.jobId};return a.directiveHash&&(e.directiveHash=a.directiveHash),a.runtimeId&&(e.runtimeId=a.runtimeId),a.modelId&&(e.modelId=a.modelId),a.runtimeSource&&(e.runtimeSource=a.runtimeSource),a.status&&(e.attemptStatus=a.status),a.reasonCode&&(e.reasonCode=a.reasonCode),a.durationMs!==void 0&&(e.durationMs=a.durationMs),a.retryCount!==void 0&&(e.retryCount=a.retryCount),a.attemptCount!==void 0&&(e.attemptCount=a.attemptCount),a.maxAttempts!==void 0&&(e.maxAttempts=a.maxAttempts),a.queuePriority!==void 0&&(e.queuePriority=a.queuePriority),a.queueScope&&(e.queueScope=a.queueScope),a.nextRetryAt&&(e.nextRetryAt=a.nextRetryAt),a.deadLetterReasonCode&&(e.deadLetterReasonCode=a.deadLetterReasonCode),a.counts&&(e.counts=a.counts),a.hashes&&(e.hashes=a.hashes),a.redactionSummary&&(e.redactionEvidence=a.redactionSummary),a.relativePaths&&(e.relativePaths=U(a.relativePaths)),K(e)}function L(a){c(a.attemptId,"headless_execution.attemptId",160,!0),c(a.executionId,"headless_execution.executionId",160,!0),c(a.jobId,"headless_execution.jobId",160,!0),c(a.directiveHash,"headless_execution.directiveHash",160),c(a.targetRef,"headless_execution.targetRef",240),c(a.storyId,"headless_execution.storyId",120),c(a.epicId,"headless_execution.epicId",120),c(a.runtimeId,"headless_execution.runtimeId",80),c(a.modelId,"headless_execution.modelId",120),c(a.runtimeSource,"headless_execution.runtimeSource",40),c(a.reasonCode,"headless_execution.reasonCode",100),c(a.publicSummary,"headless_execution.publicSummary",240),c(a.leaseId,"headless_execution.leaseId",160),c(a.runnerId,"headless_execution.runnerId",160),c(a.timestamp,"headless_execution.timestamp",80)}function w(a){if(Array.isArray(a)){const e=a[0];return E(e)?Object.values(e)[0]:e}return E(a)?Object.values(a)[0]:a}function pt(a){return ze.includes(a)}function q(a){return Array.from(new Set(a))}function bt(a){const e=E(a)&&"default"in a?a.default:a;if(typeof e!="function")throw new i("sqlite-adapter-unavailable","Optional better-sqlite3 adapter did not export a database constructor.",{guidance:ie});const t=e;return{adapterName:"better-sqlite3",open(r,s){return new t(r,s)}}}async function It(a=rt){try{const e=await a("better-sqlite3");return{ok:!0,adapter:bt(e)}}catch(e){return e instanceof i?{ok:!1,error:e}:{ok:!1,error:new i("sqlite-adapter-unavailable","Optional Enterprise continuity SQLite adapter better-sqlite3 is unavailable.",{cause:e,guidance:ie})}}}function Ee(a){return se(a,ne,We)}function Et(a){const e=se(a,ne);ae(e,{recursive:!0,mode:448});try{z(e,448)}catch{}return e}async function Dt(a){const e=a.adapter??await Ce(a.adapterLoader),t=a.filePath??Ee(a.projectRoot);if(a.strictReadOnly)fe(t);else if(a.filePath){ae(re(t),{recursive:!0,mode:448});try{z(re(t),448)}catch{}}else Et(a.projectRoot);let r;try{r=e.open(t,a.strictReadOnly?{fileMustExist:!0,readonly:!0}:void 0)}catch(n){throw m(n,"sqlite-integrity-failed")}const s=new ft({db:r,filePath:t,adapterName:e.adapterName,busyTimeoutMs:a.busyTimeoutMs??Fe,integrityCheck:a.integrityCheck??!0,now:a.now??(()=>new Date().toISOString())});try{if(a.strictReadOnly)return s.runIntegrityCheck(),s.getSchemaVersion(),s;if(s.initialize(),te(t))try{z(t,384)}catch{}return s}catch(n){try{r.close()}catch{}throw n}}function fe(a){if(!te(a))throw new i("sqlite-integrity-failed","Continuity database is unavailable for physical read-only inspection.",{mutationBlocked:!0});let e;try{const t=Ae(a,"r");try{if(e=J.alloc(20),xe(t,e,0,e.length,0)<e.length)throw new Error("short-header")}finally{ge(t)}}catch(t){throw new i("sqlite-integrity-failed","Continuity database header could not be inspected read-only.",{cause:t,mutationBlocked:!0})}if(e.subarray(0,16).toString("ascii")!=="SQLite format 3\0"||e[18]===2||e[19]===2)throw new i("sqlite-integrity-failed","Continuity database requires an unsafe WAL read profile.",{mutationBlocked:!0})}async function Ut(a){const e=Ee(a.projectRoot);fe(e);const t=a.adapter??await Ce(a.adapterLoader);let r;try{r=t.open(e,{fileMustExist:!0,readonly:!0});const n=w(r.pragma("integrity_check",{simple:!0}));if(String(n).toLowerCase()!=="ok")throw new Error("integrity")}catch(n){throw new i("sqlite-integrity-failed","Continuity database failed physical read-only inspection.",{cause:n,mutationBlocked:!0})}const s=n=>{const d=n.trim().replace(/^\(+/u,"").toLowerCase();if(!/^(select|with\s)/u.test(d)||/\b(insert|update|delete|replace|create|alter|drop|pragma|attach|detach|vacuum|reindex)\b/u.test(d))throw new i("sqlite-mutation-blocked","Statement is not in the physical read-only query profile.",{mutationBlocked:!0})};return Object.freeze({filePath:e,adapterName:t.adapterName,all(n,...d){return s(n),r.prepare(n).all(...d)},get(n,...d){return s(n),r.prepare(n).get(...d)},close(){r.close()}})}async function Ce(a){const e=a?await a():await It();if(!e.ok)throw e.error;return e.adapter}class ft{filePath;adapterName;db;busyTimeoutMs;integrityCheck;now;mutationBlocked=!1;constructor(e){this.db=e.db,this.filePath=e.filePath,this.adapterName=e.adapterName,this.busyTimeoutMs=e.busyTimeoutMs,this.integrityCheck=e.integrityCheck,this.now=e.now}initialize(){try{this.configureConnection(),this.integrityCheck&&this.runIntegrityCheck(),this.ensureMigrationTable(),this.applyMigrations(),this.integrityCheck&&this.runIntegrityCheck()}catch(e){throw m(e,"sqlite-migration-failed")}}close(){this.db.close()}pragmaValue(e){return w(this.db.pragma(e,{simple:!0}))}runIntegrityCheck(){try{if(String(w(this.db.pragma("integrity_check",{simple:!0}))??"").toLowerCase()!=="ok")throw this.mutationBlocked=!0,new i("sqlite-integrity-failed","Continuity SQLite integrity check failed; durable mutation is blocked until the store is repaired or rebuilt.",{mutationBlocked:!0})}catch(e){throw this.mutationBlocked=!0,m(e,"sqlite-integrity-failed")}}getSchemaVersion(){const e=this.db.prepare("SELECT COALESCE(MAX(version), 0) AS version FROM continuity_migrations").get();return E(e)&&typeof e.version=="number"?e.version:0}countRows(e){if(!pt(e))throw new i("sqlite-public-safety-rejected","Unknown continuity table name.");const t=this.db.prepare(`SELECT COUNT(*) AS count FROM ${e}`).get();return E(t)&&typeof t.count=="number"?t.count:0}reserveNativeAgentDispatch(e){if(this.assertCanMutate(),!Me(e.binding)||!ke(e.hostAttestation))throw new i("sqlite-public-safety-rejected","Native-agent reservation rejected invalid binding or host evidence.");const t={schemaVersion:1,storyId:e.binding.storyId,graphRevision:e.binding.graphRevision.replace(/^sha256:/,"").toLowerCase(),dispatchKey:e.binding.dispatchKey.toLowerCase(),profile:"neocortex",parentHandle:null,handle:null,topology:e.topology??"native-agent",state:"requested",requestedAt:e.requestedAt,observedAt:null,terminalAt:null,freshness:"unknown",observationSource:"local-reservation",reasonCode:e.reasonCode??"native-agent-dispatch-reserved",revision:0,publicSafe:!0};if(!X(t))throw new i("sqlite-public-safety-rejected","Native-agent reservation rejected unsafe public fields.");try{return this.runInTransaction(()=>{const r=this.db.prepare(`
1
+ /**
2
+ * @license FSL-1.1
3
+ * Copyright (c) 2026 OrNexus AI
4
+ *
5
+ * P180.04 -- Local SQLite continuity store, migrations and integrity checks.
6
+ */
7
+ import { Buffer } from 'node:buffer';
8
+ import { createHash } from 'node:crypto';
9
+ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readSync } from 'node:fs';
10
+ import { dirname, join } from 'node:path';
11
+ import { INITIAL_CONTINUITY_SQLITE_MIGRATION } from './migrations/001-initial-schema.js';
12
+ import { PROCESSING_OUTPUT_SUMMARY_SQLITE_MIGRATION } from './migrations/002-processing-output-summary-indexes.js';
13
+ import { HEADLESS_EXECUTION_ATTEMPTS_SQLITE_MIGRATION } from './migrations/003-headless-execution-attempts.js';
14
+ import { OUTBOX_DELIVERY_STATE_SQLITE_MIGRATION } from './migrations/004-outbox-delivery-state.js';
15
+ import { BOUNDED_HEADLESS_QUEUE_SQLITE_MIGRATION } from './migrations/005-bounded-headless-queue.js';
16
+ import { NATIVE_AGENT_DISPATCHES_SQLITE_MIGRATION } from './migrations/006-native-agent-dispatches.js';
17
+ import { NATIVE_AGENT_TERMINAL_STATES, isNativeAgentDispatchReceiptV1, isNativeAgentStateTransitionAllowed, isNativeDispatchBindingV1, isNativeHostAttestationV1, } from './native-agent-types.js';
18
+ import { CONTINUITY_QUEUE_REASON_CODES, CONTINUITY_RECEIPT_COMPACTION_EVENT_TYPE, CONTINUITY_RECEIPT_RETENTION_REASON_CODES, DEFAULT_CONTINUITY_QUEUE_POLICY, computeContinuityQueueRetryAt, computeContinuityOutboxFailureDecision, normalizeContinuityQueuePolicy, normalizeContinuityQueuePriority, normalizeContinuityQueueScope, normalizeContinuityRetentionPolicy, } from './outbox.js';
19
+ export const CONTINUITY_SQLITE_RELATIVE_DIR = '.neocortex/continuity';
20
+ export const CONTINUITY_SQLITE_FILENAME = 'continuity.sqlite';
21
+ export const CONTINUITY_SQLITE_SCHEMA_VERSION = 6;
22
+ export const DEFAULT_CONTINUITY_SQLITE_BUSY_TIMEOUT_MS = 5_000;
23
+ export const CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE = 'processing_output_summary';
24
+ export const CONTINUITY_PROCESSING_OUTPUT_SUMMARY_MAX_CHARS = 2_000;
25
+ export const DEFAULT_CONTINUITY_PROCESSING_SUMMARY_LIMIT = 20;
26
+ export const MAX_CONTINUITY_PROCESSING_SUMMARY_LIMIT = 100;
27
+ export const SQLITE_ADAPTER_INSTALL_GUIDANCE = 'Install the optional Enterprise continuity SQLite adapter with `npm install --workspace packages/client better-sqlite3 --include=optional` or reinstall Neocortex with optional dependencies enabled.';
28
+ /**
29
+ * P195.04: Client-local mirror of the shared continuity process-stage contract
30
+ * (SSoT: `packages/shared/src/continuity/runner-state.ts` ->
31
+ * `CONTINUITY_PUBLIC_STAGES` / `CONTINUITY_STAGE_HISTORY_MAX`). The client keeps a
32
+ * local copy so continuity SQLite persistence has no runtime `@neocortex/shared`
33
+ * import; a drift test in `continuity-sqlite.test.ts` asserts parity with the
34
+ * shared SSoT. Values are an allow-list only -- unknown/arbitrary strings never
35
+ * persist or project as a stage.
36
+ */
37
+ export const CONTINUITY_PUBLIC_STAGES = Object.freeze([
38
+ // Goal tick decision kinds.
39
+ 'discover_more',
40
+ 'create_epic',
41
+ 'run_yoloop',
42
+ 'run_yolo',
43
+ 'run_verifier',
44
+ 'run_review_gate',
45
+ 'compact_context',
46
+ 'wait_external',
47
+ 'backoff',
48
+ 'waiting_human',
49
+ 'complete',
50
+ 'abort',
51
+ // Loop lifecycle states.
52
+ 'scheduled',
53
+ 'running',
54
+ 'overlap_skipped',
55
+ 'missed_run_catchup',
56
+ 'completed',
57
+ 'aborted',
58
+ 'cancelled',
59
+ 'blocked',
60
+ // Pre-tick states (goal draft/planned before the first tick decision).
61
+ 'draft',
62
+ 'planned',
63
+ ]);
64
+ /** P195.04: Maximum retained `stageHistory` entries (newest kept). ADR-P195-03. */
65
+ export const CONTINUITY_STAGE_HISTORY_MAX = 20;
66
+ const CONTINUITY_PUBLIC_STAGE_SET = new Set(CONTINUITY_PUBLIC_STAGES);
67
+ /** P195.04: Bounded ISO-8601 date-time prefix accepted for `stageHistory.at`. */
68
+ const CONTINUITY_STAGE_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,6})?)?(?:Z|[+-]\d{2}:?\d{2})?$/;
69
+ export function isContinuityPublicStage(value) {
70
+ return typeof value === 'string' && CONTINUITY_PUBLIC_STAGE_SET.has(value);
71
+ }
72
+ export function sanitizeContinuityStageTimestamp(value) {
73
+ if (typeof value !== 'string')
74
+ return undefined;
75
+ const trimmed = value.trim();
76
+ if (trimmed.length === 0 || trimmed.length > 40)
77
+ return undefined;
78
+ if (!CONTINUITY_STAGE_TIMESTAMP_PATTERN.test(trimmed))
79
+ return undefined;
80
+ return trimmed;
81
+ }
82
+ /**
83
+ * P195.04: Validate an unknown value into a bounded, allow-listed stage history.
84
+ * Unknown stages and unsafe timestamps are dropped, and the newest
85
+ * `CONTINUITY_STAGE_HISTORY_MAX` entries are kept.
86
+ */
87
+ export function sanitizeContinuityStageHistory(value) {
88
+ if (!Array.isArray(value))
89
+ return [];
90
+ const entries = [];
91
+ for (const item of value) {
92
+ if (!isRecord(item) || !isContinuityPublicStage(item.stage))
93
+ continue;
94
+ const at = sanitizeContinuityStageTimestamp(item.at);
95
+ if (!at)
96
+ continue;
97
+ entries.push({ stage: item.stage, at });
98
+ }
99
+ return entries.slice(-CONTINUITY_STAGE_HISTORY_MAX);
100
+ }
101
+ export function normalizeContinuityStageIteration(value) {
102
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
103
+ return undefined;
104
+ return Math.floor(value);
105
+ }
106
+ export class ContinuitySqliteStoreError extends Error {
107
+ code;
108
+ reasonCode;
109
+ guidance;
110
+ mutationBlocked;
111
+ cause;
112
+ constructor(code, message, options = {}) {
113
+ super(message);
114
+ this.name = 'ContinuitySqliteStoreError';
115
+ this.code = code;
116
+ this.reasonCode = code;
117
+ this.guidance = options.guidance;
118
+ this.cause = options.cause;
119
+ this.mutationBlocked = options.mutationBlocked ?? code === 'sqlite-integrity-failed';
120
+ }
121
+ }
122
+ function toNativeAgentReceipt(row) {
123
+ const receipt = Object.freeze({
124
+ schemaVersion: 1,
125
+ storyId: row.story_id,
126
+ graphRevision: row.graph_revision,
127
+ dispatchKey: row.dispatch_key,
128
+ profile: row.profile,
129
+ parentHandle: row.parent_handle,
130
+ handle: row.handle,
131
+ topology: row.topology,
132
+ state: row.state,
133
+ requestedAt: row.requested_at,
134
+ observedAt: row.observed_at,
135
+ terminalAt: row.terminal_at,
136
+ freshness: row.freshness,
137
+ observationSource: row.observation_source,
138
+ reasonCode: row.reason_code,
139
+ revision: row.revision,
140
+ publicSafe: true,
141
+ });
142
+ if (!isNativeAgentDispatchReceiptV1(receipt)) {
143
+ throw new ContinuitySqliteStoreError('sqlite-integrity-failed', 'Native-agent receipt failed its closed public contract.', { mutationBlocked: true });
144
+ }
145
+ return receipt;
146
+ }
147
+ const CONTINUITY_MIGRATIONS = [
148
+ INITIAL_CONTINUITY_SQLITE_MIGRATION,
149
+ PROCESSING_OUTPUT_SUMMARY_SQLITE_MIGRATION,
150
+ HEADLESS_EXECUTION_ATTEMPTS_SQLITE_MIGRATION,
151
+ OUTBOX_DELIVERY_STATE_SQLITE_MIGRATION,
152
+ BOUNDED_HEADLESS_QUEUE_SQLITE_MIGRATION,
153
+ NATIVE_AGENT_DISPATCHES_SQLITE_MIGRATION,
154
+ ];
155
+ const CONTINUITY_TABLES = Object.freeze([
156
+ 'continuity_migrations',
157
+ 'jobs',
158
+ 'job_leases',
159
+ 'job_events',
160
+ 'job_receipts',
161
+ 'goal_campaigns',
162
+ 'goal_ledger_items',
163
+ 'goal_verifiers',
164
+ 'goal_review_gates',
165
+ 'loop_jobs',
166
+ 'artifacts',
167
+ 'state_snapshots',
168
+ 'headless_execution_attempts',
169
+ 'runner_config',
170
+ 'outbox',
171
+ 'native_agent_dispatches',
172
+ 'native_agent_observations',
173
+ ]);
174
+ const UNSAFE_METADATA_KEY_PATTERN = /(raw[_-]?(?:logs?|payloads?)|std(?:out|err)|stack(?:[_-]?trace)?|transcript|objective(?:[_-]?(?:text|body|prompt))|prompt(?:[_-]?(?:body|text))?|private[_-]?(?:urls?|prompts?)|secret|token|password|protected(?:[_-]?(?:field|body|bodies|internals))?|workflow(?:[_-]?body)?|step(?:[_-]?body)?|customer(?:[_-]?(?:pii|data))?|\bpii\b|authorization|api[_-]?key|license[_-]?key|argv|args|executable(?:[_-]?path)?|binary(?:[_-]?path)?|shell|cwd|command[_-]?path|vendor(?:[_-]?(?:payload|corpus|response|output)))/i;
175
+ const UNSAFE_METADATA_VALUE_PATTERN = /(https?:\/\/\S+|(?:^|\s)(?:\/(?:home|Users|media|mnt|tmp|var|etc|private)\/\S+|[A-Za-z]:\\\S+)|ghp_[A-Za-z0-9_]+|sk-[A-Za-z0-9_-]+|-----BEGIN [A-Z ]*PRIVATE KEY-----|authorization\s*:|password\s*=|token\s*=|api[_-]?key\s*=|license[_-]?key\s*=|raw[_ -]?(?:logs?|outputs?)\s*[:=]|transcript|stack\s*trace|stderr:|stdout:|system prompt|prompt[_ -]?(?:body|text)|workflow body|step body|\bargv\b|(?:bash|sh|zsh|powershell|pwsh)\s+-c\b|(?:\.{0,2}\/|[A-Za-z]:\\|\/)[^\s]*(?:\.exe|\.bat|\.cmd|\.ps1|\.sh|\/bin\/[^\s]+)|\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b|PROTECTED_|PRIVATE_INTERNALS|CUSTOMER_PII|\bPII\b|VENDOR_CORPUS|P180[_-]?SYNTHETIC[_-]?CONTINUITY|P185[_-]?SYNTHETIC[_-]?CONTINUITY|P187[_-]?SYNTHETIC[_-]?(?:CONTINUITY|RUNNER|WATCH)|P190[_-]?SYNTHETIC[_-]?(?:HEADLESS|CONTINUITY)|P192[_-]?SYNTHETIC[_-]?(?:PORTAL|RUNNER|SYNC))/i;
176
+ const PUBLIC_REDACTION_REASON_VALUES = new Set([
177
+ 'unsafe_key',
178
+ 'raw_log',
179
+ 'prompt_body',
180
+ 'private_url',
181
+ 'local_absolute_path',
182
+ 'secret',
183
+ 'protected_field',
184
+ 'unbounded_text',
185
+ 'unbounded_number',
186
+ 'unknown_required_operation',
187
+ 'missing',
188
+ ]);
189
+ const SQLITE_CORRUPT_PATTERN = /(corrupt|malformed|not a database|file is not a database|database disk image is malformed)/i;
190
+ const SQLITE_FOREIGN_KEY_PATTERN = /(foreign key constraint failed|SQLITE_CONSTRAINT_FOREIGNKEY)/i;
191
+ const PROCESSING_SUMMARY_CURSOR_PREFIX = 'p185:v1:';
192
+ const CONTINUITY_PUBLIC_METADATA_ALLOWED_KEYS = new Set([
193
+ 'schemaVersion',
194
+ 'contractVersion',
195
+ 'eventType',
196
+ 'executionId',
197
+ 'jobId',
198
+ 'operationId',
199
+ 'idempotencyKey',
200
+ 'goalId',
201
+ 'loopId',
202
+ 'campaignId',
203
+ 'itemId',
204
+ 'ledgerItemIds',
205
+ 'verifierId',
206
+ 'verifierIds',
207
+ 'gateId',
208
+ 'reviewGateIds',
209
+ 'runnerId',
210
+ 'leaseId',
211
+ 'status',
212
+ 'reasonCode',
213
+ 'reasonCodes',
214
+ 'runtimeId',
215
+ 'modelId',
216
+ 'runtimeSource',
217
+ 'headlessRuntime',
218
+ 'headlessModel',
219
+ 'headlessRuntimeReasonCode',
220
+ 'originRuntime',
221
+ 'originModel',
222
+ 'targetRef',
223
+ 'attemptId',
224
+ 'attemptStatus',
225
+ 'terminalClass',
226
+ 'releaseBlocked',
227
+ 'directiveHash',
228
+ 'timeoutMs',
229
+ 'leaseExpiresAt',
230
+ 'heartbeatAt',
231
+ 'notBeforeAt',
232
+ 'maxAttempts',
233
+ 'counts',
234
+ 'hashes',
235
+ 'relativePaths',
236
+ 'artifactRefs',
237
+ 'evidenceRefs',
238
+ 'validationRefs',
239
+ 'registryRefs',
240
+ 'storyIds',
241
+ 'epicIds',
242
+ 'debtIds',
243
+ 'iteration',
244
+ 'currentStage',
245
+ 'stageHistory',
246
+ 'durationMs',
247
+ 'retryCount',
248
+ 'attemptCount',
249
+ 'queuePriority',
250
+ 'queueScope',
251
+ 'queueCap',
252
+ 'queueDepth',
253
+ 'queueAvailable',
254
+ 'queueBackpressure',
255
+ 'nextTickAt',
256
+ 'nextAttemptAt',
257
+ 'nextRetryAt',
258
+ 'deadLetterReasonCode',
259
+ 'deliveryCursor',
260
+ 'lastAttemptAt',
261
+ 'sentAt',
262
+ 'ackedAt',
263
+ 'lastErrorReason',
264
+ 'lastErrorSummary',
265
+ 'deadLetterSummary',
266
+ 'deadLetteredAt',
267
+ 'processingOutputSource',
268
+ 'publicSummary',
269
+ 'redactionEvidence',
270
+ 'selectorKind',
271
+ 'targetId',
272
+ 'targetJobId',
273
+ 'relatedJobIds',
274
+ 'serverAccessToLocalhost',
275
+ 'serverHeldStream',
276
+ 'initialCursor',
277
+ 'targetResolutionState',
278
+ 'targetKind',
279
+ 'relatedJobCount',
280
+ 'snapshotStatus',
281
+ 'snapshotGeneratedAt',
282
+ 'renderedSummaryCount',
283
+ 'cursorState',
284
+ 'currentCursor',
285
+ 'nextCursor',
286
+ 'cursorAdvanced',
287
+ 'eventId',
288
+ 'latestEventId',
289
+ 'watchStatus',
290
+ 'stopReason',
291
+ 'syncVersion',
292
+ 'transportKind',
293
+ 'envelopeId',
294
+ 'workspaceId',
295
+ 'workspaceLabel',
296
+ 'projectId',
297
+ 'projectLabel',
298
+ 'snapshotId',
299
+ 'syncCursor',
300
+ 'previousSyncCursor',
301
+ 'nextSyncCursor',
302
+ 'commandId',
303
+ 'receiptId',
304
+ 'acknowledgedAt',
305
+ 'observedAt',
306
+ 'generatedAt',
307
+ 'syncStatus',
308
+ 'commandStatus',
309
+ 'receiptStatus',
310
+ 'actorType',
311
+ 'actorId',
312
+ 'tenantId',
313
+ 'commandKind',
314
+ 'auditStatus',
315
+ 'redactionCount',
316
+ 'autonomousGovernance',
317
+ 'governanceVersion',
318
+ 'governanceEvidence',
319
+ 'governanceSource',
320
+ 'publicSafe',
321
+ ]);
322
+ const CONTINUITY_PROCESSING_OUTPUT_SUMMARY_SOURCES = new Set([
323
+ 'foreground_invoke',
324
+ 'scheduled_tick',
325
+ 'runner_tick',
326
+ 'manual_tick',
327
+ 'headless_execution',
328
+ 'jobs_command',
329
+ 'invoke_watch',
330
+ 'portal_render',
331
+ ]);
332
+ function defaultDynamicImport(specifier) {
333
+ return import(specifier);
334
+ }
335
+ function checksumMigration(sql) {
336
+ return `sha256:${createHash('sha256').update(sql).digest('hex')}`;
337
+ }
338
+ function isRecord(value) {
339
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
340
+ }
341
+ function getErrorMessage(error) {
342
+ return error instanceof Error ? error.message : String(error);
343
+ }
344
+ function getErrorCode(error) {
345
+ return isRecord(error) && typeof error.code === 'string' ? error.code : undefined;
346
+ }
347
+ function mapSqliteError(error, fallback) {
348
+ if (error instanceof ContinuitySqliteStoreError)
349
+ return error;
350
+ const message = getErrorMessage(error);
351
+ const nativeCode = getErrorCode(error);
352
+ if (nativeCode === 'SQLITE_CORRUPT' || nativeCode === 'SQLITE_NOTADB' || SQLITE_CORRUPT_PATTERN.test(message)) {
353
+ return new ContinuitySqliteStoreError('sqlite-integrity-failed', 'Continuity SQLite integrity check failed; durable mutation is blocked until the store is repaired or rebuilt.', { cause: error, mutationBlocked: true });
354
+ }
355
+ if (nativeCode === 'SQLITE_CONSTRAINT_FOREIGNKEY' || SQLITE_FOREIGN_KEY_PATTERN.test(message)) {
356
+ return new ContinuitySqliteStoreError('sqlite-foreign-key-failed', 'Continuity SQLite foreign-key validation failed; mutation was rejected.', { cause: error });
357
+ }
358
+ return new ContinuitySqliteStoreError(fallback, message, { cause: error });
359
+ }
360
+ function assertPublicSafeValue(value, path) {
361
+ if (typeof value === 'string') {
362
+ if (/\.(?:redactionEvidence|redactionSummary)\[\d+\]\.field$/.test(path) && /^[A-Za-z0-9_.:[\]-]{1,120}$/.test(value)) {
363
+ return;
364
+ }
365
+ if (/\.(?:redactionEvidence|redactionSummary)\[\d+\]\.reason$/.test(path) && PUBLIC_REDACTION_REASON_VALUES.has(value)) {
366
+ return;
367
+ }
368
+ if (UNSAFE_METADATA_VALUE_PATTERN.test(value)) {
369
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', `Continuity SQLite metadata rejected unsafe value at ${path}.`);
370
+ }
371
+ return;
372
+ }
373
+ if (value === null || typeof value === 'number' || typeof value === 'boolean')
374
+ return;
375
+ if (Array.isArray(value)) {
376
+ value.forEach((item, index) => assertPublicSafeValue(item, `${path}[${index}]`));
377
+ return;
378
+ }
379
+ if (isRecord(value)) {
380
+ for (const [key, child] of Object.entries(value)) {
381
+ const publicReleaseBlockedSignal = key === 'terminationToken'
382
+ && path.endsWith('.releaseBlocked')
383
+ && child === '[YOLOOP_RELEASE_BLOCKED]';
384
+ if (UNSAFE_METADATA_KEY_PATTERN.test(key) && !publicReleaseBlockedSignal) {
385
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', `Continuity SQLite metadata rejected unsafe key at ${path}.${key}.`);
386
+ }
387
+ assertPublicSafeValue(child, `${path}.${key}`);
388
+ }
389
+ return;
390
+ }
391
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', `Continuity SQLite metadata rejected unsupported value at ${path}.`);
392
+ }
393
+ function assertPublicSafeText(value, path) {
394
+ if (value === undefined)
395
+ return;
396
+ if (UNSAFE_METADATA_VALUE_PATTERN.test(value)) {
397
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', `Continuity SQLite text rejected unsafe value at ${path}.`);
398
+ }
399
+ }
400
+ function assertProcessingSummaryText(value, path, maxChars = 240) {
401
+ if (value.trim().length === 0) {
402
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', `Continuity SQLite processing summary text is required at ${path}.`);
403
+ }
404
+ assertPublicSafeText(value, path);
405
+ if (value.length > maxChars) {
406
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', `Continuity SQLite processing summary text exceeded the public bound at ${path}.`);
407
+ }
408
+ }
409
+ function filterContinuityPublicMetadata(input) {
410
+ const metadata = {};
411
+ if (!input)
412
+ return metadata;
413
+ for (const [key, value] of Object.entries(input)) {
414
+ if (!CONTINUITY_PUBLIC_METADATA_ALLOWED_KEYS.has(key))
415
+ continue;
416
+ if (UNSAFE_METADATA_KEY_PATTERN.test(key))
417
+ continue;
418
+ try {
419
+ assertPublicSafeValue(value, `metadata.${key}`);
420
+ metadata[key] = value;
421
+ }
422
+ catch {
423
+ // Summary metadata is best-effort public context. Unsafe entries are
424
+ // filtered so a safe public summary can still be persisted without raw
425
+ // logs, prompts, secrets, private URLs or protected bodies.
426
+ }
427
+ }
428
+ return metadata;
429
+ }
430
+ function buildProcessingOutputSummaryMetadata(input) {
431
+ const metadata = {
432
+ ...filterContinuityPublicMetadata(input.metadata),
433
+ schemaVersion: 1,
434
+ contractVersion: 'continuity-v1',
435
+ eventType: CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE,
436
+ executionId: input.executionId,
437
+ jobId: input.jobId,
438
+ operationId: input.executionId,
439
+ publicSummary: input.publicSummary,
440
+ };
441
+ if (input.idempotencyKey)
442
+ metadata.idempotencyKey = input.idempotencyKey;
443
+ if (input.goalId)
444
+ metadata.goalId = input.goalId;
445
+ if (input.loopId)
446
+ metadata.loopId = input.loopId;
447
+ if (input.reasonCode)
448
+ metadata.reasonCode = input.reasonCode;
449
+ if (input.source && CONTINUITY_PROCESSING_OUTPUT_SUMMARY_SOURCES.has(input.source)) {
450
+ metadata.processingOutputSource = input.source;
451
+ }
452
+ return filterContinuityPublicMetadata(metadata);
453
+ }
454
+ function normalizeProcessingSummaryLimit(limit) {
455
+ const candidate = Math.floor(limit ?? DEFAULT_CONTINUITY_PROCESSING_SUMMARY_LIMIT);
456
+ if (!Number.isFinite(candidate))
457
+ return DEFAULT_CONTINUITY_PROCESSING_SUMMARY_LIMIT;
458
+ return Math.max(1, Math.min(MAX_CONTINUITY_PROCESSING_SUMMARY_LIMIT, candidate));
459
+ }
460
+ function processingSummaryEventId(input) {
461
+ if (input.eventId && input.eventId.trim().length > 0)
462
+ return input.eventId;
463
+ const safe = input.executionId.replace(/[^a-z0-9_.:-]+/gi, '-').replace(/^-+|-+$/g, '');
464
+ if (safe.length > 0)
465
+ return `event-processing-summary-${safe.slice(0, 160)}`;
466
+ return `event-processing-summary-${createHash('sha256').update(input.executionId).digest('hex').slice(0, 32)}`;
467
+ }
468
+ function encodeProcessingSummaryCursor(row) {
469
+ return `${PROCESSING_SUMMARY_CURSOR_PREFIX}${Buffer.from(JSON.stringify([row.created_at, row.event_id]), 'utf8').toString('base64url')}`;
470
+ }
471
+ function decodeProcessingSummaryCursor(cursor) {
472
+ if (!cursor?.startsWith(PROCESSING_SUMMARY_CURSOR_PREFIX))
473
+ return undefined;
474
+ try {
475
+ const payload = cursor.slice(PROCESSING_SUMMARY_CURSOR_PREFIX.length);
476
+ const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
477
+ if (!Array.isArray(decoded) || decoded.length !== 2)
478
+ return undefined;
479
+ const [createdAt, eventId] = decoded;
480
+ if (typeof createdAt !== 'string' || typeof eventId !== 'string')
481
+ return undefined;
482
+ return { createdAt, eventId };
483
+ }
484
+ catch {
485
+ return undefined;
486
+ }
487
+ }
488
+ function normalizeProcessingSummaryTargetId(kind, value) {
489
+ if (!value)
490
+ return undefined;
491
+ const normalized = value.trim().replace(/^@/, '');
492
+ if (kind === 'job' && /^job-J\d{3,}$/i.test(normalized))
493
+ return `job-J${normalized.match(/\d+$/)?.[0] ?? ''}`;
494
+ if (kind === 'goal' && /^goal-G\d{3,}$/i.test(normalized))
495
+ return `goal-G${normalized.match(/\d+$/)?.[0] ?? ''}`;
496
+ if (kind === 'loop' && /^loop-L\d{3,}$/i.test(normalized))
497
+ return `loop-L${normalized.match(/\d+$/)?.[0] ?? ''}`;
498
+ return undefined;
499
+ }
500
+ function normalizedRelatedJobIds(values) {
501
+ if (!values)
502
+ return [];
503
+ return uniqueValues(values
504
+ .map((value) => normalizeProcessingSummaryTargetId('job', value))
505
+ .filter((value) => Boolean(value)));
506
+ }
507
+ function processingSummaryTargetPredicate(options) {
508
+ const target = options.target;
509
+ if (!target) {
510
+ const jobId = normalizeProcessingSummaryTargetId('job', options.jobId);
511
+ return jobId ? { sql: ' AND job_id = ?', params: [jobId] } : { sql: '', params: [] };
512
+ }
513
+ const selectorKind = target.selectorKind;
514
+ if (selectorKind === 'job') {
515
+ const targetJobId = normalizeProcessingSummaryTargetId('job', target.jobId ?? target.targetId ?? options.jobId);
516
+ return targetJobId ? { sql: ' AND job_id = ?', params: [targetJobId] } : { sql: ' AND 1 = 0', params: [] };
517
+ }
518
+ if (selectorKind === 'goal') {
519
+ const goalId = normalizeProcessingSummaryTargetId('goal', target.goalId ?? target.targetId);
520
+ if (!goalId)
521
+ return { sql: ' AND 1 = 0', params: [] };
522
+ const relatedJobIds = normalizedRelatedJobIds(target.relatedJobIds);
523
+ const relatedPredicate = relatedJobIds.length > 0
524
+ ? ` OR job_id IN (${relatedJobIds.map(() => '?').join(', ')})`
525
+ : '';
526
+ return {
527
+ sql: ` AND (job_id = ? OR json_extract(metadata_json, '$.goalId') = ?${relatedPredicate})`,
528
+ params: [goalId, goalId, ...relatedJobIds],
529
+ };
530
+ }
531
+ if (selectorKind === 'loop') {
532
+ const loopId = normalizeProcessingSummaryTargetId('loop', target.loopId ?? target.targetId);
533
+ if (!loopId)
534
+ return { sql: ' AND 1 = 0', params: [] };
535
+ const relatedJobIds = normalizedRelatedJobIds(target.relatedJobIds);
536
+ const relatedPredicate = relatedJobIds.length > 0
537
+ ? ` OR job_id IN (${relatedJobIds.map(() => '?').join(', ')})`
538
+ : '';
539
+ return {
540
+ sql: ` AND (job_id = ? OR json_extract(metadata_json, '$.loopId') = ?${relatedPredicate})`,
541
+ params: [loopId, loopId, ...relatedJobIds],
542
+ };
543
+ }
544
+ return { sql: ' AND 1 = 0', params: [] };
545
+ }
546
+ function toProcessingOutputSummaryRow(row) {
547
+ const output = {
548
+ event_id: row.event_id,
549
+ job_id: row.job_id,
550
+ execution_id: row.operation_id,
551
+ public_summary: row.summary,
552
+ reason_code: row.reason_code,
553
+ created_at: row.created_at,
554
+ metadata_json: row.metadata_json,
555
+ };
556
+ return {
557
+ ...output,
558
+ cursor: encodeProcessingSummaryCursor(output),
559
+ };
560
+ }
561
+ function stringifyPublicMetadata(metadata) {
562
+ if (!metadata)
563
+ return null;
564
+ assertPublicSafeValue(metadata, 'metadata');
565
+ return JSON.stringify(metadata);
566
+ }
567
+ function retentionHashes(jobId, receipts, outputs) {
568
+ const digest = (values) => `sha256:${createHash('sha256')
569
+ .update(values
570
+ .map((value) => [value.id, value.status, value.reasonCode].join(':'))
571
+ .sort()
572
+ .join('\n'))
573
+ .digest('hex')}`;
574
+ const receiptHash = digest(receipts);
575
+ const outputHash = digest(outputs);
576
+ return {
577
+ receipts: receiptHash,
578
+ outputs: outputHash,
579
+ aggregate: `sha256:${createHash('sha256').update(`${jobId}:${receiptHash}:${outputHash}`).digest('hex')}`,
580
+ };
581
+ }
582
+ /**
583
+ * P195.04: Parse an existing `jobs.metadata_json` value into a mutable object for
584
+ * additive read-modify-write of stage progress. Corrupt/absent metadata returns a
585
+ * fresh object so forward-compatible reads never throw on older DBs.
586
+ */
587
+ function parseJobMetadataObject(metadataJson) {
588
+ if (!metadataJson)
589
+ return {};
590
+ try {
591
+ const parsed = JSON.parse(metadataJson);
592
+ return isRecord(parsed) ? { ...parsed } : {};
593
+ }
594
+ catch {
595
+ return {};
596
+ }
597
+ }
598
+ function assertBoundedPublicText(value, path, maxChars, required = false) {
599
+ if (value === undefined) {
600
+ if (required) {
601
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', `Continuity SQLite text is required at ${path}.`);
602
+ }
603
+ return;
604
+ }
605
+ assertPublicSafeText(value, path);
606
+ if (value.trim().length === 0 || value.length > maxChars) {
607
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', `Continuity SQLite text exceeded the public bound at ${path}.`);
608
+ }
609
+ }
610
+ function assertNonNegativeInteger(value, path) {
611
+ if (value === undefined)
612
+ return;
613
+ if (!Number.isFinite(value) || value < 0 || Math.floor(value) !== value) {
614
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', `Continuity SQLite numeric value rejected at ${path}.`);
615
+ }
616
+ }
617
+ function assertOutboxIdentity(input) {
618
+ assertBoundedPublicText(input.outboxId, 'outbox.outboxId', 160, true);
619
+ assertBoundedPublicText(input.jobId, 'outbox.jobId', 160);
620
+ assertBoundedPublicText(input.operationId, 'outbox.operationId', 160);
621
+ assertBoundedPublicText(input.idempotencyKey, 'outbox.idempotencyKey', 160);
622
+ assertBoundedPublicText(input.type, 'outbox.type', 80);
623
+ assertBoundedPublicText(input.status, 'outbox.status', 40);
624
+ assertBoundedPublicText(input.deliveryCursor, 'outbox.deliveryCursor', 200);
625
+ assertBoundedPublicText(input.timestamp, 'outbox.timestamp', 80);
626
+ assertBoundedPublicText(input.reasonCode, 'outbox.reasonCode', 100);
627
+ assertBoundedPublicText(input.summary, 'outbox.summary', 240);
628
+ }
629
+ function isOutboxStatus(value) {
630
+ return value === 'pending' || value === 'sent' || value === 'acked' || value === 'failed' || value === 'dead_letter';
631
+ }
632
+ function publicOutboxStatus(value) {
633
+ return typeof value === 'string' && isOutboxStatus(value) ? value : 'failed';
634
+ }
635
+ function optionalOutboxString(value) {
636
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
637
+ }
638
+ function parseOutboxPayload(payloadJson) {
639
+ if (typeof payloadJson !== 'string' || payloadJson.trim().length === 0)
640
+ return {};
641
+ try {
642
+ const parsed = JSON.parse(payloadJson);
643
+ if (!isRecord(parsed))
644
+ return {};
645
+ assertPublicSafeValue(parsed, 'outbox.payload');
646
+ return parsed;
647
+ }
648
+ catch {
649
+ return {};
650
+ }
651
+ }
652
+ function toOutboxDeliveryRow(row) {
653
+ return {
654
+ outboxId: String(row.outbox_id ?? ''),
655
+ ...(optionalOutboxString(row.job_id) ? { jobId: optionalOutboxString(row.job_id) } : {}),
656
+ operationId: String(row.operation_id ?? ''),
657
+ ...(optionalOutboxString(row.idempotency_key) ? { idempotencyKey: optionalOutboxString(row.idempotency_key) } : {}),
658
+ type: String(row.type ?? ''),
659
+ status: publicOutboxStatus(row.status),
660
+ payload: parseOutboxPayload(row.payload_json),
661
+ retryCount: typeof row.retry_count === 'number' ? row.retry_count : 0,
662
+ attemptCount: typeof row.attempt_count === 'number' ? row.attempt_count : 0,
663
+ ...(optionalOutboxString(row.delivery_cursor) ? { deliveryCursor: optionalOutboxString(row.delivery_cursor) } : {}),
664
+ ...(optionalOutboxString(row.next_attempt_at) ? { nextAttemptAt: optionalOutboxString(row.next_attempt_at) } : {}),
665
+ ...(optionalOutboxString(row.last_attempt_at) ? { lastAttemptAt: optionalOutboxString(row.last_attempt_at) } : {}),
666
+ ...(optionalOutboxString(row.sent_at) ? { sentAt: optionalOutboxString(row.sent_at) } : {}),
667
+ ...(optionalOutboxString(row.acked_at) ? { ackedAt: optionalOutboxString(row.acked_at) } : {}),
668
+ ...(optionalOutboxString(row.last_error_code) ? { lastErrorCode: optionalOutboxString(row.last_error_code) } : {}),
669
+ ...(optionalOutboxString(row.last_error_reason) ? { lastErrorReason: optionalOutboxString(row.last_error_reason) } : {}),
670
+ ...(optionalOutboxString(row.last_error_summary) ? { lastErrorSummary: optionalOutboxString(row.last_error_summary) } : {}),
671
+ ...(optionalOutboxString(row.dead_letter_summary) ? { deadLetterSummary: optionalOutboxString(row.dead_letter_summary) } : {}),
672
+ ...(optionalOutboxString(row.dead_lettered_at) ? { deadLetteredAt: optionalOutboxString(row.dead_lettered_at) } : {}),
673
+ createdAt: String(row.created_at ?? ''),
674
+ updatedAt: String(row.updated_at ?? ''),
675
+ publicSafe: true,
676
+ };
677
+ }
678
+ function outboxTransitionResult(input) {
679
+ return { ...input, publicSafe: true };
680
+ }
681
+ function stringifyPublicJsonValue(value, path) {
682
+ if (value === undefined)
683
+ return null;
684
+ assertPublicSafeValue(value, path);
685
+ return JSON.stringify(value);
686
+ }
687
+ function normalizePublicRelativePaths(paths) {
688
+ if (!paths)
689
+ return undefined;
690
+ const normalized = uniqueValues(paths.map((path) => path.replace(/\\/g, '/')))
691
+ .filter((path) => path.length > 0)
692
+ .slice(0, 50);
693
+ for (const [index, path] of normalized.entries()) {
694
+ assertBoundedPublicText(path, `relativePaths[${index}]`, 240, true);
695
+ const segments = path.split('/').filter(Boolean);
696
+ if (path.startsWith('/') || path.startsWith('~') || path.includes('://') || /^[A-Za-z]:\//.test(path) || segments.includes('..')) {
697
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', `Continuity SQLite rejected non-relative path at relativePaths[${index}].`);
698
+ }
699
+ }
700
+ return normalized;
701
+ }
702
+ function buildHeadlessExecutionPublicMetadata(input) {
703
+ const metadata = {
704
+ ...filterContinuityPublicMetadata(input.metadata),
705
+ schemaVersion: 1,
706
+ contractVersion: 'continuity-v1',
707
+ attemptId: input.attemptId,
708
+ executionId: input.executionId,
709
+ jobId: input.jobId,
710
+ };
711
+ if (input.directiveHash)
712
+ metadata.directiveHash = input.directiveHash;
713
+ if (input.runtimeId)
714
+ metadata.runtimeId = input.runtimeId;
715
+ if (input.modelId)
716
+ metadata.modelId = input.modelId;
717
+ if (input.runtimeSource)
718
+ metadata.runtimeSource = input.runtimeSource;
719
+ if (input.status)
720
+ metadata.attemptStatus = input.status;
721
+ if (input.reasonCode)
722
+ metadata.reasonCode = input.reasonCode;
723
+ if (input.durationMs !== undefined)
724
+ metadata.durationMs = input.durationMs;
725
+ if (input.retryCount !== undefined)
726
+ metadata.retryCount = input.retryCount;
727
+ if (input.attemptCount !== undefined)
728
+ metadata.attemptCount = input.attemptCount;
729
+ if (input.maxAttempts !== undefined)
730
+ metadata.maxAttempts = input.maxAttempts;
731
+ if (input.queuePriority !== undefined)
732
+ metadata.queuePriority = input.queuePriority;
733
+ if (input.queueScope)
734
+ metadata.queueScope = input.queueScope;
735
+ if (input.nextRetryAt)
736
+ metadata.nextRetryAt = input.nextRetryAt;
737
+ if (input.deadLetterReasonCode)
738
+ metadata.deadLetterReasonCode = input.deadLetterReasonCode;
739
+ if (input.counts)
740
+ metadata.counts = input.counts;
741
+ if (input.hashes)
742
+ metadata.hashes = input.hashes;
743
+ if (input.redactionSummary)
744
+ metadata.redactionEvidence = input.redactionSummary;
745
+ if (input.relativePaths)
746
+ metadata.relativePaths = normalizePublicRelativePaths(input.relativePaths);
747
+ return filterContinuityPublicMetadata(metadata);
748
+ }
749
+ function validateHeadlessExecutionAttemptIdentity(input) {
750
+ assertBoundedPublicText(input.attemptId, 'headless_execution.attemptId', 160, true);
751
+ assertBoundedPublicText(input.executionId, 'headless_execution.executionId', 160, true);
752
+ assertBoundedPublicText(input.jobId, 'headless_execution.jobId', 160, true);
753
+ assertBoundedPublicText(input.directiveHash, 'headless_execution.directiveHash', 160);
754
+ assertBoundedPublicText(input.targetRef, 'headless_execution.targetRef', 240);
755
+ assertBoundedPublicText(input.storyId, 'headless_execution.storyId', 120);
756
+ assertBoundedPublicText(input.epicId, 'headless_execution.epicId', 120);
757
+ assertBoundedPublicText(input.runtimeId, 'headless_execution.runtimeId', 80);
758
+ assertBoundedPublicText(input.modelId, 'headless_execution.modelId', 120);
759
+ assertBoundedPublicText(input.runtimeSource, 'headless_execution.runtimeSource', 40);
760
+ assertBoundedPublicText(input.reasonCode, 'headless_execution.reasonCode', 100);
761
+ assertBoundedPublicText(input.publicSummary, 'headless_execution.publicSummary', 240);
762
+ assertBoundedPublicText(input.leaseId, 'headless_execution.leaseId', 160);
763
+ assertBoundedPublicText(input.runnerId, 'headless_execution.runnerId', 160);
764
+ assertBoundedPublicText(input.timestamp, 'headless_execution.timestamp', 80);
765
+ }
766
+ function asSinglePragmaValue(value) {
767
+ if (Array.isArray(value)) {
768
+ const first = value[0];
769
+ if (isRecord(first))
770
+ return Object.values(first)[0];
771
+ return first;
772
+ }
773
+ if (isRecord(value))
774
+ return Object.values(value)[0];
775
+ return value;
776
+ }
777
+ function isContinuityTable(table) {
778
+ return CONTINUITY_TABLES.includes(table);
779
+ }
780
+ function uniqueValues(values) {
781
+ return Array.from(new Set(values));
782
+ }
783
+ function createBetterSqlite3Adapter(moduleValue) {
784
+ const constructorValue = isRecord(moduleValue) && 'default' in moduleValue
785
+ ? moduleValue.default
786
+ : moduleValue;
787
+ if (typeof constructorValue !== 'function') {
788
+ throw new ContinuitySqliteStoreError('sqlite-adapter-unavailable', 'Optional better-sqlite3 adapter did not export a database constructor.', { guidance: SQLITE_ADAPTER_INSTALL_GUIDANCE });
789
+ }
790
+ const Database = constructorValue;
791
+ return {
792
+ adapterName: 'better-sqlite3',
793
+ open(filePath, options) {
794
+ return new Database(filePath, options);
795
+ },
796
+ };
797
+ }
798
+ export async function loadBetterSqlite3Adapter(importer = defaultDynamicImport) {
799
+ try {
800
+ const moduleValue = await importer('better-sqlite3');
801
+ return { ok: true, adapter: createBetterSqlite3Adapter(moduleValue) };
802
+ }
803
+ catch (error) {
804
+ if (error instanceof ContinuitySqliteStoreError)
805
+ return { ok: false, error };
806
+ return {
807
+ ok: false,
808
+ error: new ContinuitySqliteStoreError('sqlite-adapter-unavailable', 'Optional Enterprise continuity SQLite adapter better-sqlite3 is unavailable.', { cause: error, guidance: SQLITE_ADAPTER_INSTALL_GUIDANCE }),
809
+ };
810
+ }
811
+ }
812
+ export function continuitySqliteStorePath(projectRoot) {
813
+ return join(projectRoot, CONTINUITY_SQLITE_RELATIVE_DIR, CONTINUITY_SQLITE_FILENAME);
814
+ }
815
+ function ensureContinuityDirectory(projectRoot) {
816
+ const directory = join(projectRoot, CONTINUITY_SQLITE_RELATIVE_DIR);
817
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
818
+ try {
819
+ chmodSync(directory, 0o700);
820
+ }
821
+ catch {
822
+ // Permission tightening is best-effort on non-POSIX filesystems.
823
+ }
824
+ return directory;
825
+ }
826
+ export async function openContinuitySqliteStore(options) {
827
+ const adapter = options.adapter ?? await resolveAdapter(options.adapterLoader);
828
+ const filePath = options.filePath ?? continuitySqliteStorePath(options.projectRoot);
829
+ if (options.strictReadOnly) {
830
+ assertContinuitySqliteReadOnlyHeader(filePath);
831
+ }
832
+ else {
833
+ if (options.filePath) {
834
+ mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
835
+ try {
836
+ chmodSync(dirname(filePath), 0o700);
837
+ }
838
+ catch { /* best-effort */ }
839
+ }
840
+ else {
841
+ ensureContinuityDirectory(options.projectRoot);
842
+ }
843
+ }
844
+ let db;
845
+ try {
846
+ db = adapter.open(filePath, options.strictReadOnly
847
+ ? { fileMustExist: true, readonly: true }
848
+ : undefined);
849
+ }
850
+ catch (error) {
851
+ throw mapSqliteError(error, 'sqlite-integrity-failed');
852
+ }
853
+ const store = new ContinuitySqliteStore({
854
+ db,
855
+ filePath,
856
+ adapterName: adapter.adapterName,
857
+ busyTimeoutMs: options.busyTimeoutMs ?? DEFAULT_CONTINUITY_SQLITE_BUSY_TIMEOUT_MS,
858
+ integrityCheck: options.integrityCheck ?? true,
859
+ now: options.now ?? (() => new Date().toISOString()),
860
+ });
861
+ try {
862
+ if (options.strictReadOnly) {
863
+ store.runIntegrityCheck();
864
+ // Prove the expected schema is readable without attempting repair.
865
+ store.getSchemaVersion();
866
+ return store;
867
+ }
868
+ store.initialize();
869
+ if (existsSync(filePath)) {
870
+ try {
871
+ chmodSync(filePath, 0o600);
872
+ }
873
+ catch {
874
+ // Best-effort on non-POSIX filesystems.
875
+ }
876
+ }
877
+ return store;
878
+ }
879
+ catch (error) {
880
+ try {
881
+ db.close();
882
+ }
883
+ catch {
884
+ // Ignore close errors while surfacing the original hard stop.
885
+ }
886
+ throw error;
887
+ }
888
+ }
889
+ function assertContinuitySqliteReadOnlyHeader(filePath) {
890
+ if (!existsSync(filePath)) {
891
+ throw new ContinuitySqliteStoreError('sqlite-integrity-failed', 'Continuity database is unavailable for physical read-only inspection.', { mutationBlocked: true });
892
+ }
893
+ let header;
894
+ try {
895
+ const fd = openSync(filePath, 'r');
896
+ try {
897
+ header = Buffer.alloc(20);
898
+ if (readSync(fd, header, 0, header.length, 0) < header.length)
899
+ throw new Error('short-header');
900
+ }
901
+ finally {
902
+ closeSync(fd);
903
+ }
904
+ }
905
+ catch (error) {
906
+ throw new ContinuitySqliteStoreError('sqlite-integrity-failed', 'Continuity database header could not be inspected read-only.', { cause: error, mutationBlocked: true });
907
+ }
908
+ if (header.subarray(0, 16).toString('ascii') !== 'SQLite format 3\0' || header[18] === 2 || header[19] === 2) {
909
+ throw new ContinuitySqliteStoreError('sqlite-integrity-failed', 'Continuity database requires an unsafe WAL read profile.', { mutationBlocked: true });
910
+ }
911
+ }
912
+ /**
913
+ * P205.05 physical inspection boundary. The database must already exist and is
914
+ * opened with the adapter's OS-backed read-only flag. No directory creation,
915
+ * chmod, initialization, migrations or mutative pragma is attempted.
916
+ */
917
+ export async function openContinuitySqliteReadOnly(options) {
918
+ const filePath = continuitySqliteStorePath(options.projectRoot);
919
+ // SQLite read-only opens can still create `-wal`/`-shm` companions when the
920
+ // database header is in WAL mode. Deny that profile before adapter open.
921
+ assertContinuitySqliteReadOnlyHeader(filePath);
922
+ const adapter = options.adapter ?? await resolveAdapter(options.adapterLoader);
923
+ let db;
924
+ try {
925
+ db = adapter.open(filePath, { fileMustExist: true, readonly: true });
926
+ const integrity = asSinglePragmaValue(db.pragma('integrity_check', { simple: true }));
927
+ if (String(integrity).toLowerCase() !== 'ok')
928
+ throw new Error('integrity');
929
+ }
930
+ catch (error) {
931
+ throw new ContinuitySqliteStoreError('sqlite-integrity-failed', 'Continuity database failed physical read-only inspection.', { cause: error, mutationBlocked: true });
932
+ }
933
+ const assertReadStatement = (source) => {
934
+ const normalized = source.trim().replace(/^\(+/u, '').toLowerCase();
935
+ if (!/^(select|with\s)/u.test(normalized) || /\b(insert|update|delete|replace|create|alter|drop|pragma|attach|detach|vacuum|reindex)\b/u.test(normalized)) {
936
+ throw new ContinuitySqliteStoreError('sqlite-mutation-blocked', 'Statement is not in the physical read-only query profile.', { mutationBlocked: true });
937
+ }
938
+ };
939
+ return Object.freeze({
940
+ filePath,
941
+ adapterName: adapter.adapterName,
942
+ all(source, ...params) { assertReadStatement(source); return db.prepare(source).all(...params); },
943
+ get(source, ...params) { assertReadStatement(source); return db.prepare(source).get(...params); },
944
+ close() { db.close(); },
945
+ });
946
+ }
947
+ async function resolveAdapter(loader) {
948
+ const result = loader ? await loader() : await loadBetterSqlite3Adapter();
949
+ if (!result.ok)
950
+ throw result.error;
951
+ return result.adapter;
952
+ }
953
+ export class ContinuitySqliteStore {
954
+ filePath;
955
+ adapterName;
956
+ db;
957
+ busyTimeoutMs;
958
+ integrityCheck;
959
+ now;
960
+ mutationBlocked = false;
961
+ constructor(options) {
962
+ this.db = options.db;
963
+ this.filePath = options.filePath;
964
+ this.adapterName = options.adapterName;
965
+ this.busyTimeoutMs = options.busyTimeoutMs;
966
+ this.integrityCheck = options.integrityCheck;
967
+ this.now = options.now;
968
+ }
969
+ initialize() {
970
+ try {
971
+ this.configureConnection();
972
+ if (this.integrityCheck)
973
+ this.runIntegrityCheck();
974
+ this.ensureMigrationTable();
975
+ this.applyMigrations();
976
+ if (this.integrityCheck)
977
+ this.runIntegrityCheck();
978
+ }
979
+ catch (error) {
980
+ throw mapSqliteError(error, 'sqlite-migration-failed');
981
+ }
982
+ }
983
+ close() {
984
+ this.db.close();
985
+ }
986
+ pragmaValue(source) {
987
+ return asSinglePragmaValue(this.db.pragma(source, { simple: true }));
988
+ }
989
+ runIntegrityCheck() {
990
+ try {
991
+ const result = String(asSinglePragmaValue(this.db.pragma('integrity_check', { simple: true })) ?? '');
992
+ if (result.toLowerCase() !== 'ok') {
993
+ this.mutationBlocked = true;
994
+ throw new ContinuitySqliteStoreError('sqlite-integrity-failed', 'Continuity SQLite integrity check failed; durable mutation is blocked until the store is repaired or rebuilt.', { mutationBlocked: true });
995
+ }
996
+ }
997
+ catch (error) {
998
+ this.mutationBlocked = true;
999
+ throw mapSqliteError(error, 'sqlite-integrity-failed');
1000
+ }
1001
+ }
1002
+ getSchemaVersion() {
1003
+ const row = this.db.prepare('SELECT COALESCE(MAX(version), 0) AS version FROM continuity_migrations').get();
1004
+ return isRecord(row) && typeof row.version === 'number' ? row.version : 0;
1005
+ }
1006
+ countRows(table) {
1007
+ if (!isContinuityTable(table)) {
1008
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Unknown continuity table name.');
1009
+ }
1010
+ const row = this.db.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get();
1011
+ return isRecord(row) && typeof row.count === 'number' ? row.count : 0;
1012
+ }
1013
+ reserveNativeAgentDispatch(input) {
1014
+ this.assertCanMutate();
1015
+ if (!isNativeDispatchBindingV1(input.binding) || !isNativeHostAttestationV1(input.hostAttestation)) {
1016
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Native-agent reservation rejected invalid binding or host evidence.');
1017
+ }
1018
+ const candidate = {
1019
+ schemaVersion: 1,
1020
+ storyId: input.binding.storyId,
1021
+ graphRevision: input.binding.graphRevision.replace(/^sha256:/, '').toLowerCase(),
1022
+ dispatchKey: input.binding.dispatchKey.toLowerCase(),
1023
+ profile: 'neocortex',
1024
+ parentHandle: null,
1025
+ handle: null,
1026
+ topology: input.topology ?? 'native-agent',
1027
+ state: 'requested',
1028
+ requestedAt: input.requestedAt,
1029
+ observedAt: null,
1030
+ terminalAt: null,
1031
+ freshness: 'unknown',
1032
+ observationSource: 'local-reservation',
1033
+ reasonCode: input.reasonCode ?? 'native-agent-dispatch-reserved',
1034
+ revision: 0,
1035
+ publicSafe: true,
1036
+ };
1037
+ if (!isNativeAgentDispatchReceiptV1(candidate)) {
1038
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Native-agent reservation rejected unsafe public fields.');
1039
+ }
1040
+ try {
1041
+ return this.runInTransaction(() => {
1042
+ const inserted = this.db.prepare(`
3
1043
  INSERT OR IGNORE INTO native_agent_dispatches (
4
1044
  dispatch_key, story_id, graph_revision, profile,
5
1045
  host_kind, host_version, host_origin, host_confidence,
6
1046
  parent_handle, handle, topology, state, requested_at, observed_at,
7
1047
  terminal_at, freshness, observation_source, reason_code, revision
8
1048
  ) VALUES (?, ?, ?, 'neocortex', ?, ?, ?, ?, NULL, NULL, ?, 'requested', ?, NULL, NULL, 'unknown', 'local-reservation', ?, 0)
9
- `).run(t.dispatchKey,t.storyId,t.graphRevision,e.hostAttestation.hostKind,e.hostAttestation.hostVersion,e.hostAttestation.origin,e.hostAttestation.confidence,t.topology,t.requestedAt,t.reasonCode),s=this.findNativeAgentDispatchRow(t.dispatchKey,t.storyId,t.graphRevision);if(!s)throw new Error("native-agent-reservation-readback-missing");if(s.dispatch_key!==t.dispatchKey||s.story_id!==t.storyId||s.graph_revision!==t.graphRevision)throw new i("sqlite-mutation-failed","Native-agent reservation conflicts with an existing story graph binding.");return Object.freeze({receipt:$(s),reserved:r.changes===1,replay:r.changes===0})})}catch(r){throw m(r,"sqlite-mutation-failed")}}observeNativeAgentDispatch(e){this.assertCanMutate();try{return this.runInTransaction(()=>{const t=this.db.prepare("SELECT * FROM native_agent_dispatches WHERE dispatch_key = ?").get(e.dispatchKey);if(!t)throw new i("sqlite-mutation-failed","Native-agent dispatch reservation is missing.");const r=$(t),s=oe.has(e.state)?e.terminalAt??e.observedAt:null,n=e.handle??r.handle,d=e.parentHandle===void 0?r.parentHandle:e.parentHandle,o={...r,handle:n,parentHandle:d,topology:e.topology,state:e.state,observedAt:e.observedAt,terminalAt:s,freshness:e.freshness,observationSource:e.observationSource,reasonCode:e.reasonCode,revision:r.revision+1};if(!X(o))throw new i("sqlite-public-safety-rejected","Native-agent observation rejected unsafe or inconsistent fields.");if(r.handle&&e.handle&&r.handle!==e.handle)throw new i("sqlite-mutation-failed","Native-agent observation handle conflicts with the reserved dispatch.");if(!je(r.state,e.state))throw new i("sqlite-mutation-failed","Native-agent observation attempted a terminal or non-monotonic regression.");if(r.state===o.state&&r.handle===o.handle&&r.parentHandle===o.parentHandle&&r.topology===o.topology&&r.observedAt===o.observedAt&&r.terminalAt===o.terminalAt&&r.freshness===o.freshness&&r.observationSource===o.observationSource&&r.reasonCode===o.reasonCode)return Object.freeze({receipt:r,applied:!1,replay:!0});if(e.expectedRevision!==r.revision)throw new i("sqlite-mutation-failed","Native-agent observation CAS revision conflict.");if(r.observedAt&&Date.parse(e.observedAt)<Date.parse(r.observedAt))throw new i("sqlite-mutation-failed","Native-agent observation timestamp regressed.");if(this.db.prepare(`
1049
+ `).run(candidate.dispatchKey, candidate.storyId, candidate.graphRevision, input.hostAttestation.hostKind, input.hostAttestation.hostVersion, input.hostAttestation.origin, input.hostAttestation.confidence, candidate.topology, candidate.requestedAt, candidate.reasonCode);
1050
+ const row = this.findNativeAgentDispatchRow(candidate.dispatchKey, candidate.storyId, candidate.graphRevision);
1051
+ if (!row)
1052
+ throw new Error('native-agent-reservation-readback-missing');
1053
+ if (row.dispatch_key !== candidate.dispatchKey
1054
+ || row.story_id !== candidate.storyId
1055
+ || row.graph_revision !== candidate.graphRevision) {
1056
+ throw new ContinuitySqliteStoreError('sqlite-mutation-failed', 'Native-agent reservation conflicts with an existing story graph binding.');
1057
+ }
1058
+ return Object.freeze({
1059
+ receipt: toNativeAgentReceipt(row),
1060
+ reserved: inserted.changes === 1,
1061
+ replay: inserted.changes === 0,
1062
+ });
1063
+ });
1064
+ }
1065
+ catch (error) {
1066
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
1067
+ }
1068
+ }
1069
+ observeNativeAgentDispatch(input) {
1070
+ this.assertCanMutate();
1071
+ try {
1072
+ return this.runInTransaction(() => {
1073
+ const currentRow = this.db.prepare('SELECT * FROM native_agent_dispatches WHERE dispatch_key = ?').get(input.dispatchKey);
1074
+ if (!currentRow) {
1075
+ throw new ContinuitySqliteStoreError('sqlite-mutation-failed', 'Native-agent dispatch reservation is missing.');
1076
+ }
1077
+ const current = toNativeAgentReceipt(currentRow);
1078
+ const terminalAt = NATIVE_AGENT_TERMINAL_STATES.has(input.state)
1079
+ ? input.terminalAt ?? input.observedAt
1080
+ : null;
1081
+ const handle = input.handle ?? current.handle;
1082
+ const parentHandle = input.parentHandle === undefined ? current.parentHandle : input.parentHandle;
1083
+ const candidate = {
1084
+ ...current,
1085
+ handle,
1086
+ parentHandle,
1087
+ topology: input.topology,
1088
+ state: input.state,
1089
+ observedAt: input.observedAt,
1090
+ terminalAt,
1091
+ freshness: input.freshness,
1092
+ observationSource: input.observationSource,
1093
+ reasonCode: input.reasonCode,
1094
+ revision: current.revision + 1,
1095
+ };
1096
+ if (!isNativeAgentDispatchReceiptV1(candidate)) {
1097
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Native-agent observation rejected unsafe or inconsistent fields.');
1098
+ }
1099
+ if (current.handle && input.handle && current.handle !== input.handle) {
1100
+ throw new ContinuitySqliteStoreError('sqlite-mutation-failed', 'Native-agent observation handle conflicts with the reserved dispatch.');
1101
+ }
1102
+ if (!isNativeAgentStateTransitionAllowed(current.state, input.state)) {
1103
+ throw new ContinuitySqliteStoreError('sqlite-mutation-failed', 'Native-agent observation attempted a terminal or non-monotonic regression.');
1104
+ }
1105
+ const sameObservation = current.state === candidate.state
1106
+ && current.handle === candidate.handle
1107
+ && current.parentHandle === candidate.parentHandle
1108
+ && current.topology === candidate.topology
1109
+ && current.observedAt === candidate.observedAt
1110
+ && current.terminalAt === candidate.terminalAt
1111
+ && current.freshness === candidate.freshness
1112
+ && current.observationSource === candidate.observationSource
1113
+ && current.reasonCode === candidate.reasonCode;
1114
+ if (sameObservation)
1115
+ return Object.freeze({ receipt: current, applied: false, replay: true });
1116
+ if (input.expectedRevision !== current.revision) {
1117
+ throw new ContinuitySqliteStoreError('sqlite-mutation-failed', 'Native-agent observation CAS revision conflict.');
1118
+ }
1119
+ if (current.observedAt && Date.parse(input.observedAt) < Date.parse(current.observedAt)) {
1120
+ throw new ContinuitySqliteStoreError('sqlite-mutation-failed', 'Native-agent observation timestamp regressed.');
1121
+ }
1122
+ const updated = this.db.prepare(`
10
1123
  UPDATE native_agent_dispatches
11
1124
  SET parent_handle = ?, handle = ?, topology = ?, state = ?, observed_at = ?,
12
1125
  terminal_at = ?, freshness = ?, observation_source = ?, reason_code = ?, revision = revision + 1
13
1126
  WHERE dispatch_key = ? AND revision = ?
14
- `).run(o.parentHandle,o.handle,o.topology,o.state,o.observedAt,o.terminalAt,o.freshness,o.observationSource,o.reasonCode,o.dispatchKey,e.expectedRevision).changes!==1)throw new i("sqlite-mutation-failed","Native-agent observation CAS update was not applied.");return this.db.prepare(`
1127
+ `).run(candidate.parentHandle, candidate.handle, candidate.topology, candidate.state, candidate.observedAt, candidate.terminalAt, candidate.freshness, candidate.observationSource, candidate.reasonCode, candidate.dispatchKey, input.expectedRevision);
1128
+ if (updated.changes !== 1) {
1129
+ throw new ContinuitySqliteStoreError('sqlite-mutation-failed', 'Native-agent observation CAS update was not applied.');
1130
+ }
1131
+ this.db.prepare(`
15
1132
  INSERT INTO native_agent_observations (
16
1133
  dispatch_key, sequence, state, handle, parent_handle, topology,
17
1134
  observed_at, terminal_at, freshness, observation_source, reason_code
18
1135
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
19
- `).run(o.dispatchKey,o.revision,o.state,o.handle,o.parentHandle,o.topology,o.observedAt,o.terminalAt,o.freshness,o.observationSource,o.reasonCode),Object.freeze({receipt:o,applied:!0,replay:!1})})}catch(t){throw m(t,"sqlite-mutation-failed")}}getNativeAgentDispatchStatus(e,t={}){const r=this.db.prepare("SELECT * FROM native_agent_dispatches WHERE dispatch_key = ?").get(e);if(!r)return null;const s=$(r),n=Math.max(1e3,t.staleAfterMs??3e4),d=Date.parse(t.nowIso??this.now()),o=s.observedAt?Date.parse(s.observedAt):NaN;return!oe.has(s.state)&&Number.isFinite(d)&&(!Number.isFinite(o)||d-o>n)?Object.freeze({...s,state:"unknown",freshness:s.observedAt?"stale":"unknown",reasonCode:s.observedAt?"native-agent-observation-stale":"native-agent-awaiting-host-evidence"}):s}listRecentNativeAgentDispatches(e=20){const t=Math.max(1,Math.min(50,Math.floor(e))),r=this.db.prepare(`
1136
+ `).run(candidate.dispatchKey, candidate.revision, candidate.state, candidate.handle, candidate.parentHandle, candidate.topology, candidate.observedAt, candidate.terminalAt, candidate.freshness, candidate.observationSource, candidate.reasonCode);
1137
+ return Object.freeze({ receipt: candidate, applied: true, replay: false });
1138
+ });
1139
+ }
1140
+ catch (error) {
1141
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
1142
+ }
1143
+ }
1144
+ getNativeAgentDispatchStatus(dispatchKey, options = {}) {
1145
+ const row = this.db.prepare('SELECT * FROM native_agent_dispatches WHERE dispatch_key = ?').get(dispatchKey);
1146
+ if (!row)
1147
+ return null;
1148
+ const receipt = toNativeAgentReceipt(row);
1149
+ const staleAfterMs = Math.max(1_000, options.staleAfterMs ?? 30_000);
1150
+ const nowMs = Date.parse(options.nowIso ?? this.now());
1151
+ const observedMs = receipt.observedAt ? Date.parse(receipt.observedAt) : NaN;
1152
+ if (!NATIVE_AGENT_TERMINAL_STATES.has(receipt.state)
1153
+ && Number.isFinite(nowMs)
1154
+ && (!Number.isFinite(observedMs) || nowMs - observedMs > staleAfterMs)) {
1155
+ return Object.freeze({
1156
+ ...receipt,
1157
+ state: 'unknown',
1158
+ freshness: receipt.observedAt ? 'stale' : 'unknown',
1159
+ reasonCode: receipt.observedAt ? 'native-agent-observation-stale' : 'native-agent-awaiting-host-evidence',
1160
+ });
1161
+ }
1162
+ return receipt;
1163
+ }
1164
+ listRecentNativeAgentDispatches(limit = 20) {
1165
+ const bounded = Math.max(1, Math.min(50, Math.floor(limit)));
1166
+ const rows = this.db.prepare(`
20
1167
  SELECT * FROM native_agent_dispatches
21
1168
  ORDER BY COALESCE(observed_at, requested_at) DESC, dispatch_key DESC
22
1169
  LIMIT ?
23
- `).all(t);return Object.freeze(r.map($))}findNativeAgentDispatchRow(e,t,r){return this.db.prepare(`
1170
+ `).all(bounded);
1171
+ return Object.freeze(rows.map(toNativeAgentReceipt));
1172
+ }
1173
+ findNativeAgentDispatchRow(dispatchKey, storyId, graphRevision) {
1174
+ return this.db.prepare(`
24
1175
  SELECT * FROM native_agent_dispatches
25
1176
  WHERE dispatch_key = ? OR (story_id = ? AND graph_revision = ?)
26
1177
  ORDER BY CASE WHEN dispatch_key = ? THEN 0 ELSE 1 END
27
1178
  LIMIT 1
28
- `).get(e,t,r,e)}getLocalConsoleAttachmentState(e){c(e,"local_console.jobId",160,!0);const t=this.db.prepare(`
1179
+ `).get(dispatchKey, storyId, graphRevision, dispatchKey);
1180
+ }
1181
+ getLocalConsoleAttachmentState(jobId) {
1182
+ assertBoundedPublicText(jobId, 'local_console.jobId', 160, true);
1183
+ const row = this.db.prepare(`
29
1184
  SELECT j.job_id AS job_id,
30
1185
  j.kind AS kind,
31
1186
  j.status AS status,
@@ -34,41 +1189,210 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
34
1189
  FROM jobs j
35
1190
  WHERE j.job_id = ?
36
1191
  LIMIT 1
37
- `).get(e);return!E(t)||typeof t.job_id!="string"?null:{jobId:t.job_id,kind:typeof t.kind=="string"?t.kind:"unknown",status:typeof t.status=="string"?t.status:"unknown",activeLeaseCount:typeof t.active_lease_count=="number"?t.active_lease_count:0,runningHeadlessAttemptCount:typeof t.running_headless_attempt_count=="number"?t.running_headless_attempt_count:0,publicSafe:!0}}recordJob(e){this.assertCanMutate(),b(e.summary,"jobs.summary");const t=S(e.metadata);try{const r=this.db.prepare(`
1192
+ `).get(jobId);
1193
+ if (!isRecord(row) || typeof row.job_id !== 'string')
1194
+ return null;
1195
+ return {
1196
+ jobId: row.job_id,
1197
+ kind: typeof row.kind === 'string' ? row.kind : 'unknown',
1198
+ status: typeof row.status === 'string' ? row.status : 'unknown',
1199
+ activeLeaseCount: typeof row.active_lease_count === 'number' ? row.active_lease_count : 0,
1200
+ runningHeadlessAttemptCount: typeof row.running_headless_attempt_count === 'number' ? row.running_headless_attempt_count : 0,
1201
+ publicSafe: true,
1202
+ };
1203
+ }
1204
+ recordJob(input) {
1205
+ this.assertCanMutate();
1206
+ assertPublicSafeText(input.summary, 'jobs.summary');
1207
+ const metadataJson = stringifyPublicMetadata(input.metadata);
1208
+ try {
1209
+ const result = this.db.prepare(`
38
1210
  INSERT OR IGNORE INTO jobs (
39
1211
  job_id, kind, status, summary, reason_code, created_at, updated_at,
40
1212
  next_run_at, goal_id, loop_id, campaign_id, operation_id,
41
1213
  idempotency_key, metadata_json
42
1214
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
43
- `).run(e.jobId,e.kind,e.status,e.summary??null,e.reasonCode??null,e.createdAt,e.updatedAt,e.nextRunAt??null,e.goalId??null,e.loopId??null,e.campaignId??null,e.operationId??null,e.idempotencyKey??null,t);return this.toWriteResult(r)}catch(r){throw m(r,"sqlite-mutation-failed")}}appendEvent(e){this.assertCanMutate(),b(e.summary,"job_events.summary");const t=S(e.metadata);try{const r=this.db.prepare(`
1215
+ `).run(input.jobId, input.kind, input.status, input.summary ?? null, input.reasonCode ?? null, input.createdAt, input.updatedAt, input.nextRunAt ?? null, input.goalId ?? null, input.loopId ?? null, input.campaignId ?? null, input.operationId ?? null, input.idempotencyKey ?? null, metadataJson);
1216
+ return this.toWriteResult(result);
1217
+ }
1218
+ catch (error) {
1219
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
1220
+ }
1221
+ }
1222
+ appendEvent(input) {
1223
+ this.assertCanMutate();
1224
+ assertPublicSafeText(input.summary, 'job_events.summary');
1225
+ const metadataJson = stringifyPublicMetadata(input.metadata);
1226
+ try {
1227
+ const result = this.db.prepare(`
44
1228
  INSERT OR IGNORE INTO job_events (
45
1229
  event_id, job_id, operation_id, idempotency_key, event_sequence,
46
1230
  type, summary, reason_code, created_at, metadata_json
47
1231
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
48
- `).run(e.eventId,e.jobId,e.operationId,e.idempotencyKey??null,e.sequence??null,e.type,e.summary,e.reasonCode??null,e.createdAt,t);return this.toWriteResult(r)}catch(r){throw m(r,"sqlite-mutation-failed")}}recordProcessingOutputSummary(e){Y(e.executionId,"processing_output_summary.executionId",160),Y(e.jobId,"processing_output_summary.jobId",160),Y(e.publicSummary,"processing_output_summary.publicSummary",Be),Y(e.createdAt,"processing_output_summary.createdAt",80),b(e.eventId,"processing_output_summary.eventId"),b(e.idempotencyKey,"processing_output_summary.idempotencyKey"),b(e.goalId,"processing_output_summary.goalId"),b(e.loopId,"processing_output_summary.loopId"),b(e.reasonCode,"processing_output_summary.reasonCode"),b(e.source,"processing_output_summary.source");const t=this.appendEvent({eventId:it(e),jobId:e.jobId,operationId:e.executionId,idempotencyKey:e.idempotencyKey??e.executionId,type:O,summary:e.publicSummary,reasonCode:e.reasonCode,createdAt:e.createdAt,metadata:dt(e)});return t.inserted&&this.recordJobStageProgress({jobId:e.jobId,currentStage:e.metadata?.currentStage,iteration:e.metadata?.iteration,at:e.createdAt}),t}enqueueHeadlessExecutionAttempt(e){if(this.assertCanMutate(),L({attemptId:e.attemptId,executionId:e.executionId,jobId:e.jobId,directiveHash:e.directiveHash,targetRef:e.targetRef,storyId:e.storyId,epicId:e.epicId,runtimeId:e.runtimeId,modelId:e.modelId,runtimeSource:e.runtimeSource,reasonCode:e.reasonCode,publicSummary:e.publicSummary,timestamp:e.enqueuedAt}),c(e.notBeforeAt,"headless_execution.notBeforeAt",80),c(e.operationId,"headless_execution.operationId",160),c(e.idempotencyKey,"headless_execution.idempotencyKey",160),N(e.retryCount,"headless_execution.retryCount"),N(e.maxAttempts,"headless_execution.maxAttempts"),e.maxAttempts!==void 0&&e.maxAttempts<1)throw new i("sqlite-public-safety-rejected","Continuity SQLite maxAttempts must be greater than zero.");e.metadata&&g(e.metadata,"headless_execution.metadata");const t=B({maxItems:e.queueCap,maxAttempts:e.maxAttempts}),r=qe(e.queuePriority),s=Pe(e.queueScope),n=U(e.relativePaths),d=V({attemptId:e.attemptId,executionId:e.executionId,jobId:e.jobId,directiveHash:e.directiveHash,runtimeId:e.runtimeId,modelId:e.modelId,runtimeSource:e.runtimeSource,status:e.status??"queued",reasonCode:e.reasonCode,retryCount:e.retryCount??0,attemptCount:0,maxAttempts:t.maxAttempts,queuePriority:r,queueScope:s,counts:e.counts,hashes:e.hashes,redactionSummary:e.redactionSummary,relativePaths:n,metadata:e.metadata});return this.runInTransaction(()=>{const o=this.db.prepare(`
1232
+ `).run(input.eventId, input.jobId, input.operationId, input.idempotencyKey ?? null, input.sequence ?? null, input.type, input.summary, input.reasonCode ?? null, input.createdAt, metadataJson);
1233
+ return this.toWriteResult(result);
1234
+ }
1235
+ catch (error) {
1236
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
1237
+ }
1238
+ }
1239
+ recordProcessingOutputSummary(input) {
1240
+ assertProcessingSummaryText(input.executionId, 'processing_output_summary.executionId', 160);
1241
+ assertProcessingSummaryText(input.jobId, 'processing_output_summary.jobId', 160);
1242
+ assertProcessingSummaryText(input.publicSummary, 'processing_output_summary.publicSummary', CONTINUITY_PROCESSING_OUTPUT_SUMMARY_MAX_CHARS);
1243
+ assertProcessingSummaryText(input.createdAt, 'processing_output_summary.createdAt', 80);
1244
+ assertPublicSafeText(input.eventId, 'processing_output_summary.eventId');
1245
+ assertPublicSafeText(input.idempotencyKey, 'processing_output_summary.idempotencyKey');
1246
+ assertPublicSafeText(input.goalId, 'processing_output_summary.goalId');
1247
+ assertPublicSafeText(input.loopId, 'processing_output_summary.loopId');
1248
+ assertPublicSafeText(input.reasonCode, 'processing_output_summary.reasonCode');
1249
+ assertPublicSafeText(input.source, 'processing_output_summary.source');
1250
+ const result = this.appendEvent({
1251
+ eventId: processingSummaryEventId(input),
1252
+ jobId: input.jobId,
1253
+ operationId: input.executionId,
1254
+ idempotencyKey: input.idempotencyKey ?? input.executionId,
1255
+ type: CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE,
1256
+ summary: input.publicSummary,
1257
+ reasonCode: input.reasonCode,
1258
+ createdAt: input.createdAt,
1259
+ metadata: buildProcessingOutputSummaryMetadata(input),
1260
+ });
1261
+ // P195.04: On a freshly applied tick summary, persist the producer-emitted
1262
+ // stage/iteration into the job row and append one bounded stageHistory entry.
1263
+ // Gating on `inserted` keeps this idempotent across replays and runner retries.
1264
+ if (result.inserted) {
1265
+ this.recordJobStageProgress({
1266
+ jobId: input.jobId,
1267
+ currentStage: input.metadata?.currentStage,
1268
+ iteration: input.metadata?.iteration,
1269
+ at: input.createdAt,
1270
+ });
1271
+ }
1272
+ return result;
1273
+ }
1274
+ enqueueHeadlessExecutionAttempt(input) {
1275
+ this.assertCanMutate();
1276
+ validateHeadlessExecutionAttemptIdentity({
1277
+ attemptId: input.attemptId,
1278
+ executionId: input.executionId,
1279
+ jobId: input.jobId,
1280
+ directiveHash: input.directiveHash,
1281
+ targetRef: input.targetRef,
1282
+ storyId: input.storyId,
1283
+ epicId: input.epicId,
1284
+ runtimeId: input.runtimeId,
1285
+ modelId: input.modelId,
1286
+ runtimeSource: input.runtimeSource,
1287
+ reasonCode: input.reasonCode,
1288
+ publicSummary: input.publicSummary,
1289
+ timestamp: input.enqueuedAt,
1290
+ });
1291
+ assertBoundedPublicText(input.notBeforeAt, 'headless_execution.notBeforeAt', 80);
1292
+ assertBoundedPublicText(input.operationId, 'headless_execution.operationId', 160);
1293
+ assertBoundedPublicText(input.idempotencyKey, 'headless_execution.idempotencyKey', 160);
1294
+ assertNonNegativeInteger(input.retryCount, 'headless_execution.retryCount');
1295
+ assertNonNegativeInteger(input.maxAttempts, 'headless_execution.maxAttempts');
1296
+ if (input.maxAttempts !== undefined && input.maxAttempts < 1) {
1297
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Continuity SQLite maxAttempts must be greater than zero.');
1298
+ }
1299
+ if (input.metadata)
1300
+ assertPublicSafeValue(input.metadata, 'headless_execution.metadata');
1301
+ const queuePolicy = normalizeContinuityQueuePolicy({
1302
+ maxItems: input.queueCap,
1303
+ maxAttempts: input.maxAttempts,
1304
+ });
1305
+ const queuePriority = normalizeContinuityQueuePriority(input.queuePriority);
1306
+ const queueScope = normalizeContinuityQueueScope(input.queueScope);
1307
+ const relativePaths = normalizePublicRelativePaths(input.relativePaths);
1308
+ const metadata = buildHeadlessExecutionPublicMetadata({
1309
+ attemptId: input.attemptId,
1310
+ executionId: input.executionId,
1311
+ jobId: input.jobId,
1312
+ directiveHash: input.directiveHash,
1313
+ runtimeId: input.runtimeId,
1314
+ modelId: input.modelId,
1315
+ runtimeSource: input.runtimeSource,
1316
+ status: input.status ?? 'queued',
1317
+ reasonCode: input.reasonCode,
1318
+ retryCount: input.retryCount ?? 0,
1319
+ attemptCount: 0,
1320
+ maxAttempts: queuePolicy.maxAttempts,
1321
+ queuePriority,
1322
+ queueScope,
1323
+ counts: input.counts,
1324
+ hashes: input.hashes,
1325
+ redactionSummary: input.redactionSummary,
1326
+ relativePaths,
1327
+ metadata: input.metadata,
1328
+ });
1329
+ return this.runInTransaction(() => {
1330
+ const configuredPolicyRow = this.db.prepare(`
49
1331
  SELECT value_json
50
1332
  FROM runner_config
51
1333
  WHERE key = 'headless_queue_policy'
52
1334
  LIMIT 1
53
- `).get();let l=t.maxItems;if(e.queueCap===void 0&&E(o)&&typeof o.value_json=="string")try{const u=JSON.parse(o.value_json);E(u)&&typeof u.maxItems=="number"&&(l=B({maxItems:u.maxItems}).maxItems)}catch{}this.db.prepare(`
1335
+ `).get();
1336
+ let effectiveQueueCap = queuePolicy.maxItems;
1337
+ if (input.queueCap === undefined && isRecord(configuredPolicyRow) && typeof configuredPolicyRow.value_json === 'string') {
1338
+ try {
1339
+ const configuredPolicy = JSON.parse(configuredPolicyRow.value_json);
1340
+ if (isRecord(configuredPolicy) && typeof configuredPolicy.maxItems === 'number') {
1341
+ effectiveQueueCap = normalizeContinuityQueuePolicy({ maxItems: configuredPolicy.maxItems }).maxItems;
1342
+ }
1343
+ }
1344
+ catch {
1345
+ // runner_config has a json_valid constraint; retain the bounded default defensively.
1346
+ }
1347
+ }
1348
+ this.db.prepare(`
54
1349
  INSERT INTO runner_config (key, value_json, updated_at)
55
1350
  VALUES ('headless_queue_policy', ?, ?)
56
1351
  ON CONFLICT(key) DO UPDATE SET
57
1352
  value_json = excluded.value_json,
58
1353
  updated_at = excluded.updated_at
59
- `).run(JSON.stringify({maxItems:l}),e.enqueuedAt);const I=this.db.prepare(`
1354
+ `).run(JSON.stringify({ maxItems: effectiveQueueCap }), input.enqueuedAt);
1355
+ const replay = this.db.prepare(`
60
1356
  SELECT attempt_id
61
1357
  FROM headless_execution_attempts
62
1358
  WHERE attempt_id = ?
63
1359
  OR (? IS NOT NULL AND operation_id = ?)
64
1360
  OR (? IS NOT NULL AND idempotency_key = ?)
65
1361
  LIMIT 1
66
- `).get(e.attemptId,e.operationId??null,e.operationId??null,e.idempotencyKey??null,e.idempotencyKey??null),y=this.db.prepare(`
1362
+ `).get(input.attemptId, input.operationId ?? null, input.operationId ?? null, input.idempotencyKey ?? null, input.idempotencyKey ?? null);
1363
+ const depthRow = this.db.prepare(`
67
1364
  SELECT COUNT(*) AS count
68
1365
  FROM headless_execution_attempts
69
1366
  WHERE status IN ('queued', 'backoff', 'running')
70
1367
  AND dead_lettered_at IS NULL
71
- `).get(),f=E(y)&&typeof y.count=="number"?y.count:0;if(I)return{inserted:!1,replay:!0,changes:0,accepted:!0,backpressure:f>=l,queueDepth:f,queueCap:l,publicSafe:!0};if(f>=l)return{inserted:!1,replay:!1,changes:0,accepted:!1,backpressure:!0,queueDepth:f,queueCap:l,reasonCode:k.CAP_REACHED,publicSafe:!0};const _=this.db.prepare(`
1368
+ `).get();
1369
+ const queueDepth = isRecord(depthRow) && typeof depthRow.count === 'number' ? depthRow.count : 0;
1370
+ if (replay) {
1371
+ return {
1372
+ inserted: false,
1373
+ replay: true,
1374
+ changes: 0,
1375
+ accepted: true,
1376
+ backpressure: queueDepth >= effectiveQueueCap,
1377
+ queueDepth,
1378
+ queueCap: effectiveQueueCap,
1379
+ publicSafe: true,
1380
+ };
1381
+ }
1382
+ if (queueDepth >= effectiveQueueCap) {
1383
+ return {
1384
+ inserted: false,
1385
+ replay: false,
1386
+ changes: 0,
1387
+ accepted: false,
1388
+ backpressure: true,
1389
+ queueDepth,
1390
+ queueCap: effectiveQueueCap,
1391
+ reasonCode: CONTINUITY_QUEUE_REASON_CODES.CAP_REACHED,
1392
+ publicSafe: true,
1393
+ };
1394
+ }
1395
+ const result = this.db.prepare(`
72
1396
  INSERT INTO headless_execution_attempts (
73
1397
  attempt_id, execution_id, job_id, directive_hash, directive_kind,
74
1398
  command_class, target_kind, target_ref, story_id, epic_id, runtime_id,
@@ -78,7 +1402,23 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
78
1402
  relative_paths_json, metadata_json, queue_priority, queue_scope,
79
1403
  attempt_count, next_retry_at
80
1404
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)
81
- `).run(e.attemptId,e.executionId,e.jobId,e.directiveHash,e.directiveKind,e.commandClass,e.targetKind,e.targetRef,e.storyId??null,e.epicId??null,e.runtimeId??null,e.modelId??null,e.runtimeSource??null,e.status??"queued",e.reasonCode??null,e.publicSummary??null,e.enqueuedAt,e.status==="backoff"?e.notBeforeAt??e.enqueuedAt:null,e.retryCount??0,t.maxAttempts,e.operationId??null,e.idempotencyKey??null,T(e.counts,"headless_execution.counts"),T(e.hashes,"headless_execution.hashes"),T(e.redactionSummary,"headless_execution.redactionSummary"),T(n,"headless_execution.relativePaths"),S(d),r,s,e.notBeforeAt??e.enqueuedAt);return{inserted:_.changes>0,replay:!1,changes:_.changes,accepted:_.changes>0,backpressure:f+_.changes>=l,queueDepth:f+_.changes,queueCap:l,publicSafe:!0}})}listDueHeadlessExecutionAttempts(e){const t=Math.max(1,Math.min(50,Math.floor(e.limit)));return c(e.nowIso,"headless_execution.nowIso",80,!0),this.db.prepare(`
1405
+ `).run(input.attemptId, input.executionId, input.jobId, input.directiveHash, input.directiveKind, input.commandClass, input.targetKind, input.targetRef, input.storyId ?? null, input.epicId ?? null, input.runtimeId ?? null, input.modelId ?? null, input.runtimeSource ?? null, input.status ?? 'queued', input.reasonCode ?? null, input.publicSummary ?? null, input.enqueuedAt, input.status === 'backoff' ? (input.notBeforeAt ?? input.enqueuedAt) : null, input.retryCount ?? 0, queuePolicy.maxAttempts, input.operationId ?? null, input.idempotencyKey ?? null, stringifyPublicJsonValue(input.counts, 'headless_execution.counts'), stringifyPublicJsonValue(input.hashes, 'headless_execution.hashes'), stringifyPublicJsonValue(input.redactionSummary, 'headless_execution.redactionSummary'), stringifyPublicJsonValue(relativePaths, 'headless_execution.relativePaths'), stringifyPublicMetadata(metadata), queuePriority, queueScope, input.notBeforeAt ?? input.enqueuedAt);
1406
+ return {
1407
+ inserted: result.changes > 0,
1408
+ replay: false,
1409
+ changes: result.changes,
1410
+ accepted: result.changes > 0,
1411
+ backpressure: queueDepth + result.changes >= effectiveQueueCap,
1412
+ queueDepth: queueDepth + result.changes,
1413
+ queueCap: effectiveQueueCap,
1414
+ publicSafe: true,
1415
+ };
1416
+ });
1417
+ }
1418
+ listDueHeadlessExecutionAttempts(options) {
1419
+ const limit = Math.max(1, Math.min(50, Math.floor(options.limit)));
1420
+ assertBoundedPublicText(options.nowIso, 'headless_execution.nowIso', 80, true);
1421
+ return this.db.prepare(`
82
1422
  SELECT attempt_id, execution_id, job_id, directive_hash, directive_kind,
83
1423
  command_class, target_kind, target_ref, story_id, epic_id,
84
1424
  runtime_id, model_id, runtime_source, status, reason_code,
@@ -97,13 +1437,36 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
97
1437
  ORDER BY queue_priority DESC, COALESCE(next_retry_at, not_before_at, enqueued_at) ASC,
98
1438
  enqueued_at ASC, attempt_id ASC
99
1439
  LIMIT ?
100
- `).all(e.nowIso,e.nowIso,t)}hasCompletedHeadlessExecutionDirectiveHash(e,t){c(e,"headless_execution.directiveHash",160,!0),c(t,"headless_execution.attemptId",160);const r=t?" AND attempt_id != ?":"",s=t?[e,t]:[e];return this.db.prepare(`
1440
+ `).all(options.nowIso, options.nowIso, limit);
1441
+ }
1442
+ hasCompletedHeadlessExecutionDirectiveHash(directiveHash, excludeAttemptId) {
1443
+ assertBoundedPublicText(directiveHash, 'headless_execution.directiveHash', 160, true);
1444
+ assertBoundedPublicText(excludeAttemptId, 'headless_execution.attemptId', 160);
1445
+ const excludePredicate = excludeAttemptId ? ' AND attempt_id != ?' : '';
1446
+ const params = excludeAttemptId ? [directiveHash, excludeAttemptId] : [directiveHash];
1447
+ const row = this.db.prepare(`
101
1448
  SELECT 1 AS found
102
1449
  FROM headless_execution_attempts
103
1450
  WHERE directive_hash = ?
104
- AND status = 'completed'${r}
1451
+ AND status = 'completed'${excludePredicate}
105
1452
  LIMIT 1
106
- `).get(...s)?.found===1}claimHeadlessExecutionAttempt(e){this.assertCanMutate(),L({attemptId:e.attemptId,executionId:e.attemptId,jobId:e.attemptId,leaseId:e.leaseId,runnerId:e.runnerId,reasonCode:e.reasonCode,timestamp:e.claimedAt}),c(e.leaseExpiresAt,"headless_execution.leaseExpiresAt",80,!0);try{const t=this.db.prepare(`
1453
+ `).get(...params);
1454
+ return row?.found === 1;
1455
+ }
1456
+ claimHeadlessExecutionAttempt(input) {
1457
+ this.assertCanMutate();
1458
+ validateHeadlessExecutionAttemptIdentity({
1459
+ attemptId: input.attemptId,
1460
+ executionId: input.attemptId,
1461
+ jobId: input.attemptId,
1462
+ leaseId: input.leaseId,
1463
+ runnerId: input.runnerId,
1464
+ reasonCode: input.reasonCode,
1465
+ timestamp: input.claimedAt,
1466
+ });
1467
+ assertBoundedPublicText(input.leaseExpiresAt, 'headless_execution.leaseExpiresAt', 80, true);
1468
+ try {
1469
+ const result = this.db.prepare(`
107
1470
  UPDATE headless_execution_attempts
108
1471
  SET status = 'running',
109
1472
  lease_id = ?,
@@ -119,7 +1482,27 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
119
1482
  AND dead_lettered_at IS NULL
120
1483
  AND (COALESCE(next_retry_at, not_before_at) IS NULL OR COALESCE(next_retry_at, not_before_at) <= ?)
121
1484
  AND (lease_id IS NULL OR lease_released_at IS NOT NULL OR lease_expires_at <= ?)
122
- `).run(e.leaseId,e.runnerId,e.claimedAt,e.claimedAt,e.leaseExpiresAt,e.reasonCode??null,e.attemptId,e.claimedAt,e.claimedAt);return this.toWriteResult(t)}catch(t){throw m(t,"sqlite-mutation-failed")}}acquireNextHeadlessExecutionAttempt(e){return this.assertCanMutate(),L({attemptId:e.leaseId,executionId:e.leaseId,jobId:e.leaseId,leaseId:e.leaseId,runnerId:e.runnerId,reasonCode:e.reasonCode,timestamp:e.claimedAt}),c(e.leaseExpiresAt,"headless_execution.leaseExpiresAt",80,!0),this.runInTransaction(()=>{const t=this.db.prepare(`
1485
+ `).run(input.leaseId, input.runnerId, input.claimedAt, input.claimedAt, input.leaseExpiresAt, input.reasonCode ?? null, input.attemptId, input.claimedAt, input.claimedAt);
1486
+ return this.toWriteResult(result);
1487
+ }
1488
+ catch (error) {
1489
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
1490
+ }
1491
+ }
1492
+ acquireNextHeadlessExecutionAttempt(input) {
1493
+ this.assertCanMutate();
1494
+ validateHeadlessExecutionAttemptIdentity({
1495
+ attemptId: input.leaseId,
1496
+ executionId: input.leaseId,
1497
+ jobId: input.leaseId,
1498
+ leaseId: input.leaseId,
1499
+ runnerId: input.runnerId,
1500
+ reasonCode: input.reasonCode,
1501
+ timestamp: input.claimedAt,
1502
+ });
1503
+ assertBoundedPublicText(input.leaseExpiresAt, 'headless_execution.leaseExpiresAt', 80, true);
1504
+ return this.runInTransaction(() => {
1505
+ const row = this.db.prepare(`
123
1506
  SELECT attempt_id
124
1507
  FROM headless_execution_attempts
125
1508
  WHERE status IN ('queued', 'backoff')
@@ -132,7 +1515,11 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
132
1515
  enqueued_at ASC,
133
1516
  attempt_id ASC
134
1517
  LIMIT 1
135
- `).get(e.directiveKind??null,e.directiveKind??null,e.claimedAt,e.claimedAt);if(typeof t?.attempt_id!="string")return{acquired:!1,reasonCode:"continuity-ok",publicSafe:!0};if(this.db.prepare(`
1518
+ `).get(input.directiveKind ?? null, input.directiveKind ?? null, input.claimedAt, input.claimedAt);
1519
+ if (typeof row?.attempt_id !== 'string') {
1520
+ return { acquired: false, reasonCode: 'continuity-ok', publicSafe: true };
1521
+ }
1522
+ const result = this.db.prepare(`
136
1523
  UPDATE headless_execution_attempts
137
1524
  SET status = 'running',
138
1525
  lease_id = ?,
@@ -148,7 +1535,29 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
148
1535
  AND dead_lettered_at IS NULL
149
1536
  AND (COALESCE(next_retry_at, not_before_at) IS NULL OR COALESCE(next_retry_at, not_before_at) <= ?)
150
1537
  AND (lease_id IS NULL OR lease_released_at IS NOT NULL OR lease_expires_at <= ?)
151
- `).run(e.leaseId,e.runnerId,e.claimedAt,e.claimedAt,e.leaseExpiresAt,e.reasonCode??null,t.attempt_id,e.claimedAt,e.claimedAt).changes===0)return{acquired:!1,reasonCode:"lease-conflict",publicSafe:!0};const s=this.getHeadlessExecutionAttempt(t.attempt_id);return s?{acquired:!0,attempt:s,reasonCode:e.reasonCode??"continuity-ok",publicSafe:!0}:{acquired:!1,reasonCode:"lease-conflict",publicSafe:!0}})}heartbeatHeadlessExecutionAttempt(e){this.assertCanMutate(),L({attemptId:e.attemptId,executionId:e.attemptId,jobId:e.attemptId,leaseId:e.leaseId,runnerId:e.runnerId,timestamp:e.heartbeatAt}),c(e.leaseExpiresAt,"headless_execution.leaseExpiresAt",80);try{const t=this.db.prepare(`
1538
+ `).run(input.leaseId, input.runnerId, input.claimedAt, input.claimedAt, input.leaseExpiresAt, input.reasonCode ?? null, row.attempt_id, input.claimedAt, input.claimedAt);
1539
+ if (result.changes === 0) {
1540
+ return { acquired: false, reasonCode: 'lease-conflict', publicSafe: true };
1541
+ }
1542
+ const attempt = this.getHeadlessExecutionAttempt(row.attempt_id);
1543
+ return attempt
1544
+ ? { acquired: true, attempt, reasonCode: input.reasonCode ?? 'continuity-ok', publicSafe: true }
1545
+ : { acquired: false, reasonCode: 'lease-conflict', publicSafe: true };
1546
+ });
1547
+ }
1548
+ heartbeatHeadlessExecutionAttempt(input) {
1549
+ this.assertCanMutate();
1550
+ validateHeadlessExecutionAttemptIdentity({
1551
+ attemptId: input.attemptId,
1552
+ executionId: input.attemptId,
1553
+ jobId: input.attemptId,
1554
+ leaseId: input.leaseId,
1555
+ runnerId: input.runnerId,
1556
+ timestamp: input.heartbeatAt,
1557
+ });
1558
+ assertBoundedPublicText(input.leaseExpiresAt, 'headless_execution.leaseExpiresAt', 80);
1559
+ try {
1560
+ const result = this.db.prepare(`
152
1561
  UPDATE headless_execution_attempts
153
1562
  SET heartbeat_at = ?,
154
1563
  lease_expires_at = COALESCE(?, lease_expires_at)
@@ -158,7 +1567,27 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
158
1567
  AND status = 'running'
159
1568
  AND lease_released_at IS NULL
160
1569
  AND lease_expires_at > ?
161
- `).run(e.heartbeatAt,e.leaseExpiresAt??null,e.attemptId,e.leaseId,e.runnerId,e.heartbeatAt);return this.toWriteResult(t)}catch(t){throw m(t,"sqlite-mutation-failed")}}releaseHeadlessExecutionAttempt(e){this.assertCanMutate(),L({attemptId:e.attemptId,executionId:e.attemptId,jobId:e.attemptId,leaseId:e.leaseId,reasonCode:e.reasonCode,timestamp:e.releasedAt}),c(e.notBeforeAt,"headless_execution.notBeforeAt",80),N(e.retryCount,"headless_execution.retryCount");try{const t=this.db.prepare(`
1570
+ `).run(input.heartbeatAt, input.leaseExpiresAt ?? null, input.attemptId, input.leaseId, input.runnerId, input.heartbeatAt);
1571
+ return this.toWriteResult(result);
1572
+ }
1573
+ catch (error) {
1574
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
1575
+ }
1576
+ }
1577
+ releaseHeadlessExecutionAttempt(input) {
1578
+ this.assertCanMutate();
1579
+ validateHeadlessExecutionAttemptIdentity({
1580
+ attemptId: input.attemptId,
1581
+ executionId: input.attemptId,
1582
+ jobId: input.attemptId,
1583
+ leaseId: input.leaseId,
1584
+ reasonCode: input.reasonCode,
1585
+ timestamp: input.releasedAt,
1586
+ });
1587
+ assertBoundedPublicText(input.notBeforeAt, 'headless_execution.notBeforeAt', 80);
1588
+ assertNonNegativeInteger(input.retryCount, 'headless_execution.retryCount');
1589
+ try {
1590
+ const result = this.db.prepare(`
162
1591
  UPDATE headless_execution_attempts
163
1592
  SET status = COALESCE(?, status),
164
1593
  reason_code = COALESCE(?, reason_code),
@@ -174,8 +1603,76 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
174
1603
  AND lease_id = ?
175
1604
  AND status = 'running'
176
1605
  AND lease_released_at IS NULL
177
- `).run(e.status??null,e.reasonCode??null,e.notBeforeAt??null,e.retryCount??null,e.status??null,e.notBeforeAt??null,e.status??null,e.releasedAt,e.attemptId,e.leaseId);return this.toWriteResult(t)}catch(t){throw m(t,"sqlite-mutation-failed")}}completeHeadlessExecutionAttempt(e){this.assertCanMutate(),L({attemptId:e.attemptId,executionId:e.attemptId,jobId:e.attemptId,leaseId:e.leaseId,runnerId:e.runnerId,reasonCode:e.reasonCode,publicSummary:e.publicSummary,timestamp:e.completedAt}),c(e.notBeforeAt,"headless_execution.notBeforeAt",80),N(e.durationMs,"headless_execution.durationMs"),N(e.retryCount,"headless_execution.retryCount"),e.metadata&&g(e.metadata,"headless_execution.metadata");const t=U(e.relativePaths),r=V({attemptId:e.attemptId,executionId:e.executionId??e.attemptId,jobId:e.jobId??e.attemptId,status:e.status,reasonCode:e.reasonCode,durationMs:e.durationMs,retryCount:e.retryCount,counts:e.counts,hashes:e.hashes,redactionSummary:e.redactionSummary,relativePaths:t,metadata:e.metadata}),s=e.leaseId?` AND lease_id = ?
178
- AND lease_released_at IS NULL${e.runnerId?" AND runner_id = ?":""}`:"",n=e.leaseId?"status = 'running'":"status NOT IN ('completed', 'failed', 'timed_out', 'skipped')",d=[e.status,e.completedAt,e.durationMs??null,e.reasonCode??null,e.publicSummary??null,e.notBeforeAt??null,e.retryCount??null,e.status,e.notBeforeAt??null,e.counts?T(e.counts,"headless_execution.counts"):null,e.hashes?T(e.hashes,"headless_execution.hashes"):null,e.redactionSummary?T(e.redactionSummary,"headless_execution.redactionSummary"):null,t?T(t,"headless_execution.relativePaths"):null,S(r),e.completedAt,e.attemptId];e.leaseId&&d.push(e.leaseId),e.leaseId&&e.runnerId&&d.push(e.runnerId);try{const o=this.db.prepare(`
1606
+ `).run(input.status ?? null, input.reasonCode ?? null, input.notBeforeAt ?? null, input.retryCount ?? null, input.status ?? null, input.notBeforeAt ?? null, input.status ?? null, input.releasedAt, input.attemptId, input.leaseId);
1607
+ return this.toWriteResult(result);
1608
+ }
1609
+ catch (error) {
1610
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
1611
+ }
1612
+ }
1613
+ completeHeadlessExecutionAttempt(input) {
1614
+ this.assertCanMutate();
1615
+ validateHeadlessExecutionAttemptIdentity({
1616
+ attemptId: input.attemptId,
1617
+ executionId: input.attemptId,
1618
+ jobId: input.attemptId,
1619
+ leaseId: input.leaseId,
1620
+ runnerId: input.runnerId,
1621
+ reasonCode: input.reasonCode,
1622
+ publicSummary: input.publicSummary,
1623
+ timestamp: input.completedAt,
1624
+ });
1625
+ assertBoundedPublicText(input.notBeforeAt, 'headless_execution.notBeforeAt', 80);
1626
+ assertNonNegativeInteger(input.durationMs, 'headless_execution.durationMs');
1627
+ assertNonNegativeInteger(input.retryCount, 'headless_execution.retryCount');
1628
+ if (input.metadata)
1629
+ assertPublicSafeValue(input.metadata, 'headless_execution.metadata');
1630
+ const relativePaths = normalizePublicRelativePaths(input.relativePaths);
1631
+ const metadata = buildHeadlessExecutionPublicMetadata({
1632
+ attemptId: input.attemptId,
1633
+ executionId: input.executionId ?? input.attemptId,
1634
+ jobId: input.jobId ?? input.attemptId,
1635
+ status: input.status,
1636
+ reasonCode: input.reasonCode,
1637
+ durationMs: input.durationMs,
1638
+ retryCount: input.retryCount,
1639
+ counts: input.counts,
1640
+ hashes: input.hashes,
1641
+ redactionSummary: input.redactionSummary,
1642
+ relativePaths,
1643
+ metadata: input.metadata,
1644
+ });
1645
+ const leasePredicate = input.leaseId
1646
+ ? ` AND lease_id = ?
1647
+ AND lease_released_at IS NULL${input.runnerId ? ' AND runner_id = ?' : ''}`
1648
+ : '';
1649
+ const statusPredicate = input.leaseId
1650
+ ? "status = 'running'"
1651
+ : "status NOT IN ('completed', 'failed', 'timed_out', 'skipped')";
1652
+ const params = [
1653
+ input.status,
1654
+ input.completedAt,
1655
+ input.durationMs ?? null,
1656
+ input.reasonCode ?? null,
1657
+ input.publicSummary ?? null,
1658
+ input.notBeforeAt ?? null,
1659
+ input.retryCount ?? null,
1660
+ input.status,
1661
+ input.notBeforeAt ?? null,
1662
+ input.counts ? stringifyPublicJsonValue(input.counts, 'headless_execution.counts') : null,
1663
+ input.hashes ? stringifyPublicJsonValue(input.hashes, 'headless_execution.hashes') : null,
1664
+ input.redactionSummary ? stringifyPublicJsonValue(input.redactionSummary, 'headless_execution.redactionSummary') : null,
1665
+ relativePaths ? stringifyPublicJsonValue(relativePaths, 'headless_execution.relativePaths') : null,
1666
+ stringifyPublicMetadata(metadata),
1667
+ input.completedAt,
1668
+ input.attemptId,
1669
+ ];
1670
+ if (input.leaseId)
1671
+ params.push(input.leaseId);
1672
+ if (input.leaseId && input.runnerId)
1673
+ params.push(input.runnerId);
1674
+ try {
1675
+ const result = this.db.prepare(`
179
1676
  UPDATE headless_execution_attempts
180
1677
  SET status = ?,
181
1678
  completed_at = ?,
@@ -194,53 +1691,426 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
194
1691
  relative_paths_json = COALESCE(?, relative_paths_json),
195
1692
  metadata_json = COALESCE(?, metadata_json),
196
1693
  lease_released_at = COALESCE(lease_released_at, ?)
197
- WHERE attempt_id = ?${s}
198
- AND ${n}
199
- `).run(...d);return this.toWriteResult(o)}catch(o){throw m(o,"sqlite-mutation-failed")}}recordHeadlessExecutionReceipt(e){L({attemptId:e.attemptId,executionId:e.executionId,jobId:e.jobId,reasonCode:e.reasonCode,timestamp:e.createdAt}),e.metadata&&g(e.metadata,"headless_execution.metadata");const t=U(e.relativePaths),r=V({attemptId:e.attemptId,executionId:e.executionId,jobId:e.jobId,status:e.status,reasonCode:e.reasonCode,counts:e.counts,hashes:e.hashes,redactionSummary:e.redactionSummary,relativePaths:t,metadata:e.metadata});return this.appendReceipt({receiptId:e.receiptId,jobId:e.jobId,operationId:e.operationId??e.executionId,idempotencyKey:e.idempotencyKey??e.executionId,status:e.status,reasonCode:e.reasonCode,appliedOperationIds:e.status==="applied"?[e.executionId]:void 0,ignoredOperationIds:e.status==="ignored"?[e.executionId]:void 0,createdAt:e.createdAt,metadata:r})}recordHeadlessExecutionSummary(e){L({attemptId:e.attemptId,executionId:e.executionId,jobId:e.jobId,runtimeId:e.runtimeId,modelId:e.modelId,reasonCode:e.reasonCode,publicSummary:e.publicSummary,timestamp:e.createdAt}),N(e.durationMs,"headless_execution.durationMs"),e.metadata&&g(e.metadata,"headless_execution.metadata");const t=U(e.relativePaths),r=V({attemptId:e.attemptId,executionId:e.executionId,jobId:e.jobId,runtimeId:e.runtimeId,modelId:e.modelId,status:e.status??"completed",reasonCode:e.reasonCode,durationMs:e.durationMs,counts:e.counts,hashes:e.hashes,redactionSummary:e.redactionSummary,relativePaths:t,metadata:e.metadata});return this.recordProcessingOutputSummary({eventId:e.eventId,executionId:e.executionId,jobId:e.jobId,idempotencyKey:e.executionId,publicSummary:e.publicSummary,reasonCode:e.reasonCode,createdAt:e.createdAt,source:"headless_execution",metadata:r})}appendReceipt(e){this.assertCanMutate();const t=S(e.metadata);try{const r=this.db.prepare(`
1694
+ WHERE attempt_id = ?${leasePredicate}
1695
+ AND ${statusPredicate}
1696
+ `).run(...params);
1697
+ return this.toWriteResult(result);
1698
+ }
1699
+ catch (error) {
1700
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
1701
+ }
1702
+ }
1703
+ recordHeadlessExecutionReceipt(input) {
1704
+ validateHeadlessExecutionAttemptIdentity({
1705
+ attemptId: input.attemptId,
1706
+ executionId: input.executionId,
1707
+ jobId: input.jobId,
1708
+ reasonCode: input.reasonCode,
1709
+ timestamp: input.createdAt,
1710
+ });
1711
+ if (input.metadata)
1712
+ assertPublicSafeValue(input.metadata, 'headless_execution.metadata');
1713
+ const relativePaths = normalizePublicRelativePaths(input.relativePaths);
1714
+ const metadata = buildHeadlessExecutionPublicMetadata({
1715
+ attemptId: input.attemptId,
1716
+ executionId: input.executionId,
1717
+ jobId: input.jobId,
1718
+ status: input.status,
1719
+ reasonCode: input.reasonCode,
1720
+ counts: input.counts,
1721
+ hashes: input.hashes,
1722
+ redactionSummary: input.redactionSummary,
1723
+ relativePaths,
1724
+ metadata: input.metadata,
1725
+ });
1726
+ return this.appendReceipt({
1727
+ receiptId: input.receiptId,
1728
+ jobId: input.jobId,
1729
+ operationId: input.operationId ?? input.executionId,
1730
+ idempotencyKey: input.idempotencyKey ?? input.executionId,
1731
+ status: input.status,
1732
+ reasonCode: input.reasonCode,
1733
+ appliedOperationIds: input.status === 'applied' ? [input.executionId] : undefined,
1734
+ ignoredOperationIds: input.status === 'ignored' ? [input.executionId] : undefined,
1735
+ createdAt: input.createdAt,
1736
+ metadata,
1737
+ });
1738
+ }
1739
+ recordHeadlessExecutionSummary(input) {
1740
+ validateHeadlessExecutionAttemptIdentity({
1741
+ attemptId: input.attemptId,
1742
+ executionId: input.executionId,
1743
+ jobId: input.jobId,
1744
+ runtimeId: input.runtimeId,
1745
+ modelId: input.modelId,
1746
+ reasonCode: input.reasonCode,
1747
+ publicSummary: input.publicSummary,
1748
+ timestamp: input.createdAt,
1749
+ });
1750
+ assertNonNegativeInteger(input.durationMs, 'headless_execution.durationMs');
1751
+ if (input.metadata)
1752
+ assertPublicSafeValue(input.metadata, 'headless_execution.metadata');
1753
+ const relativePaths = normalizePublicRelativePaths(input.relativePaths);
1754
+ const metadata = buildHeadlessExecutionPublicMetadata({
1755
+ attemptId: input.attemptId,
1756
+ executionId: input.executionId,
1757
+ jobId: input.jobId,
1758
+ runtimeId: input.runtimeId,
1759
+ modelId: input.modelId,
1760
+ status: input.status ?? 'completed',
1761
+ reasonCode: input.reasonCode,
1762
+ durationMs: input.durationMs,
1763
+ counts: input.counts,
1764
+ hashes: input.hashes,
1765
+ redactionSummary: input.redactionSummary,
1766
+ relativePaths,
1767
+ metadata: input.metadata,
1768
+ });
1769
+ return this.recordProcessingOutputSummary({
1770
+ eventId: input.eventId,
1771
+ executionId: input.executionId,
1772
+ jobId: input.jobId,
1773
+ idempotencyKey: input.executionId,
1774
+ publicSummary: input.publicSummary,
1775
+ reasonCode: input.reasonCode,
1776
+ createdAt: input.createdAt,
1777
+ source: 'headless_execution',
1778
+ metadata,
1779
+ });
1780
+ }
1781
+ appendReceipt(input) {
1782
+ this.assertCanMutate();
1783
+ const metadataJson = stringifyPublicMetadata(input.metadata);
1784
+ try {
1785
+ const result = this.db.prepare(`
200
1786
  INSERT OR IGNORE INTO job_receipts (
201
1787
  receipt_id, job_id, operation_id, idempotency_key, status, reason_code,
202
1788
  applied_operation_ids_json, ignored_operation_ids_json, created_at, metadata_json
203
1789
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
204
- `).run(e.receiptId,e.jobId,e.operationId??null,e.idempotencyKey??null,e.status,e.reasonCode??null,e.appliedOperationIds?JSON.stringify(e.appliedOperationIds):null,e.ignoredOperationIds?JSON.stringify(e.ignoredOperationIds):null,e.createdAt,t);return this.toWriteResult(r)}catch(r){throw m(r,"sqlite-mutation-failed")}}compactPublicReceiptsAndOutputs(e){this.assertCanMutate();const t=Date.parse(e.nowIso);if(!Number.isFinite(t))throw new i("sqlite-public-safety-rejected","Continuity receipt compaction requires a valid public timestamp.");const r=He(e.policy),s=new Date(t-r.receipts.maxAgeMs).toISOString(),n=new Date(t-r.processingOutputs.maxAgeMs).toISOString();try{return this.runInTransaction(()=>{const d=this.selectReceiptRetentionCandidates({cutoff:s,maxItems:r.receipts.maxItems,maxBytes:r.receipts.maxBytes,limit:r.maxDeleteRowsPerRun}),o=this.selectOutputRetentionCandidates({cutoff:n,maxItems:r.processingOutputs.maxItems,maxBytes:r.processingOutputs.maxBytes,limit:r.maxDeleteRowsPerRun}),l=[...d,...o].sort((u,p)=>u.createdAt.localeCompare(p.createdAt)||u.id.localeCompare(p.id)).slice(0,r.maxDeleteRowsPerRun);if(l.length===0)return{status:"noop",summaries:[],counts:{receiptsRemoved:0,outputsRemoved:0,bytesRemoved:0,summariesCreated:0},reasonCode:F.NOOP,publicSafe:!0};const I=l.filter(u=>u.kind==="receipt"),y=l.filter(u=>u.kind==="output");for(const u of q(I.map(p=>p.jobId)))this.preserveReceiptDerivedIteration(u,e.nowIso);this.deleteRetentionCandidates("job_receipts","receipt_id",I.map(u=>u.id)),this.deleteRetentionCandidates("job_events","event_id",y.map(u=>u.id));const f=new Map;for(const u of l)f.set(u.jobId,[...f.get(u.jobId)??[],u]);const _=[];for(const[u,p]of[...f.entries()].sort(([x],[A])=>x.localeCompare(A))){const x=p.filter(j=>j.kind==="receipt"),A=p.filter(j=>j.kind==="output"),G=this.retainedReceiptOutputCounts(u),Q=ut(u,x,A),R={receiptsRemoved:x.length,receiptsRetained:G.receipts,outputsRemoved:A.length,outputsRetained:G.outputs,bytesRemoved:p.reduce((j,Se)=>j+Se.approxBytes,0),bytesRetained:G.bytes,redactions:0},P=`Compacted public receipt and processing evidence: receipts removed ${R.receiptsRemoved}, retained ${R.receiptsRetained}; summaries removed ${R.outputsRemoved}, retained ${R.outputsRetained}; bytes removed ${R.bytesRemoved}, retained ${R.bytesRetained}.`;c(P,"receipt_compaction.publicSummary",500,!0);const H=`receipt-compaction-${M("sha256").update(`${u}:${e.nowIso}:${Q.aggregate}`).digest("hex").slice(0,32)}`,ee={schemaVersion:1,contractVersion:"continuity-v1",policyVersion:1,type:W,summaryId:H,jobId:u,scope:"local_sqlite",status:"compacted",createdAt:e.nowIso,reasonCodes:[F.COMPACTED],counts:R,hashes:Q,publicSummary:P,publicSafe:!0};this.appendEvent({eventId:H,jobId:u,operationId:`op-${H}`,idempotencyKey:`idem-${H}`,type:W,summary:P,reasonCode:F.COMPACTED,createdAt:e.nowIso,metadata:{schemaVersion:1,contractVersion:"continuity-v1",eventType:W,jobId:u,reasonCodes:ee.reasonCodes,counts:R,hashes:Q,publicSummary:P}}),this.pruneReceiptCompactionSummaries(u,r.maxCompactionSummariesPerJob),_.push(ee)}return{status:"compacted",summaries:_,counts:{receiptsRemoved:I.length,outputsRemoved:y.length,bytesRemoved:l.reduce((u,p)=>u+p.approxBytes,0),summariesCreated:_.length},reasonCode:F.COMPACTED,publicSafe:!0}})}catch(d){throw m(d,"sqlite-mutation-failed")}}updateJobStatus(e){this.assertCanMutate();try{const t=this.db.prepare(`
1790
+ `).run(input.receiptId, input.jobId, input.operationId ?? null, input.idempotencyKey ?? null, input.status, input.reasonCode ?? null, input.appliedOperationIds ? JSON.stringify(input.appliedOperationIds) : null, input.ignoredOperationIds ? JSON.stringify(input.ignoredOperationIds) : null, input.createdAt, metadataJson);
1791
+ return this.toWriteResult(result);
1792
+ }
1793
+ catch (error) {
1794
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
1795
+ }
1796
+ }
1797
+ /**
1798
+ * P199.06: Apply TTL/count/byte retention to public receipts and persisted
1799
+ * processing summaries. Deleted content is replaced with bounded audit evidence
1800
+ * that hashes only public identifiers/status/reason codes, never summaries.
1801
+ */
1802
+ compactPublicReceiptsAndOutputs(input) {
1803
+ this.assertCanMutate();
1804
+ const nowMs = Date.parse(input.nowIso);
1805
+ if (!Number.isFinite(nowMs)) {
1806
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Continuity receipt compaction requires a valid public timestamp.');
1807
+ }
1808
+ const policy = normalizeContinuityRetentionPolicy(input.policy);
1809
+ const receiptCutoff = new Date(nowMs - policy.receipts.maxAgeMs).toISOString();
1810
+ const outputCutoff = new Date(nowMs - policy.processingOutputs.maxAgeMs).toISOString();
1811
+ try {
1812
+ return this.runInTransaction(() => {
1813
+ const receiptCandidates = this.selectReceiptRetentionCandidates({
1814
+ cutoff: receiptCutoff,
1815
+ maxItems: policy.receipts.maxItems,
1816
+ maxBytes: policy.receipts.maxBytes,
1817
+ limit: policy.maxDeleteRowsPerRun,
1818
+ });
1819
+ const outputCandidates = this.selectOutputRetentionCandidates({
1820
+ cutoff: outputCutoff,
1821
+ maxItems: policy.processingOutputs.maxItems,
1822
+ maxBytes: policy.processingOutputs.maxBytes,
1823
+ limit: policy.maxDeleteRowsPerRun,
1824
+ });
1825
+ const candidates = [...receiptCandidates, ...outputCandidates]
1826
+ .sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id))
1827
+ .slice(0, policy.maxDeleteRowsPerRun);
1828
+ if (candidates.length === 0) {
1829
+ return {
1830
+ status: 'noop',
1831
+ summaries: [],
1832
+ counts: { receiptsRemoved: 0, outputsRemoved: 0, bytesRemoved: 0, summariesCreated: 0 },
1833
+ reasonCode: CONTINUITY_RECEIPT_RETENTION_REASON_CODES.NOOP,
1834
+ publicSafe: true,
1835
+ };
1836
+ }
1837
+ const selectedReceipts = candidates.filter((candidate) => candidate.kind === 'receipt');
1838
+ const selectedOutputs = candidates.filter((candidate) => candidate.kind === 'output');
1839
+ for (const jobId of uniqueValues(selectedReceipts.map((candidate) => candidate.jobId))) {
1840
+ this.preserveReceiptDerivedIteration(jobId, input.nowIso);
1841
+ }
1842
+ this.deleteRetentionCandidates('job_receipts', 'receipt_id', selectedReceipts.map((candidate) => candidate.id));
1843
+ this.deleteRetentionCandidates('job_events', 'event_id', selectedOutputs.map((candidate) => candidate.id));
1844
+ const byJob = new Map();
1845
+ for (const candidate of candidates) {
1846
+ byJob.set(candidate.jobId, [...(byJob.get(candidate.jobId) ?? []), candidate]);
1847
+ }
1848
+ const summaries = [];
1849
+ for (const [jobId, removed] of [...byJob.entries()].sort(([left], [right]) => left.localeCompare(right))) {
1850
+ const removedReceipts = removed.filter((candidate) => candidate.kind === 'receipt');
1851
+ const removedOutputs = removed.filter((candidate) => candidate.kind === 'output');
1852
+ const retained = this.retainedReceiptOutputCounts(jobId);
1853
+ const hashes = retentionHashes(jobId, removedReceipts, removedOutputs);
1854
+ const counts = {
1855
+ receiptsRemoved: removedReceipts.length,
1856
+ receiptsRetained: retained.receipts,
1857
+ outputsRemoved: removedOutputs.length,
1858
+ outputsRetained: retained.outputs,
1859
+ bytesRemoved: removed.reduce((total, candidate) => total + candidate.approxBytes, 0),
1860
+ bytesRetained: retained.bytes,
1861
+ redactions: 0,
1862
+ };
1863
+ const publicSummary = `Compacted public receipt and processing evidence: receipts removed ${counts.receiptsRemoved}, retained ${counts.receiptsRetained}; summaries removed ${counts.outputsRemoved}, retained ${counts.outputsRetained}; bytes removed ${counts.bytesRemoved}, retained ${counts.bytesRetained}.`;
1864
+ assertBoundedPublicText(publicSummary, 'receipt_compaction.publicSummary', 500, true);
1865
+ const eventDigest = createHash('sha256')
1866
+ .update(`${jobId}:${input.nowIso}:${hashes.aggregate}`)
1867
+ .digest('hex');
1868
+ const summaryId = `receipt-compaction-${eventDigest.slice(0, 32)}`;
1869
+ const summary = {
1870
+ schemaVersion: 1,
1871
+ contractVersion: 'continuity-v1',
1872
+ policyVersion: 1,
1873
+ type: CONTINUITY_RECEIPT_COMPACTION_EVENT_TYPE,
1874
+ summaryId,
1875
+ jobId,
1876
+ scope: 'local_sqlite',
1877
+ status: 'compacted',
1878
+ createdAt: input.nowIso,
1879
+ reasonCodes: [CONTINUITY_RECEIPT_RETENTION_REASON_CODES.COMPACTED],
1880
+ counts,
1881
+ hashes,
1882
+ publicSummary,
1883
+ publicSafe: true,
1884
+ };
1885
+ this.appendEvent({
1886
+ eventId: summaryId,
1887
+ jobId,
1888
+ operationId: `op-${summaryId}`,
1889
+ idempotencyKey: `idem-${summaryId}`,
1890
+ type: CONTINUITY_RECEIPT_COMPACTION_EVENT_TYPE,
1891
+ summary: publicSummary,
1892
+ reasonCode: CONTINUITY_RECEIPT_RETENTION_REASON_CODES.COMPACTED,
1893
+ createdAt: input.nowIso,
1894
+ metadata: {
1895
+ schemaVersion: 1,
1896
+ contractVersion: 'continuity-v1',
1897
+ eventType: CONTINUITY_RECEIPT_COMPACTION_EVENT_TYPE,
1898
+ jobId,
1899
+ reasonCodes: summary.reasonCodes,
1900
+ counts,
1901
+ hashes,
1902
+ publicSummary,
1903
+ },
1904
+ });
1905
+ this.pruneReceiptCompactionSummaries(jobId, policy.maxCompactionSummariesPerJob);
1906
+ summaries.push(summary);
1907
+ }
1908
+ return {
1909
+ status: 'compacted',
1910
+ summaries,
1911
+ counts: {
1912
+ receiptsRemoved: selectedReceipts.length,
1913
+ outputsRemoved: selectedOutputs.length,
1914
+ bytesRemoved: candidates.reduce((total, candidate) => total + candidate.approxBytes, 0),
1915
+ summariesCreated: summaries.length,
1916
+ },
1917
+ reasonCode: CONTINUITY_RECEIPT_RETENTION_REASON_CODES.COMPACTED,
1918
+ publicSafe: true,
1919
+ };
1920
+ });
1921
+ }
1922
+ catch (error) {
1923
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
1924
+ }
1925
+ }
1926
+ updateJobStatus(input) {
1927
+ this.assertCanMutate();
1928
+ try {
1929
+ const result = this.db.prepare(`
205
1930
  UPDATE jobs
206
1931
  SET status = ?,
207
1932
  reason_code = COALESCE(?, reason_code),
208
1933
  updated_at = ?,
209
1934
  next_run_at = COALESCE(?, next_run_at)
210
1935
  WHERE job_id = ?
211
- `).run(e.status,e.reasonCode??null,e.updatedAt,e.nextRunAt??null,e.jobId);if(t.changes===0)throw new i("sqlite-foreign-key-failed",`Continuity SQLite job ${e.jobId} was not found for status update.`);return this.toWriteResult(t)}catch(t){throw m(t,"sqlite-mutation-failed")}}recordJobStageProgress(e){this.assertCanMutate(),c(e.jobId,"job_stage.jobId",160,!0);const t=le(e.currentStage)?e.currentStage:void 0,r=Qe(e.iteration);if(!t&&r===void 0)return{inserted:!1,replay:!1,changes:0};const s=ue(e.at);try{const n=this.db.prepare("SELECT metadata_json FROM jobs WHERE job_id = ?").get(e.jobId);if(!n)return{inserted:!1,replay:!1,changes:0};const d=_t(typeof n.metadata_json=="string"?n.metadata_json:null);if(t&&(d.currentStage=t),r!==void 0&&(d.iteration=r),t&&s&&e.appendHistory!==!1){const l=Ge(d.stageHistory);l.push({stage:t,at:s}),d.stageHistory=l.slice(-ce)}const o=this.db.prepare("UPDATE jobs SET metadata_json = ? WHERE job_id = ?").run(S(d),e.jobId);return this.toWriteResult(o)}catch(n){throw m(n,"sqlite-mutation-failed")}}updateJobAndExistingGoalCampaignStatus(e){return this.runInTransaction(()=>{const t=this.db.prepare(`
1936
+ `).run(input.status, input.reasonCode ?? null, input.updatedAt, input.nextRunAt ?? null, input.jobId);
1937
+ if (result.changes === 0) {
1938
+ throw new ContinuitySqliteStoreError('sqlite-foreign-key-failed', `Continuity SQLite job ${input.jobId} was not found for status update.`);
1939
+ }
1940
+ return this.toWriteResult(result);
1941
+ }
1942
+ catch (error) {
1943
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
1944
+ }
1945
+ }
1946
+ /**
1947
+ * P195.04: Additively persist producer-emitted process stage/iteration into the
1948
+ * existing `jobs.metadata_json` column and append a bounded `stageHistory`
1949
+ * (newest `CONTINUITY_STAGE_HISTORY_MAX` kept). No schema migration is required.
1950
+ * Unknown stages, unsafe timestamps and missing jobs are no-ops so older DBs and
1951
+ * non-tick summaries never throw.
1952
+ */
1953
+ recordJobStageProgress(input) {
1954
+ this.assertCanMutate();
1955
+ assertBoundedPublicText(input.jobId, 'job_stage.jobId', 160, true);
1956
+ const stage = isContinuityPublicStage(input.currentStage) ? input.currentStage : undefined;
1957
+ const iteration = normalizeContinuityStageIteration(input.iteration);
1958
+ if (!stage && iteration === undefined) {
1959
+ return { inserted: false, replay: false, changes: 0 };
1960
+ }
1961
+ const at = sanitizeContinuityStageTimestamp(input.at);
1962
+ try {
1963
+ const existing = this.db
1964
+ .prepare('SELECT metadata_json FROM jobs WHERE job_id = ?')
1965
+ .get(input.jobId);
1966
+ if (!existing)
1967
+ return { inserted: false, replay: false, changes: 0 };
1968
+ const metadata = parseJobMetadataObject(typeof existing.metadata_json === 'string' ? existing.metadata_json : null);
1969
+ if (stage)
1970
+ metadata.currentStage = stage;
1971
+ if (iteration !== undefined)
1972
+ metadata.iteration = iteration;
1973
+ if (stage && at && input.appendHistory !== false) {
1974
+ const history = sanitizeContinuityStageHistory(metadata.stageHistory);
1975
+ history.push({ stage, at });
1976
+ metadata.stageHistory = history.slice(-CONTINUITY_STAGE_HISTORY_MAX);
1977
+ }
1978
+ const result = this.db
1979
+ .prepare('UPDATE jobs SET metadata_json = ? WHERE job_id = ?')
1980
+ .run(stringifyPublicMetadata(metadata), input.jobId);
1981
+ return this.toWriteResult(result);
1982
+ }
1983
+ catch (error) {
1984
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
1985
+ }
1986
+ }
1987
+ updateJobAndExistingGoalCampaignStatus(input) {
1988
+ return this.runInTransaction(() => {
1989
+ const jobResult = this.db.prepare(`
212
1990
  UPDATE jobs
213
1991
  SET status = ?,
214
1992
  reason_code = COALESCE(?, reason_code),
215
1993
  updated_at = ?,
216
1994
  next_run_at = COALESCE(?, next_run_at)
217
1995
  WHERE job_id = ?
218
- `).run(e.status,e.reasonCode??null,e.updatedAt,e.nextRunAt??null,e.jobId);if(t.changes===0)throw new i("sqlite-foreign-key-failed",`Continuity SQLite job ${e.jobId} was not found for status parity update.`);const r=this.db.prepare(`
1996
+ `).run(input.status, input.reasonCode ?? null, input.updatedAt, input.nextRunAt ?? null, input.jobId);
1997
+ if (jobResult.changes === 0) {
1998
+ throw new ContinuitySqliteStoreError('sqlite-foreign-key-failed', `Continuity SQLite job ${input.jobId} was not found for status parity update.`);
1999
+ }
2000
+ const campaignResult = this.db.prepare(`
219
2001
  UPDATE goal_campaigns
220
2002
  SET status = ?,
221
2003
  reason_code = COALESCE(?, reason_code),
222
2004
  updated_at = ?
223
2005
  WHERE job_id = ?
224
- `).run(e.status,e.reasonCode??null,e.updatedAt,e.jobId);return this.toWriteResult({changes:t.changes+r.changes})})}scheduleNextTick(e){return this.updateJobStatus({jobId:e.jobId,status:"active",updatedAt:e.updatedAt,reasonCode:e.reasonCode,nextRunAt:e.scheduledAt})}acquireLease(e){this.assertCanMutate();const t=S(e.metadata);try{const r=this.db.prepare(`
2006
+ `).run(input.status, input.reasonCode ?? null, input.updatedAt, input.jobId);
2007
+ return this.toWriteResult({ changes: jobResult.changes + campaignResult.changes });
2008
+ });
2009
+ }
2010
+ scheduleNextTick(input) {
2011
+ return this.updateJobStatus({
2012
+ jobId: input.jobId,
2013
+ status: 'active',
2014
+ updatedAt: input.updatedAt,
2015
+ reasonCode: input.reasonCode,
2016
+ nextRunAt: input.scheduledAt,
2017
+ });
2018
+ }
2019
+ acquireLease(input) {
2020
+ this.assertCanMutate();
2021
+ const metadataJson = stringifyPublicMetadata(input.metadata);
2022
+ try {
2023
+ const result = this.db.prepare(`
225
2024
  INSERT OR IGNORE INTO job_leases (
226
2025
  lease_id, job_id, runner_id, status, acquired_at, expires_at,
227
2026
  operation_id, idempotency_key, metadata_json
228
2027
  ) VALUES (?, ?, ?, 'acquired', ?, ?, ?, ?, ?)
229
- `).run(e.leaseId,e.jobId,e.runnerId,e.acquiredAt,e.expiresAt,e.operationId??null,e.idempotencyKey??null,t);return this.toWriteResult(r)}catch(r){throw m(r,"sqlite-mutation-failed")}}releaseLease(e){this.assertCanMutate();try{const t=this.db.prepare(`
2028
+ `).run(input.leaseId, input.jobId, input.runnerId, input.acquiredAt, input.expiresAt, input.operationId ?? null, input.idempotencyKey ?? null, metadataJson);
2029
+ return this.toWriteResult(result);
2030
+ }
2031
+ catch (error) {
2032
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
2033
+ }
2034
+ }
2035
+ releaseLease(input) {
2036
+ this.assertCanMutate();
2037
+ try {
2038
+ const result = this.db.prepare(`
230
2039
  UPDATE job_leases
231
2040
  SET status = 'released', released_at = ?
232
2041
  WHERE lease_id = ? AND released_at IS NULL
233
- `).run(e.releasedAt,e.leaseId);if(t.changes===0)throw new i("sqlite-foreign-key-failed",`Continuity SQLite lease ${e.leaseId} was not found for release.`);return this.toWriteResult(t)}catch(t){throw m(t,"sqlite-mutation-failed")}}recordStateSnapshot(e){this.assertCanMutate(),b(e.summary,"state_snapshots.summary"),b(e.relativePath,"state_snapshots.relative_path");const t=S(e.metadata);try{const r=this.db.prepare(`
2042
+ `).run(input.releasedAt, input.leaseId);
2043
+ if (result.changes === 0) {
2044
+ throw new ContinuitySqliteStoreError('sqlite-foreign-key-failed', `Continuity SQLite lease ${input.leaseId} was not found for release.`);
2045
+ }
2046
+ return this.toWriteResult(result);
2047
+ }
2048
+ catch (error) {
2049
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
2050
+ }
2051
+ }
2052
+ recordStateSnapshot(input) {
2053
+ this.assertCanMutate();
2054
+ assertPublicSafeText(input.summary, 'state_snapshots.summary');
2055
+ assertPublicSafeText(input.relativePath, 'state_snapshots.relative_path');
2056
+ const metadataJson = stringifyPublicMetadata(input.metadata);
2057
+ try {
2058
+ const result = this.db.prepare(`
234
2059
  INSERT OR IGNORE INTO state_snapshots (
235
2060
  snapshot_id, job_id, kind, relative_path, sha256, summary, captured_at,
236
2061
  operation_id, idempotency_key, metadata_json
237
2062
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
238
- `).run(e.snapshotId,e.jobId??null,e.kind,e.relativePath??null,e.sha256??null,e.summary??null,e.capturedAt,e.operationId??null,e.idempotencyKey??null,t);return this.toWriteResult(r)}catch(r){throw m(r,"sqlite-mutation-failed")}}recordArtifact(e){this.assertCanMutate(),b(e.kind,"artifacts.kind"),b(e.relativePath,"artifacts.relative_path"),b(e.summary,"artifacts.summary");const t=S(e.metadata);try{const r=this.db.prepare(`
2063
+ `).run(input.snapshotId, input.jobId ?? null, input.kind, input.relativePath ?? null, input.sha256 ?? null, input.summary ?? null, input.capturedAt, input.operationId ?? null, input.idempotencyKey ?? null, metadataJson);
2064
+ return this.toWriteResult(result);
2065
+ }
2066
+ catch (error) {
2067
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
2068
+ }
2069
+ }
2070
+ recordArtifact(input) {
2071
+ this.assertCanMutate();
2072
+ assertPublicSafeText(input.kind, 'artifacts.kind');
2073
+ assertPublicSafeText(input.relativePath, 'artifacts.relative_path');
2074
+ assertPublicSafeText(input.summary, 'artifacts.summary');
2075
+ const metadataJson = stringifyPublicMetadata(input.metadata);
2076
+ try {
2077
+ const result = this.db.prepare(`
239
2078
  INSERT OR IGNORE INTO artifacts (
240
2079
  artifact_id, job_id, kind, relative_path, sha256, summary,
241
2080
  operation_id, idempotency_key, created_at, metadata_json
242
2081
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
243
- `).run(e.artifactId,e.jobId??null,e.kind,e.relativePath,e.sha256??null,e.summary??null,e.operationId??null,e.idempotencyKey??null,e.createdAt,t);return this.toWriteResult(r)}catch(r){throw m(r,"sqlite-mutation-failed")}}enqueueOutbox(e){this.assertCanMutate(),v({outboxId:e.outboxId,jobId:e.jobId,operationId:e.operationId,idempotencyKey:e.idempotencyKey,type:e.type,status:e.status,deliveryCursor:e.deliveryCursor,timestamp:e.createdAt,reasonCode:e.lastErrorReason??e.lastErrorCode,summary:e.lastErrorSummary??e.deadLetterSummary}),v({outboxId:e.outboxId,timestamp:e.updatedAt}),c(e.nextAttemptAt,"outbox.nextAttemptAt",80),c(e.lastAttemptAt,"outbox.lastAttemptAt",80),c(e.sentAt,"outbox.sentAt",80),c(e.ackedAt,"outbox.ackedAt",80),c(e.deadLetteredAt,"outbox.deadLetteredAt",80),N(e.retryCount,"outbox.retryCount"),N(e.attemptCount,"outbox.attemptCount");const t=S(e.payload);try{const r=this.db.prepare(`
2082
+ `).run(input.artifactId, input.jobId ?? null, input.kind, input.relativePath, input.sha256 ?? null, input.summary ?? null, input.operationId ?? null, input.idempotencyKey ?? null, input.createdAt, metadataJson);
2083
+ return this.toWriteResult(result);
2084
+ }
2085
+ catch (error) {
2086
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
2087
+ }
2088
+ }
2089
+ enqueueOutbox(input) {
2090
+ this.assertCanMutate();
2091
+ assertOutboxIdentity({
2092
+ outboxId: input.outboxId,
2093
+ jobId: input.jobId,
2094
+ operationId: input.operationId,
2095
+ idempotencyKey: input.idempotencyKey,
2096
+ type: input.type,
2097
+ status: input.status,
2098
+ deliveryCursor: input.deliveryCursor,
2099
+ timestamp: input.createdAt,
2100
+ reasonCode: input.lastErrorReason ?? input.lastErrorCode,
2101
+ summary: input.lastErrorSummary ?? input.deadLetterSummary,
2102
+ });
2103
+ assertOutboxIdentity({ outboxId: input.outboxId, timestamp: input.updatedAt });
2104
+ assertBoundedPublicText(input.nextAttemptAt, 'outbox.nextAttemptAt', 80);
2105
+ assertBoundedPublicText(input.lastAttemptAt, 'outbox.lastAttemptAt', 80);
2106
+ assertBoundedPublicText(input.sentAt, 'outbox.sentAt', 80);
2107
+ assertBoundedPublicText(input.ackedAt, 'outbox.ackedAt', 80);
2108
+ assertBoundedPublicText(input.deadLetteredAt, 'outbox.deadLetteredAt', 80);
2109
+ assertNonNegativeInteger(input.retryCount, 'outbox.retryCount');
2110
+ assertNonNegativeInteger(input.attemptCount, 'outbox.attemptCount');
2111
+ const payloadJson = stringifyPublicMetadata(input.payload);
2112
+ try {
2113
+ const result = this.db.prepare(`
244
2114
  INSERT OR IGNORE INTO outbox (
245
2115
  outbox_id, job_id, operation_id, idempotency_key, type, status,
246
2116
  payload_json, retry_count, attempt_count, delivery_cursor,
@@ -248,18 +2118,57 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
248
2118
  last_error_reason, last_error_summary, dead_letter_summary,
249
2119
  dead_lettered_at, created_at, updated_at
250
2120
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
251
- `).run(e.outboxId,e.jobId??null,e.operationId,e.idempotencyKey??null,e.type,e.status??"pending",t??"{}",e.retryCount??0,e.attemptCount??0,e.deliveryCursor??null,e.nextAttemptAt??null,e.lastAttemptAt??null,e.sentAt??null,e.ackedAt??null,e.lastErrorCode??null,e.lastErrorReason??e.lastErrorCode??null,e.lastErrorSummary??null,e.deadLetterSummary??null,e.deadLetteredAt??null,e.createdAt,e.updatedAt);return this.toWriteResult(r)}catch(r){throw m(r,"sqlite-mutation-failed")}}selectOutboxBatch(e){v({outboxId:"outbox-query",timestamp:e.nowIso});const t=Math.max(1,Math.min(100,Math.floor(e.limit))),r=q(e.statuses?.length?e.statuses:["pending","failed"]);for(const d of r)if(!Ie(d))throw new i("sqlite-public-safety-rejected","Continuity SQLite outbox batch query rejected an unknown status.");const s=r.map(()=>"?").join(", ");return this.db.prepare(`
2121
+ `).run(input.outboxId, input.jobId ?? null, input.operationId, input.idempotencyKey ?? null, input.type, input.status ?? 'pending', payloadJson ?? '{}', input.retryCount ?? 0, input.attemptCount ?? 0, input.deliveryCursor ?? null, input.nextAttemptAt ?? null, input.lastAttemptAt ?? null, input.sentAt ?? null, input.ackedAt ?? null, input.lastErrorCode ?? null, input.lastErrorReason ?? input.lastErrorCode ?? null, input.lastErrorSummary ?? null, input.deadLetterSummary ?? null, input.deadLetteredAt ?? null, input.createdAt, input.updatedAt);
2122
+ return this.toWriteResult(result);
2123
+ }
2124
+ catch (error) {
2125
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
2126
+ }
2127
+ }
2128
+ selectOutboxBatch(options) {
2129
+ assertOutboxIdentity({ outboxId: 'outbox-query', timestamp: options.nowIso });
2130
+ const limit = Math.max(1, Math.min(100, Math.floor(options.limit)));
2131
+ const statuses = uniqueValues(options.statuses?.length ? options.statuses : ['pending', 'failed']);
2132
+ for (const status of statuses) {
2133
+ if (!isOutboxStatus(status)) {
2134
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Continuity SQLite outbox batch query rejected an unknown status.');
2135
+ }
2136
+ }
2137
+ const placeholders = statuses.map(() => '?').join(', ');
2138
+ const rows = this.db.prepare(`
252
2139
  SELECT outbox_id, job_id, operation_id, idempotency_key, type, status,
253
2140
  payload_json, retry_count, attempt_count, delivery_cursor,
254
2141
  next_attempt_at, last_attempt_at, sent_at, acked_at, last_error_code,
255
2142
  last_error_reason, last_error_summary, dead_letter_summary,
256
2143
  dead_lettered_at, created_at, updated_at
257
2144
  FROM outbox
258
- WHERE status IN (${s})
2145
+ WHERE status IN (${placeholders})
259
2146
  AND (status NOT IN ('pending', 'failed') OR next_attempt_at IS NULL OR next_attempt_at <= ?)
260
2147
  ORDER BY COALESCE(next_attempt_at, created_at) ASC, created_at ASC, outbox_id ASC
261
2148
  LIMIT ?
262
- `).all(...r,e.nowIso,t).map(ht)}markOutboxSent(e){this.assertCanMutate(),v({outboxId:e.outboxId,deliveryCursor:e.deliveryCursor,timestamp:e.sentAt,reasonCode:e.reasonCode});try{return this.runInTransaction(()=>{const t=this.getOutboxTransitionRow(e.outboxId);if(!t)return C({outboxId:e.outboxId,status:"missing",changes:0,replay:!1,reasonCode:"outbox-missing"});if((t.status==="sent"||t.status==="acked")&&t.delivery_cursor===e.deliveryCursor)return C({outboxId:e.outboxId,status:t.status,changes:0,replay:!0,deliveryCursor:e.deliveryCursor,reasonCode:e.reasonCode??"outbox-delivery-replay"});if(t.status==="sent"||t.status==="acked"||t.status==="dead_letter")return C({outboxId:e.outboxId,status:"conflict",changes:0,replay:!1,deliveryCursor:t.delivery_cursor??void 0,reasonCode:"outbox-delivery-conflict"});const r=this.db.prepare(`
2149
+ `).all(...statuses, options.nowIso, limit);
2150
+ return rows.map(toOutboxDeliveryRow);
2151
+ }
2152
+ markOutboxSent(input) {
2153
+ this.assertCanMutate();
2154
+ assertOutboxIdentity({
2155
+ outboxId: input.outboxId,
2156
+ deliveryCursor: input.deliveryCursor,
2157
+ timestamp: input.sentAt,
2158
+ reasonCode: input.reasonCode,
2159
+ });
2160
+ try {
2161
+ return this.runInTransaction(() => {
2162
+ const row = this.getOutboxTransitionRow(input.outboxId);
2163
+ if (!row)
2164
+ return outboxTransitionResult({ outboxId: input.outboxId, status: 'missing', changes: 0, replay: false, reasonCode: 'outbox-missing' });
2165
+ if ((row.status === 'sent' || row.status === 'acked') && row.delivery_cursor === input.deliveryCursor) {
2166
+ return outboxTransitionResult({ outboxId: input.outboxId, status: row.status, changes: 0, replay: true, deliveryCursor: input.deliveryCursor, reasonCode: input.reasonCode ?? 'outbox-delivery-replay' });
2167
+ }
2168
+ if (row.status === 'sent' || row.status === 'acked' || row.status === 'dead_letter') {
2169
+ return outboxTransitionResult({ outboxId: input.outboxId, status: 'conflict', changes: 0, replay: false, deliveryCursor: row.delivery_cursor ?? undefined, reasonCode: 'outbox-delivery-conflict' });
2170
+ }
2171
+ const result = this.db.prepare(`
263
2172
  UPDATE outbox
264
2173
  SET status = 'sent',
265
2174
  delivery_cursor = ?,
@@ -270,7 +2179,34 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
270
2179
  updated_at = ?
271
2180
  WHERE outbox_id = ?
272
2181
  AND status IN ('pending', 'failed')
273
- `).run(e.deliveryCursor,e.sentAt,e.sentAt,e.sentAt,e.outboxId);return C({outboxId:e.outboxId,status:"sent",changes:r.changes,replay:!1,deliveryCursor:e.deliveryCursor,reasonCode:e.reasonCode??"outbox-sent"})})}catch(t){throw m(t,"sqlite-mutation-failed")}}ackOutbox(e){this.assertCanMutate(),v({outboxId:e.outboxId,deliveryCursor:e.deliveryCursor,timestamp:e.ackedAt,reasonCode:e.reasonCode});try{return this.runInTransaction(()=>{const t=this.getOutboxTransitionRow(e.outboxId);if(!t)return C({outboxId:e.outboxId,status:"missing",changes:0,replay:!1,reasonCode:"outbox-missing"});if(t.status==="acked"&&(!e.deliveryCursor||t.delivery_cursor===e.deliveryCursor))return C({outboxId:e.outboxId,status:"acked",changes:0,replay:!0,deliveryCursor:t.delivery_cursor??void 0,reasonCode:e.reasonCode??"outbox-ack-replay"});if(t.status!=="sent"||e.deliveryCursor&&t.delivery_cursor!==e.deliveryCursor)return C({outboxId:e.outboxId,status:"conflict",changes:0,replay:!1,deliveryCursor:t.delivery_cursor??void 0,reasonCode:"outbox-ack-conflict"});const r=this.db.prepare(`
2182
+ `).run(input.deliveryCursor, input.sentAt, input.sentAt, input.sentAt, input.outboxId);
2183
+ return outboxTransitionResult({ outboxId: input.outboxId, status: 'sent', changes: result.changes, replay: false, deliveryCursor: input.deliveryCursor, reasonCode: input.reasonCode ?? 'outbox-sent' });
2184
+ });
2185
+ }
2186
+ catch (error) {
2187
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
2188
+ }
2189
+ }
2190
+ ackOutbox(input) {
2191
+ this.assertCanMutate();
2192
+ assertOutboxIdentity({
2193
+ outboxId: input.outboxId,
2194
+ deliveryCursor: input.deliveryCursor,
2195
+ timestamp: input.ackedAt,
2196
+ reasonCode: input.reasonCode,
2197
+ });
2198
+ try {
2199
+ return this.runInTransaction(() => {
2200
+ const row = this.getOutboxTransitionRow(input.outboxId);
2201
+ if (!row)
2202
+ return outboxTransitionResult({ outboxId: input.outboxId, status: 'missing', changes: 0, replay: false, reasonCode: 'outbox-missing' });
2203
+ if (row.status === 'acked' && (!input.deliveryCursor || row.delivery_cursor === input.deliveryCursor)) {
2204
+ return outboxTransitionResult({ outboxId: input.outboxId, status: 'acked', changes: 0, replay: true, deliveryCursor: row.delivery_cursor ?? undefined, reasonCode: input.reasonCode ?? 'outbox-ack-replay' });
2205
+ }
2206
+ if (row.status !== 'sent' || (input.deliveryCursor && row.delivery_cursor !== input.deliveryCursor)) {
2207
+ return outboxTransitionResult({ outboxId: input.outboxId, status: 'conflict', changes: 0, replay: false, deliveryCursor: row.delivery_cursor ?? undefined, reasonCode: 'outbox-ack-conflict' });
2208
+ }
2209
+ const result = this.db.prepare(`
274
2210
  UPDATE outbox
275
2211
  SET status = 'acked',
276
2212
  acked_at = ?,
@@ -280,7 +2216,48 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
280
2216
  last_error_reason = NULL
281
2217
  WHERE outbox_id = ?
282
2218
  AND status = 'sent'
283
- `).run(e.ackedAt,e.ackedAt,e.outboxId);return C({outboxId:e.outboxId,status:"acked",changes:r.changes,replay:!1,deliveryCursor:t.delivery_cursor??void 0,reasonCode:e.reasonCode??"outbox-acked"})})}catch(t){throw m(t,"sqlite-mutation-failed")}}failOutboxDelivery(e){this.assertCanMutate(),v({outboxId:e.outboxId,deliveryCursor:e.deliveryCursor,timestamp:e.failedAt,reasonCode:e.reasonCode,summary:e.summary}),c(e.nextRetryAt,"outbox.nextRetryAt",80);try{return this.runInTransaction(()=>{const t=this.getOutboxTransitionRow(e.outboxId);if(!t)return C({outboxId:e.outboxId,status:"missing",changes:0,replay:!1,reasonCode:"outbox-missing"});if(t.status==="acked")return C({outboxId:e.outboxId,status:"acked",changes:0,replay:!0,deliveryCursor:t.delivery_cursor??void 0,reasonCode:"outbox-already-acked"});if(t.status==="dead_letter")return C({outboxId:e.outboxId,status:"dead_letter",changes:0,replay:!0,deliveryCursor:t.delivery_cursor??void 0,deadLetteredAt:t.dead_lettered_at??void 0,reasonCode:t.last_error_reason??t.last_error_code??e.reasonCode});if(e.deliveryCursor&&t.delivery_cursor&&t.delivery_cursor!==e.deliveryCursor)return C({outboxId:e.outboxId,status:"conflict",changes:0,replay:!1,deliveryCursor:t.delivery_cursor,reasonCode:"outbox-fail-conflict"});const r=Math.max(1,t.attempt_count??0,t.retry_count??0),s=we({nowIso:e.failedAt,attemptCount:r,reasonCode:e.reasonCode,summary:e.summary,nextRetryAt:e.nextRetryAt,policy:e.policy}),n=this.db.prepare(`
2219
+ `).run(input.ackedAt, input.ackedAt, input.outboxId);
2220
+ return outboxTransitionResult({ outboxId: input.outboxId, status: 'acked', changes: result.changes, replay: false, deliveryCursor: row.delivery_cursor ?? undefined, reasonCode: input.reasonCode ?? 'outbox-acked' });
2221
+ });
2222
+ }
2223
+ catch (error) {
2224
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
2225
+ }
2226
+ }
2227
+ failOutboxDelivery(input) {
2228
+ this.assertCanMutate();
2229
+ assertOutboxIdentity({
2230
+ outboxId: input.outboxId,
2231
+ deliveryCursor: input.deliveryCursor,
2232
+ timestamp: input.failedAt,
2233
+ reasonCode: input.reasonCode,
2234
+ summary: input.summary,
2235
+ });
2236
+ assertBoundedPublicText(input.nextRetryAt, 'outbox.nextRetryAt', 80);
2237
+ try {
2238
+ return this.runInTransaction(() => {
2239
+ const row = this.getOutboxTransitionRow(input.outboxId);
2240
+ if (!row)
2241
+ return outboxTransitionResult({ outboxId: input.outboxId, status: 'missing', changes: 0, replay: false, reasonCode: 'outbox-missing' });
2242
+ if (row.status === 'acked') {
2243
+ return outboxTransitionResult({ outboxId: input.outboxId, status: 'acked', changes: 0, replay: true, deliveryCursor: row.delivery_cursor ?? undefined, reasonCode: 'outbox-already-acked' });
2244
+ }
2245
+ if (row.status === 'dead_letter') {
2246
+ return outboxTransitionResult({ outboxId: input.outboxId, status: 'dead_letter', changes: 0, replay: true, deliveryCursor: row.delivery_cursor ?? undefined, deadLetteredAt: row.dead_lettered_at ?? undefined, reasonCode: row.last_error_reason ?? row.last_error_code ?? input.reasonCode });
2247
+ }
2248
+ if (input.deliveryCursor && row.delivery_cursor && row.delivery_cursor !== input.deliveryCursor) {
2249
+ return outboxTransitionResult({ outboxId: input.outboxId, status: 'conflict', changes: 0, replay: false, deliveryCursor: row.delivery_cursor, reasonCode: 'outbox-fail-conflict' });
2250
+ }
2251
+ const attemptCount = Math.max(1, row.attempt_count ?? 0, row.retry_count ?? 0);
2252
+ const decision = computeContinuityOutboxFailureDecision({
2253
+ nowIso: input.failedAt,
2254
+ attemptCount,
2255
+ reasonCode: input.reasonCode,
2256
+ summary: input.summary,
2257
+ nextRetryAt: input.nextRetryAt,
2258
+ policy: input.policy,
2259
+ });
2260
+ const result = this.db.prepare(`
284
2261
  UPDATE outbox
285
2262
  SET status = ?,
286
2263
  retry_count = MAX(retry_count, ?),
@@ -293,7 +2270,40 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
293
2270
  updated_at = ?
294
2271
  WHERE outbox_id = ?
295
2272
  AND status IN ('pending', 'sent', 'failed')
296
- `).run(s.status,r,s.status==="failed"?s.nextRetryAt??e.failedAt:null,e.reasonCode,e.reasonCode,e.summary??null,s.status==="dead_letter"?e.summary??e.reasonCode:null,s.deadLetteredAt??null,e.failedAt,e.outboxId);return C({outboxId:e.outboxId,status:s.status,changes:n.changes,replay:!1,deliveryCursor:t.delivery_cursor??e.deliveryCursor,reasonCode:s.reasonCode,...s.nextRetryAt?{nextRetryAt:s.nextRetryAt}:{},...s.deadLetteredAt?{deadLetteredAt:s.deadLetteredAt}:{}})})}catch(t){throw m(t,"sqlite-mutation-failed")}}deadLetterOutbox(e){this.assertCanMutate(),v({outboxId:e.outboxId,timestamp:e.deadLetteredAt,reasonCode:e.reasonCode,summary:e.summary});try{return this.runInTransaction(()=>{const t=this.getOutboxTransitionRow(e.outboxId);if(!t)return C({outboxId:e.outboxId,status:"missing",changes:0,replay:!1,reasonCode:"outbox-missing"});if(t.status==="dead_letter")return C({outboxId:e.outboxId,status:"dead_letter",changes:0,replay:!0,deliveryCursor:t.delivery_cursor??void 0,deadLetteredAt:t.dead_lettered_at??void 0,reasonCode:t.last_error_reason??t.last_error_code??e.reasonCode});const r=this.db.prepare(`
2273
+ `).run(decision.status, attemptCount, decision.status === 'failed' ? decision.nextRetryAt ?? input.failedAt : null, input.reasonCode, input.reasonCode, input.summary ?? null, decision.status === 'dead_letter' ? input.summary ?? input.reasonCode : null, decision.deadLetteredAt ?? null, input.failedAt, input.outboxId);
2274
+ return outboxTransitionResult({
2275
+ outboxId: input.outboxId,
2276
+ status: decision.status,
2277
+ changes: result.changes,
2278
+ replay: false,
2279
+ deliveryCursor: row.delivery_cursor ?? input.deliveryCursor,
2280
+ reasonCode: decision.reasonCode,
2281
+ ...(decision.nextRetryAt ? { nextRetryAt: decision.nextRetryAt } : {}),
2282
+ ...(decision.deadLetteredAt ? { deadLetteredAt: decision.deadLetteredAt } : {}),
2283
+ });
2284
+ });
2285
+ }
2286
+ catch (error) {
2287
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
2288
+ }
2289
+ }
2290
+ deadLetterOutbox(input) {
2291
+ this.assertCanMutate();
2292
+ assertOutboxIdentity({
2293
+ outboxId: input.outboxId,
2294
+ timestamp: input.deadLetteredAt,
2295
+ reasonCode: input.reasonCode,
2296
+ summary: input.summary,
2297
+ });
2298
+ try {
2299
+ return this.runInTransaction(() => {
2300
+ const row = this.getOutboxTransitionRow(input.outboxId);
2301
+ if (!row)
2302
+ return outboxTransitionResult({ outboxId: input.outboxId, status: 'missing', changes: 0, replay: false, reasonCode: 'outbox-missing' });
2303
+ if (row.status === 'dead_letter') {
2304
+ return outboxTransitionResult({ outboxId: input.outboxId, status: 'dead_letter', changes: 0, replay: true, deliveryCursor: row.delivery_cursor ?? undefined, deadLetteredAt: row.dead_lettered_at ?? undefined, reasonCode: row.last_error_reason ?? row.last_error_code ?? input.reasonCode });
2305
+ }
2306
+ const result = this.db.prepare(`
297
2307
  UPDATE outbox
298
2308
  SET status = 'dead_letter',
299
2309
  next_attempt_at = NULL,
@@ -305,13 +2315,39 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
305
2315
  updated_at = ?
306
2316
  WHERE outbox_id = ?
307
2317
  AND status <> 'acked'
308
- `).run(e.reasonCode,e.reasonCode,e.summary,e.summary,e.deadLetteredAt,e.deadLetteredAt,e.outboxId);return C({outboxId:e.outboxId,status:"dead_letter",changes:r.changes,replay:!1,deliveryCursor:t.delivery_cursor??void 0,deadLetteredAt:e.deadLetteredAt,reasonCode:e.reasonCode})})}catch(t){throw m(t,"sqlite-mutation-failed")}}retryOutboxEntries(e){this.assertCanMutate();const t=Math.max(1,Math.min(100,Math.floor(e.limit))),r=q(e.statuses?.length?e.statuses:["failed","dead_letter"]),s=e.reasonCode??"backoff-scheduled";b(s,"outbox.last_error_code"),b(e.nextAttemptAt,"outbox.next_attempt_at");try{return this.runInTransaction(()=>{const n=r.map(()=>"?").join(", "),o=this.db.prepare(`
2318
+ `).run(input.reasonCode, input.reasonCode, input.summary, input.summary, input.deadLetteredAt, input.deadLetteredAt, input.outboxId);
2319
+ return outboxTransitionResult({ outboxId: input.outboxId, status: 'dead_letter', changes: result.changes, replay: false, deliveryCursor: row.delivery_cursor ?? undefined, deadLetteredAt: input.deadLetteredAt, reasonCode: input.reasonCode });
2320
+ });
2321
+ }
2322
+ catch (error) {
2323
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
2324
+ }
2325
+ }
2326
+ retryOutboxEntries(options) {
2327
+ this.assertCanMutate();
2328
+ const limit = Math.max(1, Math.min(100, Math.floor(options.limit)));
2329
+ const statuses = uniqueValues(options.statuses?.length ? options.statuses : ['failed', 'dead_letter']);
2330
+ const reasonCode = options.reasonCode ?? 'backoff-scheduled';
2331
+ assertPublicSafeText(reasonCode, 'outbox.last_error_code');
2332
+ assertPublicSafeText(options.nextAttemptAt, 'outbox.next_attempt_at');
2333
+ try {
2334
+ return this.runInTransaction(() => {
2335
+ const placeholders = statuses.map(() => '?').join(', ');
2336
+ const rows = this.db.prepare(`
309
2337
  SELECT outbox_id
310
2338
  FROM outbox
311
- WHERE status IN (${n})
2339
+ WHERE status IN (${placeholders})
312
2340
  ORDER BY updated_at ASC, outbox_id ASC
313
2341
  LIMIT ?
314
- `).all(...r,t).map(y=>y.outbox_id).filter(y=>typeof y=="string"&&y.length>0);if(o.length===0)return{retriedOutboxIds:[],changes:0,reasonCode:s,publicSafe:!0};const l=o.map(()=>"?").join(", "),I=this.db.prepare(`
2342
+ `).all(...statuses, limit);
2343
+ const retriedOutboxIds = rows
2344
+ .map((row) => row.outbox_id)
2345
+ .filter((outboxId) => typeof outboxId === 'string' && outboxId.length > 0);
2346
+ if (retriedOutboxIds.length === 0) {
2347
+ return { retriedOutboxIds: [], changes: 0, reasonCode, publicSafe: true };
2348
+ }
2349
+ const idPlaceholders = retriedOutboxIds.map(() => '?').join(', ');
2350
+ const result = this.db.prepare(`
315
2351
  UPDATE outbox
316
2352
  SET status = 'pending',
317
2353
  retry_count = retry_count + 1,
@@ -324,8 +2360,22 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
324
2360
  dead_letter_summary = NULL,
325
2361
  dead_lettered_at = NULL,
326
2362
  updated_at = ?
327
- WHERE outbox_id IN (${l})
328
- `).run(e.nextAttemptAt??e.nowIso,s,s,e.nowIso,...o);return{retriedOutboxIds:o,changes:I.changes,reasonCode:s,publicSafe:!0}})}catch(n){throw m(n,"sqlite-mutation-failed")}}upsertGoalCampaign(e){this.assertCanMutate(),b(e.title,"goal_campaigns.title"),b(e.publicSummary,"goal_campaigns.public_summary");const t=S(e.metadata);try{const r=this.db.prepare(`
2363
+ WHERE outbox_id IN (${idPlaceholders})
2364
+ `).run(options.nextAttemptAt ?? options.nowIso, reasonCode, reasonCode, options.nowIso, ...retriedOutboxIds);
2365
+ return { retriedOutboxIds, changes: result.changes, reasonCode, publicSafe: true };
2366
+ });
2367
+ }
2368
+ catch (error) {
2369
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
2370
+ }
2371
+ }
2372
+ upsertGoalCampaign(input) {
2373
+ this.assertCanMutate();
2374
+ assertPublicSafeText(input.title, 'goal_campaigns.title');
2375
+ assertPublicSafeText(input.publicSummary, 'goal_campaigns.public_summary');
2376
+ const metadataJson = stringifyPublicMetadata(input.metadata);
2377
+ try {
2378
+ const result = this.db.prepare(`
329
2379
  INSERT INTO goal_campaigns (
330
2380
  campaign_id, job_id, status, title, public_summary, reason_code,
331
2381
  created_at, updated_at, operation_id, idempotency_key, metadata_json
@@ -339,7 +2389,20 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
339
2389
  operation_id = excluded.operation_id,
340
2390
  idempotency_key = excluded.idempotency_key,
341
2391
  metadata_json = excluded.metadata_json
342
- `).run(e.campaignId,e.jobId,e.status,e.title??null,e.publicSummary??null,e.reasonCode??null,e.createdAt,e.updatedAt,e.operationId??null,e.idempotencyKey??null,t);return this.toWriteResult(r)}catch(r){throw m(r,"sqlite-mutation-failed")}}upsertGoalLedgerItem(e){this.assertCanMutate(),b(e.summary,"goal_ledger_items.summary");const t=S(e.metadata);try{this.ensureGoalCampaign(e.campaignId,e.jobId,e.createdAt,e.updatedAt);const r=this.db.prepare(`
2392
+ `).run(input.campaignId, input.jobId, input.status, input.title ?? null, input.publicSummary ?? null, input.reasonCode ?? null, input.createdAt, input.updatedAt, input.operationId ?? null, input.idempotencyKey ?? null, metadataJson);
2393
+ return this.toWriteResult(result);
2394
+ }
2395
+ catch (error) {
2396
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
2397
+ }
2398
+ }
2399
+ upsertGoalLedgerItem(input) {
2400
+ this.assertCanMutate();
2401
+ assertPublicSafeText(input.summary, 'goal_ledger_items.summary');
2402
+ const metadataJson = stringifyPublicMetadata(input.metadata);
2403
+ try {
2404
+ this.ensureGoalCampaign(input.campaignId, input.jobId, input.createdAt, input.updatedAt);
2405
+ const result = this.db.prepare(`
343
2406
  INSERT INTO goal_ledger_items (
344
2407
  item_id, campaign_id, operation_id, idempotency_key, item_type, status,
345
2408
  summary, reason_code, created_at, updated_at, metadata_json
@@ -350,7 +2413,21 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
350
2413
  reason_code = excluded.reason_code,
351
2414
  updated_at = excluded.updated_at,
352
2415
  metadata_json = excluded.metadata_json
353
- `).run(e.itemId,e.campaignId,e.operationId??null,e.idempotencyKey??null,e.itemType,e.status,e.summary,e.reasonCode??null,e.createdAt,e.updatedAt,t);return this.toWriteResult(r)}catch(r){throw m(r,"sqlite-mutation-failed")}}upsertGoalVerifier(e){this.assertCanMutate(),b(e.summary,"goal_verifiers.summary");const t=S(e.metadata);try{const r=e.lastCheckedAt??this.now();this.ensureGoalCampaign(e.campaignId,e.jobId,r,r);const s=this.db.prepare(`
2416
+ `).run(input.itemId, input.campaignId, input.operationId ?? null, input.idempotencyKey ?? null, input.itemType, input.status, input.summary, input.reasonCode ?? null, input.createdAt, input.updatedAt, metadataJson);
2417
+ return this.toWriteResult(result);
2418
+ }
2419
+ catch (error) {
2420
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
2421
+ }
2422
+ }
2423
+ upsertGoalVerifier(input) {
2424
+ this.assertCanMutate();
2425
+ assertPublicSafeText(input.summary, 'goal_verifiers.summary');
2426
+ const metadataJson = stringifyPublicMetadata(input.metadata);
2427
+ try {
2428
+ const fallbackTimestamp = input.lastCheckedAt ?? this.now();
2429
+ this.ensureGoalCampaign(input.campaignId, input.jobId, fallbackTimestamp, fallbackTimestamp);
2430
+ const result = this.db.prepare(`
354
2431
  INSERT INTO goal_verifiers (
355
2432
  verifier_id, campaign_id, kind, status, summary, reason_code,
356
2433
  last_checked_at, operation_id, idempotency_key, metadata_json
@@ -361,7 +2438,21 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
361
2438
  reason_code = excluded.reason_code,
362
2439
  last_checked_at = excluded.last_checked_at,
363
2440
  metadata_json = excluded.metadata_json
364
- `).run(e.verifierId,e.campaignId,e.kind,e.status,e.summary??null,e.reasonCode??null,e.lastCheckedAt??null,e.operationId??null,e.idempotencyKey??null,t);return this.toWriteResult(s)}catch(r){throw m(r,"sqlite-mutation-failed")}}upsertGoalReviewGate(e){this.assertCanMutate(),b(e.summary,"goal_review_gates.summary");const t=S(e.metadata);try{const r=this.now();this.ensureGoalCampaign(e.campaignId,e.jobId,r,r);const s=this.db.prepare(`
2441
+ `).run(input.verifierId, input.campaignId, input.kind, input.status, input.summary ?? null, input.reasonCode ?? null, input.lastCheckedAt ?? null, input.operationId ?? null, input.idempotencyKey ?? null, metadataJson);
2442
+ return this.toWriteResult(result);
2443
+ }
2444
+ catch (error) {
2445
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
2446
+ }
2447
+ }
2448
+ upsertGoalReviewGate(input) {
2449
+ this.assertCanMutate();
2450
+ assertPublicSafeText(input.summary, 'goal_review_gates.summary');
2451
+ const metadataJson = stringifyPublicMetadata(input.metadata);
2452
+ try {
2453
+ const fallbackTimestamp = this.now();
2454
+ this.ensureGoalCampaign(input.campaignId, input.jobId, fallbackTimestamp, fallbackTimestamp);
2455
+ const result = this.db.prepare(`
365
2456
  INSERT INTO goal_review_gates (
366
2457
  gate_id, campaign_id, status, summary, required, reason_code,
367
2458
  operation_id, idempotency_key, metadata_json
@@ -372,7 +2463,45 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
372
2463
  required = excluded.required,
373
2464
  reason_code = excluded.reason_code,
374
2465
  metadata_json = excluded.metadata_json
375
- `).run(e.gateId,e.campaignId,e.status,e.summary??null,e.required?1:0,e.reasonCode??null,e.operationId??null,e.idempotencyKey??null,t);return this.toWriteResult(s)}catch(r){throw m(r,"sqlite-mutation-failed")}}recordOperationAudit(e){return this.appendReceipt(e)}hasOperationRef(e,t){this.assertCanMutate();const r=["jobs","job_leases","job_events","job_receipts","goal_campaigns","goal_ledger_items","goal_verifiers","goal_review_gates","loop_jobs","artifacts","state_snapshots","headless_execution_attempts","runner_config","outbox"],s=r.map(o=>`SELECT 1 AS found FROM ${o} WHERE operation_id = ?${t?" OR idempotency_key = ?":""}`),n=r.flatMap(()=>t?[e,t]:[e]);return!!this.db.prepare(`${s.join(" UNION ALL ")} LIMIT 1`).get(...n)}jobExists(e){return!!this.db.prepare("SELECT 1 AS found FROM jobs WHERE job_id = ? LIMIT 1").get(e)}getHeadlessExecutionAttempt(e){return this.db.prepare(`
2466
+ `).run(input.gateId, input.campaignId, input.status, input.summary ?? null, input.required ? 1 : 0, input.reasonCode ?? null, input.operationId ?? null, input.idempotencyKey ?? null, metadataJson);
2467
+ return this.toWriteResult(result);
2468
+ }
2469
+ catch (error) {
2470
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
2471
+ }
2472
+ }
2473
+ recordOperationAudit(input) {
2474
+ return this.appendReceipt(input);
2475
+ }
2476
+ hasOperationRef(operationId, idempotencyKey) {
2477
+ this.assertCanMutate();
2478
+ const tables = [
2479
+ 'jobs',
2480
+ 'job_leases',
2481
+ 'job_events',
2482
+ 'job_receipts',
2483
+ 'goal_campaigns',
2484
+ 'goal_ledger_items',
2485
+ 'goal_verifiers',
2486
+ 'goal_review_gates',
2487
+ 'loop_jobs',
2488
+ 'artifacts',
2489
+ 'state_snapshots',
2490
+ 'headless_execution_attempts',
2491
+ 'runner_config',
2492
+ 'outbox',
2493
+ ];
2494
+ const clauses = tables.map((table) => `SELECT 1 AS found FROM ${table} WHERE operation_id = ?${idempotencyKey ? ' OR idempotency_key = ?' : ''}`);
2495
+ const params = tables.flatMap(() => idempotencyKey ? [operationId, idempotencyKey] : [operationId]);
2496
+ const row = this.db.prepare(`${clauses.join(' UNION ALL ')} LIMIT 1`).get(...params);
2497
+ return Boolean(row);
2498
+ }
2499
+ jobExists(jobId) {
2500
+ const row = this.db.prepare('SELECT 1 AS found FROM jobs WHERE job_id = ? LIMIT 1').get(jobId);
2501
+ return Boolean(row);
2502
+ }
2503
+ getHeadlessExecutionAttempt(attemptId) {
2504
+ return this.db.prepare(`
376
2505
  SELECT attempt_id, execution_id, job_id, directive_hash, directive_kind,
377
2506
  command_class, target_kind, target_ref, story_id, epic_id,
378
2507
  runtime_id, model_id, runtime_source, status, reason_code,
@@ -386,13 +2515,22 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
386
2515
  FROM headless_execution_attempts
387
2516
  WHERE attempt_id = ?
388
2517
  LIMIT 1
389
- `).get(e)}getOutboxTransitionRow(e){const t=this.db.prepare(`
2518
+ `).get(attemptId);
2519
+ }
2520
+ getOutboxTransitionRow(outboxId) {
2521
+ const row = this.db.prepare(`
390
2522
  SELECT outbox_id, status, delivery_cursor, retry_count, attempt_count,
391
2523
  last_error_code, last_error_reason, dead_lettered_at
392
2524
  FROM outbox
393
2525
  WHERE outbox_id = ?
394
2526
  LIMIT 1
395
- `).get(e);if(!(!t||!E(t)))return t}selectReceiptRetentionCandidates(e){return this.db.prepare(`
2527
+ `).get(outboxId);
2528
+ if (!row || !isRecord(row))
2529
+ return undefined;
2530
+ return row;
2531
+ }
2532
+ selectReceiptRetentionCandidates(input) {
2533
+ const rows = this.db.prepare(`
396
2534
  WITH sized AS (
397
2535
  SELECT receipt_id, job_id, status, reason_code, created_at,
398
2536
  length(receipt_id) + length(job_id) + length(status) + length(created_at)
@@ -411,7 +2549,23 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
411
2549
  WHERE created_at < ? OR retain_rank > ? OR cumulative_bytes > ?
412
2550
  ORDER BY created_at ASC, receipt_id ASC
413
2551
  LIMIT ?
414
- `).all(e.cutoff,e.maxItems,e.maxBytes,e.limit).flatMap(r=>typeof r.receipt_id!="string"||typeof r.job_id!="string"?[]:[{kind:"receipt",id:r.receipt_id,jobId:r.job_id,status:typeof r.status=="string"?r.status:"unknown",reasonCode:typeof r.reason_code=="string"?r.reason_code:"continuity-ok",createdAt:typeof r.created_at=="string"?r.created_at:e.cutoff,approxBytes:typeof r.approx_bytes=="number"&&r.approx_bytes>=0?Math.floor(r.approx_bytes):0}])}selectOutputRetentionCandidates(e){return this.db.prepare(`
2552
+ `).all(input.cutoff, input.maxItems, input.maxBytes, input.limit);
2553
+ return rows.flatMap((row) => {
2554
+ if (typeof row.receipt_id !== 'string' || typeof row.job_id !== 'string')
2555
+ return [];
2556
+ return [{
2557
+ kind: 'receipt',
2558
+ id: row.receipt_id,
2559
+ jobId: row.job_id,
2560
+ status: typeof row.status === 'string' ? row.status : 'unknown',
2561
+ reasonCode: typeof row.reason_code === 'string' ? row.reason_code : 'continuity-ok',
2562
+ createdAt: typeof row.created_at === 'string' ? row.created_at : input.cutoff,
2563
+ approxBytes: typeof row.approx_bytes === 'number' && row.approx_bytes >= 0 ? Math.floor(row.approx_bytes) : 0,
2564
+ }];
2565
+ });
2566
+ }
2567
+ selectOutputRetentionCandidates(input) {
2568
+ const rows = this.db.prepare(`
415
2569
  WITH sized AS (
416
2570
  SELECT event_id, job_id, type, reason_code, created_at,
417
2571
  length(event_id) + length(job_id) + length(operation_id) + length(type)
@@ -430,13 +2584,42 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
430
2584
  WHERE created_at < ? OR retain_rank > ? OR cumulative_bytes > ?
431
2585
  ORDER BY created_at ASC, event_id ASC
432
2586
  LIMIT ?
433
- `).all(O,e.cutoff,e.maxItems,e.maxBytes,e.limit).flatMap(r=>typeof r.event_id!="string"||typeof r.job_id!="string"?[]:[{kind:"output",id:r.event_id,jobId:r.job_id,status:typeof r.type=="string"?r.type:O,reasonCode:typeof r.reason_code=="string"?r.reason_code:"continuity-ok",createdAt:typeof r.created_at=="string"?r.created_at:e.cutoff,approxBytes:typeof r.approx_bytes=="number"&&r.approx_bytes>=0?Math.floor(r.approx_bytes):0}])}deleteRetentionCandidates(e,t,r){if(r.length===0)return;const s=r.map(()=>"?").join(", ");this.db.prepare(`DELETE FROM ${e} WHERE ${t} IN (${s})`).run(...r)}preserveReceiptDerivedIteration(e,t){const r=this.db.prepare(`
2587
+ `).all(CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE, input.cutoff, input.maxItems, input.maxBytes, input.limit);
2588
+ return rows.flatMap((row) => {
2589
+ if (typeof row.event_id !== 'string' || typeof row.job_id !== 'string')
2590
+ return [];
2591
+ return [{
2592
+ kind: 'output',
2593
+ id: row.event_id,
2594
+ jobId: row.job_id,
2595
+ status: typeof row.type === 'string' ? row.type : CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE,
2596
+ reasonCode: typeof row.reason_code === 'string' ? row.reason_code : 'continuity-ok',
2597
+ createdAt: typeof row.created_at === 'string' ? row.created_at : input.cutoff,
2598
+ approxBytes: typeof row.approx_bytes === 'number' && row.approx_bytes >= 0 ? Math.floor(row.approx_bytes) : 0,
2599
+ }];
2600
+ });
2601
+ }
2602
+ deleteRetentionCandidates(table, idColumn, ids) {
2603
+ if (ids.length === 0)
2604
+ return;
2605
+ const placeholders = ids.map(() => '?').join(', ');
2606
+ this.db.prepare(`DELETE FROM ${table} WHERE ${idColumn} IN (${placeholders})`).run(...ids);
2607
+ }
2608
+ preserveReceiptDerivedIteration(jobId, at) {
2609
+ const row = this.db.prepare(`
434
2610
  SELECT COUNT(*) AS iteration
435
2611
  FROM job_receipts
436
2612
  WHERE job_id = ?
437
2613
  AND (operation_id LIKE 'tick-%' OR operation_id LIKE 'loop-tick-%')
438
2614
  AND status IN ('accepted', 'applied')
439
- `).get(e),s=typeof r?.iteration=="number"?r.iteration:0;s>0&&this.recordJobStageProgress({jobId:e,iteration:s,at:t,appendHistory:!1})}retainedReceiptOutputCounts(e){const t=this.db.prepare(`
2615
+ `).get(jobId);
2616
+ const iteration = typeof row?.iteration === 'number' ? row.iteration : 0;
2617
+ if (iteration > 0) {
2618
+ this.recordJobStageProgress({ jobId, iteration, at, appendHistory: false });
2619
+ }
2620
+ }
2621
+ retainedReceiptOutputCounts(jobId) {
2622
+ const receipt = this.db.prepare(`
440
2623
  SELECT COUNT(*) AS count,
441
2624
  COALESCE(SUM(length(receipt_id) + length(job_id) + length(status) + length(created_at)
442
2625
  + COALESCE(length(operation_id), 0) + COALESCE(length(idempotency_key), 0)
@@ -444,14 +2627,23 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
444
2627
  + COALESCE(length(ignored_operation_ids_json), 0) + COALESCE(length(metadata_json), 0)), 0) AS bytes
445
2628
  FROM job_receipts
446
2629
  WHERE job_id = ?
447
- `).get(e),r=this.db.prepare(`
2630
+ `).get(jobId);
2631
+ const output = this.db.prepare(`
448
2632
  SELECT COUNT(*) AS count,
449
2633
  COALESCE(SUM(length(event_id) + length(job_id) + length(operation_id) + length(type)
450
2634
  + length(summary) + length(created_at) + COALESCE(length(idempotency_key), 0)
451
2635
  + COALESCE(length(reason_code), 0) + COALESCE(length(metadata_json), 0)), 0) AS bytes
452
2636
  FROM job_events
453
2637
  WHERE job_id = ? AND type = ?
454
- `).get(e,O);return{receipts:typeof t?.count=="number"?t.count:0,outputs:typeof r?.count=="number"?r.count:0,bytes:(typeof t?.bytes=="number"?t.bytes:0)+(typeof r?.bytes=="number"?r.bytes:0)}}pruneReceiptCompactionSummaries(e,t){this.db.prepare(`
2638
+ `).get(jobId, CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE);
2639
+ return {
2640
+ receipts: typeof receipt?.count === 'number' ? receipt.count : 0,
2641
+ outputs: typeof output?.count === 'number' ? output.count : 0,
2642
+ bytes: (typeof receipt?.bytes === 'number' ? receipt.bytes : 0) + (typeof output?.bytes === 'number' ? output.bytes : 0),
2643
+ };
2644
+ }
2645
+ pruneReceiptCompactionSummaries(jobId, limit) {
2646
+ this.db.prepare(`
455
2647
  DELETE FROM job_events
456
2648
  WHERE event_id IN (
457
2649
  SELECT event_id
@@ -460,20 +2652,69 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
460
2652
  ORDER BY created_at DESC, event_id DESC
461
2653
  LIMIT -1 OFFSET ?
462
2654
  )
463
- `).run(e,W,t)}runInTransaction(e){this.assertCanMutate();const t=this.db.transaction(e);try{return typeof t.immediate=="function"?t.immediate():t()}catch(r){throw m(r,"sqlite-mutation-failed")}}collectSnapshotRows(e){const t=(p,...x)=>{const A=this.db.prepare(p).get(...x);return E(A)&&typeof A.count=="number"?A.count:0},r=this.db.prepare(`
2655
+ `).run(jobId, CONTINUITY_RECEIPT_COMPACTION_EVENT_TYPE, limit);
2656
+ }
2657
+ runInTransaction(fn) {
2658
+ this.assertCanMutate();
2659
+ const tx = this.db.transaction(fn);
2660
+ try {
2661
+ if (typeof tx.immediate === 'function')
2662
+ return tx.immediate();
2663
+ return tx();
2664
+ }
2665
+ catch (error) {
2666
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
2667
+ }
2668
+ }
2669
+ collectSnapshotRows(options) {
2670
+ const countRows = (sql, ...params) => {
2671
+ const row = this.db.prepare(sql).get(...params);
2672
+ return isRecord(row) && typeof row.count === 'number' ? row.count : 0;
2673
+ };
2674
+ const outboxRows = this.db.prepare(`
464
2675
  SELECT status, COUNT(*) AS count FROM outbox GROUP BY status
465
- `).all(),s={};for(const p of r)typeof p.status=="string"&&typeof p.count=="number"&&(s[p.status]=p.count);const n=this.db.prepare(`
2676
+ `).all();
2677
+ const outboxCounts = {};
2678
+ for (const row of outboxRows) {
2679
+ if (typeof row.status === 'string' && typeof row.count === 'number')
2680
+ outboxCounts[row.status] = row.count;
2681
+ }
2682
+ const queuePolicyRow = this.db.prepare(`
466
2683
  SELECT value_json
467
2684
  FROM runner_config
468
2685
  WHERE key = 'headless_queue_policy'
469
2686
  LIMIT 1
470
- `).get();let d=De;if(E(n)&&typeof n.value_json=="string")try{const p=JSON.parse(n.value_json);E(p)&&typeof p.maxItems=="number"&&(d=B({maxItems:p.maxItems}))}catch{}const o=t("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status = 'queued' AND dead_lettered_at IS NULL"),l=t("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status = 'backoff' AND dead_lettered_at IS NULL"),I=t("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status = 'running' AND dead_lettered_at IS NULL"),y=t("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE dead_lettered_at IS NOT NULL"),f=o+l+I,_=this.db.prepare(`
2687
+ `).get();
2688
+ let queuePolicy = DEFAULT_CONTINUITY_QUEUE_POLICY;
2689
+ if (isRecord(queuePolicyRow) && typeof queuePolicyRow.value_json === 'string') {
2690
+ try {
2691
+ const parsed = JSON.parse(queuePolicyRow.value_json);
2692
+ if (isRecord(parsed) && typeof parsed.maxItems === 'number') {
2693
+ queuePolicy = normalizeContinuityQueuePolicy({ maxItems: parsed.maxItems });
2694
+ }
2695
+ }
2696
+ catch {
2697
+ // SQLite json_valid guards this row; retain bounded defaults on unexpected parse failure.
2698
+ }
2699
+ }
2700
+ const queueQueued = countRows("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status = 'queued' AND dead_lettered_at IS NULL");
2701
+ const queueRetrying = countRows("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status = 'backoff' AND dead_lettered_at IS NULL");
2702
+ const queueRunning = countRows("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status = 'running' AND dead_lettered_at IS NULL");
2703
+ const queueDeadLetter = countRows('SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE dead_lettered_at IS NOT NULL');
2704
+ const queueDepth = queueQueued + queueRetrying + queueRunning;
2705
+ const nextRetryRow = this.db.prepare(`
471
2706
  SELECT MIN(next_retry_at) AS next_retry_at
472
2707
  FROM headless_execution_attempts
473
2708
  WHERE status = 'backoff'
474
2709
  AND dead_lettered_at IS NULL
475
2710
  AND next_retry_at IS NOT NULL
476
- `).get(),u=E(_)&&typeof _.next_retry_at=="string"?_.next_retry_at:void 0;return{schemaVersion:this.getSchemaVersion(),jobs:this.db.prepare(`
2711
+ `).get();
2712
+ const nextRetryAt = isRecord(nextRetryRow) && typeof nextRetryRow.next_retry_at === 'string'
2713
+ ? nextRetryRow.next_retry_at
2714
+ : undefined;
2715
+ return {
2716
+ schemaVersion: this.getSchemaVersion(),
2717
+ jobs: this.db.prepare(`
477
2718
  SELECT job_id, kind, status, summary, reason_code, created_at, updated_at,
478
2719
  next_run_at, metadata_json,
479
2720
  (SELECT COUNT(*)
@@ -523,30 +2764,35 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
523
2764
  FROM jobs
524
2765
  ORDER BY updated_at DESC, job_id ASC
525
2766
  LIMIT ?
526
- `).all(e.jobLimit),staleLeases:this.db.prepare(`
2767
+ `).all(options.jobLimit),
2768
+ staleLeases: this.db.prepare(`
527
2769
  SELECT lease_id, job_id, runner_id, expires_at
528
2770
  FROM job_leases
529
2771
  WHERE released_at IS NULL AND expires_at <= ?
530
2772
  ORDER BY expires_at ASC, lease_id ASC
531
2773
  LIMIT ?
532
- `).all(e.nowIso,e.staleLeaseLimit),receipts:this.db.prepare(`
2774
+ `).all(options.nowIso, options.staleLeaseLimit),
2775
+ receipts: this.db.prepare(`
533
2776
  SELECT receipt_id, job_id, operation_id, status, reason_code, created_at, metadata_json
534
2777
  FROM job_receipts
535
2778
  ORDER BY created_at DESC, receipt_id ASC
536
2779
  LIMIT ?
537
- `).all(e.receiptLimit),events:this.db.prepare(`
2780
+ `).all(options.receiptLimit),
2781
+ events: this.db.prepare(`
538
2782
  SELECT event_id, job_id, operation_id, type, summary, reason_code, created_at, metadata_json
539
2783
  FROM job_events
540
2784
  ORDER BY created_at DESC, event_id ASC
541
2785
  LIMIT ?
542
- `).all(e.eventLimit),stateSnapshots:this.db.prepare(`
2786
+ `).all(options.eventLimit),
2787
+ stateSnapshots: this.db.prepare(`
543
2788
  SELECT snapshot_id, job_id, kind, relative_path, sha256, summary,
544
2789
  captured_at, operation_id, metadata_json
545
2790
  FROM state_snapshots
546
2791
  WHERE kind = 'context_compaction'
547
2792
  ORDER BY captured_at DESC, snapshot_id ASC
548
2793
  LIMIT ?
549
- `).all(e.snapshotLimit),headlessExecutionAttempts:this.db.prepare(`
2794
+ `).all(options.snapshotLimit),
2795
+ headlessExecutionAttempts: this.db.prepare(`
550
2796
  SELECT attempt_id, execution_id, job_id, status, reason_code, runtime_id,
551
2797
  model_id, retry_count, max_attempts, lease_id, runner_id,
552
2798
  lease_expires_at, heartbeat_at, public_summary, enqueued_at,
@@ -557,19 +2803,90 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
557
2803
  FROM headless_execution_attempts
558
2804
  ORDER BY enqueued_at DESC, attempt_id ASC
559
2805
  LIMIT ?
560
- `).all(e.jobLimit),counts:{jobs:t("SELECT COUNT(*) AS count FROM jobs"),activeJobs:t("SELECT COUNT(*) AS count FROM jobs WHERE status IN ('planned', 'active', 'running', 'waiting_external', 'waiting_human', 'backoff', 'paused', 'blocked')"),activeLeases:t("SELECT COUNT(*) AS count FROM job_leases WHERE released_at IS NULL"),dueJobs:t("SELECT COUNT(*) AS count FROM jobs WHERE next_run_at IS NOT NULL AND next_run_at <= ? AND status NOT IN ('completed', 'aborted', 'cancelled')",e.nowIso),staleLeases:t("SELECT COUNT(*) AS count FROM job_leases WHERE released_at IS NULL AND expires_at <= ?",e.nowIso),receipts:t("SELECT COUNT(*) AS count FROM job_receipts"),events:t("SELECT COUNT(*) AS count FROM job_events"),processingOutputSummaries:t("SELECT COUNT(*) AS count FROM job_events WHERE type = ?",O),goalLedgerItems:t("SELECT COUNT(*) AS count FROM goal_ledger_items"),goalVerifiers:t("SELECT COUNT(*) AS count FROM goal_verifiers"),goalReviewGates:t("SELECT COUNT(*) AS count FROM goal_review_gates"),stateSnapshots:t("SELECT COUNT(*) AS count FROM state_snapshots"),headlessExecutionAttempts:t("SELECT COUNT(*) AS count FROM headless_execution_attempts"),headlessQueuedAttempts:t("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status IN ('queued', 'backoff')"),headlessRunningAttempts:t("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status = 'running'"),headlessRetryingAttempts:l,headlessDeadLetterAttempts:y,staleHeadlessExecutionAttempts:t("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status = 'running' AND lease_released_at IS NULL AND lease_expires_at <= ?",e.nowIso),outbox:t("SELECT COUNT(*) AS count FROM outbox")},outboxCounts:s,queue:{depth:f,queued:o,running:I,retrying:l,deadLetter:y,capacity:d.maxItems,available:Math.max(0,d.maxItems-f),backpressure:f>=d.maxItems,generatedAt:e.nowIso,...u?{nextRetryAt:u}:{},...f>=d.maxItems?{reasonCode:k.CAP_REACHED}:{},publicSafe:!0}}}listProcessingOutputSummaries(e={}){const t=ye(e.limit),r=pe(e),s=`
2806
+ `).all(options.jobLimit),
2807
+ counts: {
2808
+ jobs: countRows('SELECT COUNT(*) AS count FROM jobs'),
2809
+ activeJobs: countRows("SELECT COUNT(*) AS count FROM jobs WHERE status IN ('planned', 'active', 'running', 'waiting_external', 'waiting_human', 'backoff', 'paused', 'blocked')"),
2810
+ activeLeases: countRows('SELECT COUNT(*) AS count FROM job_leases WHERE released_at IS NULL'),
2811
+ dueJobs: countRows("SELECT COUNT(*) AS count FROM jobs WHERE next_run_at IS NOT NULL AND next_run_at <= ? AND status NOT IN ('completed', 'aborted', 'cancelled')", options.nowIso),
2812
+ staleLeases: countRows('SELECT COUNT(*) AS count FROM job_leases WHERE released_at IS NULL AND expires_at <= ?', options.nowIso),
2813
+ receipts: countRows('SELECT COUNT(*) AS count FROM job_receipts'),
2814
+ events: countRows('SELECT COUNT(*) AS count FROM job_events'),
2815
+ processingOutputSummaries: countRows('SELECT COUNT(*) AS count FROM job_events WHERE type = ?', CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE),
2816
+ goalLedgerItems: countRows('SELECT COUNT(*) AS count FROM goal_ledger_items'),
2817
+ goalVerifiers: countRows('SELECT COUNT(*) AS count FROM goal_verifiers'),
2818
+ goalReviewGates: countRows('SELECT COUNT(*) AS count FROM goal_review_gates'),
2819
+ stateSnapshots: countRows('SELECT COUNT(*) AS count FROM state_snapshots'),
2820
+ headlessExecutionAttempts: countRows('SELECT COUNT(*) AS count FROM headless_execution_attempts'),
2821
+ headlessQueuedAttempts: countRows("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status IN ('queued', 'backoff')"),
2822
+ headlessRunningAttempts: countRows("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status = 'running'"),
2823
+ headlessRetryingAttempts: queueRetrying,
2824
+ headlessDeadLetterAttempts: queueDeadLetter,
2825
+ staleHeadlessExecutionAttempts: countRows("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status = 'running' AND lease_released_at IS NULL AND lease_expires_at <= ?", options.nowIso),
2826
+ outbox: countRows('SELECT COUNT(*) AS count FROM outbox'),
2827
+ },
2828
+ outboxCounts,
2829
+ queue: {
2830
+ depth: queueDepth,
2831
+ queued: queueQueued,
2832
+ running: queueRunning,
2833
+ retrying: queueRetrying,
2834
+ deadLetter: queueDeadLetter,
2835
+ capacity: queuePolicy.maxItems,
2836
+ available: Math.max(0, queuePolicy.maxItems - queueDepth),
2837
+ backpressure: queueDepth >= queuePolicy.maxItems,
2838
+ generatedAt: options.nowIso,
2839
+ ...(nextRetryAt ? { nextRetryAt } : {}),
2840
+ ...(queueDepth >= queuePolicy.maxItems ? { reasonCode: CONTINUITY_QUEUE_REASON_CODES.CAP_REACHED } : {}),
2841
+ publicSafe: true,
2842
+ },
2843
+ };
2844
+ }
2845
+ listProcessingOutputSummaries(options = {}) {
2846
+ const limit = normalizeProcessingSummaryLimit(options.limit);
2847
+ const targetPredicate = processingSummaryTargetPredicate(options);
2848
+ const selectSql = `
561
2849
  SELECT event_id, job_id, operation_id, summary, reason_code, created_at, metadata_json
562
2850
  FROM job_events
563
- WHERE type = ?${r.sql}
2851
+ WHERE type = ?${targetPredicate.sql}
564
2852
  ORDER BY created_at DESC, event_id DESC
565
2853
  LIMIT ?
566
- `;return this.db.prepare(`SELECT * FROM (${s}) ORDER BY created_at ASC, event_id ASC`).all(O,...r.params,t).map(be)}pollProcessingOutputSummaries(e={}){const t=ye(e.limit),r=lt(e.cursor),s=pe(e),n=r?" AND (created_at > ? OR (created_at = ? AND event_id > ?))":"",d=`
2854
+ `;
2855
+ const rows = this.db.prepare(`SELECT * FROM (${selectSql}) ORDER BY created_at ASC, event_id ASC`).all(CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE, ...targetPredicate.params, limit);
2856
+ return rows.map(toProcessingOutputSummaryRow);
2857
+ }
2858
+ pollProcessingOutputSummaries(options = {}) {
2859
+ const limit = normalizeProcessingSummaryLimit(options.limit);
2860
+ const decodedCursor = decodeProcessingSummaryCursor(options.cursor);
2861
+ const targetPredicate = processingSummaryTargetPredicate(options);
2862
+ const cursorPredicate = decodedCursor
2863
+ ? ' AND (created_at > ? OR (created_at = ? AND event_id > ?))'
2864
+ : '';
2865
+ const sql = `
567
2866
  SELECT event_id, job_id, operation_id, summary, reason_code, created_at, metadata_json
568
2867
  FROM job_events
569
- WHERE type = ?${s.sql}${n}
2868
+ WHERE type = ?${targetPredicate.sql}${cursorPredicate}
570
2869
  ORDER BY created_at ASC, event_id ASC
571
2870
  LIMIT ?
572
- `,o=[O];o.push(...s.params),r&&o.push(r.createdAt,r.createdAt,r.eventId),o.push(t+1);const l=this.db.prepare(d).all(...o),I=l.slice(0,t).map(be),y=I[I.length-1];return{summaries:I,...y?{nextCursor:y.cursor}:e.cursor?{nextCursor:e.cursor}:{},hasMore:l.length>t,publicSafe:!0}}listDueJobs(e){const t=Math.max(1,Math.min(50,Math.floor(e.limit)));return this.db.prepare(`
2871
+ `;
2872
+ const params = [CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE];
2873
+ params.push(...targetPredicate.params);
2874
+ if (decodedCursor)
2875
+ params.push(decodedCursor.createdAt, decodedCursor.createdAt, decodedCursor.eventId);
2876
+ params.push(limit + 1);
2877
+ const rows = this.db.prepare(sql).all(...params);
2878
+ const summaries = rows.slice(0, limit).map(toProcessingOutputSummaryRow);
2879
+ const last = summaries[summaries.length - 1];
2880
+ return {
2881
+ summaries,
2882
+ ...(last ? { nextCursor: last.cursor } : options.cursor ? { nextCursor: options.cursor } : {}),
2883
+ hasMore: rows.length > limit,
2884
+ publicSafe: true,
2885
+ };
2886
+ }
2887
+ listDueJobs(options) {
2888
+ const limit = Math.max(1, Math.min(50, Math.floor(options.limit)));
2889
+ return this.db.prepare(`
573
2890
  SELECT j.job_id, j.kind, j.status, j.summary, j.reason_code, j.created_at,
574
2891
  j.updated_at, j.next_run_at, j.metadata_json,
575
2892
  (SELECT COUNT(*)
@@ -587,19 +2904,63 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
587
2904
  AND j.status NOT IN ('waiting_external', 'waiting_human', 'paused', 'blocked', 'completed', 'aborted', 'cancelled')
588
2905
  ORDER BY j.next_run_at ASC, j.job_id ASC
589
2906
  LIMIT ?
590
- `).all(e.nowIso,t)}recoverStaleLeases(e){this.assertCanMutate();const t=Date.parse(e.nowIso);if(!Number.isFinite(t))throw new i("sqlite-public-safety-rejected","Continuity SQLite stale lease recovery received an invalid timestamp.");const r=Math.max(0,Math.floor(e.staleAfterMs)),s=Math.max(1,Math.min(100,Math.floor(e.limit))),n=new Date(t-r).toISOString();try{return this.runInTransaction(()=>{const o=this.db.prepare(`
2907
+ `).all(options.nowIso, limit);
2908
+ }
2909
+ recoverStaleLeases(options) {
2910
+ this.assertCanMutate();
2911
+ const nowMs = Date.parse(options.nowIso);
2912
+ if (!Number.isFinite(nowMs)) {
2913
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Continuity SQLite stale lease recovery received an invalid timestamp.');
2914
+ }
2915
+ const staleAfterMs = Math.max(0, Math.floor(options.staleAfterMs));
2916
+ const limit = Math.max(1, Math.min(100, Math.floor(options.limit)));
2917
+ const safeExpiredAtOrBefore = new Date(nowMs - staleAfterMs).toISOString();
2918
+ try {
2919
+ return this.runInTransaction(() => {
2920
+ const rows = this.db.prepare(`
591
2921
  SELECT lease_id
592
2922
  FROM job_leases
593
2923
  WHERE released_at IS NULL
594
2924
  AND expires_at <= ?
595
2925
  ORDER BY expires_at ASC, lease_id ASC
596
2926
  LIMIT ?
597
- `).all(n,s).map(y=>y.lease_id).filter(y=>typeof y=="string"&&y.length>0);if(o.length===0)return{recoveredLeaseIds:[],safeExpiredAtOrBefore:n,changes:0};const l=o.map(()=>"?").join(", "),I=this.db.prepare(`
2927
+ `).all(safeExpiredAtOrBefore, limit);
2928
+ const recoveredLeaseIds = rows
2929
+ .map((row) => row.lease_id)
2930
+ .filter((leaseId) => typeof leaseId === 'string' && leaseId.length > 0);
2931
+ if (recoveredLeaseIds.length === 0) {
2932
+ return { recoveredLeaseIds: [], safeExpiredAtOrBefore, changes: 0 };
2933
+ }
2934
+ const placeholders = recoveredLeaseIds.map(() => '?').join(', ');
2935
+ const result = this.db.prepare(`
598
2936
  UPDATE job_leases
599
2937
  SET status = 'expired', released_at = ?
600
2938
  WHERE released_at IS NULL
601
- AND lease_id IN (${l})
602
- `).run(e.nowIso,...o);return{recoveredLeaseIds:o,safeExpiredAtOrBefore:n,changes:I.changes}})}catch(d){throw m(d,"sqlite-mutation-failed")}}recoverStaleHeadlessExecutionAttempts(e){this.assertCanMutate();const t=Date.parse(e.nowIso);if(!Number.isFinite(t))throw new i("sqlite-public-safety-rejected","Continuity SQLite headless stale recovery received an invalid timestamp.");const r=Math.max(0,Math.floor(e.staleAfterMs)),s=Math.max(1,Math.min(100,Math.floor(e.limit))),n=new Date(t-r).toISOString();try{return this.runInTransaction(()=>{const d=this.db.prepare(`
2939
+ AND lease_id IN (${placeholders})
2940
+ `).run(options.nowIso, ...recoveredLeaseIds);
2941
+ return {
2942
+ recoveredLeaseIds,
2943
+ safeExpiredAtOrBefore,
2944
+ changes: result.changes,
2945
+ };
2946
+ });
2947
+ }
2948
+ catch (error) {
2949
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
2950
+ }
2951
+ }
2952
+ recoverStaleHeadlessExecutionAttempts(options) {
2953
+ this.assertCanMutate();
2954
+ const nowMs = Date.parse(options.nowIso);
2955
+ if (!Number.isFinite(nowMs)) {
2956
+ throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Continuity SQLite headless stale recovery received an invalid timestamp.');
2957
+ }
2958
+ const staleAfterMs = Math.max(0, Math.floor(options.staleAfterMs));
2959
+ const limit = Math.max(1, Math.min(100, Math.floor(options.limit)));
2960
+ const safeExpiredAtOrBefore = new Date(nowMs - staleAfterMs).toISOString();
2961
+ try {
2962
+ return this.runInTransaction(() => {
2963
+ const rows = this.db.prepare(`
603
2964
  SELECT attempt_id, retry_count, max_attempts, attempt_count
604
2965
  FROM headless_execution_attempts
605
2966
  WHERE status = 'running'
@@ -607,7 +2968,30 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
607
2968
  AND lease_expires_at <= ?
608
2969
  ORDER BY lease_expires_at ASC, attempt_id ASC
609
2970
  LIMIT ?
610
- `).all(n,s),o=[],l=[],I=B(e.policy);for(const _ of d){if(typeof _.attempt_id!="string"||_.attempt_id.length===0)continue;const u=typeof _.retry_count=="number"?_.retry_count:0,p=typeof _.max_attempts=="number"?_.max_attempts:1,x=typeof _.attempt_count=="number"?_.attempt_count:0;Math.max(x,u+1)>=p?l.push(_.attempt_id):o.push({attemptId:_.attempt_id,nextRetryAt:Ue(e.nowIso,u+1,I)})}let y=0;for(const _ of o)y+=this.db.prepare(`
2971
+ `).all(safeExpiredAtOrBefore, limit);
2972
+ const retryableAttempts = [];
2973
+ const deadLetteredAttemptIds = [];
2974
+ const policy = normalizeContinuityQueuePolicy(options.policy);
2975
+ for (const row of rows) {
2976
+ if (typeof row.attempt_id !== 'string' || row.attempt_id.length === 0)
2977
+ continue;
2978
+ const retryCount = typeof row.retry_count === 'number' ? row.retry_count : 0;
2979
+ const maxAttempts = typeof row.max_attempts === 'number' ? row.max_attempts : 1;
2980
+ const attemptCount = typeof row.attempt_count === 'number' ? row.attempt_count : 0;
2981
+ const consumedAttempts = Math.max(attemptCount, retryCount + 1);
2982
+ if (consumedAttempts >= maxAttempts) {
2983
+ deadLetteredAttemptIds.push(row.attempt_id);
2984
+ }
2985
+ else {
2986
+ retryableAttempts.push({
2987
+ attemptId: row.attempt_id,
2988
+ nextRetryAt: computeContinuityQueueRetryAt(options.nowIso, retryCount + 1, policy),
2989
+ });
2990
+ }
2991
+ }
2992
+ let changes = 0;
2993
+ for (const retryable of retryableAttempts) {
2994
+ changes += this.db.prepare(`
611
2995
  UPDATE headless_execution_attempts
612
2996
  SET status = 'backoff',
613
2997
  retry_count = retry_count + 1,
@@ -618,7 +3002,11 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
618
3002
  WHERE attempt_id = ?
619
3003
  AND status = 'running'
620
3004
  AND lease_released_at IS NULL
621
- `).run(k.LEASE_TIMEOUT_RETRY,e.nowIso,_.nextRetryAt,_.nextRetryAt,_.attemptId).changes;if(l.length>0){const _=l.map(()=>"?").join(", ");y+=this.db.prepare(`
3005
+ `).run(CONTINUITY_QUEUE_REASON_CODES.LEASE_TIMEOUT_RETRY, options.nowIso, retryable.nextRetryAt, retryable.nextRetryAt, retryable.attemptId).changes;
3006
+ }
3007
+ if (deadLetteredAttemptIds.length > 0) {
3008
+ const placeholders = deadLetteredAttemptIds.map(() => '?').join(', ');
3009
+ changes += this.db.prepare(`
622
3010
  UPDATE headless_execution_attempts
623
3011
  SET status = 'failed',
624
3012
  retry_count = retry_count + 1,
@@ -628,18 +3016,85 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
628
3016
  dead_lettered_at = ?,
629
3017
  dead_letter_reason_code = ?,
630
3018
  next_retry_at = NULL
631
- WHERE attempt_id IN (${_})
3019
+ WHERE attempt_id IN (${placeholders})
632
3020
  AND status = 'running'
633
3021
  AND lease_released_at IS NULL
634
- `).run(k.ATTEMPTS_EXHAUSTED,e.nowIso,e.nowIso,e.nowIso,k.ATTEMPTS_EXHAUSTED,...l).changes}const f=o.map(_=>_.attemptId);return{recoveredAttemptIds:[...f,...l],failedAttemptIds:l,retryingAttemptIds:f,deadLetteredAttemptIds:l,safeExpiredAtOrBefore:n,changes:y,publicSafe:!0}})}catch(d){throw m(d,"sqlite-mutation-failed")}}ensureGoalCampaign(e,t,r,s){this.db.prepare(`
3022
+ `).run(CONTINUITY_QUEUE_REASON_CODES.ATTEMPTS_EXHAUSTED, options.nowIso, options.nowIso, options.nowIso, CONTINUITY_QUEUE_REASON_CODES.ATTEMPTS_EXHAUSTED, ...deadLetteredAttemptIds).changes;
3023
+ }
3024
+ const retryingAttemptIds = retryableAttempts.map((attempt) => attempt.attemptId);
3025
+ return {
3026
+ recoveredAttemptIds: [...retryingAttemptIds, ...deadLetteredAttemptIds],
3027
+ failedAttemptIds: deadLetteredAttemptIds,
3028
+ retryingAttemptIds,
3029
+ deadLetteredAttemptIds,
3030
+ safeExpiredAtOrBefore,
3031
+ changes,
3032
+ publicSafe: true,
3033
+ };
3034
+ });
3035
+ }
3036
+ catch (error) {
3037
+ throw mapSqliteError(error, 'sqlite-mutation-failed');
3038
+ }
3039
+ }
3040
+ ensureGoalCampaign(campaignId, jobId, createdAt, updatedAt) {
3041
+ this.db.prepare(`
635
3042
  INSERT OR IGNORE INTO goal_campaigns (
636
3043
  campaign_id, job_id, status, public_summary, created_at, updated_at
637
3044
  ) VALUES (?, ?, 'active', 'Public goal campaign summary unavailable', ?, ?)
638
- `).run(e,t,r,s)}configureConnection(){const e=String(w(this.db.pragma("journal_mode = WAL",{simple:!0}))??"").toLowerCase();if(e!=="wal")throw new i("sqlite-migration-failed",`Continuity SQLite journal_mode did not switch to WAL (observed ${e||"unknown"}).`);if(this.db.pragma("foreign_keys = ON"),this.db.pragma(`busy_timeout = ${this.busyTimeoutMs}`),Number(w(this.db.pragma("foreign_keys",{simple:!0}))??0)!==1)throw new i("sqlite-migration-failed","Continuity SQLite foreign_keys pragma is not enabled.")}ensureMigrationTable(){this.db.exec(`
3045
+ `).run(campaignId, jobId, createdAt, updatedAt);
3046
+ }
3047
+ configureConnection() {
3048
+ const journalMode = String(asSinglePragmaValue(this.db.pragma('journal_mode = WAL', { simple: true })) ?? '').toLowerCase();
3049
+ if (journalMode !== 'wal') {
3050
+ throw new ContinuitySqliteStoreError('sqlite-migration-failed', `Continuity SQLite journal_mode did not switch to WAL (observed ${journalMode || 'unknown'}).`);
3051
+ }
3052
+ this.db.pragma('foreign_keys = ON');
3053
+ this.db.pragma(`busy_timeout = ${this.busyTimeoutMs}`);
3054
+ const foreignKeys = Number(asSinglePragmaValue(this.db.pragma('foreign_keys', { simple: true })) ?? 0);
3055
+ if (foreignKeys !== 1) {
3056
+ throw new ContinuitySqliteStoreError('sqlite-migration-failed', 'Continuity SQLite foreign_keys pragma is not enabled.');
3057
+ }
3058
+ }
3059
+ ensureMigrationTable() {
3060
+ this.db.exec(`
639
3061
  CREATE TABLE IF NOT EXISTS continuity_migrations (
640
3062
  version INTEGER PRIMARY KEY,
641
3063
  name TEXT NOT NULL,
642
3064
  checksum TEXT NOT NULL,
643
3065
  applied_at TEXT NOT NULL
644
3066
  );
645
- `)}applyMigrations(){this.db.transaction(()=>{for(const t of Je){const r=st(t.sql),s=this.db.prepare("SELECT version, name, checksum FROM continuity_migrations WHERE version = ?").get(t.version);if(s){if(s.checksum!==r||s.name!==t.name)throw new i("sqlite-migration-conflict",`Continuity SQLite migration ${t.version} checksum conflict.`);continue}this.db.exec(t.sql),this.db.prepare("INSERT INTO continuity_migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)").run(t.version,t.name,r,this.now()),this.db.pragma(`user_version = ${t.version}`)}})()}assertCanMutate(){if(this.mutationBlocked)throw new i("sqlite-mutation-blocked","Continuity SQLite mutation is blocked after integrity failure.",{mutationBlocked:!0})}toWriteResult(e){const t=e.changes>0;return{inserted:t,replay:!t,changes:e.changes}}}export{O as CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE,Be as CONTINUITY_PROCESSING_OUTPUT_SUMMARY_MAX_CHARS,Ye as CONTINUITY_PUBLIC_STAGES,We as CONTINUITY_SQLITE_FILENAME,ne as CONTINUITY_SQLITE_RELATIVE_DIR,kt as CONTINUITY_SQLITE_SCHEMA_VERSION,ce as CONTINUITY_STAGE_HISTORY_MAX,ft as ContinuitySqliteStore,i as ContinuitySqliteStoreError,de as DEFAULT_CONTINUITY_PROCESSING_SUMMARY_LIMIT,Fe as DEFAULT_CONTINUITY_SQLITE_BUSY_TIMEOUT_MS,$e as MAX_CONTINUITY_PROCESSING_SUMMARY_LIMIT,ie as SQLITE_ADAPTER_INSTALL_GUIDANCE,Ee as continuitySqliteStorePath,le as isContinuityPublicStage,It as loadBetterSqlite3Adapter,Qe as normalizeContinuityStageIteration,Ut as openContinuitySqliteReadOnly,Dt as openContinuitySqliteStore,Ge as sanitizeContinuityStageHistory,ue as sanitizeContinuityStageTimestamp};
3067
+ `);
3068
+ }
3069
+ applyMigrations() {
3070
+ const apply = this.db.transaction(() => {
3071
+ for (const migration of CONTINUITY_MIGRATIONS) {
3072
+ const checksum = checksumMigration(migration.sql);
3073
+ const existing = this.db.prepare('SELECT version, name, checksum FROM continuity_migrations WHERE version = ?').get(migration.version);
3074
+ if (existing) {
3075
+ if (existing.checksum !== checksum || existing.name !== migration.name) {
3076
+ throw new ContinuitySqliteStoreError('sqlite-migration-conflict', `Continuity SQLite migration ${migration.version} checksum conflict.`);
3077
+ }
3078
+ continue;
3079
+ }
3080
+ this.db.exec(migration.sql);
3081
+ this.db.prepare('INSERT INTO continuity_migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)').run(migration.version, migration.name, checksum, this.now());
3082
+ this.db.pragma(`user_version = ${migration.version}`);
3083
+ }
3084
+ });
3085
+ apply();
3086
+ }
3087
+ assertCanMutate() {
3088
+ if (this.mutationBlocked) {
3089
+ throw new ContinuitySqliteStoreError('sqlite-mutation-blocked', 'Continuity SQLite mutation is blocked after integrity failure.', { mutationBlocked: true });
3090
+ }
3091
+ }
3092
+ toWriteResult(result) {
3093
+ const inserted = result.changes > 0;
3094
+ return {
3095
+ inserted,
3096
+ replay: !inserted,
3097
+ changes: result.changes,
3098
+ };
3099
+ }
3100
+ }