@ornexus/neocortex 4.60.72 → 4.60.73
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.
- package/dist/release-source.json +1 -1
- package/dist/sbom.cdx.json +24 -24
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/package.json +2 -2
- package/packages/client/dist/adapters/adapter-registry.js +108 -1
- package/packages/client/dist/adapters/antigravity-adapter.js +77 -2
- package/packages/client/dist/adapters/claude-code-adapter.js +79 -3
- package/packages/client/dist/adapters/codex-adapter.js +80 -2
- package/packages/client/dist/adapters/cursor-adapter.js +115 -4
- package/packages/client/dist/adapters/gemini-adapter.js +71 -2
- package/packages/client/dist/adapters/grok-adapter.js +42 -2
- package/packages/client/dist/adapters/index.js +22 -1
- package/packages/client/dist/adapters/platform-detector.js +212 -1
- package/packages/client/dist/adapters/target-adapter.js +12 -0
- package/packages/client/dist/adapters/vscode-adapter.js +72 -2
- package/packages/client/dist/agent/refresh-stubs.js +417 -2
- package/packages/client/dist/agent/update-agent-yaml.js +102 -1
- package/packages/client/dist/agent/update-description.js +251 -1
- package/packages/client/dist/cache/crypto-utils.js +78 -1
- package/packages/client/dist/cache/encrypted-cache.js +108 -1
- package/packages/client/dist/cache/in-memory-asset-cache.js +73 -1
- package/packages/client/dist/cache/index.js +13 -1
- package/packages/client/dist/cache/protected-pi-boundary.js +44 -1
- package/packages/client/dist/checkpoint/checkpoint-client-reader.js +127 -2
- package/packages/client/dist/checkpoint/index.js +11 -1
- package/packages/client/dist/checkpoint/shared-checkpoint-types.js +32 -1
- package/packages/client/dist/cli.js +358 -5
- package/packages/client/dist/commands/activate.js +394 -8
- package/packages/client/dist/commands/architecture-answers.js +274 -7
- package/packages/client/dist/commands/architecture-memory-terminal.js +253 -4
- package/packages/client/dist/commands/architecture-non-ui-continuation.js +100 -1
- package/packages/client/dist/commands/architecture-root-binding.js +51 -1
- package/packages/client/dist/commands/cache-status.js +112 -2
- package/packages/client/dist/commands/config.js +88 -1
- package/packages/client/dist/commands/doctor.js +635 -2
- package/packages/client/dist/commands/invoke.js +7200 -143
- package/packages/client/dist/commands/legacy-epic-terminal.js +71 -1
- package/packages/client/dist/commands/legacy-story-branch.js +96 -1
- package/packages/client/dist/commands/legacy-story-pr.js +276 -1
- package/packages/client/dist/commands/mutation-qa-checkpoint.js +285 -5
- package/packages/client/dist/commands/mutation-qa-gate.js +54 -1
- package/packages/client/dist/commands/mutation-qa-git.js +41 -1
- package/packages/client/dist/commands/mutation-qa-policy.js +102 -1
- package/packages/client/dist/commands/mutation-qa-run.js +219 -3
- package/packages/client/dist/commands/mutation-qa-verdict.js +149 -1
- package/packages/client/dist/commands/mutation-qa.js +220 -1
- package/packages/client/dist/commands/native-agent.js +136 -2
- package/packages/client/dist/commands/planning-diagnosis.js +43 -1
- package/packages/client/dist/commands/planning-generation.js +53 -1
- package/packages/client/dist/commands/planning-memory.js +40 -1
- package/packages/client/dist/commands/planning-research.js +42 -1
- package/packages/client/dist/commands/private-session.js +75 -1
- package/packages/client/dist/commands/property-qa.js +139 -1
- package/packages/client/dist/commands/qa-control.js +16 -1
- package/packages/client/dist/commands/qa-scan-browser-launch.js +55 -1
- package/packages/client/dist/commands/qa-scan-browser-route.js +28 -1
- package/packages/client/dist/commands/qa-scan-capture.js +663 -5
- package/packages/client/dist/commands/qa-scan-discovery.js +376 -3
- package/packages/client/dist/commands/qa-scan-review.js +826 -5
- package/packages/client/dist/commands/qa-synthetic-backend-capture.js +266 -1
- package/packages/client/dist/commands/qa-synthetic-backend-review.js +297 -1
- package/packages/client/dist/commands/qa-synthetic-backend.js +340 -2
- package/packages/client/dist/commands/qa-synthetic-broker-port.js +85 -1
- package/packages/client/dist/commands/qa-synthetic-browser.js +39 -1
- package/packages/client/dist/commands/refactor-characterization.js +671 -21
- package/packages/client/dist/commands/refactor-commit.js +399 -13
- package/packages/client/dist/commands/refactor-control.js +142 -3
- package/packages/client/dist/commands/refactor-cpp-assertions.js +93 -3
- package/packages/client/dist/commands/refactor-review.js +283 -5
- package/packages/client/dist/commands/refactor-root.js +588 -7
- package/packages/client/dist/commands/refresh-memory.js +156 -3
- package/packages/client/dist/commands/shared-architecture-non-ui.js +24 -1
- package/packages/client/dist/commands/story-checkpoint.js +171 -1
- package/packages/client/dist/config/profile-directory.js +13 -1
- package/packages/client/dist/config/resolver-selection.js +278 -1
- package/packages/client/dist/config/secure-config.js +281 -12
- package/packages/client/dist/config.js +223 -2
- package/packages/client/dist/constants.js +25 -1
- package/packages/client/dist/context/context-collector.js +222 -2
- package/packages/client/dist/context/context-sanitizer.js +145 -1
- package/packages/client/dist/continuity/autonomous-governance.js +285 -1
- package/packages/client/dist/continuity/continuity-client-state-store.js +1587 -1
- package/packages/client/dist/continuity/invoke-hooks.js +129 -1
- package/packages/client/dist/continuity/migrations/001-initial-schema.js +12 -2
- package/packages/client/dist/continuity/migrations/002-processing-output-summary-indexes.js +12 -2
- package/packages/client/dist/continuity/migrations/003-headless-execution-attempts.js +12 -2
- package/packages/client/dist/continuity/migrations/004-outbox-delivery-state.js +12 -2
- package/packages/client/dist/continuity/migrations/005-bounded-headless-queue.js +12 -2
- package/packages/client/dist/continuity/migrations/006-native-agent-dispatches.js +10 -2
- package/packages/client/dist/continuity/native-agent-store.js +69 -1
- package/packages/client/dist/continuity/native-agent-types.js +90 -1
- package/packages/client/dist/continuity/outbox.js +117 -1
- package/packages/client/dist/continuity/runner-bootstrap-policy.js +172 -1
- package/packages/client/dist/continuity/sqlite-store.d.ts +22 -1
- package/packages/client/dist/continuity/sqlite-store.js +2612 -81
- package/packages/client/dist/effects/invoke-effect-gateways.js +62 -1
- package/packages/client/dist/errors/error-messages.js +228 -2
- package/packages/client/dist/errors/frontmatter-diagnostics.js +125 -1
- package/packages/client/dist/evidence/collector.js +347 -1
- package/packages/client/dist/evidence/index.js +1 -1
- package/packages/client/dist/evidence/types.js +2 -0
- package/packages/client/dist/fs/atomic-state-replace.js +152 -1
- package/packages/client/dist/generated/command-registry.generated.js +4076 -1
- package/packages/client/dist/generated/public-projections.generated.js +149 -1
- package/packages/client/dist/git/direct-yolo-c01-retry.js +138 -4
- package/packages/client/dist/git/direct-yolo-checkpoint.js +1234 -4
- package/packages/client/dist/git/epic-operational-state.d.ts +198 -0
- package/packages/client/dist/git/epic-operational-state.js +672 -0
- package/packages/client/dist/git/git-lifecycle-step-ids.js +14 -1
- package/packages/client/dist/git/git-lifecycle-store.d.ts +9 -1
- package/packages/client/dist/git/git-lifecycle-store.js +1981 -9
- package/packages/client/dist/git/legacy-story-branch-checkpoint.js +259 -9
- package/packages/client/dist/git/legacy-story-branch-setup.js +348 -4
- package/packages/client/dist/git/legacy-story-pr.js +3538 -70
- package/packages/client/dist/git/secure-filesystem-boundary.js +182 -5
- package/packages/client/dist/graph-retrieval/pre-command-hook.js +284 -1
- package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.js +237 -1
- package/packages/client/dist/i18n/first-run.js +60 -2
- package/packages/client/dist/index.js +47 -1
- package/packages/client/dist/invocation/invoke-input.js +24 -1
- package/packages/client/dist/license/index.js +5 -1
- package/packages/client/dist/license/license-client.js +304 -1
- package/packages/client/dist/machine/fingerprint.js +163 -2
- package/packages/client/dist/machine/index.js +5 -1
- package/packages/client/dist/memory/project-memory-path-scope.js +62 -1
- package/packages/client/dist/memory/project-memory-writer.js +1007 -40
- package/packages/client/dist/memory/shared-project-memory-types.js +253 -2
- package/packages/client/dist/migrations/epic-lifecycle-backfill.d.ts +97 -0
- package/packages/client/dist/migrations/epic-lifecycle-backfill.js +1060 -0
- package/packages/client/dist/migrations/project-identity-capability.js +50 -1
- package/packages/client/dist/migrations/project-identity.js +493 -7
- package/packages/client/dist/migrations/state-json.d.ts +22 -0
- package/packages/client/dist/migrations/state-json.js +393 -4
- package/packages/client/dist/policy/architecture-policy.js +184 -2
- package/packages/client/dist/policy/index.js +7 -1
- package/packages/client/dist/policy/shared-policy-types.js +38 -0
- package/packages/client/dist/policy/subagent-approval-policy.js +55 -1
- package/packages/client/dist/private-execution/clean-baseline.js +279 -2
- package/packages/client/dist/private-execution/codex-executor.js +574 -4
- package/packages/client/dist/private-execution/codex-host-runtime.js +137 -1
- package/packages/client/dist/private-execution/codex-lifecycle-guard.js +97 -1
- package/packages/client/dist/private-execution/codex-scoped-permissions.js +128 -1
- package/packages/client/dist/private-execution/delivery.js +410 -13
- package/packages/client/dist/private-execution/diagnosis-root-review.js +294 -2
- package/packages/client/dist/private-execution/frozen-handoff.js +19 -6
- package/packages/client/dist/private-execution/implementation-validation.js +106 -2
- package/packages/client/dist/private-execution/isolated-admission.js +1432 -23
- package/packages/client/dist/private-execution/isolated-session.js +725 -5
- package/packages/client/dist/private-execution/isolated-workspace.js +122 -1
- package/packages/client/dist/private-execution/pact-consumer-runtime.js +409 -3
- package/packages/client/dist/private-execution/pact-test-source.js +258 -1
- package/packages/client/dist/private-execution/pact-toolchain.js +290 -3
- package/packages/client/dist/private-execution/pact-write-set.js +172 -1
- package/packages/client/dist/private-execution/planning-diagnosis-checkpoint.js +406 -5
- package/packages/client/dist/private-execution/planning-generation-checkpoint.js +462 -5
- package/packages/client/dist/private-execution/planning-memory-checkpoint.js +139 -3
- package/packages/client/dist/private-execution/planning-research-checkpoint.js +325 -4
- package/packages/client/dist/private-execution/private-report-store.js +42 -1
- package/packages/client/dist/private-execution/property-test-candidate.js +103 -1
- package/packages/client/dist/private-execution/property-test-choices.js +147 -3
- package/packages/client/dist/private-execution/property-test-plan.js +271 -7
- package/packages/client/dist/private-execution/property-test-review.js +258 -4
- package/packages/client/dist/private-execution/property-test-runtime.js +372 -9
- package/packages/client/dist/private-execution/property-test-source.js +139 -1
- package/packages/client/dist/private-execution/root-checkpoint.js +80 -1
- package/packages/client/dist/private-execution/root-memory-git-evidence.js +135 -3
- package/packages/client/dist/private-execution/root-memory-recovery.js +93 -1
- package/packages/client/dist/private-execution/root-memory-request.js +29 -1
- package/packages/client/dist/private-execution/root-memory-scope.js +178 -1
- package/packages/client/dist/private-execution/root-memory-writer.js +102 -1
- package/packages/client/dist/private-execution/session-lease.js +63 -1
- package/packages/client/dist/private-execution/shared-private-admission-types.js +50 -1
- package/packages/client/dist/private-execution/shared-project-validation.js +66 -1
- package/packages/client/dist/private-execution/shared-root-memory-types.js +61 -1
- package/packages/client/dist/private-execution/ux-source-validation.js +372 -1
- package/packages/client/dist/private-execution/ux-write-set.js +191 -2
- package/packages/client/dist/registry/registry-reconciliation.js +234 -1
- package/packages/client/dist/resilience/circuit-breaker.js +170 -1
- package/packages/client/dist/resilience/degradation-manager.js +164 -1
- package/packages/client/dist/resilience/freshness-indicator.js +100 -1
- package/packages/client/dist/resilience/index.js +8 -1
- package/packages/client/dist/resilience/recovery-detector.js +74 -1
- package/packages/client/dist/resolvers/asset-resolver.js +13 -0
- package/packages/client/dist/resolvers/local-resolver.js +255 -8
- package/packages/client/dist/resolvers/remote-resolver.js +233 -1
- package/packages/client/dist/runner/binary.js +404 -1
- package/packages/client/dist/runner/cli.js +1571 -17
- package/packages/client/dist/runner/headless/adapters.js +49 -1
- package/packages/client/dist/runner/headless/claude-code-adapter.js +79 -1
- package/packages/client/dist/runner/headless/continuous-story-workspace.js +1628 -11
- package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.js +69 -1
- package/packages/client/dist/runner/headless/epic-merge-candidate-qa.js +110 -2
- package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.js +1236 -8
- package/packages/client/dist/runner/headless/epic-terminal-git-runner.js +318 -2
- package/packages/client/dist/runner/headless/fake-adapter.js +62 -1
- package/packages/client/dist/runner/headless/managed-profile.js +132 -1
- package/packages/client/dist/runner/headless/opencode-adapter.js +145 -2
- package/packages/client/dist/runner/headless/prepared-story-bundle.js +43 -1
- package/packages/client/dist/runner/headless/resolver.js +231 -1
- package/packages/client/dist/runner/headless/run-yolo.js +885 -1
- package/packages/client/dist/runner/headless/run-yoloop.js +2574 -2
- package/packages/client/dist/runner/headless/types.js +574 -2
- package/packages/client/dist/runner/local-console.js +396 -13
- package/packages/client/dist/runner/portal-sync-worker.js +1487 -1
- package/packages/client/dist/runner/project-identity.js +106 -1
- package/packages/client/dist/runner/scheduler.js +995 -7
- package/packages/client/dist/runner-cli.js +18 -2
- package/packages/client/dist/state/invocation-shape.js +13 -1
- package/packages/client/dist/state/project-state-snapshot.d.ts +8 -0
- package/packages/client/dist/state/project-state-snapshot.js +1731 -1
- package/packages/client/dist/state/state-json-repair.js +785 -3
- package/packages/client/dist/state/structural-dag-projection.js +118 -1
- package/packages/client/dist/telemetry/index.js +5 -1
- package/packages/client/dist/telemetry/offline-queue.js +131 -1
- package/packages/client/dist/tier/index.js +5 -1
- package/packages/client/dist/tier/tier-aware-client.js +405 -1
- package/packages/client/dist/types/index.js +38 -1
- package/packages/client/dist/version.js +27 -1
- package/packages/client/dist/yolo/change-evidence-collector.js +126 -1
- package/packages/client/dist/yolo/invoke-envelope-capability.js +62 -1
- package/packages/client/dist/yolo/spec-envelope-collector.js +184 -5
- package/packages/client/dist/yoloop/discovery-hook.js +300 -2
- package/packages/client/dist/yoloop/index.js +8 -1
- package/packages/client/dist/yoloop/invoke-hooks.js +669 -12
- package/packages/client/dist/yoloop/shared-discover-epics.js +1007 -1
- package/packages/client/dist/yoloop/shared-yoloop-types.d.ts +65 -1
- package/packages/client/dist/yoloop/shared-yoloop-types.js +33 -1
- package/packages/client/dist/yoloop/target-normalization.js +180 -1
- package/packages/client/dist/yoloop/yoloop-client-state-store.d.ts +8 -0
- package/packages/client/dist/yoloop/yoloop-client-state-store.js +1920 -1
- package/packages/shared/dist/yolo/spec-envelope.js +3 -6
- package/targets-stubs/antigravity/gemini.md +1 -1
- package/targets-stubs/antigravity/skill/SKILL.md +1 -1
- package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex-root.md +2 -2
- package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex.md +2 -2
- package/targets-stubs/codex/AGENTS.md +1 -1
- package/targets-stubs/cursor/agent.md +2 -2
- package/targets-stubs/gemini-cli/agent.md +2 -2
- package/targets-stubs/opencode/neocortex-root.md +1 -1
- package/targets-stubs/vscode/neocortex.agent.md +2 -2
|
@@ -1,31 +1,1262 @@
|
|
|
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","terminalClass","releaseBlocked","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)){const s=t==="terminationToken"&&e.endsWith(".releaseBlocked")&&r==="[YOLOOP_RELEASE_BLOCKED]";if(_e.test(t)&&!s)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-projection-missing', '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') {
|
|
909
|
+
throw new ContinuitySqliteStoreError('sqlite-integrity-failed', 'Continuity database header is not a recognized SQLite database.', { mutationBlocked: true });
|
|
910
|
+
}
|
|
911
|
+
if (header[18] === 2 || header[19] === 2) {
|
|
912
|
+
throw new ContinuitySqliteStoreError('sqlite-projection-wal-unsafe', 'Continuity database requires an unsafe WAL read profile.', { mutationBlocked: true });
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* P205.05 physical inspection boundary. The database must already exist and is
|
|
917
|
+
* opened with the adapter's OS-backed read-only flag. No directory creation,
|
|
918
|
+
* chmod, initialization, migrations or mutative pragma is attempted.
|
|
919
|
+
*/
|
|
920
|
+
export async function openContinuitySqliteReadOnly(options) {
|
|
921
|
+
const filePath = continuitySqliteStorePath(options.projectRoot);
|
|
922
|
+
// SQLite read-only opens can still create `-wal`/`-shm` companions when the
|
|
923
|
+
// database header is in WAL mode. Deny that profile before adapter open.
|
|
924
|
+
assertContinuitySqliteReadOnlyHeader(filePath);
|
|
925
|
+
const adapter = options.adapter ?? await resolveAdapter(options.adapterLoader);
|
|
926
|
+
let db;
|
|
927
|
+
let opened = false;
|
|
928
|
+
try {
|
|
929
|
+
db = adapter.open(filePath, { fileMustExist: true, readonly: true });
|
|
930
|
+
opened = true;
|
|
931
|
+
const integrity = asSinglePragmaValue(db.pragma('integrity_check', { simple: true }));
|
|
932
|
+
if (String(integrity).toLowerCase() !== 'ok')
|
|
933
|
+
throw new Error('integrity');
|
|
934
|
+
}
|
|
935
|
+
catch (error) {
|
|
936
|
+
throw new ContinuitySqliteStoreError(!opened ? 'sqlite-projection-adapter-incompatible' : 'sqlite-integrity-failed', 'Continuity database failed physical read-only inspection.', { cause: error, mutationBlocked: true });
|
|
937
|
+
}
|
|
938
|
+
const assertReadStatement = (source) => {
|
|
939
|
+
const normalized = source.trim().replace(/^\(+/u, '').toLowerCase();
|
|
940
|
+
if (!/^(select|with\s)/u.test(normalized) || /\b(insert|update|delete|replace|create|alter|drop|pragma|attach|detach|vacuum|reindex)\b/u.test(normalized)) {
|
|
941
|
+
throw new ContinuitySqliteStoreError('sqlite-mutation-blocked', 'Statement is not in the physical read-only query profile.', { mutationBlocked: true });
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
return Object.freeze({
|
|
945
|
+
filePath,
|
|
946
|
+
adapterName: adapter.adapterName,
|
|
947
|
+
all(source, ...params) { assertReadStatement(source); return db.prepare(source).all(...params); },
|
|
948
|
+
get(source, ...params) { assertReadStatement(source); return db.prepare(source).get(...params); },
|
|
949
|
+
close() { db.close(); },
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* P224.01 strict projection binding. The existing state_snapshots table is
|
|
954
|
+
* inspected without schema initialization, WAL, chmod, mkdir or repair. A
|
|
955
|
+
* missing/optional adapter is explicit and never promoted to equality.
|
|
956
|
+
*/
|
|
957
|
+
export async function inspectEpicOperationalSqliteBinding(options) {
|
|
958
|
+
if (!Number.isSafeInteger(options.authorityRevision) || options.authorityRevision < 0
|
|
959
|
+
|| !/^[a-f0-9]{64}$/.test(options.sourceDigest)) {
|
|
960
|
+
throw new ContinuitySqliteStoreError('sqlite-integrity-failed', 'Epic operational SQLite binding input is invalid.', { mutationBlocked: true });
|
|
961
|
+
}
|
|
962
|
+
let inspector;
|
|
963
|
+
try {
|
|
964
|
+
inspector = await openContinuitySqliteReadOnly(options);
|
|
965
|
+
let rows;
|
|
966
|
+
try {
|
|
967
|
+
rows = inspector.all('SELECT sha256, metadata_json FROM state_snapshots WHERE kind = ? ORDER BY captured_at DESC LIMIT 2', 'epic-operational-state');
|
|
968
|
+
}
|
|
969
|
+
catch {
|
|
970
|
+
return Object.freeze({
|
|
971
|
+
schemaVersion: 'EpicOperationalSqliteBindingInspection/1.0',
|
|
972
|
+
status: 'unavailable',
|
|
973
|
+
authorityRevision: options.authorityRevision,
|
|
974
|
+
sourceDigest: null,
|
|
975
|
+
adapterName: inspector.adapterName,
|
|
976
|
+
reasonCodes: ['sqlite-projection-schema-unsupported'],
|
|
977
|
+
publicSafe: true,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
const matching = rows.filter((row) => {
|
|
981
|
+
if (!isRecord(row) || row.sha256 !== options.sourceDigest)
|
|
982
|
+
return false;
|
|
983
|
+
if (typeof row.metadata_json !== 'string')
|
|
984
|
+
return false;
|
|
985
|
+
try {
|
|
986
|
+
const metadata = JSON.parse(row.metadata_json);
|
|
987
|
+
return isRecord(metadata) && metadata.authorityRevision === options.authorityRevision;
|
|
988
|
+
}
|
|
989
|
+
catch {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
return Object.freeze({
|
|
994
|
+
schemaVersion: 'EpicOperationalSqliteBindingInspection/1.0',
|
|
995
|
+
status: matching.length === 1 ? 'bound' : 'unbound',
|
|
996
|
+
authorityRevision: options.authorityRevision,
|
|
997
|
+
sourceDigest: matching.length === 1 ? options.sourceDigest : null,
|
|
998
|
+
adapterName: inspector.adapterName,
|
|
999
|
+
reasonCodes: matching.length === 1
|
|
1000
|
+
? ['epic-operational-sqlite-binding-verified']
|
|
1001
|
+
: ['epic-operational-projection-unbound'],
|
|
1002
|
+
publicSafe: true,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
catch (error) {
|
|
1006
|
+
const reasonCode = error instanceof ContinuitySqliteStoreError
|
|
1007
|
+
? error.reasonCode
|
|
1008
|
+
: 'sqlite-integrity-failed';
|
|
1009
|
+
return Object.freeze({
|
|
1010
|
+
schemaVersion: 'EpicOperationalSqliteBindingInspection/1.0',
|
|
1011
|
+
status: 'unavailable',
|
|
1012
|
+
authorityRevision: options.authorityRevision,
|
|
1013
|
+
sourceDigest: null,
|
|
1014
|
+
adapterName: null,
|
|
1015
|
+
reasonCodes: [reasonCode],
|
|
1016
|
+
publicSafe: true,
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
finally {
|
|
1020
|
+
inspector?.close();
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
async function resolveAdapter(loader) {
|
|
1024
|
+
const result = loader ? await loader() : await loadBetterSqlite3Adapter();
|
|
1025
|
+
if (!result.ok)
|
|
1026
|
+
throw result.error;
|
|
1027
|
+
return result.adapter;
|
|
1028
|
+
}
|
|
1029
|
+
export class ContinuitySqliteStore {
|
|
1030
|
+
filePath;
|
|
1031
|
+
adapterName;
|
|
1032
|
+
db;
|
|
1033
|
+
busyTimeoutMs;
|
|
1034
|
+
integrityCheck;
|
|
1035
|
+
now;
|
|
1036
|
+
mutationBlocked = false;
|
|
1037
|
+
constructor(options) {
|
|
1038
|
+
this.db = options.db;
|
|
1039
|
+
this.filePath = options.filePath;
|
|
1040
|
+
this.adapterName = options.adapterName;
|
|
1041
|
+
this.busyTimeoutMs = options.busyTimeoutMs;
|
|
1042
|
+
this.integrityCheck = options.integrityCheck;
|
|
1043
|
+
this.now = options.now;
|
|
1044
|
+
}
|
|
1045
|
+
initialize() {
|
|
1046
|
+
try {
|
|
1047
|
+
this.configureConnection();
|
|
1048
|
+
if (this.integrityCheck)
|
|
1049
|
+
this.runIntegrityCheck();
|
|
1050
|
+
this.ensureMigrationTable();
|
|
1051
|
+
this.applyMigrations();
|
|
1052
|
+
if (this.integrityCheck)
|
|
1053
|
+
this.runIntegrityCheck();
|
|
1054
|
+
}
|
|
1055
|
+
catch (error) {
|
|
1056
|
+
throw mapSqliteError(error, 'sqlite-migration-failed');
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
close() {
|
|
1060
|
+
this.db.close();
|
|
1061
|
+
}
|
|
1062
|
+
pragmaValue(source) {
|
|
1063
|
+
return asSinglePragmaValue(this.db.pragma(source, { simple: true }));
|
|
1064
|
+
}
|
|
1065
|
+
runIntegrityCheck() {
|
|
1066
|
+
try {
|
|
1067
|
+
const result = String(asSinglePragmaValue(this.db.pragma('integrity_check', { simple: true })) ?? '');
|
|
1068
|
+
if (result.toLowerCase() !== 'ok') {
|
|
1069
|
+
this.mutationBlocked = true;
|
|
1070
|
+
throw new ContinuitySqliteStoreError('sqlite-integrity-failed', 'Continuity SQLite integrity check failed; durable mutation is blocked until the store is repaired or rebuilt.', { mutationBlocked: true });
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
catch (error) {
|
|
1074
|
+
this.mutationBlocked = true;
|
|
1075
|
+
throw mapSqliteError(error, 'sqlite-integrity-failed');
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
getSchemaVersion() {
|
|
1079
|
+
const row = this.db.prepare('SELECT COALESCE(MAX(version), 0) AS version FROM continuity_migrations').get();
|
|
1080
|
+
return isRecord(row) && typeof row.version === 'number' ? row.version : 0;
|
|
1081
|
+
}
|
|
1082
|
+
countRows(table) {
|
|
1083
|
+
if (!isContinuityTable(table)) {
|
|
1084
|
+
throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Unknown continuity table name.');
|
|
1085
|
+
}
|
|
1086
|
+
const row = this.db.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get();
|
|
1087
|
+
return isRecord(row) && typeof row.count === 'number' ? row.count : 0;
|
|
1088
|
+
}
|
|
1089
|
+
reserveNativeAgentDispatch(input) {
|
|
1090
|
+
this.assertCanMutate();
|
|
1091
|
+
if (!isNativeDispatchBindingV1(input.binding) || !isNativeHostAttestationV1(input.hostAttestation)) {
|
|
1092
|
+
throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Native-agent reservation rejected invalid binding or host evidence.');
|
|
1093
|
+
}
|
|
1094
|
+
const candidate = {
|
|
1095
|
+
schemaVersion: 1,
|
|
1096
|
+
storyId: input.binding.storyId,
|
|
1097
|
+
graphRevision: input.binding.graphRevision.replace(/^sha256:/, '').toLowerCase(),
|
|
1098
|
+
dispatchKey: input.binding.dispatchKey.toLowerCase(),
|
|
1099
|
+
profile: 'neocortex',
|
|
1100
|
+
parentHandle: null,
|
|
1101
|
+
handle: null,
|
|
1102
|
+
topology: input.topology ?? 'native-agent',
|
|
1103
|
+
state: 'requested',
|
|
1104
|
+
requestedAt: input.requestedAt,
|
|
1105
|
+
observedAt: null,
|
|
1106
|
+
terminalAt: null,
|
|
1107
|
+
freshness: 'unknown',
|
|
1108
|
+
observationSource: 'local-reservation',
|
|
1109
|
+
reasonCode: input.reasonCode ?? 'native-agent-dispatch-reserved',
|
|
1110
|
+
revision: 0,
|
|
1111
|
+
publicSafe: true,
|
|
1112
|
+
};
|
|
1113
|
+
if (!isNativeAgentDispatchReceiptV1(candidate)) {
|
|
1114
|
+
throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Native-agent reservation rejected unsafe public fields.');
|
|
1115
|
+
}
|
|
1116
|
+
try {
|
|
1117
|
+
return this.runInTransaction(() => {
|
|
1118
|
+
const inserted = this.db.prepare(`
|
|
3
1119
|
INSERT OR IGNORE INTO native_agent_dispatches (
|
|
4
1120
|
dispatch_key, story_id, graph_revision, profile,
|
|
5
1121
|
host_kind, host_version, host_origin, host_confidence,
|
|
6
1122
|
parent_handle, handle, topology, state, requested_at, observed_at,
|
|
7
1123
|
terminal_at, freshness, observation_source, reason_code, revision
|
|
8
1124
|
) VALUES (?, ?, ?, 'neocortex', ?, ?, ?, ?, NULL, NULL, ?, 'requested', ?, NULL, NULL, 'unknown', 'local-reservation', ?, 0)
|
|
9
|
-
`).run(
|
|
1125
|
+
`).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);
|
|
1126
|
+
const row = this.findNativeAgentDispatchRow(candidate.dispatchKey, candidate.storyId, candidate.graphRevision);
|
|
1127
|
+
if (!row)
|
|
1128
|
+
throw new Error('native-agent-reservation-readback-missing');
|
|
1129
|
+
if (row.dispatch_key !== candidate.dispatchKey
|
|
1130
|
+
|| row.story_id !== candidate.storyId
|
|
1131
|
+
|| row.graph_revision !== candidate.graphRevision) {
|
|
1132
|
+
throw new ContinuitySqliteStoreError('sqlite-mutation-failed', 'Native-agent reservation conflicts with an existing story graph binding.');
|
|
1133
|
+
}
|
|
1134
|
+
return Object.freeze({
|
|
1135
|
+
receipt: toNativeAgentReceipt(row),
|
|
1136
|
+
reserved: inserted.changes === 1,
|
|
1137
|
+
replay: inserted.changes === 0,
|
|
1138
|
+
});
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
catch (error) {
|
|
1142
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
observeNativeAgentDispatch(input) {
|
|
1146
|
+
this.assertCanMutate();
|
|
1147
|
+
try {
|
|
1148
|
+
return this.runInTransaction(() => {
|
|
1149
|
+
const currentRow = this.db.prepare('SELECT * FROM native_agent_dispatches WHERE dispatch_key = ?').get(input.dispatchKey);
|
|
1150
|
+
if (!currentRow) {
|
|
1151
|
+
throw new ContinuitySqliteStoreError('sqlite-mutation-failed', 'Native-agent dispatch reservation is missing.');
|
|
1152
|
+
}
|
|
1153
|
+
const current = toNativeAgentReceipt(currentRow);
|
|
1154
|
+
const terminalAt = NATIVE_AGENT_TERMINAL_STATES.has(input.state)
|
|
1155
|
+
? input.terminalAt ?? input.observedAt
|
|
1156
|
+
: null;
|
|
1157
|
+
const handle = input.handle ?? current.handle;
|
|
1158
|
+
const parentHandle = input.parentHandle === undefined ? current.parentHandle : input.parentHandle;
|
|
1159
|
+
const candidate = {
|
|
1160
|
+
...current,
|
|
1161
|
+
handle,
|
|
1162
|
+
parentHandle,
|
|
1163
|
+
topology: input.topology,
|
|
1164
|
+
state: input.state,
|
|
1165
|
+
observedAt: input.observedAt,
|
|
1166
|
+
terminalAt,
|
|
1167
|
+
freshness: input.freshness,
|
|
1168
|
+
observationSource: input.observationSource,
|
|
1169
|
+
reasonCode: input.reasonCode,
|
|
1170
|
+
revision: current.revision + 1,
|
|
1171
|
+
};
|
|
1172
|
+
if (!isNativeAgentDispatchReceiptV1(candidate)) {
|
|
1173
|
+
throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Native-agent observation rejected unsafe or inconsistent fields.');
|
|
1174
|
+
}
|
|
1175
|
+
if (current.handle && input.handle && current.handle !== input.handle) {
|
|
1176
|
+
throw new ContinuitySqliteStoreError('sqlite-mutation-failed', 'Native-agent observation handle conflicts with the reserved dispatch.');
|
|
1177
|
+
}
|
|
1178
|
+
if (!isNativeAgentStateTransitionAllowed(current.state, input.state)) {
|
|
1179
|
+
throw new ContinuitySqliteStoreError('sqlite-mutation-failed', 'Native-agent observation attempted a terminal or non-monotonic regression.');
|
|
1180
|
+
}
|
|
1181
|
+
const sameObservation = current.state === candidate.state
|
|
1182
|
+
&& current.handle === candidate.handle
|
|
1183
|
+
&& current.parentHandle === candidate.parentHandle
|
|
1184
|
+
&& current.topology === candidate.topology
|
|
1185
|
+
&& current.observedAt === candidate.observedAt
|
|
1186
|
+
&& current.terminalAt === candidate.terminalAt
|
|
1187
|
+
&& current.freshness === candidate.freshness
|
|
1188
|
+
&& current.observationSource === candidate.observationSource
|
|
1189
|
+
&& current.reasonCode === candidate.reasonCode;
|
|
1190
|
+
if (sameObservation)
|
|
1191
|
+
return Object.freeze({ receipt: current, applied: false, replay: true });
|
|
1192
|
+
if (input.expectedRevision !== current.revision) {
|
|
1193
|
+
throw new ContinuitySqliteStoreError('sqlite-mutation-failed', 'Native-agent observation CAS revision conflict.');
|
|
1194
|
+
}
|
|
1195
|
+
if (current.observedAt && Date.parse(input.observedAt) < Date.parse(current.observedAt)) {
|
|
1196
|
+
throw new ContinuitySqliteStoreError('sqlite-mutation-failed', 'Native-agent observation timestamp regressed.');
|
|
1197
|
+
}
|
|
1198
|
+
const updated = this.db.prepare(`
|
|
10
1199
|
UPDATE native_agent_dispatches
|
|
11
1200
|
SET parent_handle = ?, handle = ?, topology = ?, state = ?, observed_at = ?,
|
|
12
1201
|
terminal_at = ?, freshness = ?, observation_source = ?, reason_code = ?, revision = revision + 1
|
|
13
1202
|
WHERE dispatch_key = ? AND revision = ?
|
|
14
|
-
`).run(
|
|
1203
|
+
`).run(candidate.parentHandle, candidate.handle, candidate.topology, candidate.state, candidate.observedAt, candidate.terminalAt, candidate.freshness, candidate.observationSource, candidate.reasonCode, candidate.dispatchKey, input.expectedRevision);
|
|
1204
|
+
if (updated.changes !== 1) {
|
|
1205
|
+
throw new ContinuitySqliteStoreError('sqlite-mutation-failed', 'Native-agent observation CAS update was not applied.');
|
|
1206
|
+
}
|
|
1207
|
+
this.db.prepare(`
|
|
15
1208
|
INSERT INTO native_agent_observations (
|
|
16
1209
|
dispatch_key, sequence, state, handle, parent_handle, topology,
|
|
17
1210
|
observed_at, terminal_at, freshness, observation_source, reason_code
|
|
18
1211
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
19
|
-
`).run(
|
|
1212
|
+
`).run(candidate.dispatchKey, candidate.revision, candidate.state, candidate.handle, candidate.parentHandle, candidate.topology, candidate.observedAt, candidate.terminalAt, candidate.freshness, candidate.observationSource, candidate.reasonCode);
|
|
1213
|
+
return Object.freeze({ receipt: candidate, applied: true, replay: false });
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
catch (error) {
|
|
1217
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
getNativeAgentDispatchStatus(dispatchKey, options = {}) {
|
|
1221
|
+
const row = this.db.prepare('SELECT * FROM native_agent_dispatches WHERE dispatch_key = ?').get(dispatchKey);
|
|
1222
|
+
if (!row)
|
|
1223
|
+
return null;
|
|
1224
|
+
const receipt = toNativeAgentReceipt(row);
|
|
1225
|
+
const staleAfterMs = Math.max(1_000, options.staleAfterMs ?? 30_000);
|
|
1226
|
+
const nowMs = Date.parse(options.nowIso ?? this.now());
|
|
1227
|
+
const observedMs = receipt.observedAt ? Date.parse(receipt.observedAt) : NaN;
|
|
1228
|
+
if (!NATIVE_AGENT_TERMINAL_STATES.has(receipt.state)
|
|
1229
|
+
&& Number.isFinite(nowMs)
|
|
1230
|
+
&& (!Number.isFinite(observedMs) || nowMs - observedMs > staleAfterMs)) {
|
|
1231
|
+
return Object.freeze({
|
|
1232
|
+
...receipt,
|
|
1233
|
+
state: 'unknown',
|
|
1234
|
+
freshness: receipt.observedAt ? 'stale' : 'unknown',
|
|
1235
|
+
reasonCode: receipt.observedAt ? 'native-agent-observation-stale' : 'native-agent-awaiting-host-evidence',
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
return receipt;
|
|
1239
|
+
}
|
|
1240
|
+
listRecentNativeAgentDispatches(limit = 20) {
|
|
1241
|
+
const bounded = Math.max(1, Math.min(50, Math.floor(limit)));
|
|
1242
|
+
const rows = this.db.prepare(`
|
|
20
1243
|
SELECT * FROM native_agent_dispatches
|
|
21
1244
|
ORDER BY COALESCE(observed_at, requested_at) DESC, dispatch_key DESC
|
|
22
1245
|
LIMIT ?
|
|
23
|
-
`).all(
|
|
1246
|
+
`).all(bounded);
|
|
1247
|
+
return Object.freeze(rows.map(toNativeAgentReceipt));
|
|
1248
|
+
}
|
|
1249
|
+
findNativeAgentDispatchRow(dispatchKey, storyId, graphRevision) {
|
|
1250
|
+
return this.db.prepare(`
|
|
24
1251
|
SELECT * FROM native_agent_dispatches
|
|
25
1252
|
WHERE dispatch_key = ? OR (story_id = ? AND graph_revision = ?)
|
|
26
1253
|
ORDER BY CASE WHEN dispatch_key = ? THEN 0 ELSE 1 END
|
|
27
1254
|
LIMIT 1
|
|
28
|
-
`).get(
|
|
1255
|
+
`).get(dispatchKey, storyId, graphRevision, dispatchKey);
|
|
1256
|
+
}
|
|
1257
|
+
getLocalConsoleAttachmentState(jobId) {
|
|
1258
|
+
assertBoundedPublicText(jobId, 'local_console.jobId', 160, true);
|
|
1259
|
+
const row = this.db.prepare(`
|
|
29
1260
|
SELECT j.job_id AS job_id,
|
|
30
1261
|
j.kind AS kind,
|
|
31
1262
|
j.status AS status,
|
|
@@ -34,41 +1265,210 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
34
1265
|
FROM jobs j
|
|
35
1266
|
WHERE j.job_id = ?
|
|
36
1267
|
LIMIT 1
|
|
37
|
-
`).get(
|
|
1268
|
+
`).get(jobId);
|
|
1269
|
+
if (!isRecord(row) || typeof row.job_id !== 'string')
|
|
1270
|
+
return null;
|
|
1271
|
+
return {
|
|
1272
|
+
jobId: row.job_id,
|
|
1273
|
+
kind: typeof row.kind === 'string' ? row.kind : 'unknown',
|
|
1274
|
+
status: typeof row.status === 'string' ? row.status : 'unknown',
|
|
1275
|
+
activeLeaseCount: typeof row.active_lease_count === 'number' ? row.active_lease_count : 0,
|
|
1276
|
+
runningHeadlessAttemptCount: typeof row.running_headless_attempt_count === 'number' ? row.running_headless_attempt_count : 0,
|
|
1277
|
+
publicSafe: true,
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
recordJob(input) {
|
|
1281
|
+
this.assertCanMutate();
|
|
1282
|
+
assertPublicSafeText(input.summary, 'jobs.summary');
|
|
1283
|
+
const metadataJson = stringifyPublicMetadata(input.metadata);
|
|
1284
|
+
try {
|
|
1285
|
+
const result = this.db.prepare(`
|
|
38
1286
|
INSERT OR IGNORE INTO jobs (
|
|
39
1287
|
job_id, kind, status, summary, reason_code, created_at, updated_at,
|
|
40
1288
|
next_run_at, goal_id, loop_id, campaign_id, operation_id,
|
|
41
1289
|
idempotency_key, metadata_json
|
|
42
1290
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
43
|
-
`).run(
|
|
1291
|
+
`).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);
|
|
1292
|
+
return this.toWriteResult(result);
|
|
1293
|
+
}
|
|
1294
|
+
catch (error) {
|
|
1295
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
appendEvent(input) {
|
|
1299
|
+
this.assertCanMutate();
|
|
1300
|
+
assertPublicSafeText(input.summary, 'job_events.summary');
|
|
1301
|
+
const metadataJson = stringifyPublicMetadata(input.metadata);
|
|
1302
|
+
try {
|
|
1303
|
+
const result = this.db.prepare(`
|
|
44
1304
|
INSERT OR IGNORE INTO job_events (
|
|
45
1305
|
event_id, job_id, operation_id, idempotency_key, event_sequence,
|
|
46
1306
|
type, summary, reason_code, created_at, metadata_json
|
|
47
1307
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
48
|
-
`).run(
|
|
1308
|
+
`).run(input.eventId, input.jobId, input.operationId, input.idempotencyKey ?? null, input.sequence ?? null, input.type, input.summary, input.reasonCode ?? null, input.createdAt, metadataJson);
|
|
1309
|
+
return this.toWriteResult(result);
|
|
1310
|
+
}
|
|
1311
|
+
catch (error) {
|
|
1312
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
recordProcessingOutputSummary(input) {
|
|
1316
|
+
assertProcessingSummaryText(input.executionId, 'processing_output_summary.executionId', 160);
|
|
1317
|
+
assertProcessingSummaryText(input.jobId, 'processing_output_summary.jobId', 160);
|
|
1318
|
+
assertProcessingSummaryText(input.publicSummary, 'processing_output_summary.publicSummary', CONTINUITY_PROCESSING_OUTPUT_SUMMARY_MAX_CHARS);
|
|
1319
|
+
assertProcessingSummaryText(input.createdAt, 'processing_output_summary.createdAt', 80);
|
|
1320
|
+
assertPublicSafeText(input.eventId, 'processing_output_summary.eventId');
|
|
1321
|
+
assertPublicSafeText(input.idempotencyKey, 'processing_output_summary.idempotencyKey');
|
|
1322
|
+
assertPublicSafeText(input.goalId, 'processing_output_summary.goalId');
|
|
1323
|
+
assertPublicSafeText(input.loopId, 'processing_output_summary.loopId');
|
|
1324
|
+
assertPublicSafeText(input.reasonCode, 'processing_output_summary.reasonCode');
|
|
1325
|
+
assertPublicSafeText(input.source, 'processing_output_summary.source');
|
|
1326
|
+
const result = this.appendEvent({
|
|
1327
|
+
eventId: processingSummaryEventId(input),
|
|
1328
|
+
jobId: input.jobId,
|
|
1329
|
+
operationId: input.executionId,
|
|
1330
|
+
idempotencyKey: input.idempotencyKey ?? input.executionId,
|
|
1331
|
+
type: CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE,
|
|
1332
|
+
summary: input.publicSummary,
|
|
1333
|
+
reasonCode: input.reasonCode,
|
|
1334
|
+
createdAt: input.createdAt,
|
|
1335
|
+
metadata: buildProcessingOutputSummaryMetadata(input),
|
|
1336
|
+
});
|
|
1337
|
+
// P195.04: On a freshly applied tick summary, persist the producer-emitted
|
|
1338
|
+
// stage/iteration into the job row and append one bounded stageHistory entry.
|
|
1339
|
+
// Gating on `inserted` keeps this idempotent across replays and runner retries.
|
|
1340
|
+
if (result.inserted) {
|
|
1341
|
+
this.recordJobStageProgress({
|
|
1342
|
+
jobId: input.jobId,
|
|
1343
|
+
currentStage: input.metadata?.currentStage,
|
|
1344
|
+
iteration: input.metadata?.iteration,
|
|
1345
|
+
at: input.createdAt,
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
return result;
|
|
1349
|
+
}
|
|
1350
|
+
enqueueHeadlessExecutionAttempt(input) {
|
|
1351
|
+
this.assertCanMutate();
|
|
1352
|
+
validateHeadlessExecutionAttemptIdentity({
|
|
1353
|
+
attemptId: input.attemptId,
|
|
1354
|
+
executionId: input.executionId,
|
|
1355
|
+
jobId: input.jobId,
|
|
1356
|
+
directiveHash: input.directiveHash,
|
|
1357
|
+
targetRef: input.targetRef,
|
|
1358
|
+
storyId: input.storyId,
|
|
1359
|
+
epicId: input.epicId,
|
|
1360
|
+
runtimeId: input.runtimeId,
|
|
1361
|
+
modelId: input.modelId,
|
|
1362
|
+
runtimeSource: input.runtimeSource,
|
|
1363
|
+
reasonCode: input.reasonCode,
|
|
1364
|
+
publicSummary: input.publicSummary,
|
|
1365
|
+
timestamp: input.enqueuedAt,
|
|
1366
|
+
});
|
|
1367
|
+
assertBoundedPublicText(input.notBeforeAt, 'headless_execution.notBeforeAt', 80);
|
|
1368
|
+
assertBoundedPublicText(input.operationId, 'headless_execution.operationId', 160);
|
|
1369
|
+
assertBoundedPublicText(input.idempotencyKey, 'headless_execution.idempotencyKey', 160);
|
|
1370
|
+
assertNonNegativeInteger(input.retryCount, 'headless_execution.retryCount');
|
|
1371
|
+
assertNonNegativeInteger(input.maxAttempts, 'headless_execution.maxAttempts');
|
|
1372
|
+
if (input.maxAttempts !== undefined && input.maxAttempts < 1) {
|
|
1373
|
+
throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Continuity SQLite maxAttempts must be greater than zero.');
|
|
1374
|
+
}
|
|
1375
|
+
if (input.metadata)
|
|
1376
|
+
assertPublicSafeValue(input.metadata, 'headless_execution.metadata');
|
|
1377
|
+
const queuePolicy = normalizeContinuityQueuePolicy({
|
|
1378
|
+
maxItems: input.queueCap,
|
|
1379
|
+
maxAttempts: input.maxAttempts,
|
|
1380
|
+
});
|
|
1381
|
+
const queuePriority = normalizeContinuityQueuePriority(input.queuePriority);
|
|
1382
|
+
const queueScope = normalizeContinuityQueueScope(input.queueScope);
|
|
1383
|
+
const relativePaths = normalizePublicRelativePaths(input.relativePaths);
|
|
1384
|
+
const metadata = buildHeadlessExecutionPublicMetadata({
|
|
1385
|
+
attemptId: input.attemptId,
|
|
1386
|
+
executionId: input.executionId,
|
|
1387
|
+
jobId: input.jobId,
|
|
1388
|
+
directiveHash: input.directiveHash,
|
|
1389
|
+
runtimeId: input.runtimeId,
|
|
1390
|
+
modelId: input.modelId,
|
|
1391
|
+
runtimeSource: input.runtimeSource,
|
|
1392
|
+
status: input.status ?? 'queued',
|
|
1393
|
+
reasonCode: input.reasonCode,
|
|
1394
|
+
retryCount: input.retryCount ?? 0,
|
|
1395
|
+
attemptCount: 0,
|
|
1396
|
+
maxAttempts: queuePolicy.maxAttempts,
|
|
1397
|
+
queuePriority,
|
|
1398
|
+
queueScope,
|
|
1399
|
+
counts: input.counts,
|
|
1400
|
+
hashes: input.hashes,
|
|
1401
|
+
redactionSummary: input.redactionSummary,
|
|
1402
|
+
relativePaths,
|
|
1403
|
+
metadata: input.metadata,
|
|
1404
|
+
});
|
|
1405
|
+
return this.runInTransaction(() => {
|
|
1406
|
+
const configuredPolicyRow = this.db.prepare(`
|
|
49
1407
|
SELECT value_json
|
|
50
1408
|
FROM runner_config
|
|
51
1409
|
WHERE key = 'headless_queue_policy'
|
|
52
1410
|
LIMIT 1
|
|
53
|
-
`).get();
|
|
1411
|
+
`).get();
|
|
1412
|
+
let effectiveQueueCap = queuePolicy.maxItems;
|
|
1413
|
+
if (input.queueCap === undefined && isRecord(configuredPolicyRow) && typeof configuredPolicyRow.value_json === 'string') {
|
|
1414
|
+
try {
|
|
1415
|
+
const configuredPolicy = JSON.parse(configuredPolicyRow.value_json);
|
|
1416
|
+
if (isRecord(configuredPolicy) && typeof configuredPolicy.maxItems === 'number') {
|
|
1417
|
+
effectiveQueueCap = normalizeContinuityQueuePolicy({ maxItems: configuredPolicy.maxItems }).maxItems;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
catch {
|
|
1421
|
+
// runner_config has a json_valid constraint; retain the bounded default defensively.
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
this.db.prepare(`
|
|
54
1425
|
INSERT INTO runner_config (key, value_json, updated_at)
|
|
55
1426
|
VALUES ('headless_queue_policy', ?, ?)
|
|
56
1427
|
ON CONFLICT(key) DO UPDATE SET
|
|
57
1428
|
value_json = excluded.value_json,
|
|
58
1429
|
updated_at = excluded.updated_at
|
|
59
|
-
`).run(JSON.stringify({maxItems:
|
|
1430
|
+
`).run(JSON.stringify({ maxItems: effectiveQueueCap }), input.enqueuedAt);
|
|
1431
|
+
const replay = this.db.prepare(`
|
|
60
1432
|
SELECT attempt_id
|
|
61
1433
|
FROM headless_execution_attempts
|
|
62
1434
|
WHERE attempt_id = ?
|
|
63
1435
|
OR (? IS NOT NULL AND operation_id = ?)
|
|
64
1436
|
OR (? IS NOT NULL AND idempotency_key = ?)
|
|
65
1437
|
LIMIT 1
|
|
66
|
-
`).get(
|
|
1438
|
+
`).get(input.attemptId, input.operationId ?? null, input.operationId ?? null, input.idempotencyKey ?? null, input.idempotencyKey ?? null);
|
|
1439
|
+
const depthRow = this.db.prepare(`
|
|
67
1440
|
SELECT COUNT(*) AS count
|
|
68
1441
|
FROM headless_execution_attempts
|
|
69
1442
|
WHERE status IN ('queued', 'backoff', 'running')
|
|
70
1443
|
AND dead_lettered_at IS NULL
|
|
71
|
-
`).get()
|
|
1444
|
+
`).get();
|
|
1445
|
+
const queueDepth = isRecord(depthRow) && typeof depthRow.count === 'number' ? depthRow.count : 0;
|
|
1446
|
+
if (replay) {
|
|
1447
|
+
return {
|
|
1448
|
+
inserted: false,
|
|
1449
|
+
replay: true,
|
|
1450
|
+
changes: 0,
|
|
1451
|
+
accepted: true,
|
|
1452
|
+
backpressure: queueDepth >= effectiveQueueCap,
|
|
1453
|
+
queueDepth,
|
|
1454
|
+
queueCap: effectiveQueueCap,
|
|
1455
|
+
publicSafe: true,
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
if (queueDepth >= effectiveQueueCap) {
|
|
1459
|
+
return {
|
|
1460
|
+
inserted: false,
|
|
1461
|
+
replay: false,
|
|
1462
|
+
changes: 0,
|
|
1463
|
+
accepted: false,
|
|
1464
|
+
backpressure: true,
|
|
1465
|
+
queueDepth,
|
|
1466
|
+
queueCap: effectiveQueueCap,
|
|
1467
|
+
reasonCode: CONTINUITY_QUEUE_REASON_CODES.CAP_REACHED,
|
|
1468
|
+
publicSafe: true,
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
const result = this.db.prepare(`
|
|
72
1472
|
INSERT INTO headless_execution_attempts (
|
|
73
1473
|
attempt_id, execution_id, job_id, directive_hash, directive_kind,
|
|
74
1474
|
command_class, target_kind, target_ref, story_id, epic_id, runtime_id,
|
|
@@ -78,7 +1478,23 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
78
1478
|
relative_paths_json, metadata_json, queue_priority, queue_scope,
|
|
79
1479
|
attempt_count, next_retry_at
|
|
80
1480
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)
|
|
81
|
-
`).run(
|
|
1481
|
+
`).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);
|
|
1482
|
+
return {
|
|
1483
|
+
inserted: result.changes > 0,
|
|
1484
|
+
replay: false,
|
|
1485
|
+
changes: result.changes,
|
|
1486
|
+
accepted: result.changes > 0,
|
|
1487
|
+
backpressure: queueDepth + result.changes >= effectiveQueueCap,
|
|
1488
|
+
queueDepth: queueDepth + result.changes,
|
|
1489
|
+
queueCap: effectiveQueueCap,
|
|
1490
|
+
publicSafe: true,
|
|
1491
|
+
};
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
listDueHeadlessExecutionAttempts(options) {
|
|
1495
|
+
const limit = Math.max(1, Math.min(50, Math.floor(options.limit)));
|
|
1496
|
+
assertBoundedPublicText(options.nowIso, 'headless_execution.nowIso', 80, true);
|
|
1497
|
+
return this.db.prepare(`
|
|
82
1498
|
SELECT attempt_id, execution_id, job_id, directive_hash, directive_kind,
|
|
83
1499
|
command_class, target_kind, target_ref, story_id, epic_id,
|
|
84
1500
|
runtime_id, model_id, runtime_source, status, reason_code,
|
|
@@ -97,13 +1513,36 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
97
1513
|
ORDER BY queue_priority DESC, COALESCE(next_retry_at, not_before_at, enqueued_at) ASC,
|
|
98
1514
|
enqueued_at ASC, attempt_id ASC
|
|
99
1515
|
LIMIT ?
|
|
100
|
-
`).all(
|
|
1516
|
+
`).all(options.nowIso, options.nowIso, limit);
|
|
1517
|
+
}
|
|
1518
|
+
hasCompletedHeadlessExecutionDirectiveHash(directiveHash, excludeAttemptId) {
|
|
1519
|
+
assertBoundedPublicText(directiveHash, 'headless_execution.directiveHash', 160, true);
|
|
1520
|
+
assertBoundedPublicText(excludeAttemptId, 'headless_execution.attemptId', 160);
|
|
1521
|
+
const excludePredicate = excludeAttemptId ? ' AND attempt_id != ?' : '';
|
|
1522
|
+
const params = excludeAttemptId ? [directiveHash, excludeAttemptId] : [directiveHash];
|
|
1523
|
+
const row = this.db.prepare(`
|
|
101
1524
|
SELECT 1 AS found
|
|
102
1525
|
FROM headless_execution_attempts
|
|
103
1526
|
WHERE directive_hash = ?
|
|
104
|
-
AND status = 'completed'${
|
|
1527
|
+
AND status = 'completed'${excludePredicate}
|
|
105
1528
|
LIMIT 1
|
|
106
|
-
`).get(...
|
|
1529
|
+
`).get(...params);
|
|
1530
|
+
return row?.found === 1;
|
|
1531
|
+
}
|
|
1532
|
+
claimHeadlessExecutionAttempt(input) {
|
|
1533
|
+
this.assertCanMutate();
|
|
1534
|
+
validateHeadlessExecutionAttemptIdentity({
|
|
1535
|
+
attemptId: input.attemptId,
|
|
1536
|
+
executionId: input.attemptId,
|
|
1537
|
+
jobId: input.attemptId,
|
|
1538
|
+
leaseId: input.leaseId,
|
|
1539
|
+
runnerId: input.runnerId,
|
|
1540
|
+
reasonCode: input.reasonCode,
|
|
1541
|
+
timestamp: input.claimedAt,
|
|
1542
|
+
});
|
|
1543
|
+
assertBoundedPublicText(input.leaseExpiresAt, 'headless_execution.leaseExpiresAt', 80, true);
|
|
1544
|
+
try {
|
|
1545
|
+
const result = this.db.prepare(`
|
|
107
1546
|
UPDATE headless_execution_attempts
|
|
108
1547
|
SET status = 'running',
|
|
109
1548
|
lease_id = ?,
|
|
@@ -119,7 +1558,27 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
119
1558
|
AND dead_lettered_at IS NULL
|
|
120
1559
|
AND (COALESCE(next_retry_at, not_before_at) IS NULL OR COALESCE(next_retry_at, not_before_at) <= ?)
|
|
121
1560
|
AND (lease_id IS NULL OR lease_released_at IS NOT NULL OR lease_expires_at <= ?)
|
|
122
|
-
`).run(
|
|
1561
|
+
`).run(input.leaseId, input.runnerId, input.claimedAt, input.claimedAt, input.leaseExpiresAt, input.reasonCode ?? null, input.attemptId, input.claimedAt, input.claimedAt);
|
|
1562
|
+
return this.toWriteResult(result);
|
|
1563
|
+
}
|
|
1564
|
+
catch (error) {
|
|
1565
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
acquireNextHeadlessExecutionAttempt(input) {
|
|
1569
|
+
this.assertCanMutate();
|
|
1570
|
+
validateHeadlessExecutionAttemptIdentity({
|
|
1571
|
+
attemptId: input.leaseId,
|
|
1572
|
+
executionId: input.leaseId,
|
|
1573
|
+
jobId: input.leaseId,
|
|
1574
|
+
leaseId: input.leaseId,
|
|
1575
|
+
runnerId: input.runnerId,
|
|
1576
|
+
reasonCode: input.reasonCode,
|
|
1577
|
+
timestamp: input.claimedAt,
|
|
1578
|
+
});
|
|
1579
|
+
assertBoundedPublicText(input.leaseExpiresAt, 'headless_execution.leaseExpiresAt', 80, true);
|
|
1580
|
+
return this.runInTransaction(() => {
|
|
1581
|
+
const row = this.db.prepare(`
|
|
123
1582
|
SELECT attempt_id
|
|
124
1583
|
FROM headless_execution_attempts
|
|
125
1584
|
WHERE status IN ('queued', 'backoff')
|
|
@@ -132,7 +1591,11 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
132
1591
|
enqueued_at ASC,
|
|
133
1592
|
attempt_id ASC
|
|
134
1593
|
LIMIT 1
|
|
135
|
-
`).get(
|
|
1594
|
+
`).get(input.directiveKind ?? null, input.directiveKind ?? null, input.claimedAt, input.claimedAt);
|
|
1595
|
+
if (typeof row?.attempt_id !== 'string') {
|
|
1596
|
+
return { acquired: false, reasonCode: 'continuity-ok', publicSafe: true };
|
|
1597
|
+
}
|
|
1598
|
+
const result = this.db.prepare(`
|
|
136
1599
|
UPDATE headless_execution_attempts
|
|
137
1600
|
SET status = 'running',
|
|
138
1601
|
lease_id = ?,
|
|
@@ -148,7 +1611,29 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
148
1611
|
AND dead_lettered_at IS NULL
|
|
149
1612
|
AND (COALESCE(next_retry_at, not_before_at) IS NULL OR COALESCE(next_retry_at, not_before_at) <= ?)
|
|
150
1613
|
AND (lease_id IS NULL OR lease_released_at IS NOT NULL OR lease_expires_at <= ?)
|
|
151
|
-
`).run(
|
|
1614
|
+
`).run(input.leaseId, input.runnerId, input.claimedAt, input.claimedAt, input.leaseExpiresAt, input.reasonCode ?? null, row.attempt_id, input.claimedAt, input.claimedAt);
|
|
1615
|
+
if (result.changes === 0) {
|
|
1616
|
+
return { acquired: false, reasonCode: 'lease-conflict', publicSafe: true };
|
|
1617
|
+
}
|
|
1618
|
+
const attempt = this.getHeadlessExecutionAttempt(row.attempt_id);
|
|
1619
|
+
return attempt
|
|
1620
|
+
? { acquired: true, attempt, reasonCode: input.reasonCode ?? 'continuity-ok', publicSafe: true }
|
|
1621
|
+
: { acquired: false, reasonCode: 'lease-conflict', publicSafe: true };
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
heartbeatHeadlessExecutionAttempt(input) {
|
|
1625
|
+
this.assertCanMutate();
|
|
1626
|
+
validateHeadlessExecutionAttemptIdentity({
|
|
1627
|
+
attemptId: input.attemptId,
|
|
1628
|
+
executionId: input.attemptId,
|
|
1629
|
+
jobId: input.attemptId,
|
|
1630
|
+
leaseId: input.leaseId,
|
|
1631
|
+
runnerId: input.runnerId,
|
|
1632
|
+
timestamp: input.heartbeatAt,
|
|
1633
|
+
});
|
|
1634
|
+
assertBoundedPublicText(input.leaseExpiresAt, 'headless_execution.leaseExpiresAt', 80);
|
|
1635
|
+
try {
|
|
1636
|
+
const result = this.db.prepare(`
|
|
152
1637
|
UPDATE headless_execution_attempts
|
|
153
1638
|
SET heartbeat_at = ?,
|
|
154
1639
|
lease_expires_at = COALESCE(?, lease_expires_at)
|
|
@@ -158,7 +1643,27 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
158
1643
|
AND status = 'running'
|
|
159
1644
|
AND lease_released_at IS NULL
|
|
160
1645
|
AND lease_expires_at > ?
|
|
161
|
-
`).run(
|
|
1646
|
+
`).run(input.heartbeatAt, input.leaseExpiresAt ?? null, input.attemptId, input.leaseId, input.runnerId, input.heartbeatAt);
|
|
1647
|
+
return this.toWriteResult(result);
|
|
1648
|
+
}
|
|
1649
|
+
catch (error) {
|
|
1650
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
releaseHeadlessExecutionAttempt(input) {
|
|
1654
|
+
this.assertCanMutate();
|
|
1655
|
+
validateHeadlessExecutionAttemptIdentity({
|
|
1656
|
+
attemptId: input.attemptId,
|
|
1657
|
+
executionId: input.attemptId,
|
|
1658
|
+
jobId: input.attemptId,
|
|
1659
|
+
leaseId: input.leaseId,
|
|
1660
|
+
reasonCode: input.reasonCode,
|
|
1661
|
+
timestamp: input.releasedAt,
|
|
1662
|
+
});
|
|
1663
|
+
assertBoundedPublicText(input.notBeforeAt, 'headless_execution.notBeforeAt', 80);
|
|
1664
|
+
assertNonNegativeInteger(input.retryCount, 'headless_execution.retryCount');
|
|
1665
|
+
try {
|
|
1666
|
+
const result = this.db.prepare(`
|
|
162
1667
|
UPDATE headless_execution_attempts
|
|
163
1668
|
SET status = COALESCE(?, status),
|
|
164
1669
|
reason_code = COALESCE(?, reason_code),
|
|
@@ -174,8 +1679,76 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
174
1679
|
AND lease_id = ?
|
|
175
1680
|
AND status = 'running'
|
|
176
1681
|
AND lease_released_at IS NULL
|
|
177
|
-
`).run(
|
|
178
|
-
|
|
1682
|
+
`).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);
|
|
1683
|
+
return this.toWriteResult(result);
|
|
1684
|
+
}
|
|
1685
|
+
catch (error) {
|
|
1686
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
completeHeadlessExecutionAttempt(input) {
|
|
1690
|
+
this.assertCanMutate();
|
|
1691
|
+
validateHeadlessExecutionAttemptIdentity({
|
|
1692
|
+
attemptId: input.attemptId,
|
|
1693
|
+
executionId: input.attemptId,
|
|
1694
|
+
jobId: input.attemptId,
|
|
1695
|
+
leaseId: input.leaseId,
|
|
1696
|
+
runnerId: input.runnerId,
|
|
1697
|
+
reasonCode: input.reasonCode,
|
|
1698
|
+
publicSummary: input.publicSummary,
|
|
1699
|
+
timestamp: input.completedAt,
|
|
1700
|
+
});
|
|
1701
|
+
assertBoundedPublicText(input.notBeforeAt, 'headless_execution.notBeforeAt', 80);
|
|
1702
|
+
assertNonNegativeInteger(input.durationMs, 'headless_execution.durationMs');
|
|
1703
|
+
assertNonNegativeInteger(input.retryCount, 'headless_execution.retryCount');
|
|
1704
|
+
if (input.metadata)
|
|
1705
|
+
assertPublicSafeValue(input.metadata, 'headless_execution.metadata');
|
|
1706
|
+
const relativePaths = normalizePublicRelativePaths(input.relativePaths);
|
|
1707
|
+
const metadata = buildHeadlessExecutionPublicMetadata({
|
|
1708
|
+
attemptId: input.attemptId,
|
|
1709
|
+
executionId: input.executionId ?? input.attemptId,
|
|
1710
|
+
jobId: input.jobId ?? input.attemptId,
|
|
1711
|
+
status: input.status,
|
|
1712
|
+
reasonCode: input.reasonCode,
|
|
1713
|
+
durationMs: input.durationMs,
|
|
1714
|
+
retryCount: input.retryCount,
|
|
1715
|
+
counts: input.counts,
|
|
1716
|
+
hashes: input.hashes,
|
|
1717
|
+
redactionSummary: input.redactionSummary,
|
|
1718
|
+
relativePaths,
|
|
1719
|
+
metadata: input.metadata,
|
|
1720
|
+
});
|
|
1721
|
+
const leasePredicate = input.leaseId
|
|
1722
|
+
? ` AND lease_id = ?
|
|
1723
|
+
AND lease_released_at IS NULL${input.runnerId ? ' AND runner_id = ?' : ''}`
|
|
1724
|
+
: '';
|
|
1725
|
+
const statusPredicate = input.leaseId
|
|
1726
|
+
? "status = 'running'"
|
|
1727
|
+
: "status NOT IN ('completed', 'failed', 'timed_out', 'skipped')";
|
|
1728
|
+
const params = [
|
|
1729
|
+
input.status,
|
|
1730
|
+
input.completedAt,
|
|
1731
|
+
input.durationMs ?? null,
|
|
1732
|
+
input.reasonCode ?? null,
|
|
1733
|
+
input.publicSummary ?? null,
|
|
1734
|
+
input.notBeforeAt ?? null,
|
|
1735
|
+
input.retryCount ?? null,
|
|
1736
|
+
input.status,
|
|
1737
|
+
input.notBeforeAt ?? null,
|
|
1738
|
+
input.counts ? stringifyPublicJsonValue(input.counts, 'headless_execution.counts') : null,
|
|
1739
|
+
input.hashes ? stringifyPublicJsonValue(input.hashes, 'headless_execution.hashes') : null,
|
|
1740
|
+
input.redactionSummary ? stringifyPublicJsonValue(input.redactionSummary, 'headless_execution.redactionSummary') : null,
|
|
1741
|
+
relativePaths ? stringifyPublicJsonValue(relativePaths, 'headless_execution.relativePaths') : null,
|
|
1742
|
+
stringifyPublicMetadata(metadata),
|
|
1743
|
+
input.completedAt,
|
|
1744
|
+
input.attemptId,
|
|
1745
|
+
];
|
|
1746
|
+
if (input.leaseId)
|
|
1747
|
+
params.push(input.leaseId);
|
|
1748
|
+
if (input.leaseId && input.runnerId)
|
|
1749
|
+
params.push(input.runnerId);
|
|
1750
|
+
try {
|
|
1751
|
+
const result = this.db.prepare(`
|
|
179
1752
|
UPDATE headless_execution_attempts
|
|
180
1753
|
SET status = ?,
|
|
181
1754
|
completed_at = ?,
|
|
@@ -194,53 +1767,426 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
194
1767
|
relative_paths_json = COALESCE(?, relative_paths_json),
|
|
195
1768
|
metadata_json = COALESCE(?, metadata_json),
|
|
196
1769
|
lease_released_at = COALESCE(lease_released_at, ?)
|
|
197
|
-
WHERE attempt_id = ?${
|
|
198
|
-
AND ${
|
|
199
|
-
`).run(...
|
|
1770
|
+
WHERE attempt_id = ?${leasePredicate}
|
|
1771
|
+
AND ${statusPredicate}
|
|
1772
|
+
`).run(...params);
|
|
1773
|
+
return this.toWriteResult(result);
|
|
1774
|
+
}
|
|
1775
|
+
catch (error) {
|
|
1776
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
recordHeadlessExecutionReceipt(input) {
|
|
1780
|
+
validateHeadlessExecutionAttemptIdentity({
|
|
1781
|
+
attemptId: input.attemptId,
|
|
1782
|
+
executionId: input.executionId,
|
|
1783
|
+
jobId: input.jobId,
|
|
1784
|
+
reasonCode: input.reasonCode,
|
|
1785
|
+
timestamp: input.createdAt,
|
|
1786
|
+
});
|
|
1787
|
+
if (input.metadata)
|
|
1788
|
+
assertPublicSafeValue(input.metadata, 'headless_execution.metadata');
|
|
1789
|
+
const relativePaths = normalizePublicRelativePaths(input.relativePaths);
|
|
1790
|
+
const metadata = buildHeadlessExecutionPublicMetadata({
|
|
1791
|
+
attemptId: input.attemptId,
|
|
1792
|
+
executionId: input.executionId,
|
|
1793
|
+
jobId: input.jobId,
|
|
1794
|
+
status: input.status,
|
|
1795
|
+
reasonCode: input.reasonCode,
|
|
1796
|
+
counts: input.counts,
|
|
1797
|
+
hashes: input.hashes,
|
|
1798
|
+
redactionSummary: input.redactionSummary,
|
|
1799
|
+
relativePaths,
|
|
1800
|
+
metadata: input.metadata,
|
|
1801
|
+
});
|
|
1802
|
+
return this.appendReceipt({
|
|
1803
|
+
receiptId: input.receiptId,
|
|
1804
|
+
jobId: input.jobId,
|
|
1805
|
+
operationId: input.operationId ?? input.executionId,
|
|
1806
|
+
idempotencyKey: input.idempotencyKey ?? input.executionId,
|
|
1807
|
+
status: input.status,
|
|
1808
|
+
reasonCode: input.reasonCode,
|
|
1809
|
+
appliedOperationIds: input.status === 'applied' ? [input.executionId] : undefined,
|
|
1810
|
+
ignoredOperationIds: input.status === 'ignored' ? [input.executionId] : undefined,
|
|
1811
|
+
createdAt: input.createdAt,
|
|
1812
|
+
metadata,
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
recordHeadlessExecutionSummary(input) {
|
|
1816
|
+
validateHeadlessExecutionAttemptIdentity({
|
|
1817
|
+
attemptId: input.attemptId,
|
|
1818
|
+
executionId: input.executionId,
|
|
1819
|
+
jobId: input.jobId,
|
|
1820
|
+
runtimeId: input.runtimeId,
|
|
1821
|
+
modelId: input.modelId,
|
|
1822
|
+
reasonCode: input.reasonCode,
|
|
1823
|
+
publicSummary: input.publicSummary,
|
|
1824
|
+
timestamp: input.createdAt,
|
|
1825
|
+
});
|
|
1826
|
+
assertNonNegativeInteger(input.durationMs, 'headless_execution.durationMs');
|
|
1827
|
+
if (input.metadata)
|
|
1828
|
+
assertPublicSafeValue(input.metadata, 'headless_execution.metadata');
|
|
1829
|
+
const relativePaths = normalizePublicRelativePaths(input.relativePaths);
|
|
1830
|
+
const metadata = buildHeadlessExecutionPublicMetadata({
|
|
1831
|
+
attemptId: input.attemptId,
|
|
1832
|
+
executionId: input.executionId,
|
|
1833
|
+
jobId: input.jobId,
|
|
1834
|
+
runtimeId: input.runtimeId,
|
|
1835
|
+
modelId: input.modelId,
|
|
1836
|
+
status: input.status ?? 'completed',
|
|
1837
|
+
reasonCode: input.reasonCode,
|
|
1838
|
+
durationMs: input.durationMs,
|
|
1839
|
+
counts: input.counts,
|
|
1840
|
+
hashes: input.hashes,
|
|
1841
|
+
redactionSummary: input.redactionSummary,
|
|
1842
|
+
relativePaths,
|
|
1843
|
+
metadata: input.metadata,
|
|
1844
|
+
});
|
|
1845
|
+
return this.recordProcessingOutputSummary({
|
|
1846
|
+
eventId: input.eventId,
|
|
1847
|
+
executionId: input.executionId,
|
|
1848
|
+
jobId: input.jobId,
|
|
1849
|
+
idempotencyKey: input.executionId,
|
|
1850
|
+
publicSummary: input.publicSummary,
|
|
1851
|
+
reasonCode: input.reasonCode,
|
|
1852
|
+
createdAt: input.createdAt,
|
|
1853
|
+
source: 'headless_execution',
|
|
1854
|
+
metadata,
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
appendReceipt(input) {
|
|
1858
|
+
this.assertCanMutate();
|
|
1859
|
+
const metadataJson = stringifyPublicMetadata(input.metadata);
|
|
1860
|
+
try {
|
|
1861
|
+
const result = this.db.prepare(`
|
|
200
1862
|
INSERT OR IGNORE INTO job_receipts (
|
|
201
1863
|
receipt_id, job_id, operation_id, idempotency_key, status, reason_code,
|
|
202
1864
|
applied_operation_ids_json, ignored_operation_ids_json, created_at, metadata_json
|
|
203
1865
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
204
|
-
`).run(
|
|
1866
|
+
`).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);
|
|
1867
|
+
return this.toWriteResult(result);
|
|
1868
|
+
}
|
|
1869
|
+
catch (error) {
|
|
1870
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* P199.06: Apply TTL/count/byte retention to public receipts and persisted
|
|
1875
|
+
* processing summaries. Deleted content is replaced with bounded audit evidence
|
|
1876
|
+
* that hashes only public identifiers/status/reason codes, never summaries.
|
|
1877
|
+
*/
|
|
1878
|
+
compactPublicReceiptsAndOutputs(input) {
|
|
1879
|
+
this.assertCanMutate();
|
|
1880
|
+
const nowMs = Date.parse(input.nowIso);
|
|
1881
|
+
if (!Number.isFinite(nowMs)) {
|
|
1882
|
+
throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Continuity receipt compaction requires a valid public timestamp.');
|
|
1883
|
+
}
|
|
1884
|
+
const policy = normalizeContinuityRetentionPolicy(input.policy);
|
|
1885
|
+
const receiptCutoff = new Date(nowMs - policy.receipts.maxAgeMs).toISOString();
|
|
1886
|
+
const outputCutoff = new Date(nowMs - policy.processingOutputs.maxAgeMs).toISOString();
|
|
1887
|
+
try {
|
|
1888
|
+
return this.runInTransaction(() => {
|
|
1889
|
+
const receiptCandidates = this.selectReceiptRetentionCandidates({
|
|
1890
|
+
cutoff: receiptCutoff,
|
|
1891
|
+
maxItems: policy.receipts.maxItems,
|
|
1892
|
+
maxBytes: policy.receipts.maxBytes,
|
|
1893
|
+
limit: policy.maxDeleteRowsPerRun,
|
|
1894
|
+
});
|
|
1895
|
+
const outputCandidates = this.selectOutputRetentionCandidates({
|
|
1896
|
+
cutoff: outputCutoff,
|
|
1897
|
+
maxItems: policy.processingOutputs.maxItems,
|
|
1898
|
+
maxBytes: policy.processingOutputs.maxBytes,
|
|
1899
|
+
limit: policy.maxDeleteRowsPerRun,
|
|
1900
|
+
});
|
|
1901
|
+
const candidates = [...receiptCandidates, ...outputCandidates]
|
|
1902
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id))
|
|
1903
|
+
.slice(0, policy.maxDeleteRowsPerRun);
|
|
1904
|
+
if (candidates.length === 0) {
|
|
1905
|
+
return {
|
|
1906
|
+
status: 'noop',
|
|
1907
|
+
summaries: [],
|
|
1908
|
+
counts: { receiptsRemoved: 0, outputsRemoved: 0, bytesRemoved: 0, summariesCreated: 0 },
|
|
1909
|
+
reasonCode: CONTINUITY_RECEIPT_RETENTION_REASON_CODES.NOOP,
|
|
1910
|
+
publicSafe: true,
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
const selectedReceipts = candidates.filter((candidate) => candidate.kind === 'receipt');
|
|
1914
|
+
const selectedOutputs = candidates.filter((candidate) => candidate.kind === 'output');
|
|
1915
|
+
for (const jobId of uniqueValues(selectedReceipts.map((candidate) => candidate.jobId))) {
|
|
1916
|
+
this.preserveReceiptDerivedIteration(jobId, input.nowIso);
|
|
1917
|
+
}
|
|
1918
|
+
this.deleteRetentionCandidates('job_receipts', 'receipt_id', selectedReceipts.map((candidate) => candidate.id));
|
|
1919
|
+
this.deleteRetentionCandidates('job_events', 'event_id', selectedOutputs.map((candidate) => candidate.id));
|
|
1920
|
+
const byJob = new Map();
|
|
1921
|
+
for (const candidate of candidates) {
|
|
1922
|
+
byJob.set(candidate.jobId, [...(byJob.get(candidate.jobId) ?? []), candidate]);
|
|
1923
|
+
}
|
|
1924
|
+
const summaries = [];
|
|
1925
|
+
for (const [jobId, removed] of [...byJob.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
1926
|
+
const removedReceipts = removed.filter((candidate) => candidate.kind === 'receipt');
|
|
1927
|
+
const removedOutputs = removed.filter((candidate) => candidate.kind === 'output');
|
|
1928
|
+
const retained = this.retainedReceiptOutputCounts(jobId);
|
|
1929
|
+
const hashes = retentionHashes(jobId, removedReceipts, removedOutputs);
|
|
1930
|
+
const counts = {
|
|
1931
|
+
receiptsRemoved: removedReceipts.length,
|
|
1932
|
+
receiptsRetained: retained.receipts,
|
|
1933
|
+
outputsRemoved: removedOutputs.length,
|
|
1934
|
+
outputsRetained: retained.outputs,
|
|
1935
|
+
bytesRemoved: removed.reduce((total, candidate) => total + candidate.approxBytes, 0),
|
|
1936
|
+
bytesRetained: retained.bytes,
|
|
1937
|
+
redactions: 0,
|
|
1938
|
+
};
|
|
1939
|
+
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}.`;
|
|
1940
|
+
assertBoundedPublicText(publicSummary, 'receipt_compaction.publicSummary', 500, true);
|
|
1941
|
+
const eventDigest = createHash('sha256')
|
|
1942
|
+
.update(`${jobId}:${input.nowIso}:${hashes.aggregate}`)
|
|
1943
|
+
.digest('hex');
|
|
1944
|
+
const summaryId = `receipt-compaction-${eventDigest.slice(0, 32)}`;
|
|
1945
|
+
const summary = {
|
|
1946
|
+
schemaVersion: 1,
|
|
1947
|
+
contractVersion: 'continuity-v1',
|
|
1948
|
+
policyVersion: 1,
|
|
1949
|
+
type: CONTINUITY_RECEIPT_COMPACTION_EVENT_TYPE,
|
|
1950
|
+
summaryId,
|
|
1951
|
+
jobId,
|
|
1952
|
+
scope: 'local_sqlite',
|
|
1953
|
+
status: 'compacted',
|
|
1954
|
+
createdAt: input.nowIso,
|
|
1955
|
+
reasonCodes: [CONTINUITY_RECEIPT_RETENTION_REASON_CODES.COMPACTED],
|
|
1956
|
+
counts,
|
|
1957
|
+
hashes,
|
|
1958
|
+
publicSummary,
|
|
1959
|
+
publicSafe: true,
|
|
1960
|
+
};
|
|
1961
|
+
this.appendEvent({
|
|
1962
|
+
eventId: summaryId,
|
|
1963
|
+
jobId,
|
|
1964
|
+
operationId: `op-${summaryId}`,
|
|
1965
|
+
idempotencyKey: `idem-${summaryId}`,
|
|
1966
|
+
type: CONTINUITY_RECEIPT_COMPACTION_EVENT_TYPE,
|
|
1967
|
+
summary: publicSummary,
|
|
1968
|
+
reasonCode: CONTINUITY_RECEIPT_RETENTION_REASON_CODES.COMPACTED,
|
|
1969
|
+
createdAt: input.nowIso,
|
|
1970
|
+
metadata: {
|
|
1971
|
+
schemaVersion: 1,
|
|
1972
|
+
contractVersion: 'continuity-v1',
|
|
1973
|
+
eventType: CONTINUITY_RECEIPT_COMPACTION_EVENT_TYPE,
|
|
1974
|
+
jobId,
|
|
1975
|
+
reasonCodes: summary.reasonCodes,
|
|
1976
|
+
counts,
|
|
1977
|
+
hashes,
|
|
1978
|
+
publicSummary,
|
|
1979
|
+
},
|
|
1980
|
+
});
|
|
1981
|
+
this.pruneReceiptCompactionSummaries(jobId, policy.maxCompactionSummariesPerJob);
|
|
1982
|
+
summaries.push(summary);
|
|
1983
|
+
}
|
|
1984
|
+
return {
|
|
1985
|
+
status: 'compacted',
|
|
1986
|
+
summaries,
|
|
1987
|
+
counts: {
|
|
1988
|
+
receiptsRemoved: selectedReceipts.length,
|
|
1989
|
+
outputsRemoved: selectedOutputs.length,
|
|
1990
|
+
bytesRemoved: candidates.reduce((total, candidate) => total + candidate.approxBytes, 0),
|
|
1991
|
+
summariesCreated: summaries.length,
|
|
1992
|
+
},
|
|
1993
|
+
reasonCode: CONTINUITY_RECEIPT_RETENTION_REASON_CODES.COMPACTED,
|
|
1994
|
+
publicSafe: true,
|
|
1995
|
+
};
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
catch (error) {
|
|
1999
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
updateJobStatus(input) {
|
|
2003
|
+
this.assertCanMutate();
|
|
2004
|
+
try {
|
|
2005
|
+
const result = this.db.prepare(`
|
|
205
2006
|
UPDATE jobs
|
|
206
2007
|
SET status = ?,
|
|
207
2008
|
reason_code = COALESCE(?, reason_code),
|
|
208
2009
|
updated_at = ?,
|
|
209
2010
|
next_run_at = COALESCE(?, next_run_at)
|
|
210
2011
|
WHERE job_id = ?
|
|
211
|
-
`).run(
|
|
2012
|
+
`).run(input.status, input.reasonCode ?? null, input.updatedAt, input.nextRunAt ?? null, input.jobId);
|
|
2013
|
+
if (result.changes === 0) {
|
|
2014
|
+
throw new ContinuitySqliteStoreError('sqlite-foreign-key-failed', `Continuity SQLite job ${input.jobId} was not found for status update.`);
|
|
2015
|
+
}
|
|
2016
|
+
return this.toWriteResult(result);
|
|
2017
|
+
}
|
|
2018
|
+
catch (error) {
|
|
2019
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* P195.04: Additively persist producer-emitted process stage/iteration into the
|
|
2024
|
+
* existing `jobs.metadata_json` column and append a bounded `stageHistory`
|
|
2025
|
+
* (newest `CONTINUITY_STAGE_HISTORY_MAX` kept). No schema migration is required.
|
|
2026
|
+
* Unknown stages, unsafe timestamps and missing jobs are no-ops so older DBs and
|
|
2027
|
+
* non-tick summaries never throw.
|
|
2028
|
+
*/
|
|
2029
|
+
recordJobStageProgress(input) {
|
|
2030
|
+
this.assertCanMutate();
|
|
2031
|
+
assertBoundedPublicText(input.jobId, 'job_stage.jobId', 160, true);
|
|
2032
|
+
const stage = isContinuityPublicStage(input.currentStage) ? input.currentStage : undefined;
|
|
2033
|
+
const iteration = normalizeContinuityStageIteration(input.iteration);
|
|
2034
|
+
if (!stage && iteration === undefined) {
|
|
2035
|
+
return { inserted: false, replay: false, changes: 0 };
|
|
2036
|
+
}
|
|
2037
|
+
const at = sanitizeContinuityStageTimestamp(input.at);
|
|
2038
|
+
try {
|
|
2039
|
+
const existing = this.db
|
|
2040
|
+
.prepare('SELECT metadata_json FROM jobs WHERE job_id = ?')
|
|
2041
|
+
.get(input.jobId);
|
|
2042
|
+
if (!existing)
|
|
2043
|
+
return { inserted: false, replay: false, changes: 0 };
|
|
2044
|
+
const metadata = parseJobMetadataObject(typeof existing.metadata_json === 'string' ? existing.metadata_json : null);
|
|
2045
|
+
if (stage)
|
|
2046
|
+
metadata.currentStage = stage;
|
|
2047
|
+
if (iteration !== undefined)
|
|
2048
|
+
metadata.iteration = iteration;
|
|
2049
|
+
if (stage && at && input.appendHistory !== false) {
|
|
2050
|
+
const history = sanitizeContinuityStageHistory(metadata.stageHistory);
|
|
2051
|
+
history.push({ stage, at });
|
|
2052
|
+
metadata.stageHistory = history.slice(-CONTINUITY_STAGE_HISTORY_MAX);
|
|
2053
|
+
}
|
|
2054
|
+
const result = this.db
|
|
2055
|
+
.prepare('UPDATE jobs SET metadata_json = ? WHERE job_id = ?')
|
|
2056
|
+
.run(stringifyPublicMetadata(metadata), input.jobId);
|
|
2057
|
+
return this.toWriteResult(result);
|
|
2058
|
+
}
|
|
2059
|
+
catch (error) {
|
|
2060
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
updateJobAndExistingGoalCampaignStatus(input) {
|
|
2064
|
+
return this.runInTransaction(() => {
|
|
2065
|
+
const jobResult = this.db.prepare(`
|
|
212
2066
|
UPDATE jobs
|
|
213
2067
|
SET status = ?,
|
|
214
2068
|
reason_code = COALESCE(?, reason_code),
|
|
215
2069
|
updated_at = ?,
|
|
216
2070
|
next_run_at = COALESCE(?, next_run_at)
|
|
217
2071
|
WHERE job_id = ?
|
|
218
|
-
`).run(
|
|
2072
|
+
`).run(input.status, input.reasonCode ?? null, input.updatedAt, input.nextRunAt ?? null, input.jobId);
|
|
2073
|
+
if (jobResult.changes === 0) {
|
|
2074
|
+
throw new ContinuitySqliteStoreError('sqlite-foreign-key-failed', `Continuity SQLite job ${input.jobId} was not found for status parity update.`);
|
|
2075
|
+
}
|
|
2076
|
+
const campaignResult = this.db.prepare(`
|
|
219
2077
|
UPDATE goal_campaigns
|
|
220
2078
|
SET status = ?,
|
|
221
2079
|
reason_code = COALESCE(?, reason_code),
|
|
222
2080
|
updated_at = ?
|
|
223
2081
|
WHERE job_id = ?
|
|
224
|
-
`).run(
|
|
2082
|
+
`).run(input.status, input.reasonCode ?? null, input.updatedAt, input.jobId);
|
|
2083
|
+
return this.toWriteResult({ changes: jobResult.changes + campaignResult.changes });
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
scheduleNextTick(input) {
|
|
2087
|
+
return this.updateJobStatus({
|
|
2088
|
+
jobId: input.jobId,
|
|
2089
|
+
status: 'active',
|
|
2090
|
+
updatedAt: input.updatedAt,
|
|
2091
|
+
reasonCode: input.reasonCode,
|
|
2092
|
+
nextRunAt: input.scheduledAt,
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
acquireLease(input) {
|
|
2096
|
+
this.assertCanMutate();
|
|
2097
|
+
const metadataJson = stringifyPublicMetadata(input.metadata);
|
|
2098
|
+
try {
|
|
2099
|
+
const result = this.db.prepare(`
|
|
225
2100
|
INSERT OR IGNORE INTO job_leases (
|
|
226
2101
|
lease_id, job_id, runner_id, status, acquired_at, expires_at,
|
|
227
2102
|
operation_id, idempotency_key, metadata_json
|
|
228
2103
|
) VALUES (?, ?, ?, 'acquired', ?, ?, ?, ?, ?)
|
|
229
|
-
`).run(
|
|
2104
|
+
`).run(input.leaseId, input.jobId, input.runnerId, input.acquiredAt, input.expiresAt, input.operationId ?? null, input.idempotencyKey ?? null, metadataJson);
|
|
2105
|
+
return this.toWriteResult(result);
|
|
2106
|
+
}
|
|
2107
|
+
catch (error) {
|
|
2108
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
releaseLease(input) {
|
|
2112
|
+
this.assertCanMutate();
|
|
2113
|
+
try {
|
|
2114
|
+
const result = this.db.prepare(`
|
|
230
2115
|
UPDATE job_leases
|
|
231
2116
|
SET status = 'released', released_at = ?
|
|
232
2117
|
WHERE lease_id = ? AND released_at IS NULL
|
|
233
|
-
`).run(
|
|
2118
|
+
`).run(input.releasedAt, input.leaseId);
|
|
2119
|
+
if (result.changes === 0) {
|
|
2120
|
+
throw new ContinuitySqliteStoreError('sqlite-foreign-key-failed', `Continuity SQLite lease ${input.leaseId} was not found for release.`);
|
|
2121
|
+
}
|
|
2122
|
+
return this.toWriteResult(result);
|
|
2123
|
+
}
|
|
2124
|
+
catch (error) {
|
|
2125
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
recordStateSnapshot(input) {
|
|
2129
|
+
this.assertCanMutate();
|
|
2130
|
+
assertPublicSafeText(input.summary, 'state_snapshots.summary');
|
|
2131
|
+
assertPublicSafeText(input.relativePath, 'state_snapshots.relative_path');
|
|
2132
|
+
const metadataJson = stringifyPublicMetadata(input.metadata);
|
|
2133
|
+
try {
|
|
2134
|
+
const result = this.db.prepare(`
|
|
234
2135
|
INSERT OR IGNORE INTO state_snapshots (
|
|
235
2136
|
snapshot_id, job_id, kind, relative_path, sha256, summary, captured_at,
|
|
236
2137
|
operation_id, idempotency_key, metadata_json
|
|
237
2138
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
238
|
-
`).run(
|
|
2139
|
+
`).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);
|
|
2140
|
+
return this.toWriteResult(result);
|
|
2141
|
+
}
|
|
2142
|
+
catch (error) {
|
|
2143
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
recordArtifact(input) {
|
|
2147
|
+
this.assertCanMutate();
|
|
2148
|
+
assertPublicSafeText(input.kind, 'artifacts.kind');
|
|
2149
|
+
assertPublicSafeText(input.relativePath, 'artifacts.relative_path');
|
|
2150
|
+
assertPublicSafeText(input.summary, 'artifacts.summary');
|
|
2151
|
+
const metadataJson = stringifyPublicMetadata(input.metadata);
|
|
2152
|
+
try {
|
|
2153
|
+
const result = this.db.prepare(`
|
|
239
2154
|
INSERT OR IGNORE INTO artifacts (
|
|
240
2155
|
artifact_id, job_id, kind, relative_path, sha256, summary,
|
|
241
2156
|
operation_id, idempotency_key, created_at, metadata_json
|
|
242
2157
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
243
|
-
`).run(
|
|
2158
|
+
`).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);
|
|
2159
|
+
return this.toWriteResult(result);
|
|
2160
|
+
}
|
|
2161
|
+
catch (error) {
|
|
2162
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
enqueueOutbox(input) {
|
|
2166
|
+
this.assertCanMutate();
|
|
2167
|
+
assertOutboxIdentity({
|
|
2168
|
+
outboxId: input.outboxId,
|
|
2169
|
+
jobId: input.jobId,
|
|
2170
|
+
operationId: input.operationId,
|
|
2171
|
+
idempotencyKey: input.idempotencyKey,
|
|
2172
|
+
type: input.type,
|
|
2173
|
+
status: input.status,
|
|
2174
|
+
deliveryCursor: input.deliveryCursor,
|
|
2175
|
+
timestamp: input.createdAt,
|
|
2176
|
+
reasonCode: input.lastErrorReason ?? input.lastErrorCode,
|
|
2177
|
+
summary: input.lastErrorSummary ?? input.deadLetterSummary,
|
|
2178
|
+
});
|
|
2179
|
+
assertOutboxIdentity({ outboxId: input.outboxId, timestamp: input.updatedAt });
|
|
2180
|
+
assertBoundedPublicText(input.nextAttemptAt, 'outbox.nextAttemptAt', 80);
|
|
2181
|
+
assertBoundedPublicText(input.lastAttemptAt, 'outbox.lastAttemptAt', 80);
|
|
2182
|
+
assertBoundedPublicText(input.sentAt, 'outbox.sentAt', 80);
|
|
2183
|
+
assertBoundedPublicText(input.ackedAt, 'outbox.ackedAt', 80);
|
|
2184
|
+
assertBoundedPublicText(input.deadLetteredAt, 'outbox.deadLetteredAt', 80);
|
|
2185
|
+
assertNonNegativeInteger(input.retryCount, 'outbox.retryCount');
|
|
2186
|
+
assertNonNegativeInteger(input.attemptCount, 'outbox.attemptCount');
|
|
2187
|
+
const payloadJson = stringifyPublicMetadata(input.payload);
|
|
2188
|
+
try {
|
|
2189
|
+
const result = this.db.prepare(`
|
|
244
2190
|
INSERT OR IGNORE INTO outbox (
|
|
245
2191
|
outbox_id, job_id, operation_id, idempotency_key, type, status,
|
|
246
2192
|
payload_json, retry_count, attempt_count, delivery_cursor,
|
|
@@ -248,18 +2194,57 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
248
2194
|
last_error_reason, last_error_summary, dead_letter_summary,
|
|
249
2195
|
dead_lettered_at, created_at, updated_at
|
|
250
2196
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
251
|
-
`).run(
|
|
2197
|
+
`).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);
|
|
2198
|
+
return this.toWriteResult(result);
|
|
2199
|
+
}
|
|
2200
|
+
catch (error) {
|
|
2201
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
selectOutboxBatch(options) {
|
|
2205
|
+
assertOutboxIdentity({ outboxId: 'outbox-query', timestamp: options.nowIso });
|
|
2206
|
+
const limit = Math.max(1, Math.min(100, Math.floor(options.limit)));
|
|
2207
|
+
const statuses = uniqueValues(options.statuses?.length ? options.statuses : ['pending', 'failed']);
|
|
2208
|
+
for (const status of statuses) {
|
|
2209
|
+
if (!isOutboxStatus(status)) {
|
|
2210
|
+
throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Continuity SQLite outbox batch query rejected an unknown status.');
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
const placeholders = statuses.map(() => '?').join(', ');
|
|
2214
|
+
const rows = this.db.prepare(`
|
|
252
2215
|
SELECT outbox_id, job_id, operation_id, idempotency_key, type, status,
|
|
253
2216
|
payload_json, retry_count, attempt_count, delivery_cursor,
|
|
254
2217
|
next_attempt_at, last_attempt_at, sent_at, acked_at, last_error_code,
|
|
255
2218
|
last_error_reason, last_error_summary, dead_letter_summary,
|
|
256
2219
|
dead_lettered_at, created_at, updated_at
|
|
257
2220
|
FROM outbox
|
|
258
|
-
WHERE status IN (${
|
|
2221
|
+
WHERE status IN (${placeholders})
|
|
259
2222
|
AND (status NOT IN ('pending', 'failed') OR next_attempt_at IS NULL OR next_attempt_at <= ?)
|
|
260
2223
|
ORDER BY COALESCE(next_attempt_at, created_at) ASC, created_at ASC, outbox_id ASC
|
|
261
2224
|
LIMIT ?
|
|
262
|
-
`).all(...
|
|
2225
|
+
`).all(...statuses, options.nowIso, limit);
|
|
2226
|
+
return rows.map(toOutboxDeliveryRow);
|
|
2227
|
+
}
|
|
2228
|
+
markOutboxSent(input) {
|
|
2229
|
+
this.assertCanMutate();
|
|
2230
|
+
assertOutboxIdentity({
|
|
2231
|
+
outboxId: input.outboxId,
|
|
2232
|
+
deliveryCursor: input.deliveryCursor,
|
|
2233
|
+
timestamp: input.sentAt,
|
|
2234
|
+
reasonCode: input.reasonCode,
|
|
2235
|
+
});
|
|
2236
|
+
try {
|
|
2237
|
+
return this.runInTransaction(() => {
|
|
2238
|
+
const row = this.getOutboxTransitionRow(input.outboxId);
|
|
2239
|
+
if (!row)
|
|
2240
|
+
return outboxTransitionResult({ outboxId: input.outboxId, status: 'missing', changes: 0, replay: false, reasonCode: 'outbox-missing' });
|
|
2241
|
+
if ((row.status === 'sent' || row.status === 'acked') && row.delivery_cursor === input.deliveryCursor) {
|
|
2242
|
+
return outboxTransitionResult({ outboxId: input.outboxId, status: row.status, changes: 0, replay: true, deliveryCursor: input.deliveryCursor, reasonCode: input.reasonCode ?? 'outbox-delivery-replay' });
|
|
2243
|
+
}
|
|
2244
|
+
if (row.status === 'sent' || row.status === 'acked' || row.status === 'dead_letter') {
|
|
2245
|
+
return outboxTransitionResult({ outboxId: input.outboxId, status: 'conflict', changes: 0, replay: false, deliveryCursor: row.delivery_cursor ?? undefined, reasonCode: 'outbox-delivery-conflict' });
|
|
2246
|
+
}
|
|
2247
|
+
const result = this.db.prepare(`
|
|
263
2248
|
UPDATE outbox
|
|
264
2249
|
SET status = 'sent',
|
|
265
2250
|
delivery_cursor = ?,
|
|
@@ -270,7 +2255,34 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
270
2255
|
updated_at = ?
|
|
271
2256
|
WHERE outbox_id = ?
|
|
272
2257
|
AND status IN ('pending', 'failed')
|
|
273
|
-
`).run(
|
|
2258
|
+
`).run(input.deliveryCursor, input.sentAt, input.sentAt, input.sentAt, input.outboxId);
|
|
2259
|
+
return outboxTransitionResult({ outboxId: input.outboxId, status: 'sent', changes: result.changes, replay: false, deliveryCursor: input.deliveryCursor, reasonCode: input.reasonCode ?? 'outbox-sent' });
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
catch (error) {
|
|
2263
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
ackOutbox(input) {
|
|
2267
|
+
this.assertCanMutate();
|
|
2268
|
+
assertOutboxIdentity({
|
|
2269
|
+
outboxId: input.outboxId,
|
|
2270
|
+
deliveryCursor: input.deliveryCursor,
|
|
2271
|
+
timestamp: input.ackedAt,
|
|
2272
|
+
reasonCode: input.reasonCode,
|
|
2273
|
+
});
|
|
2274
|
+
try {
|
|
2275
|
+
return this.runInTransaction(() => {
|
|
2276
|
+
const row = this.getOutboxTransitionRow(input.outboxId);
|
|
2277
|
+
if (!row)
|
|
2278
|
+
return outboxTransitionResult({ outboxId: input.outboxId, status: 'missing', changes: 0, replay: false, reasonCode: 'outbox-missing' });
|
|
2279
|
+
if (row.status === 'acked' && (!input.deliveryCursor || row.delivery_cursor === input.deliveryCursor)) {
|
|
2280
|
+
return outboxTransitionResult({ outboxId: input.outboxId, status: 'acked', changes: 0, replay: true, deliveryCursor: row.delivery_cursor ?? undefined, reasonCode: input.reasonCode ?? 'outbox-ack-replay' });
|
|
2281
|
+
}
|
|
2282
|
+
if (row.status !== 'sent' || (input.deliveryCursor && row.delivery_cursor !== input.deliveryCursor)) {
|
|
2283
|
+
return outboxTransitionResult({ outboxId: input.outboxId, status: 'conflict', changes: 0, replay: false, deliveryCursor: row.delivery_cursor ?? undefined, reasonCode: 'outbox-ack-conflict' });
|
|
2284
|
+
}
|
|
2285
|
+
const result = this.db.prepare(`
|
|
274
2286
|
UPDATE outbox
|
|
275
2287
|
SET status = 'acked',
|
|
276
2288
|
acked_at = ?,
|
|
@@ -280,7 +2292,48 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
280
2292
|
last_error_reason = NULL
|
|
281
2293
|
WHERE outbox_id = ?
|
|
282
2294
|
AND status = 'sent'
|
|
283
|
-
`).run(
|
|
2295
|
+
`).run(input.ackedAt, input.ackedAt, input.outboxId);
|
|
2296
|
+
return outboxTransitionResult({ outboxId: input.outboxId, status: 'acked', changes: result.changes, replay: false, deliveryCursor: row.delivery_cursor ?? undefined, reasonCode: input.reasonCode ?? 'outbox-acked' });
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
catch (error) {
|
|
2300
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
failOutboxDelivery(input) {
|
|
2304
|
+
this.assertCanMutate();
|
|
2305
|
+
assertOutboxIdentity({
|
|
2306
|
+
outboxId: input.outboxId,
|
|
2307
|
+
deliveryCursor: input.deliveryCursor,
|
|
2308
|
+
timestamp: input.failedAt,
|
|
2309
|
+
reasonCode: input.reasonCode,
|
|
2310
|
+
summary: input.summary,
|
|
2311
|
+
});
|
|
2312
|
+
assertBoundedPublicText(input.nextRetryAt, 'outbox.nextRetryAt', 80);
|
|
2313
|
+
try {
|
|
2314
|
+
return this.runInTransaction(() => {
|
|
2315
|
+
const row = this.getOutboxTransitionRow(input.outboxId);
|
|
2316
|
+
if (!row)
|
|
2317
|
+
return outboxTransitionResult({ outboxId: input.outboxId, status: 'missing', changes: 0, replay: false, reasonCode: 'outbox-missing' });
|
|
2318
|
+
if (row.status === 'acked') {
|
|
2319
|
+
return outboxTransitionResult({ outboxId: input.outboxId, status: 'acked', changes: 0, replay: true, deliveryCursor: row.delivery_cursor ?? undefined, reasonCode: 'outbox-already-acked' });
|
|
2320
|
+
}
|
|
2321
|
+
if (row.status === 'dead_letter') {
|
|
2322
|
+
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 });
|
|
2323
|
+
}
|
|
2324
|
+
if (input.deliveryCursor && row.delivery_cursor && row.delivery_cursor !== input.deliveryCursor) {
|
|
2325
|
+
return outboxTransitionResult({ outboxId: input.outboxId, status: 'conflict', changes: 0, replay: false, deliveryCursor: row.delivery_cursor, reasonCode: 'outbox-fail-conflict' });
|
|
2326
|
+
}
|
|
2327
|
+
const attemptCount = Math.max(1, row.attempt_count ?? 0, row.retry_count ?? 0);
|
|
2328
|
+
const decision = computeContinuityOutboxFailureDecision({
|
|
2329
|
+
nowIso: input.failedAt,
|
|
2330
|
+
attemptCount,
|
|
2331
|
+
reasonCode: input.reasonCode,
|
|
2332
|
+
summary: input.summary,
|
|
2333
|
+
nextRetryAt: input.nextRetryAt,
|
|
2334
|
+
policy: input.policy,
|
|
2335
|
+
});
|
|
2336
|
+
const result = this.db.prepare(`
|
|
284
2337
|
UPDATE outbox
|
|
285
2338
|
SET status = ?,
|
|
286
2339
|
retry_count = MAX(retry_count, ?),
|
|
@@ -293,7 +2346,40 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
293
2346
|
updated_at = ?
|
|
294
2347
|
WHERE outbox_id = ?
|
|
295
2348
|
AND status IN ('pending', 'sent', 'failed')
|
|
296
|
-
`).run(
|
|
2349
|
+
`).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);
|
|
2350
|
+
return outboxTransitionResult({
|
|
2351
|
+
outboxId: input.outboxId,
|
|
2352
|
+
status: decision.status,
|
|
2353
|
+
changes: result.changes,
|
|
2354
|
+
replay: false,
|
|
2355
|
+
deliveryCursor: row.delivery_cursor ?? input.deliveryCursor,
|
|
2356
|
+
reasonCode: decision.reasonCode,
|
|
2357
|
+
...(decision.nextRetryAt ? { nextRetryAt: decision.nextRetryAt } : {}),
|
|
2358
|
+
...(decision.deadLetteredAt ? { deadLetteredAt: decision.deadLetteredAt } : {}),
|
|
2359
|
+
});
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
catch (error) {
|
|
2363
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
deadLetterOutbox(input) {
|
|
2367
|
+
this.assertCanMutate();
|
|
2368
|
+
assertOutboxIdentity({
|
|
2369
|
+
outboxId: input.outboxId,
|
|
2370
|
+
timestamp: input.deadLetteredAt,
|
|
2371
|
+
reasonCode: input.reasonCode,
|
|
2372
|
+
summary: input.summary,
|
|
2373
|
+
});
|
|
2374
|
+
try {
|
|
2375
|
+
return this.runInTransaction(() => {
|
|
2376
|
+
const row = this.getOutboxTransitionRow(input.outboxId);
|
|
2377
|
+
if (!row)
|
|
2378
|
+
return outboxTransitionResult({ outboxId: input.outboxId, status: 'missing', changes: 0, replay: false, reasonCode: 'outbox-missing' });
|
|
2379
|
+
if (row.status === 'dead_letter') {
|
|
2380
|
+
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 });
|
|
2381
|
+
}
|
|
2382
|
+
const result = this.db.prepare(`
|
|
297
2383
|
UPDATE outbox
|
|
298
2384
|
SET status = 'dead_letter',
|
|
299
2385
|
next_attempt_at = NULL,
|
|
@@ -305,13 +2391,39 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
305
2391
|
updated_at = ?
|
|
306
2392
|
WHERE outbox_id = ?
|
|
307
2393
|
AND status <> 'acked'
|
|
308
|
-
`).run(
|
|
2394
|
+
`).run(input.reasonCode, input.reasonCode, input.summary, input.summary, input.deadLetteredAt, input.deadLetteredAt, input.outboxId);
|
|
2395
|
+
return outboxTransitionResult({ outboxId: input.outboxId, status: 'dead_letter', changes: result.changes, replay: false, deliveryCursor: row.delivery_cursor ?? undefined, deadLetteredAt: input.deadLetteredAt, reasonCode: input.reasonCode });
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
catch (error) {
|
|
2399
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
retryOutboxEntries(options) {
|
|
2403
|
+
this.assertCanMutate();
|
|
2404
|
+
const limit = Math.max(1, Math.min(100, Math.floor(options.limit)));
|
|
2405
|
+
const statuses = uniqueValues(options.statuses?.length ? options.statuses : ['failed', 'dead_letter']);
|
|
2406
|
+
const reasonCode = options.reasonCode ?? 'backoff-scheduled';
|
|
2407
|
+
assertPublicSafeText(reasonCode, 'outbox.last_error_code');
|
|
2408
|
+
assertPublicSafeText(options.nextAttemptAt, 'outbox.next_attempt_at');
|
|
2409
|
+
try {
|
|
2410
|
+
return this.runInTransaction(() => {
|
|
2411
|
+
const placeholders = statuses.map(() => '?').join(', ');
|
|
2412
|
+
const rows = this.db.prepare(`
|
|
309
2413
|
SELECT outbox_id
|
|
310
2414
|
FROM outbox
|
|
311
|
-
WHERE status IN (${
|
|
2415
|
+
WHERE status IN (${placeholders})
|
|
312
2416
|
ORDER BY updated_at ASC, outbox_id ASC
|
|
313
2417
|
LIMIT ?
|
|
314
|
-
`).all(...
|
|
2418
|
+
`).all(...statuses, limit);
|
|
2419
|
+
const retriedOutboxIds = rows
|
|
2420
|
+
.map((row) => row.outbox_id)
|
|
2421
|
+
.filter((outboxId) => typeof outboxId === 'string' && outboxId.length > 0);
|
|
2422
|
+
if (retriedOutboxIds.length === 0) {
|
|
2423
|
+
return { retriedOutboxIds: [], changes: 0, reasonCode, publicSafe: true };
|
|
2424
|
+
}
|
|
2425
|
+
const idPlaceholders = retriedOutboxIds.map(() => '?').join(', ');
|
|
2426
|
+
const result = this.db.prepare(`
|
|
315
2427
|
UPDATE outbox
|
|
316
2428
|
SET status = 'pending',
|
|
317
2429
|
retry_count = retry_count + 1,
|
|
@@ -324,8 +2436,22 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
324
2436
|
dead_letter_summary = NULL,
|
|
325
2437
|
dead_lettered_at = NULL,
|
|
326
2438
|
updated_at = ?
|
|
327
|
-
WHERE outbox_id IN (${
|
|
328
|
-
`).run(
|
|
2439
|
+
WHERE outbox_id IN (${idPlaceholders})
|
|
2440
|
+
`).run(options.nextAttemptAt ?? options.nowIso, reasonCode, reasonCode, options.nowIso, ...retriedOutboxIds);
|
|
2441
|
+
return { retriedOutboxIds, changes: result.changes, reasonCode, publicSafe: true };
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
catch (error) {
|
|
2445
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
upsertGoalCampaign(input) {
|
|
2449
|
+
this.assertCanMutate();
|
|
2450
|
+
assertPublicSafeText(input.title, 'goal_campaigns.title');
|
|
2451
|
+
assertPublicSafeText(input.publicSummary, 'goal_campaigns.public_summary');
|
|
2452
|
+
const metadataJson = stringifyPublicMetadata(input.metadata);
|
|
2453
|
+
try {
|
|
2454
|
+
const result = this.db.prepare(`
|
|
329
2455
|
INSERT INTO goal_campaigns (
|
|
330
2456
|
campaign_id, job_id, status, title, public_summary, reason_code,
|
|
331
2457
|
created_at, updated_at, operation_id, idempotency_key, metadata_json
|
|
@@ -339,7 +2465,20 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
339
2465
|
operation_id = excluded.operation_id,
|
|
340
2466
|
idempotency_key = excluded.idempotency_key,
|
|
341
2467
|
metadata_json = excluded.metadata_json
|
|
342
|
-
`).run(
|
|
2468
|
+
`).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);
|
|
2469
|
+
return this.toWriteResult(result);
|
|
2470
|
+
}
|
|
2471
|
+
catch (error) {
|
|
2472
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
upsertGoalLedgerItem(input) {
|
|
2476
|
+
this.assertCanMutate();
|
|
2477
|
+
assertPublicSafeText(input.summary, 'goal_ledger_items.summary');
|
|
2478
|
+
const metadataJson = stringifyPublicMetadata(input.metadata);
|
|
2479
|
+
try {
|
|
2480
|
+
this.ensureGoalCampaign(input.campaignId, input.jobId, input.createdAt, input.updatedAt);
|
|
2481
|
+
const result = this.db.prepare(`
|
|
343
2482
|
INSERT INTO goal_ledger_items (
|
|
344
2483
|
item_id, campaign_id, operation_id, idempotency_key, item_type, status,
|
|
345
2484
|
summary, reason_code, created_at, updated_at, metadata_json
|
|
@@ -350,7 +2489,21 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
350
2489
|
reason_code = excluded.reason_code,
|
|
351
2490
|
updated_at = excluded.updated_at,
|
|
352
2491
|
metadata_json = excluded.metadata_json
|
|
353
|
-
`).run(
|
|
2492
|
+
`).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);
|
|
2493
|
+
return this.toWriteResult(result);
|
|
2494
|
+
}
|
|
2495
|
+
catch (error) {
|
|
2496
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
upsertGoalVerifier(input) {
|
|
2500
|
+
this.assertCanMutate();
|
|
2501
|
+
assertPublicSafeText(input.summary, 'goal_verifiers.summary');
|
|
2502
|
+
const metadataJson = stringifyPublicMetadata(input.metadata);
|
|
2503
|
+
try {
|
|
2504
|
+
const fallbackTimestamp = input.lastCheckedAt ?? this.now();
|
|
2505
|
+
this.ensureGoalCampaign(input.campaignId, input.jobId, fallbackTimestamp, fallbackTimestamp);
|
|
2506
|
+
const result = this.db.prepare(`
|
|
354
2507
|
INSERT INTO goal_verifiers (
|
|
355
2508
|
verifier_id, campaign_id, kind, status, summary, reason_code,
|
|
356
2509
|
last_checked_at, operation_id, idempotency_key, metadata_json
|
|
@@ -361,7 +2514,21 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
361
2514
|
reason_code = excluded.reason_code,
|
|
362
2515
|
last_checked_at = excluded.last_checked_at,
|
|
363
2516
|
metadata_json = excluded.metadata_json
|
|
364
|
-
`).run(
|
|
2517
|
+
`).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);
|
|
2518
|
+
return this.toWriteResult(result);
|
|
2519
|
+
}
|
|
2520
|
+
catch (error) {
|
|
2521
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
upsertGoalReviewGate(input) {
|
|
2525
|
+
this.assertCanMutate();
|
|
2526
|
+
assertPublicSafeText(input.summary, 'goal_review_gates.summary');
|
|
2527
|
+
const metadataJson = stringifyPublicMetadata(input.metadata);
|
|
2528
|
+
try {
|
|
2529
|
+
const fallbackTimestamp = this.now();
|
|
2530
|
+
this.ensureGoalCampaign(input.campaignId, input.jobId, fallbackTimestamp, fallbackTimestamp);
|
|
2531
|
+
const result = this.db.prepare(`
|
|
365
2532
|
INSERT INTO goal_review_gates (
|
|
366
2533
|
gate_id, campaign_id, status, summary, required, reason_code,
|
|
367
2534
|
operation_id, idempotency_key, metadata_json
|
|
@@ -372,7 +2539,45 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
372
2539
|
required = excluded.required,
|
|
373
2540
|
reason_code = excluded.reason_code,
|
|
374
2541
|
metadata_json = excluded.metadata_json
|
|
375
|
-
`).run(
|
|
2542
|
+
`).run(input.gateId, input.campaignId, input.status, input.summary ?? null, input.required ? 1 : 0, input.reasonCode ?? null, input.operationId ?? null, input.idempotencyKey ?? null, metadataJson);
|
|
2543
|
+
return this.toWriteResult(result);
|
|
2544
|
+
}
|
|
2545
|
+
catch (error) {
|
|
2546
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
recordOperationAudit(input) {
|
|
2550
|
+
return this.appendReceipt(input);
|
|
2551
|
+
}
|
|
2552
|
+
hasOperationRef(operationId, idempotencyKey) {
|
|
2553
|
+
this.assertCanMutate();
|
|
2554
|
+
const tables = [
|
|
2555
|
+
'jobs',
|
|
2556
|
+
'job_leases',
|
|
2557
|
+
'job_events',
|
|
2558
|
+
'job_receipts',
|
|
2559
|
+
'goal_campaigns',
|
|
2560
|
+
'goal_ledger_items',
|
|
2561
|
+
'goal_verifiers',
|
|
2562
|
+
'goal_review_gates',
|
|
2563
|
+
'loop_jobs',
|
|
2564
|
+
'artifacts',
|
|
2565
|
+
'state_snapshots',
|
|
2566
|
+
'headless_execution_attempts',
|
|
2567
|
+
'runner_config',
|
|
2568
|
+
'outbox',
|
|
2569
|
+
];
|
|
2570
|
+
const clauses = tables.map((table) => `SELECT 1 AS found FROM ${table} WHERE operation_id = ?${idempotencyKey ? ' OR idempotency_key = ?' : ''}`);
|
|
2571
|
+
const params = tables.flatMap(() => idempotencyKey ? [operationId, idempotencyKey] : [operationId]);
|
|
2572
|
+
const row = this.db.prepare(`${clauses.join(' UNION ALL ')} LIMIT 1`).get(...params);
|
|
2573
|
+
return Boolean(row);
|
|
2574
|
+
}
|
|
2575
|
+
jobExists(jobId) {
|
|
2576
|
+
const row = this.db.prepare('SELECT 1 AS found FROM jobs WHERE job_id = ? LIMIT 1').get(jobId);
|
|
2577
|
+
return Boolean(row);
|
|
2578
|
+
}
|
|
2579
|
+
getHeadlessExecutionAttempt(attemptId) {
|
|
2580
|
+
return this.db.prepare(`
|
|
376
2581
|
SELECT attempt_id, execution_id, job_id, directive_hash, directive_kind,
|
|
377
2582
|
command_class, target_kind, target_ref, story_id, epic_id,
|
|
378
2583
|
runtime_id, model_id, runtime_source, status, reason_code,
|
|
@@ -386,13 +2591,22 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
386
2591
|
FROM headless_execution_attempts
|
|
387
2592
|
WHERE attempt_id = ?
|
|
388
2593
|
LIMIT 1
|
|
389
|
-
`).get(
|
|
2594
|
+
`).get(attemptId);
|
|
2595
|
+
}
|
|
2596
|
+
getOutboxTransitionRow(outboxId) {
|
|
2597
|
+
const row = this.db.prepare(`
|
|
390
2598
|
SELECT outbox_id, status, delivery_cursor, retry_count, attempt_count,
|
|
391
2599
|
last_error_code, last_error_reason, dead_lettered_at
|
|
392
2600
|
FROM outbox
|
|
393
2601
|
WHERE outbox_id = ?
|
|
394
2602
|
LIMIT 1
|
|
395
|
-
`).get(
|
|
2603
|
+
`).get(outboxId);
|
|
2604
|
+
if (!row || !isRecord(row))
|
|
2605
|
+
return undefined;
|
|
2606
|
+
return row;
|
|
2607
|
+
}
|
|
2608
|
+
selectReceiptRetentionCandidates(input) {
|
|
2609
|
+
const rows = this.db.prepare(`
|
|
396
2610
|
WITH sized AS (
|
|
397
2611
|
SELECT receipt_id, job_id, status, reason_code, created_at,
|
|
398
2612
|
length(receipt_id) + length(job_id) + length(status) + length(created_at)
|
|
@@ -411,7 +2625,23 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
411
2625
|
WHERE created_at < ? OR retain_rank > ? OR cumulative_bytes > ?
|
|
412
2626
|
ORDER BY created_at ASC, receipt_id ASC
|
|
413
2627
|
LIMIT ?
|
|
414
|
-
`).all(
|
|
2628
|
+
`).all(input.cutoff, input.maxItems, input.maxBytes, input.limit);
|
|
2629
|
+
return rows.flatMap((row) => {
|
|
2630
|
+
if (typeof row.receipt_id !== 'string' || typeof row.job_id !== 'string')
|
|
2631
|
+
return [];
|
|
2632
|
+
return [{
|
|
2633
|
+
kind: 'receipt',
|
|
2634
|
+
id: row.receipt_id,
|
|
2635
|
+
jobId: row.job_id,
|
|
2636
|
+
status: typeof row.status === 'string' ? row.status : 'unknown',
|
|
2637
|
+
reasonCode: typeof row.reason_code === 'string' ? row.reason_code : 'continuity-ok',
|
|
2638
|
+
createdAt: typeof row.created_at === 'string' ? row.created_at : input.cutoff,
|
|
2639
|
+
approxBytes: typeof row.approx_bytes === 'number' && row.approx_bytes >= 0 ? Math.floor(row.approx_bytes) : 0,
|
|
2640
|
+
}];
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
2643
|
+
selectOutputRetentionCandidates(input) {
|
|
2644
|
+
const rows = this.db.prepare(`
|
|
415
2645
|
WITH sized AS (
|
|
416
2646
|
SELECT event_id, job_id, type, reason_code, created_at,
|
|
417
2647
|
length(event_id) + length(job_id) + length(operation_id) + length(type)
|
|
@@ -430,13 +2660,42 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
430
2660
|
WHERE created_at < ? OR retain_rank > ? OR cumulative_bytes > ?
|
|
431
2661
|
ORDER BY created_at ASC, event_id ASC
|
|
432
2662
|
LIMIT ?
|
|
433
|
-
`).all(
|
|
2663
|
+
`).all(CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE, input.cutoff, input.maxItems, input.maxBytes, input.limit);
|
|
2664
|
+
return rows.flatMap((row) => {
|
|
2665
|
+
if (typeof row.event_id !== 'string' || typeof row.job_id !== 'string')
|
|
2666
|
+
return [];
|
|
2667
|
+
return [{
|
|
2668
|
+
kind: 'output',
|
|
2669
|
+
id: row.event_id,
|
|
2670
|
+
jobId: row.job_id,
|
|
2671
|
+
status: typeof row.type === 'string' ? row.type : CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE,
|
|
2672
|
+
reasonCode: typeof row.reason_code === 'string' ? row.reason_code : 'continuity-ok',
|
|
2673
|
+
createdAt: typeof row.created_at === 'string' ? row.created_at : input.cutoff,
|
|
2674
|
+
approxBytes: typeof row.approx_bytes === 'number' && row.approx_bytes >= 0 ? Math.floor(row.approx_bytes) : 0,
|
|
2675
|
+
}];
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
deleteRetentionCandidates(table, idColumn, ids) {
|
|
2679
|
+
if (ids.length === 0)
|
|
2680
|
+
return;
|
|
2681
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
2682
|
+
this.db.prepare(`DELETE FROM ${table} WHERE ${idColumn} IN (${placeholders})`).run(...ids);
|
|
2683
|
+
}
|
|
2684
|
+
preserveReceiptDerivedIteration(jobId, at) {
|
|
2685
|
+
const row = this.db.prepare(`
|
|
434
2686
|
SELECT COUNT(*) AS iteration
|
|
435
2687
|
FROM job_receipts
|
|
436
2688
|
WHERE job_id = ?
|
|
437
2689
|
AND (operation_id LIKE 'tick-%' OR operation_id LIKE 'loop-tick-%')
|
|
438
2690
|
AND status IN ('accepted', 'applied')
|
|
439
|
-
`).get(
|
|
2691
|
+
`).get(jobId);
|
|
2692
|
+
const iteration = typeof row?.iteration === 'number' ? row.iteration : 0;
|
|
2693
|
+
if (iteration > 0) {
|
|
2694
|
+
this.recordJobStageProgress({ jobId, iteration, at, appendHistory: false });
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
retainedReceiptOutputCounts(jobId) {
|
|
2698
|
+
const receipt = this.db.prepare(`
|
|
440
2699
|
SELECT COUNT(*) AS count,
|
|
441
2700
|
COALESCE(SUM(length(receipt_id) + length(job_id) + length(status) + length(created_at)
|
|
442
2701
|
+ COALESCE(length(operation_id), 0) + COALESCE(length(idempotency_key), 0)
|
|
@@ -444,14 +2703,23 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
444
2703
|
+ COALESCE(length(ignored_operation_ids_json), 0) + COALESCE(length(metadata_json), 0)), 0) AS bytes
|
|
445
2704
|
FROM job_receipts
|
|
446
2705
|
WHERE job_id = ?
|
|
447
|
-
`).get(
|
|
2706
|
+
`).get(jobId);
|
|
2707
|
+
const output = this.db.prepare(`
|
|
448
2708
|
SELECT COUNT(*) AS count,
|
|
449
2709
|
COALESCE(SUM(length(event_id) + length(job_id) + length(operation_id) + length(type)
|
|
450
2710
|
+ length(summary) + length(created_at) + COALESCE(length(idempotency_key), 0)
|
|
451
2711
|
+ COALESCE(length(reason_code), 0) + COALESCE(length(metadata_json), 0)), 0) AS bytes
|
|
452
2712
|
FROM job_events
|
|
453
2713
|
WHERE job_id = ? AND type = ?
|
|
454
|
-
`).get(
|
|
2714
|
+
`).get(jobId, CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE);
|
|
2715
|
+
return {
|
|
2716
|
+
receipts: typeof receipt?.count === 'number' ? receipt.count : 0,
|
|
2717
|
+
outputs: typeof output?.count === 'number' ? output.count : 0,
|
|
2718
|
+
bytes: (typeof receipt?.bytes === 'number' ? receipt.bytes : 0) + (typeof output?.bytes === 'number' ? output.bytes : 0),
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
pruneReceiptCompactionSummaries(jobId, limit) {
|
|
2722
|
+
this.db.prepare(`
|
|
455
2723
|
DELETE FROM job_events
|
|
456
2724
|
WHERE event_id IN (
|
|
457
2725
|
SELECT event_id
|
|
@@ -460,20 +2728,69 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
460
2728
|
ORDER BY created_at DESC, event_id DESC
|
|
461
2729
|
LIMIT -1 OFFSET ?
|
|
462
2730
|
)
|
|
463
|
-
`).run(
|
|
2731
|
+
`).run(jobId, CONTINUITY_RECEIPT_COMPACTION_EVENT_TYPE, limit);
|
|
2732
|
+
}
|
|
2733
|
+
runInTransaction(fn) {
|
|
2734
|
+
this.assertCanMutate();
|
|
2735
|
+
const tx = this.db.transaction(fn);
|
|
2736
|
+
try {
|
|
2737
|
+
if (typeof tx.immediate === 'function')
|
|
2738
|
+
return tx.immediate();
|
|
2739
|
+
return tx();
|
|
2740
|
+
}
|
|
2741
|
+
catch (error) {
|
|
2742
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
collectSnapshotRows(options) {
|
|
2746
|
+
const countRows = (sql, ...params) => {
|
|
2747
|
+
const row = this.db.prepare(sql).get(...params);
|
|
2748
|
+
return isRecord(row) && typeof row.count === 'number' ? row.count : 0;
|
|
2749
|
+
};
|
|
2750
|
+
const outboxRows = this.db.prepare(`
|
|
464
2751
|
SELECT status, COUNT(*) AS count FROM outbox GROUP BY status
|
|
465
|
-
`).all()
|
|
2752
|
+
`).all();
|
|
2753
|
+
const outboxCounts = {};
|
|
2754
|
+
for (const row of outboxRows) {
|
|
2755
|
+
if (typeof row.status === 'string' && typeof row.count === 'number')
|
|
2756
|
+
outboxCounts[row.status] = row.count;
|
|
2757
|
+
}
|
|
2758
|
+
const queuePolicyRow = this.db.prepare(`
|
|
466
2759
|
SELECT value_json
|
|
467
2760
|
FROM runner_config
|
|
468
2761
|
WHERE key = 'headless_queue_policy'
|
|
469
2762
|
LIMIT 1
|
|
470
|
-
`).get();
|
|
2763
|
+
`).get();
|
|
2764
|
+
let queuePolicy = DEFAULT_CONTINUITY_QUEUE_POLICY;
|
|
2765
|
+
if (isRecord(queuePolicyRow) && typeof queuePolicyRow.value_json === 'string') {
|
|
2766
|
+
try {
|
|
2767
|
+
const parsed = JSON.parse(queuePolicyRow.value_json);
|
|
2768
|
+
if (isRecord(parsed) && typeof parsed.maxItems === 'number') {
|
|
2769
|
+
queuePolicy = normalizeContinuityQueuePolicy({ maxItems: parsed.maxItems });
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
catch {
|
|
2773
|
+
// SQLite json_valid guards this row; retain bounded defaults on unexpected parse failure.
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
const queueQueued = countRows("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status = 'queued' AND dead_lettered_at IS NULL");
|
|
2777
|
+
const queueRetrying = countRows("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status = 'backoff' AND dead_lettered_at IS NULL");
|
|
2778
|
+
const queueRunning = countRows("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status = 'running' AND dead_lettered_at IS NULL");
|
|
2779
|
+
const queueDeadLetter = countRows('SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE dead_lettered_at IS NOT NULL');
|
|
2780
|
+
const queueDepth = queueQueued + queueRetrying + queueRunning;
|
|
2781
|
+
const nextRetryRow = this.db.prepare(`
|
|
471
2782
|
SELECT MIN(next_retry_at) AS next_retry_at
|
|
472
2783
|
FROM headless_execution_attempts
|
|
473
2784
|
WHERE status = 'backoff'
|
|
474
2785
|
AND dead_lettered_at IS NULL
|
|
475
2786
|
AND next_retry_at IS NOT NULL
|
|
476
|
-
`).get()
|
|
2787
|
+
`).get();
|
|
2788
|
+
const nextRetryAt = isRecord(nextRetryRow) && typeof nextRetryRow.next_retry_at === 'string'
|
|
2789
|
+
? nextRetryRow.next_retry_at
|
|
2790
|
+
: undefined;
|
|
2791
|
+
return {
|
|
2792
|
+
schemaVersion: this.getSchemaVersion(),
|
|
2793
|
+
jobs: this.db.prepare(`
|
|
477
2794
|
SELECT job_id, kind, status, summary, reason_code, created_at, updated_at,
|
|
478
2795
|
next_run_at, metadata_json,
|
|
479
2796
|
(SELECT COUNT(*)
|
|
@@ -523,30 +2840,35 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
523
2840
|
FROM jobs
|
|
524
2841
|
ORDER BY updated_at DESC, job_id ASC
|
|
525
2842
|
LIMIT ?
|
|
526
|
-
`).all(
|
|
2843
|
+
`).all(options.jobLimit),
|
|
2844
|
+
staleLeases: this.db.prepare(`
|
|
527
2845
|
SELECT lease_id, job_id, runner_id, expires_at
|
|
528
2846
|
FROM job_leases
|
|
529
2847
|
WHERE released_at IS NULL AND expires_at <= ?
|
|
530
2848
|
ORDER BY expires_at ASC, lease_id ASC
|
|
531
2849
|
LIMIT ?
|
|
532
|
-
`).all(
|
|
2850
|
+
`).all(options.nowIso, options.staleLeaseLimit),
|
|
2851
|
+
receipts: this.db.prepare(`
|
|
533
2852
|
SELECT receipt_id, job_id, operation_id, status, reason_code, created_at, metadata_json
|
|
534
2853
|
FROM job_receipts
|
|
535
2854
|
ORDER BY created_at DESC, receipt_id ASC
|
|
536
2855
|
LIMIT ?
|
|
537
|
-
`).all(
|
|
2856
|
+
`).all(options.receiptLimit),
|
|
2857
|
+
events: this.db.prepare(`
|
|
538
2858
|
SELECT event_id, job_id, operation_id, type, summary, reason_code, created_at, metadata_json
|
|
539
2859
|
FROM job_events
|
|
540
2860
|
ORDER BY created_at DESC, event_id ASC
|
|
541
2861
|
LIMIT ?
|
|
542
|
-
`).all(
|
|
2862
|
+
`).all(options.eventLimit),
|
|
2863
|
+
stateSnapshots: this.db.prepare(`
|
|
543
2864
|
SELECT snapshot_id, job_id, kind, relative_path, sha256, summary,
|
|
544
2865
|
captured_at, operation_id, metadata_json
|
|
545
2866
|
FROM state_snapshots
|
|
546
2867
|
WHERE kind = 'context_compaction'
|
|
547
2868
|
ORDER BY captured_at DESC, snapshot_id ASC
|
|
548
2869
|
LIMIT ?
|
|
549
|
-
`).all(
|
|
2870
|
+
`).all(options.snapshotLimit),
|
|
2871
|
+
headlessExecutionAttempts: this.db.prepare(`
|
|
550
2872
|
SELECT attempt_id, execution_id, job_id, status, reason_code, runtime_id,
|
|
551
2873
|
model_id, retry_count, max_attempts, lease_id, runner_id,
|
|
552
2874
|
lease_expires_at, heartbeat_at, public_summary, enqueued_at,
|
|
@@ -557,19 +2879,90 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
557
2879
|
FROM headless_execution_attempts
|
|
558
2880
|
ORDER BY enqueued_at DESC, attempt_id ASC
|
|
559
2881
|
LIMIT ?
|
|
560
|
-
`).all(
|
|
2882
|
+
`).all(options.jobLimit),
|
|
2883
|
+
counts: {
|
|
2884
|
+
jobs: countRows('SELECT COUNT(*) AS count FROM jobs'),
|
|
2885
|
+
activeJobs: countRows("SELECT COUNT(*) AS count FROM jobs WHERE status IN ('planned', 'active', 'running', 'waiting_external', 'waiting_human', 'backoff', 'paused', 'blocked')"),
|
|
2886
|
+
activeLeases: countRows('SELECT COUNT(*) AS count FROM job_leases WHERE released_at IS NULL'),
|
|
2887
|
+
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),
|
|
2888
|
+
staleLeases: countRows('SELECT COUNT(*) AS count FROM job_leases WHERE released_at IS NULL AND expires_at <= ?', options.nowIso),
|
|
2889
|
+
receipts: countRows('SELECT COUNT(*) AS count FROM job_receipts'),
|
|
2890
|
+
events: countRows('SELECT COUNT(*) AS count FROM job_events'),
|
|
2891
|
+
processingOutputSummaries: countRows('SELECT COUNT(*) AS count FROM job_events WHERE type = ?', CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE),
|
|
2892
|
+
goalLedgerItems: countRows('SELECT COUNT(*) AS count FROM goal_ledger_items'),
|
|
2893
|
+
goalVerifiers: countRows('SELECT COUNT(*) AS count FROM goal_verifiers'),
|
|
2894
|
+
goalReviewGates: countRows('SELECT COUNT(*) AS count FROM goal_review_gates'),
|
|
2895
|
+
stateSnapshots: countRows('SELECT COUNT(*) AS count FROM state_snapshots'),
|
|
2896
|
+
headlessExecutionAttempts: countRows('SELECT COUNT(*) AS count FROM headless_execution_attempts'),
|
|
2897
|
+
headlessQueuedAttempts: countRows("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status IN ('queued', 'backoff')"),
|
|
2898
|
+
headlessRunningAttempts: countRows("SELECT COUNT(*) AS count FROM headless_execution_attempts WHERE status = 'running'"),
|
|
2899
|
+
headlessRetryingAttempts: queueRetrying,
|
|
2900
|
+
headlessDeadLetterAttempts: queueDeadLetter,
|
|
2901
|
+
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),
|
|
2902
|
+
outbox: countRows('SELECT COUNT(*) AS count FROM outbox'),
|
|
2903
|
+
},
|
|
2904
|
+
outboxCounts,
|
|
2905
|
+
queue: {
|
|
2906
|
+
depth: queueDepth,
|
|
2907
|
+
queued: queueQueued,
|
|
2908
|
+
running: queueRunning,
|
|
2909
|
+
retrying: queueRetrying,
|
|
2910
|
+
deadLetter: queueDeadLetter,
|
|
2911
|
+
capacity: queuePolicy.maxItems,
|
|
2912
|
+
available: Math.max(0, queuePolicy.maxItems - queueDepth),
|
|
2913
|
+
backpressure: queueDepth >= queuePolicy.maxItems,
|
|
2914
|
+
generatedAt: options.nowIso,
|
|
2915
|
+
...(nextRetryAt ? { nextRetryAt } : {}),
|
|
2916
|
+
...(queueDepth >= queuePolicy.maxItems ? { reasonCode: CONTINUITY_QUEUE_REASON_CODES.CAP_REACHED } : {}),
|
|
2917
|
+
publicSafe: true,
|
|
2918
|
+
},
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
listProcessingOutputSummaries(options = {}) {
|
|
2922
|
+
const limit = normalizeProcessingSummaryLimit(options.limit);
|
|
2923
|
+
const targetPredicate = processingSummaryTargetPredicate(options);
|
|
2924
|
+
const selectSql = `
|
|
561
2925
|
SELECT event_id, job_id, operation_id, summary, reason_code, created_at, metadata_json
|
|
562
2926
|
FROM job_events
|
|
563
|
-
WHERE type = ?${
|
|
2927
|
+
WHERE type = ?${targetPredicate.sql}
|
|
564
2928
|
ORDER BY created_at DESC, event_id DESC
|
|
565
2929
|
LIMIT ?
|
|
566
|
-
`;
|
|
2930
|
+
`;
|
|
2931
|
+
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);
|
|
2932
|
+
return rows.map(toProcessingOutputSummaryRow);
|
|
2933
|
+
}
|
|
2934
|
+
pollProcessingOutputSummaries(options = {}) {
|
|
2935
|
+
const limit = normalizeProcessingSummaryLimit(options.limit);
|
|
2936
|
+
const decodedCursor = decodeProcessingSummaryCursor(options.cursor);
|
|
2937
|
+
const targetPredicate = processingSummaryTargetPredicate(options);
|
|
2938
|
+
const cursorPredicate = decodedCursor
|
|
2939
|
+
? ' AND (created_at > ? OR (created_at = ? AND event_id > ?))'
|
|
2940
|
+
: '';
|
|
2941
|
+
const sql = `
|
|
567
2942
|
SELECT event_id, job_id, operation_id, summary, reason_code, created_at, metadata_json
|
|
568
2943
|
FROM job_events
|
|
569
|
-
WHERE type = ?${
|
|
2944
|
+
WHERE type = ?${targetPredicate.sql}${cursorPredicate}
|
|
570
2945
|
ORDER BY created_at ASC, event_id ASC
|
|
571
2946
|
LIMIT ?
|
|
572
|
-
|
|
2947
|
+
`;
|
|
2948
|
+
const params = [CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE];
|
|
2949
|
+
params.push(...targetPredicate.params);
|
|
2950
|
+
if (decodedCursor)
|
|
2951
|
+
params.push(decodedCursor.createdAt, decodedCursor.createdAt, decodedCursor.eventId);
|
|
2952
|
+
params.push(limit + 1);
|
|
2953
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
2954
|
+
const summaries = rows.slice(0, limit).map(toProcessingOutputSummaryRow);
|
|
2955
|
+
const last = summaries[summaries.length - 1];
|
|
2956
|
+
return {
|
|
2957
|
+
summaries,
|
|
2958
|
+
...(last ? { nextCursor: last.cursor } : options.cursor ? { nextCursor: options.cursor } : {}),
|
|
2959
|
+
hasMore: rows.length > limit,
|
|
2960
|
+
publicSafe: true,
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
listDueJobs(options) {
|
|
2964
|
+
const limit = Math.max(1, Math.min(50, Math.floor(options.limit)));
|
|
2965
|
+
return this.db.prepare(`
|
|
573
2966
|
SELECT j.job_id, j.kind, j.status, j.summary, j.reason_code, j.created_at,
|
|
574
2967
|
j.updated_at, j.next_run_at, j.metadata_json,
|
|
575
2968
|
(SELECT COUNT(*)
|
|
@@ -587,19 +2980,63 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
587
2980
|
AND j.status NOT IN ('waiting_external', 'waiting_human', 'paused', 'blocked', 'completed', 'aborted', 'cancelled')
|
|
588
2981
|
ORDER BY j.next_run_at ASC, j.job_id ASC
|
|
589
2982
|
LIMIT ?
|
|
590
|
-
`).all(
|
|
2983
|
+
`).all(options.nowIso, limit);
|
|
2984
|
+
}
|
|
2985
|
+
recoverStaleLeases(options) {
|
|
2986
|
+
this.assertCanMutate();
|
|
2987
|
+
const nowMs = Date.parse(options.nowIso);
|
|
2988
|
+
if (!Number.isFinite(nowMs)) {
|
|
2989
|
+
throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Continuity SQLite stale lease recovery received an invalid timestamp.');
|
|
2990
|
+
}
|
|
2991
|
+
const staleAfterMs = Math.max(0, Math.floor(options.staleAfterMs));
|
|
2992
|
+
const limit = Math.max(1, Math.min(100, Math.floor(options.limit)));
|
|
2993
|
+
const safeExpiredAtOrBefore = new Date(nowMs - staleAfterMs).toISOString();
|
|
2994
|
+
try {
|
|
2995
|
+
return this.runInTransaction(() => {
|
|
2996
|
+
const rows = this.db.prepare(`
|
|
591
2997
|
SELECT lease_id
|
|
592
2998
|
FROM job_leases
|
|
593
2999
|
WHERE released_at IS NULL
|
|
594
3000
|
AND expires_at <= ?
|
|
595
3001
|
ORDER BY expires_at ASC, lease_id ASC
|
|
596
3002
|
LIMIT ?
|
|
597
|
-
`).all(
|
|
3003
|
+
`).all(safeExpiredAtOrBefore, limit);
|
|
3004
|
+
const recoveredLeaseIds = rows
|
|
3005
|
+
.map((row) => row.lease_id)
|
|
3006
|
+
.filter((leaseId) => typeof leaseId === 'string' && leaseId.length > 0);
|
|
3007
|
+
if (recoveredLeaseIds.length === 0) {
|
|
3008
|
+
return { recoveredLeaseIds: [], safeExpiredAtOrBefore, changes: 0 };
|
|
3009
|
+
}
|
|
3010
|
+
const placeholders = recoveredLeaseIds.map(() => '?').join(', ');
|
|
3011
|
+
const result = this.db.prepare(`
|
|
598
3012
|
UPDATE job_leases
|
|
599
3013
|
SET status = 'expired', released_at = ?
|
|
600
3014
|
WHERE released_at IS NULL
|
|
601
|
-
AND lease_id IN (${
|
|
602
|
-
`).run(
|
|
3015
|
+
AND lease_id IN (${placeholders})
|
|
3016
|
+
`).run(options.nowIso, ...recoveredLeaseIds);
|
|
3017
|
+
return {
|
|
3018
|
+
recoveredLeaseIds,
|
|
3019
|
+
safeExpiredAtOrBefore,
|
|
3020
|
+
changes: result.changes,
|
|
3021
|
+
};
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
catch (error) {
|
|
3025
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
recoverStaleHeadlessExecutionAttempts(options) {
|
|
3029
|
+
this.assertCanMutate();
|
|
3030
|
+
const nowMs = Date.parse(options.nowIso);
|
|
3031
|
+
if (!Number.isFinite(nowMs)) {
|
|
3032
|
+
throw new ContinuitySqliteStoreError('sqlite-public-safety-rejected', 'Continuity SQLite headless stale recovery received an invalid timestamp.');
|
|
3033
|
+
}
|
|
3034
|
+
const staleAfterMs = Math.max(0, Math.floor(options.staleAfterMs));
|
|
3035
|
+
const limit = Math.max(1, Math.min(100, Math.floor(options.limit)));
|
|
3036
|
+
const safeExpiredAtOrBefore = new Date(nowMs - staleAfterMs).toISOString();
|
|
3037
|
+
try {
|
|
3038
|
+
return this.runInTransaction(() => {
|
|
3039
|
+
const rows = this.db.prepare(`
|
|
603
3040
|
SELECT attempt_id, retry_count, max_attempts, attempt_count
|
|
604
3041
|
FROM headless_execution_attempts
|
|
605
3042
|
WHERE status = 'running'
|
|
@@ -607,7 +3044,30 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
607
3044
|
AND lease_expires_at <= ?
|
|
608
3045
|
ORDER BY lease_expires_at ASC, attempt_id ASC
|
|
609
3046
|
LIMIT ?
|
|
610
|
-
`).all(
|
|
3047
|
+
`).all(safeExpiredAtOrBefore, limit);
|
|
3048
|
+
const retryableAttempts = [];
|
|
3049
|
+
const deadLetteredAttemptIds = [];
|
|
3050
|
+
const policy = normalizeContinuityQueuePolicy(options.policy);
|
|
3051
|
+
for (const row of rows) {
|
|
3052
|
+
if (typeof row.attempt_id !== 'string' || row.attempt_id.length === 0)
|
|
3053
|
+
continue;
|
|
3054
|
+
const retryCount = typeof row.retry_count === 'number' ? row.retry_count : 0;
|
|
3055
|
+
const maxAttempts = typeof row.max_attempts === 'number' ? row.max_attempts : 1;
|
|
3056
|
+
const attemptCount = typeof row.attempt_count === 'number' ? row.attempt_count : 0;
|
|
3057
|
+
const consumedAttempts = Math.max(attemptCount, retryCount + 1);
|
|
3058
|
+
if (consumedAttempts >= maxAttempts) {
|
|
3059
|
+
deadLetteredAttemptIds.push(row.attempt_id);
|
|
3060
|
+
}
|
|
3061
|
+
else {
|
|
3062
|
+
retryableAttempts.push({
|
|
3063
|
+
attemptId: row.attempt_id,
|
|
3064
|
+
nextRetryAt: computeContinuityQueueRetryAt(options.nowIso, retryCount + 1, policy),
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
let changes = 0;
|
|
3069
|
+
for (const retryable of retryableAttempts) {
|
|
3070
|
+
changes += this.db.prepare(`
|
|
611
3071
|
UPDATE headless_execution_attempts
|
|
612
3072
|
SET status = 'backoff',
|
|
613
3073
|
retry_count = retry_count + 1,
|
|
@@ -618,7 +3078,11 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
618
3078
|
WHERE attempt_id = ?
|
|
619
3079
|
AND status = 'running'
|
|
620
3080
|
AND lease_released_at IS NULL
|
|
621
|
-
`).run(
|
|
3081
|
+
`).run(CONTINUITY_QUEUE_REASON_CODES.LEASE_TIMEOUT_RETRY, options.nowIso, retryable.nextRetryAt, retryable.nextRetryAt, retryable.attemptId).changes;
|
|
3082
|
+
}
|
|
3083
|
+
if (deadLetteredAttemptIds.length > 0) {
|
|
3084
|
+
const placeholders = deadLetteredAttemptIds.map(() => '?').join(', ');
|
|
3085
|
+
changes += this.db.prepare(`
|
|
622
3086
|
UPDATE headless_execution_attempts
|
|
623
3087
|
SET status = 'failed',
|
|
624
3088
|
retry_count = retry_count + 1,
|
|
@@ -628,18 +3092,85 @@ import{Buffer as J}from"node:buffer";import{createHash as M}from"node:crypto";im
|
|
|
628
3092
|
dead_lettered_at = ?,
|
|
629
3093
|
dead_letter_reason_code = ?,
|
|
630
3094
|
next_retry_at = NULL
|
|
631
|
-
WHERE attempt_id IN (${
|
|
3095
|
+
WHERE attempt_id IN (${placeholders})
|
|
632
3096
|
AND status = 'running'
|
|
633
3097
|
AND lease_released_at IS NULL
|
|
634
|
-
`).run(
|
|
3098
|
+
`).run(CONTINUITY_QUEUE_REASON_CODES.ATTEMPTS_EXHAUSTED, options.nowIso, options.nowIso, options.nowIso, CONTINUITY_QUEUE_REASON_CODES.ATTEMPTS_EXHAUSTED, ...deadLetteredAttemptIds).changes;
|
|
3099
|
+
}
|
|
3100
|
+
const retryingAttemptIds = retryableAttempts.map((attempt) => attempt.attemptId);
|
|
3101
|
+
return {
|
|
3102
|
+
recoveredAttemptIds: [...retryingAttemptIds, ...deadLetteredAttemptIds],
|
|
3103
|
+
failedAttemptIds: deadLetteredAttemptIds,
|
|
3104
|
+
retryingAttemptIds,
|
|
3105
|
+
deadLetteredAttemptIds,
|
|
3106
|
+
safeExpiredAtOrBefore,
|
|
3107
|
+
changes,
|
|
3108
|
+
publicSafe: true,
|
|
3109
|
+
};
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
catch (error) {
|
|
3113
|
+
throw mapSqliteError(error, 'sqlite-mutation-failed');
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
ensureGoalCampaign(campaignId, jobId, createdAt, updatedAt) {
|
|
3117
|
+
this.db.prepare(`
|
|
635
3118
|
INSERT OR IGNORE INTO goal_campaigns (
|
|
636
3119
|
campaign_id, job_id, status, public_summary, created_at, updated_at
|
|
637
3120
|
) VALUES (?, ?, 'active', 'Public goal campaign summary unavailable', ?, ?)
|
|
638
|
-
`).run(
|
|
3121
|
+
`).run(campaignId, jobId, createdAt, updatedAt);
|
|
3122
|
+
}
|
|
3123
|
+
configureConnection() {
|
|
3124
|
+
const journalMode = String(asSinglePragmaValue(this.db.pragma('journal_mode = WAL', { simple: true })) ?? '').toLowerCase();
|
|
3125
|
+
if (journalMode !== 'wal') {
|
|
3126
|
+
throw new ContinuitySqliteStoreError('sqlite-migration-failed', `Continuity SQLite journal_mode did not switch to WAL (observed ${journalMode || 'unknown'}).`);
|
|
3127
|
+
}
|
|
3128
|
+
this.db.pragma('foreign_keys = ON');
|
|
3129
|
+
this.db.pragma(`busy_timeout = ${this.busyTimeoutMs}`);
|
|
3130
|
+
const foreignKeys = Number(asSinglePragmaValue(this.db.pragma('foreign_keys', { simple: true })) ?? 0);
|
|
3131
|
+
if (foreignKeys !== 1) {
|
|
3132
|
+
throw new ContinuitySqliteStoreError('sqlite-migration-failed', 'Continuity SQLite foreign_keys pragma is not enabled.');
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
ensureMigrationTable() {
|
|
3136
|
+
this.db.exec(`
|
|
639
3137
|
CREATE TABLE IF NOT EXISTS continuity_migrations (
|
|
640
3138
|
version INTEGER PRIMARY KEY,
|
|
641
3139
|
name TEXT NOT NULL,
|
|
642
3140
|
checksum TEXT NOT NULL,
|
|
643
3141
|
applied_at TEXT NOT NULL
|
|
644
3142
|
);
|
|
645
|
-
`)
|
|
3143
|
+
`);
|
|
3144
|
+
}
|
|
3145
|
+
applyMigrations() {
|
|
3146
|
+
const apply = this.db.transaction(() => {
|
|
3147
|
+
for (const migration of CONTINUITY_MIGRATIONS) {
|
|
3148
|
+
const checksum = checksumMigration(migration.sql);
|
|
3149
|
+
const existing = this.db.prepare('SELECT version, name, checksum FROM continuity_migrations WHERE version = ?').get(migration.version);
|
|
3150
|
+
if (existing) {
|
|
3151
|
+
if (existing.checksum !== checksum || existing.name !== migration.name) {
|
|
3152
|
+
throw new ContinuitySqliteStoreError('sqlite-migration-conflict', `Continuity SQLite migration ${migration.version} checksum conflict.`);
|
|
3153
|
+
}
|
|
3154
|
+
continue;
|
|
3155
|
+
}
|
|
3156
|
+
this.db.exec(migration.sql);
|
|
3157
|
+
this.db.prepare('INSERT INTO continuity_migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)').run(migration.version, migration.name, checksum, this.now());
|
|
3158
|
+
this.db.pragma(`user_version = ${migration.version}`);
|
|
3159
|
+
}
|
|
3160
|
+
});
|
|
3161
|
+
apply();
|
|
3162
|
+
}
|
|
3163
|
+
assertCanMutate() {
|
|
3164
|
+
if (this.mutationBlocked) {
|
|
3165
|
+
throw new ContinuitySqliteStoreError('sqlite-mutation-blocked', 'Continuity SQLite mutation is blocked after integrity failure.', { mutationBlocked: true });
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
toWriteResult(result) {
|
|
3169
|
+
const inserted = result.changes > 0;
|
|
3170
|
+
return {
|
|
3171
|
+
inserted,
|
|
3172
|
+
replay: !inserted,
|
|
3173
|
+
changes: result.changes,
|
|
3174
|
+
};
|
|
3175
|
+
}
|
|
3176
|
+
}
|