hippo-memory 1.44.0 → 1.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -24
- package/bin/hippo.js +3 -1
- package/dist/ablation.d.ts +10 -1
- package/dist/ablation.js +17 -1
- package/dist/api.d.ts +81 -22
- package/dist/api.js +258 -49
- package/dist/audit.d.ts +2 -2
- package/dist/audit.js +9 -0
- package/dist/autolearn.d.ts +1 -1
- package/dist/autolearn.js +2 -1
- package/dist/capture-error.d.ts +20 -0
- package/dist/capture-error.js +82 -0
- package/dist/capture.d.ts +25 -8
- package/dist/capture.js +100 -5
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +531 -119
- package/dist/client.d.ts +4 -21
- package/dist/client.js +3 -112
- package/dist/config.d.ts +20 -0
- package/dist/config.js +37 -1
- package/dist/consolidate.d.ts +9 -0
- package/dist/consolidate.js +133 -46
- package/dist/dag.d.ts +1 -0
- package/dist/dag.js +10 -5
- package/dist/dashboard.js +7 -3
- package/dist/db.js +69 -24
- package/dist/dedupe.d.ts +1 -0
- package/dist/dedupe.js +4 -7
- package/dist/doctor.d.ts +34 -0
- package/dist/doctor.js +174 -0
- package/dist/dormant.d.ts +91 -0
- package/dist/dormant.js +121 -0
- package/dist/embedding-provider.js +2 -1
- package/dist/embeddings.js +81 -8
- package/dist/eval-stats.d.ts +123 -0
- package/dist/eval-stats.js +187 -0
- package/dist/extract.d.ts +2 -0
- package/dist/extract.js +9 -4
- package/dist/half-life-migration.d.ts +55 -0
- package/dist/half-life-migration.js +111 -0
- package/dist/hooks.d.ts +4 -0
- package/dist/hooks.js +47 -0
- package/dist/mcp/server.d.ts +6 -0
- package/dist/mcp/server.js +90 -48
- package/dist/memory.d.ts +18 -1
- package/dist/memory.js +33 -5
- package/dist/physics-config.js +5 -1
- package/dist/recall-scope.d.ts +24 -0
- package/dist/recall-scope.js +41 -0
- package/dist/refine-llm.js +3 -2
- package/dist/reject-flow.d.ts +3 -3
- package/dist/reject-flow.js +10 -3
- package/dist/rerankers/jev.js +3 -2
- package/dist/rerankers/llm.js +3 -2
- package/dist/search.d.ts +4 -4
- package/dist/search.js +26 -21
- package/dist/server.d.ts +5 -0
- package/dist/server.js +49 -29
- package/dist/shared.js +1 -1
- package/dist/store.d.ts +29 -11
- package/dist/store.js +197 -70
- package/dist/token-ledger.d.ts +119 -0
- package/dist/token-ledger.js +181 -0
- package/dist/version.d.ts +4 -6
- package/dist/version.js +4 -7
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +7 -3
- package/dist/ablation.d.ts.map +0 -1
- package/dist/ablation.js.map +0 -1
- package/dist/ambient.d.ts.map +0 -1
- package/dist/ambient.js.map +0 -1
- package/dist/api.d.ts.map +0 -1
- package/dist/api.js.map +0 -1
- package/dist/audit-prune.d.ts.map +0 -1
- package/dist/audit-prune.js.map +0 -1
- package/dist/audit.d.ts.map +0 -1
- package/dist/audit.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/autolearn.d.ts.map +0 -1
- package/dist/autolearn.js.map +0 -1
- package/dist/availability.d.ts.map +0 -1
- package/dist/availability.js.map +0 -1
- package/dist/benchmarks/e1.3/incident-recall-eval.js +0 -78
- package/dist/benchmarks/e1.3/incident-recall-eval.js.map +0 -1
- package/dist/benchmarks/e1.3/slack-1000-event-smoke.js +0 -103
- package/dist/benchmarks/e1.3/slack-1000-event-smoke.js.map +0 -1
- package/dist/capture.d.ts.map +0 -1
- package/dist/capture.js.map +0 -1
- package/dist/card-detail.d.ts.map +0 -1
- package/dist/card-detail.js.map +0 -1
- package/dist/card.d.ts.map +0 -1
- package/dist/card.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/compare.d.ts.map +0 -1
- package/dist/compare.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/connectors/github/backfill.d.ts.map +0 -1
- package/dist/connectors/github/backfill.js.map +0 -1
- package/dist/connectors/github/cli-impl.d.ts.map +0 -1
- package/dist/connectors/github/cli-impl.js.map +0 -1
- package/dist/connectors/github/deletion.d.ts.map +0 -1
- package/dist/connectors/github/deletion.js.map +0 -1
- package/dist/connectors/github/dlq.d.ts.map +0 -1
- package/dist/connectors/github/dlq.js.map +0 -1
- package/dist/connectors/github/idempotency.d.ts.map +0 -1
- package/dist/connectors/github/idempotency.js.map +0 -1
- package/dist/connectors/github/ingest.d.ts.map +0 -1
- package/dist/connectors/github/ingest.js.map +0 -1
- package/dist/connectors/github/octokit-client.d.ts.map +0 -1
- package/dist/connectors/github/octokit-client.js.map +0 -1
- package/dist/connectors/github/ratelimit.d.ts.map +0 -1
- package/dist/connectors/github/ratelimit.js.map +0 -1
- package/dist/connectors/github/scope.d.ts.map +0 -1
- package/dist/connectors/github/scope.js.map +0 -1
- package/dist/connectors/github/signature.d.ts.map +0 -1
- package/dist/connectors/github/signature.js.map +0 -1
- package/dist/connectors/github/tenant-routing.d.ts.map +0 -1
- package/dist/connectors/github/tenant-routing.js.map +0 -1
- package/dist/connectors/github/transform.d.ts.map +0 -1
- package/dist/connectors/github/transform.js.map +0 -1
- package/dist/connectors/github/types.d.ts.map +0 -1
- package/dist/connectors/github/types.js.map +0 -1
- package/dist/connectors/slack/backfill.d.ts.map +0 -1
- package/dist/connectors/slack/backfill.js.map +0 -1
- package/dist/connectors/slack/deletion.d.ts.map +0 -1
- package/dist/connectors/slack/deletion.js.map +0 -1
- package/dist/connectors/slack/dlq.d.ts.map +0 -1
- package/dist/connectors/slack/dlq.js.map +0 -1
- package/dist/connectors/slack/idempotency.d.ts.map +0 -1
- package/dist/connectors/slack/idempotency.js.map +0 -1
- package/dist/connectors/slack/ingest.d.ts.map +0 -1
- package/dist/connectors/slack/ingest.js.map +0 -1
- package/dist/connectors/slack/ratelimit.d.ts.map +0 -1
- package/dist/connectors/slack/ratelimit.js.map +0 -1
- package/dist/connectors/slack/scope.d.ts.map +0 -1
- package/dist/connectors/slack/scope.js.map +0 -1
- package/dist/connectors/slack/signature.d.ts.map +0 -1
- package/dist/connectors/slack/signature.js.map +0 -1
- package/dist/connectors/slack/tenant-routing.d.ts.map +0 -1
- package/dist/connectors/slack/tenant-routing.js.map +0 -1
- package/dist/connectors/slack/transform.d.ts.map +0 -1
- package/dist/connectors/slack/transform.js.map +0 -1
- package/dist/connectors/slack/types.d.ts.map +0 -1
- package/dist/connectors/slack/types.js.map +0 -1
- package/dist/connectors/slack/web-client.d.ts.map +0 -1
- package/dist/connectors/slack/web-client.js.map +0 -1
- package/dist/connectors/slack/workspaces.d.ts.map +0 -1
- package/dist/connectors/slack/workspaces.js.map +0 -1
- package/dist/consolidate.d.ts.map +0 -1
- package/dist/consolidate.js.map +0 -1
- package/dist/correction-latency.d.ts.map +0 -1
- package/dist/correction-latency.js.map +0 -1
- package/dist/customer-notes.d.ts.map +0 -1
- package/dist/customer-notes.js.map +0 -1
- package/dist/dag.d.ts.map +0 -1
- package/dist/dag.js.map +0 -1
- package/dist/dashboard.d.ts.map +0 -1
- package/dist/dashboard.js.map +0 -1
- package/dist/db.d.ts.map +0 -1
- package/dist/db.js.map +0 -1
- package/dist/decisions.d.ts.map +0 -1
- package/dist/decisions.js.map +0 -1
- package/dist/dedupe.d.ts.map +0 -1
- package/dist/dedupe.js.map +0 -1
- package/dist/embedding-provider.d.ts.map +0 -1
- package/dist/embedding-provider.js.map +0 -1
- package/dist/embeddings.d.ts.map +0 -1
- package/dist/embeddings.js.map +0 -1
- package/dist/eval-suite.d.ts.map +0 -1
- package/dist/eval-suite.js.map +0 -1
- package/dist/eval.d.ts.map +0 -1
- package/dist/eval.js.map +0 -1
- package/dist/extensions/openclaw-plugin/index.js.map +0 -1
- package/dist/extract.d.ts.map +0 -1
- package/dist/extract.js.map +0 -1
- package/dist/forward-claim-detector.d.ts.map +0 -1
- package/dist/forward-claim-detector.js.map +0 -1
- package/dist/goals.d.ts.map +0 -1
- package/dist/goals.js.map +0 -1
- package/dist/graph-extract.d.ts.map +0 -1
- package/dist/graph-extract.js.map +0 -1
- package/dist/graph-recall.d.ts.map +0 -1
- package/dist/graph-recall.js.map +0 -1
- package/dist/graph-stream.d.ts.map +0 -1
- package/dist/graph-stream.js.map +0 -1
- package/dist/graph-view.d.ts.map +0 -1
- package/dist/graph-view.js.map +0 -1
- package/dist/graph.d.ts.map +0 -1
- package/dist/graph.js.map +0 -1
- package/dist/handoff.d.ts.map +0 -1
- package/dist/handoff.js.map +0 -1
- package/dist/hooks.d.ts.map +0 -1
- package/dist/hooks.js.map +0 -1
- package/dist/importers.d.ts.map +0 -1
- package/dist/importers.js.map +0 -1
- package/dist/incidents.d.ts.map +0 -1
- package/dist/incidents.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/invalidation.d.ts.map +0 -1
- package/dist/invalidation.js.map +0 -1
- package/dist/mcp/framing.d.ts.map +0 -1
- package/dist/mcp/framing.js.map +0 -1
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js.map +0 -1
- package/dist/memory-value-weights.d.ts.map +0 -1
- package/dist/memory-value-weights.js.map +0 -1
- package/dist/memory-value.d.ts.map +0 -1
- package/dist/memory-value.js.map +0 -1
- package/dist/memory.d.ts.map +0 -1
- package/dist/memory.js.map +0 -1
- package/dist/multihop.d.ts.map +0 -1
- package/dist/multihop.js.map +0 -1
- package/dist/owner-validation.d.ts.map +0 -1
- package/dist/owner-validation.js.map +0 -1
- package/dist/path-context.d.ts.map +0 -1
- package/dist/path-context.js.map +0 -1
- package/dist/physics-config.d.ts.map +0 -1
- package/dist/physics-config.js.map +0 -1
- package/dist/physics-state.d.ts.map +0 -1
- package/dist/physics-state.js.map +0 -1
- package/dist/physics.d.ts.map +0 -1
- package/dist/physics.js.map +0 -1
- package/dist/policies.d.ts.map +0 -1
- package/dist/policies.js.map +0 -1
- package/dist/postinstall.d.ts.map +0 -1
- package/dist/postinstall.js.map +0 -1
- package/dist/predictions.d.ts.map +0 -1
- package/dist/predictions.js.map +0 -1
- package/dist/processes.d.ts.map +0 -1
- package/dist/processes.js.map +0 -1
- package/dist/project-briefs.d.ts.map +0 -1
- package/dist/project-briefs.js.map +0 -1
- package/dist/project-identity.d.ts.map +0 -1
- package/dist/project-identity.js.map +0 -1
- package/dist/provenance-coverage.d.ts.map +0 -1
- package/dist/provenance-coverage.js.map +0 -1
- package/dist/rate-limit.d.ts.map +0 -1
- package/dist/rate-limit.js.map +0 -1
- package/dist/raw-archive-mirror-cleanup.d.ts.map +0 -1
- package/dist/raw-archive-mirror-cleanup.js.map +0 -1
- package/dist/raw-archive.d.ts.map +0 -1
- package/dist/raw-archive.js.map +0 -1
- package/dist/recall-history.d.ts.map +0 -1
- package/dist/recall-history.js.map +0 -1
- package/dist/recall-scope.d.ts.map +0 -1
- package/dist/recall-scope.js.map +0 -1
- package/dist/recall-trace.d.ts.map +0 -1
- package/dist/recall-trace.js.map +0 -1
- package/dist/refine-llm.d.ts.map +0 -1
- package/dist/refine-llm.js.map +0 -1
- package/dist/reject-flow.d.ts.map +0 -1
- package/dist/reject-flow.js.map +0 -1
- package/dist/rejection.d.ts.map +0 -1
- package/dist/rejection.js.map +0 -1
- package/dist/replay.d.ts.map +0 -1
- package/dist/replay.js.map +0 -1
- package/dist/rerankers/cross-encoder.d.ts.map +0 -1
- package/dist/rerankers/cross-encoder.js.map +0 -1
- package/dist/rerankers/index.d.ts.map +0 -1
- package/dist/rerankers/index.js.map +0 -1
- package/dist/rerankers/jev.d.ts.map +0 -1
- package/dist/rerankers/jev.js.map +0 -1
- package/dist/rerankers/llm.d.ts.map +0 -1
- package/dist/rerankers/llm.js.map +0 -1
- package/dist/rerankers/types.d.ts.map +0 -1
- package/dist/rerankers/types.js.map +0 -1
- package/dist/rrf.d.ts.map +0 -1
- package/dist/rrf.js.map +0 -1
- package/dist/salience.d.ts.map +0 -1
- package/dist/salience.js.map +0 -1
- package/dist/scheduler.d.ts.map +0 -1
- package/dist/scheduler.js.map +0 -1
- package/dist/scope.d.ts.map +0 -1
- package/dist/scope.js.map +0 -1
- package/dist/search.d.ts.map +0 -1
- package/dist/search.js.map +0 -1
- package/dist/secret-detect.d.ts.map +0 -1
- package/dist/secret-detect.js.map +0 -1
- package/dist/server-detect.d.ts.map +0 -1
- package/dist/server-detect.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/shared.d.ts.map +0 -1
- package/dist/shared.js.map +0 -1
- package/dist/skills.d.ts.map +0 -1
- package/dist/skills.js.map +0 -1
- package/dist/sleep-redact.d.ts +0 -58
- package/dist/sleep-redact.d.ts.map +0 -1
- package/dist/sleep-redact.js +0 -80
- package/dist/sleep-redact.js.map +0 -1
- package/dist/src/ablation.js +0 -138
- package/dist/src/ablation.js.map +0 -1
- package/dist/src/ambient.js +0 -148
- package/dist/src/ambient.js.map +0 -1
- package/dist/src/api.js +0 -2109
- package/dist/src/api.js.map +0 -1
- package/dist/src/audit-prune.js +0 -109
- package/dist/src/audit-prune.js.map +0 -1
- package/dist/src/audit.js +0 -213
- package/dist/src/audit.js.map +0 -1
- package/dist/src/auth.js +0 -97
- package/dist/src/auth.js.map +0 -1
- package/dist/src/autolearn.js +0 -174
- package/dist/src/autolearn.js.map +0 -1
- package/dist/src/availability.js +0 -94
- package/dist/src/availability.js.map +0 -1
- package/dist/src/capture.js +0 -1346
- package/dist/src/capture.js.map +0 -1
- package/dist/src/card-detail.js +0 -15
- package/dist/src/card-detail.js.map +0 -1
- package/dist/src/card.js +0 -19
- package/dist/src/card.js.map +0 -1
- package/dist/src/cli.js +0 -9387
- package/dist/src/cli.js.map +0 -1
- package/dist/src/client.js +0 -237
- package/dist/src/client.js.map +0 -1
- package/dist/src/compare.js +0 -122
- package/dist/src/compare.js.map +0 -1
- package/dist/src/config.js +0 -135
- package/dist/src/config.js.map +0 -1
- package/dist/src/connectors/github/backfill.js +0 -281
- package/dist/src/connectors/github/backfill.js.map +0 -1
- package/dist/src/connectors/github/cli-impl.js +0 -234
- package/dist/src/connectors/github/cli-impl.js.map +0 -1
- package/dist/src/connectors/github/deletion.js +0 -85
- package/dist/src/connectors/github/deletion.js.map +0 -1
- package/dist/src/connectors/github/dlq.js +0 -190
- package/dist/src/connectors/github/dlq.js.map +0 -1
- package/dist/src/connectors/github/idempotency.js +0 -27
- package/dist/src/connectors/github/idempotency.js.map +0 -1
- package/dist/src/connectors/github/ingest.js +0 -156
- package/dist/src/connectors/github/ingest.js.map +0 -1
- package/dist/src/connectors/github/octokit-client.js +0 -70
- package/dist/src/connectors/github/octokit-client.js.map +0 -1
- package/dist/src/connectors/github/ratelimit.js +0 -31
- package/dist/src/connectors/github/ratelimit.js.map +0 -1
- package/dist/src/connectors/github/scope.js +0 -13
- package/dist/src/connectors/github/scope.js.map +0 -1
- package/dist/src/connectors/github/signature.js +0 -81
- package/dist/src/connectors/github/signature.js.map +0 -1
- package/dist/src/connectors/github/tenant-routing.js +0 -69
- package/dist/src/connectors/github/tenant-routing.js.map +0 -1
- package/dist/src/connectors/github/transform.js +0 -103
- package/dist/src/connectors/github/transform.js.map +0 -1
- package/dist/src/connectors/github/types.js +0 -100
- package/dist/src/connectors/github/types.js.map +0 -1
- package/dist/src/connectors/slack/backfill.js +0 -78
- package/dist/src/connectors/slack/backfill.js.map +0 -1
- package/dist/src/connectors/slack/deletion.js +0 -59
- package/dist/src/connectors/slack/deletion.js.map +0 -1
- package/dist/src/connectors/slack/dlq.js +0 -224
- package/dist/src/connectors/slack/dlq.js.map +0 -1
- package/dist/src/connectors/slack/idempotency.js +0 -31
- package/dist/src/connectors/slack/idempotency.js.map +0 -1
- package/dist/src/connectors/slack/ingest.js +0 -109
- package/dist/src/connectors/slack/ingest.js.map +0 -1
- package/dist/src/connectors/slack/ratelimit.js +0 -18
- package/dist/src/connectors/slack/ratelimit.js.map +0 -1
- package/dist/src/connectors/slack/scope.js +0 -13
- package/dist/src/connectors/slack/scope.js.map +0 -1
- package/dist/src/connectors/slack/signature.js +0 -27
- package/dist/src/connectors/slack/signature.js.map +0 -1
- package/dist/src/connectors/slack/tenant-routing.js +0 -41
- package/dist/src/connectors/slack/tenant-routing.js.map +0 -1
- package/dist/src/connectors/slack/transform.js +0 -47
- package/dist/src/connectors/slack/transform.js.map +0 -1
- package/dist/src/connectors/slack/types.js +0 -35
- package/dist/src/connectors/slack/types.js.map +0 -1
- package/dist/src/connectors/slack/web-client.js +0 -43
- package/dist/src/connectors/slack/web-client.js.map +0 -1
- package/dist/src/connectors/slack/workspaces.js +0 -62
- package/dist/src/connectors/slack/workspaces.js.map +0 -1
- package/dist/src/consolidate.js +0 -978
- package/dist/src/consolidate.js.map +0 -1
- package/dist/src/correction-latency.js +0 -74
- package/dist/src/correction-latency.js.map +0 -1
- package/dist/src/customer-notes.js +0 -325
- package/dist/src/customer-notes.js.map +0 -1
- package/dist/src/dag.js +0 -352
- package/dist/src/dag.js.map +0 -1
- package/dist/src/dashboard.js +0 -258
- package/dist/src/dashboard.js.map +0 -1
- package/dist/src/db.js +0 -2731
- package/dist/src/db.js.map +0 -1
- package/dist/src/decisions.js +0 -304
- package/dist/src/decisions.js.map +0 -1
- package/dist/src/dedupe.js +0 -141
- package/dist/src/dedupe.js.map +0 -1
- package/dist/src/embedding-provider.js +0 -314
- package/dist/src/embedding-provider.js.map +0 -1
- package/dist/src/embeddings.js +0 -543
- package/dist/src/embeddings.js.map +0 -1
- package/dist/src/eval-suite.js +0 -294
- package/dist/src/eval-suite.js.map +0 -1
- package/dist/src/eval.js +0 -187
- package/dist/src/eval.js.map +0 -1
- package/dist/src/extract.js +0 -117
- package/dist/src/extract.js.map +0 -1
- package/dist/src/forward-claim-detector.js +0 -117
- package/dist/src/forward-claim-detector.js.map +0 -1
- package/dist/src/goals.js +0 -399
- package/dist/src/goals.js.map +0 -1
- package/dist/src/graph-extract.js +0 -314
- package/dist/src/graph-extract.js.map +0 -1
- package/dist/src/graph-recall.js +0 -277
- package/dist/src/graph-recall.js.map +0 -1
- package/dist/src/graph-stream.js +0 -176
- package/dist/src/graph-stream.js.map +0 -1
- package/dist/src/graph-view.js +0 -310
- package/dist/src/graph-view.js.map +0 -1
- package/dist/src/graph.js +0 -762
- package/dist/src/graph.js.map +0 -1
- package/dist/src/handoff.js +0 -65
- package/dist/src/handoff.js.map +0 -1
- package/dist/src/hooks.js +0 -926
- package/dist/src/hooks.js.map +0 -1
- package/dist/src/importers.js +0 -881
- package/dist/src/importers.js.map +0 -1
- package/dist/src/incidents.js +0 -336
- package/dist/src/incidents.js.map +0 -1
- package/dist/src/index.js +0 -32
- package/dist/src/index.js.map +0 -1
- package/dist/src/invalidation.js +0 -129
- package/dist/src/invalidation.js.map +0 -1
- package/dist/src/mcp/framing.js +0 -45
- package/dist/src/mcp/framing.js.map +0 -1
- package/dist/src/mcp/server.js +0 -1298
- package/dist/src/mcp/server.js.map +0 -1
- package/dist/src/memory-value-weights.js +0 -31
- package/dist/src/memory-value-weights.js.map +0 -1
- package/dist/src/memory-value.js +0 -261
- package/dist/src/memory-value.js.map +0 -1
- package/dist/src/memory.js +0 -425
- package/dist/src/memory.js.map +0 -1
- package/dist/src/multihop.js +0 -35
- package/dist/src/multihop.js.map +0 -1
- package/dist/src/owner-validation.js +0 -56
- package/dist/src/owner-validation.js.map +0 -1
- package/dist/src/path-context.js +0 -48
- package/dist/src/path-context.js.map +0 -1
- package/dist/src/physics-config.js +0 -26
- package/dist/src/physics-config.js.map +0 -1
- package/dist/src/physics-state.js +0 -172
- package/dist/src/physics-state.js.map +0 -1
- package/dist/src/physics.js +0 -378
- package/dist/src/physics.js.map +0 -1
- package/dist/src/policies.js +0 -414
- package/dist/src/policies.js.map +0 -1
- package/dist/src/postinstall.js +0 -101
- package/dist/src/postinstall.js.map +0 -1
- package/dist/src/predictions.js +0 -629
- package/dist/src/predictions.js.map +0 -1
- package/dist/src/processes.js +0 -349
- package/dist/src/processes.js.map +0 -1
- package/dist/src/project-briefs.js +0 -492
- package/dist/src/project-briefs.js.map +0 -1
- package/dist/src/project-identity.js +0 -200
- package/dist/src/project-identity.js.map +0 -1
- package/dist/src/provenance-coverage.js +0 -23
- package/dist/src/provenance-coverage.js.map +0 -1
- package/dist/src/rate-limit.js +0 -60
- package/dist/src/rate-limit.js.map +0 -1
- package/dist/src/raw-archive-mirror-cleanup.js +0 -55
- package/dist/src/raw-archive-mirror-cleanup.js.map +0 -1
- package/dist/src/raw-archive.js +0 -91
- package/dist/src/raw-archive.js.map +0 -1
- package/dist/src/recall-history.js +0 -235
- package/dist/src/recall-history.js.map +0 -1
- package/dist/src/recall-scope.js +0 -89
- package/dist/src/recall-scope.js.map +0 -1
- package/dist/src/recall-trace.js +0 -186
- package/dist/src/recall-trace.js.map +0 -1
- package/dist/src/refine-llm.js +0 -156
- package/dist/src/refine-llm.js.map +0 -1
- package/dist/src/reject-flow.js +0 -209
- package/dist/src/reject-flow.js.map +0 -1
- package/dist/src/rejection.js +0 -160
- package/dist/src/rejection.js.map +0 -1
- package/dist/src/replay.js +0 -122
- package/dist/src/replay.js.map +0 -1
- package/dist/src/rerankers/cross-encoder.js +0 -137
- package/dist/src/rerankers/cross-encoder.js.map +0 -1
- package/dist/src/rerankers/index.js +0 -20
- package/dist/src/rerankers/index.js.map +0 -1
- package/dist/src/rerankers/jev.js +0 -119
- package/dist/src/rerankers/jev.js.map +0 -1
- package/dist/src/rerankers/llm.js +0 -80
- package/dist/src/rerankers/llm.js.map +0 -1
- package/dist/src/rerankers/types.js +0 -2
- package/dist/src/rerankers/types.js.map +0 -1
- package/dist/src/rrf.js +0 -61
- package/dist/src/rrf.js.map +0 -1
- package/dist/src/salience.js +0 -74
- package/dist/src/salience.js.map +0 -1
- package/dist/src/scheduler.js +0 -77
- package/dist/src/scheduler.js.map +0 -1
- package/dist/src/scope.js +0 -35
- package/dist/src/scope.js.map +0 -1
- package/dist/src/search.js +0 -1003
- package/dist/src/search.js.map +0 -1
- package/dist/src/secret-detect.js +0 -95
- package/dist/src/secret-detect.js.map +0 -1
- package/dist/src/server-detect.js +0 -231
- package/dist/src/server-detect.js.map +0 -1
- package/dist/src/server.js +0 -3255
- package/dist/src/server.js.map +0 -1
- package/dist/src/shared.js +0 -502
- package/dist/src/shared.js.map +0 -1
- package/dist/src/skills.js +0 -346
- package/dist/src/skills.js.map +0 -1
- package/dist/src/sleep-redact.js +0 -80
- package/dist/src/sleep-redact.js.map +0 -1
- package/dist/src/sso.js +0 -22
- package/dist/src/sso.js.map +0 -1
- package/dist/src/stdin.js +0 -41
- package/dist/src/stdin.js.map +0 -1
- package/dist/src/store.js +0 -3749
- package/dist/src/store.js.map +0 -1
- package/dist/src/tenant.js +0 -17
- package/dist/src/tenant.js.map +0 -1
- package/dist/src/trace.js +0 -72
- package/dist/src/trace.js.map +0 -1
- package/dist/src/version.js +0 -40
- package/dist/src/version.js.map +0 -1
- package/dist/src/working-memory.js +0 -157
- package/dist/src/working-memory.js.map +0 -1
- package/dist/src/yaml.js +0 -107
- package/dist/src/yaml.js.map +0 -1
- package/dist/sso.d.ts +0 -13
- package/dist/sso.d.ts.map +0 -1
- package/dist/sso.js +0 -22
- package/dist/sso.js.map +0 -1
- package/dist/stdin.d.ts.map +0 -1
- package/dist/stdin.js.map +0 -1
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js.map +0 -1
- package/dist/tenant.d.ts.map +0 -1
- package/dist/tenant.js.map +0 -1
- package/dist/trace.d.ts.map +0 -1
- package/dist/trace.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
- package/dist/working-memory.d.ts.map +0 -1
- package/dist/working-memory.js.map +0 -1
- package/dist/yaml.d.ts.map +0 -1
- package/dist/yaml.js.map +0 -1
package/dist/src/db.js
DELETED
|
@@ -1,2731 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as os from 'os';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { createRequire } from 'module';
|
|
5
|
-
import { createPhysicsTable } from './physics-state.js';
|
|
6
|
-
import { cleanupArchivedMirrors } from './raw-archive-mirror-cleanup.js';
|
|
7
|
-
import { PACKAGE_VERSION, compareSemver } from './version.js';
|
|
8
|
-
import { deriveOriginProject, originFromSource, isGlobalStoreRoot } from './project-identity.js';
|
|
9
|
-
const require = createRequire(import.meta.url);
|
|
10
|
-
// SAFETY: node:sqlite's DatabaseSync constructor genuinely has this shape at
|
|
11
|
-
// runtime (Node's built-in synchronous SQLite module); there are no bundled
|
|
12
|
-
// types for it here, so this require + cast is the module's documented boundary.
|
|
13
|
-
const { DatabaseSync } = require('node:sqlite');
|
|
14
|
-
const CURRENT_SCHEMA_VERSION = 43;
|
|
15
|
-
const MIGRATIONS = [
|
|
16
|
-
{
|
|
17
|
-
version: 1,
|
|
18
|
-
up: (db) => {
|
|
19
|
-
db.exec(`
|
|
20
|
-
CREATE TABLE IF NOT EXISTS memories (
|
|
21
|
-
id TEXT PRIMARY KEY,
|
|
22
|
-
created TEXT NOT NULL,
|
|
23
|
-
last_retrieved TEXT NOT NULL,
|
|
24
|
-
retrieval_count INTEGER NOT NULL,
|
|
25
|
-
strength REAL NOT NULL,
|
|
26
|
-
half_life_days REAL NOT NULL,
|
|
27
|
-
layer TEXT NOT NULL,
|
|
28
|
-
tags_json TEXT NOT NULL,
|
|
29
|
-
emotional_valence TEXT NOT NULL,
|
|
30
|
-
schema_fit REAL NOT NULL,
|
|
31
|
-
source TEXT NOT NULL,
|
|
32
|
-
outcome_score REAL,
|
|
33
|
-
conflicts_with_json TEXT NOT NULL,
|
|
34
|
-
pinned INTEGER NOT NULL,
|
|
35
|
-
confidence TEXT NOT NULL,
|
|
36
|
-
content TEXT NOT NULL,
|
|
37
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
CREATE INDEX IF NOT EXISTS idx_memories_layer_created ON memories(layer, created);
|
|
41
|
-
CREATE INDEX IF NOT EXISTS idx_memories_last_retrieved ON memories(last_retrieved);
|
|
42
|
-
|
|
43
|
-
CREATE TABLE IF NOT EXISTS consolidation_runs (
|
|
44
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
45
|
-
timestamp TEXT NOT NULL,
|
|
46
|
-
decayed INTEGER NOT NULL,
|
|
47
|
-
merged INTEGER NOT NULL,
|
|
48
|
-
removed INTEGER NOT NULL
|
|
49
|
-
);
|
|
50
|
-
`);
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
version: 2,
|
|
55
|
-
up: (db) => {
|
|
56
|
-
db.exec(`
|
|
57
|
-
CREATE TABLE IF NOT EXISTS task_snapshots (
|
|
58
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
-
task TEXT NOT NULL,
|
|
60
|
-
summary TEXT NOT NULL,
|
|
61
|
-
next_step TEXT NOT NULL,
|
|
62
|
-
status TEXT NOT NULL,
|
|
63
|
-
source TEXT NOT NULL,
|
|
64
|
-
created_at TEXT NOT NULL,
|
|
65
|
-
updated_at TEXT NOT NULL
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
CREATE INDEX IF NOT EXISTS idx_task_snapshots_status_updated
|
|
69
|
-
ON task_snapshots(status, updated_at DESC, id DESC);
|
|
70
|
-
`);
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
version: 3,
|
|
75
|
-
up: (db) => {
|
|
76
|
-
db.exec(`
|
|
77
|
-
CREATE TABLE IF NOT EXISTS memory_conflicts (
|
|
78
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
79
|
-
memory_a_id TEXT NOT NULL,
|
|
80
|
-
memory_b_id TEXT NOT NULL,
|
|
81
|
-
reason TEXT NOT NULL,
|
|
82
|
-
score REAL NOT NULL DEFAULT 0,
|
|
83
|
-
status TEXT NOT NULL,
|
|
84
|
-
detected_at TEXT NOT NULL,
|
|
85
|
-
updated_at TEXT NOT NULL,
|
|
86
|
-
UNIQUE(memory_a_id, memory_b_id)
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
CREATE INDEX IF NOT EXISTS idx_memory_conflicts_status_updated
|
|
90
|
-
ON memory_conflicts(status, updated_at DESC, id DESC);
|
|
91
|
-
`);
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
version: 4,
|
|
96
|
-
up: (db) => {
|
|
97
|
-
if (!tableHasColumn(db, 'task_snapshots', 'session_id')) {
|
|
98
|
-
db.exec(`ALTER TABLE task_snapshots ADD COLUMN session_id TEXT`);
|
|
99
|
-
}
|
|
100
|
-
db.exec(`
|
|
101
|
-
CREATE TABLE IF NOT EXISTS session_events (
|
|
102
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
103
|
-
session_id TEXT NOT NULL,
|
|
104
|
-
task TEXT,
|
|
105
|
-
event_type TEXT NOT NULL,
|
|
106
|
-
content TEXT NOT NULL,
|
|
107
|
-
source TEXT NOT NULL,
|
|
108
|
-
metadata_json TEXT NOT NULL,
|
|
109
|
-
created_at TEXT NOT NULL
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
CREATE INDEX IF NOT EXISTS idx_session_events_session_created
|
|
113
|
-
ON session_events(session_id, created_at DESC, id DESC);
|
|
114
|
-
|
|
115
|
-
CREATE INDEX IF NOT EXISTS idx_session_events_task_created
|
|
116
|
-
ON session_events(task, created_at DESC, id DESC);
|
|
117
|
-
`);
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
version: 5,
|
|
122
|
-
up: (db) => {
|
|
123
|
-
db.exec(`
|
|
124
|
-
CREATE TABLE IF NOT EXISTS session_handoffs (
|
|
125
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
126
|
-
session_id TEXT NOT NULL,
|
|
127
|
-
repo_root TEXT,
|
|
128
|
-
task_id TEXT,
|
|
129
|
-
summary TEXT NOT NULL,
|
|
130
|
-
next_action TEXT,
|
|
131
|
-
artifacts_json TEXT NOT NULL DEFAULT '[]',
|
|
132
|
-
created_at TEXT NOT NULL
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
CREATE INDEX IF NOT EXISTS idx_session_handoffs_session
|
|
136
|
-
ON session_handoffs(session_id, created_at DESC);
|
|
137
|
-
`);
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
version: 6,
|
|
142
|
-
up: (db) => {
|
|
143
|
-
db.exec(`
|
|
144
|
-
CREATE TABLE IF NOT EXISTS working_memory (
|
|
145
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
146
|
-
scope TEXT NOT NULL,
|
|
147
|
-
session_id TEXT,
|
|
148
|
-
task_id TEXT,
|
|
149
|
-
importance REAL NOT NULL DEFAULT 0,
|
|
150
|
-
content TEXT NOT NULL,
|
|
151
|
-
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
152
|
-
created_at TEXT NOT NULL,
|
|
153
|
-
updated_at TEXT NOT NULL
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
CREATE INDEX IF NOT EXISTS idx_working_memory_scope
|
|
157
|
-
ON working_memory(scope, importance DESC, created_at DESC);
|
|
158
|
-
|
|
159
|
-
CREATE INDEX IF NOT EXISTS idx_working_memory_session
|
|
160
|
-
ON working_memory(session_id, created_at DESC);
|
|
161
|
-
`);
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
version: 7,
|
|
166
|
-
up: (db) => {
|
|
167
|
-
if (!tableHasColumn(db, 'memories', 'outcome_positive')) {
|
|
168
|
-
db.exec(`ALTER TABLE memories ADD COLUMN outcome_positive INTEGER NOT NULL DEFAULT 0`);
|
|
169
|
-
}
|
|
170
|
-
if (!tableHasColumn(db, 'memories', 'outcome_negative')) {
|
|
171
|
-
db.exec(`ALTER TABLE memories ADD COLUMN outcome_negative INTEGER NOT NULL DEFAULT 0`);
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
version: 8,
|
|
177
|
-
up: (db) => {
|
|
178
|
-
createPhysicsTable(db);
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
version: 9,
|
|
183
|
-
up: (db) => {
|
|
184
|
-
if (!tableHasColumn(db, 'memories', 'parents_json')) {
|
|
185
|
-
db.exec(`ALTER TABLE memories ADD COLUMN parents_json TEXT NOT NULL DEFAULT '[]'`);
|
|
186
|
-
}
|
|
187
|
-
if (!tableHasColumn(db, 'memories', 'starred')) {
|
|
188
|
-
db.exec(`ALTER TABLE memories ADD COLUMN starred INTEGER NOT NULL DEFAULT 0`);
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
version: 10,
|
|
194
|
-
up: (db) => {
|
|
195
|
-
if (!tableHasColumn(db, 'memories', 'trace_outcome')) {
|
|
196
|
-
db.exec(`ALTER TABLE memories ADD COLUMN trace_outcome TEXT`);
|
|
197
|
-
}
|
|
198
|
-
if (!tableHasColumn(db, 'memories', 'source_session_id')) {
|
|
199
|
-
db.exec(`ALTER TABLE memories ADD COLUMN source_session_id TEXT`);
|
|
200
|
-
}
|
|
201
|
-
db.exec(`
|
|
202
|
-
CREATE INDEX IF NOT EXISTS idx_memories_source_session_id
|
|
203
|
-
ON memories(source_session_id) WHERE source_session_id IS NOT NULL
|
|
204
|
-
`);
|
|
205
|
-
},
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
version: 11,
|
|
209
|
-
up: (db) => {
|
|
210
|
-
if (!tableHasColumn(db, 'memories', 'valid_from')) {
|
|
211
|
-
db.exec(`ALTER TABLE memories ADD COLUMN valid_from TEXT`);
|
|
212
|
-
db.exec(`UPDATE memories SET valid_from = created WHERE valid_from IS NULL`);
|
|
213
|
-
}
|
|
214
|
-
if (!tableHasColumn(db, 'memories', 'superseded_by')) {
|
|
215
|
-
db.exec(`ALTER TABLE memories ADD COLUMN superseded_by TEXT`);
|
|
216
|
-
}
|
|
217
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_current ON memories(layer, created) WHERE superseded_by IS NULL`);
|
|
218
|
-
},
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
version: 12,
|
|
222
|
-
up: (db) => {
|
|
223
|
-
if (!tableHasColumn(db, 'memories', 'extracted_from')) {
|
|
224
|
-
db.exec(`ALTER TABLE memories ADD COLUMN extracted_from TEXT`);
|
|
225
|
-
}
|
|
226
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_extracted_from ON memories(extracted_from) WHERE extracted_from IS NOT NULL`);
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
version: 13,
|
|
231
|
-
up: (db) => {
|
|
232
|
-
if (!tableHasColumn(db, 'memories', 'dag_level')) {
|
|
233
|
-
db.exec(`ALTER TABLE memories ADD COLUMN dag_level INTEGER NOT NULL DEFAULT 0`);
|
|
234
|
-
}
|
|
235
|
-
if (!tableHasColumn(db, 'memories', 'dag_parent_id')) {
|
|
236
|
-
db.exec(`ALTER TABLE memories ADD COLUMN dag_parent_id TEXT`);
|
|
237
|
-
}
|
|
238
|
-
db.exec(`UPDATE memories SET dag_level = 1 WHERE extracted_from IS NOT NULL AND dag_level = 0`);
|
|
239
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_dag_parent ON memories(dag_parent_id) WHERE dag_parent_id IS NOT NULL`);
|
|
240
|
-
},
|
|
241
|
-
},
|
|
242
|
-
{
|
|
243
|
-
version: 14,
|
|
244
|
-
up: (db) => {
|
|
245
|
-
// A3 provenance envelope: kind, scope, owner, artifact_ref.
|
|
246
|
-
// SQLite ALTER TABLE ADD COLUMN cannot add CHECK; CHECK enforcement lives
|
|
247
|
-
// in INSERT/UPDATE triggers added later in this migration.
|
|
248
|
-
if (!tableHasColumn(db, 'memories', 'kind')) {
|
|
249
|
-
db.exec(`ALTER TABLE memories ADD COLUMN kind TEXT DEFAULT 'distilled'`);
|
|
250
|
-
}
|
|
251
|
-
if (!tableHasColumn(db, 'memories', 'scope')) {
|
|
252
|
-
db.exec(`ALTER TABLE memories ADD COLUMN scope TEXT`);
|
|
253
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope) WHERE scope IS NOT NULL`);
|
|
254
|
-
}
|
|
255
|
-
if (!tableHasColumn(db, 'memories', 'owner')) {
|
|
256
|
-
db.exec(`ALTER TABLE memories ADD COLUMN owner TEXT`);
|
|
257
|
-
}
|
|
258
|
-
if (!tableHasColumn(db, 'memories', 'artifact_ref')) {
|
|
259
|
-
db.exec(`ALTER TABLE memories ADD COLUMN artifact_ref TEXT`);
|
|
260
|
-
}
|
|
261
|
-
// Backfill kind for any rows where it's NULL (pre-migration data).
|
|
262
|
-
db.exec(`UPDATE memories SET kind = 'superseded' WHERE kind IS NULL AND superseded_by IS NOT NULL`);
|
|
263
|
-
db.exec(`UPDATE memories SET kind = 'distilled' WHERE kind IS NULL`);
|
|
264
|
-
// raw_archive: legitimate path for kind='raw' removal (used by archiveRawMemory).
|
|
265
|
-
db.exec(`
|
|
266
|
-
CREATE TABLE IF NOT EXISTS raw_archive (
|
|
267
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
268
|
-
memory_id TEXT NOT NULL,
|
|
269
|
-
archived_at TEXT NOT NULL,
|
|
270
|
-
reason TEXT NOT NULL,
|
|
271
|
-
archived_by TEXT,
|
|
272
|
-
payload_json TEXT NOT NULL
|
|
273
|
-
)
|
|
274
|
-
`);
|
|
275
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_raw_archive_memory_id ON raw_archive(memory_id)`);
|
|
276
|
-
// Append-only invariant: kind='raw' rows cannot be deleted directly.
|
|
277
|
-
// Use raw_archive flow: archive-then-update-then-delete (see src/raw-archive.ts).
|
|
278
|
-
db.exec(`
|
|
279
|
-
CREATE TRIGGER IF NOT EXISTS trg_memories_raw_append_only
|
|
280
|
-
BEFORE DELETE ON memories
|
|
281
|
-
WHEN OLD.kind = 'raw'
|
|
282
|
-
BEGIN
|
|
283
|
-
SELECT RAISE(ABORT, 'raw is append-only');
|
|
284
|
-
END
|
|
285
|
-
`);
|
|
286
|
-
// CHECK substitute: ALTER TABLE cannot add CHECK, so enforce kind allowed-set
|
|
287
|
-
// via INSERT/UPDATE triggers.
|
|
288
|
-
db.exec(`
|
|
289
|
-
CREATE TRIGGER IF NOT EXISTS trg_memories_kind_check_insert
|
|
290
|
-
BEFORE INSERT ON memories
|
|
291
|
-
WHEN NEW.kind IS NOT NULL AND NEW.kind NOT IN ('raw','distilled','superseded','archived')
|
|
292
|
-
BEGIN
|
|
293
|
-
SELECT RAISE(ABORT, 'invalid kind: must be raw|distilled|superseded|archived');
|
|
294
|
-
END
|
|
295
|
-
`);
|
|
296
|
-
db.exec(`
|
|
297
|
-
CREATE TRIGGER IF NOT EXISTS trg_memories_kind_check_update
|
|
298
|
-
BEFORE UPDATE ON memories
|
|
299
|
-
WHEN NEW.kind IS NOT NULL AND NEW.kind NOT IN ('raw','distilled','superseded','archived')
|
|
300
|
-
BEGIN
|
|
301
|
-
SELECT RAISE(ABORT, 'invalid kind: must be raw|distilled|superseded|archived');
|
|
302
|
-
END
|
|
303
|
-
`);
|
|
304
|
-
},
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
version: 15,
|
|
308
|
-
up: (db) => {
|
|
309
|
-
// A3 hardening (post-review): close the NULL-kind bypass and add raw_archive
|
|
310
|
-
// dedup safety. Both findings landed in /review on commits 41b1f4d..6456e7d.
|
|
311
|
-
//
|
|
312
|
-
// (1) Original v14 triggers used `WHEN NEW.kind IS NOT NULL AND NEW.kind NOT IN (...)`.
|
|
313
|
-
// A direct INSERT/UPDATE setting kind=NULL bypassed the CHECK substitute. Replace
|
|
314
|
-
// with `WHEN NEW.kind IS NULL OR NEW.kind NOT IN (...)` so NULL is rejected too.
|
|
315
|
-
// (2) Add UNIQUE(memory_id, archived_at) to raw_archive so re-archiving the same id
|
|
316
|
-
// in the same instant cannot produce ambiguous audit rows. Per-id history is still
|
|
317
|
-
// allowed (different timestamps).
|
|
318
|
-
db.exec(`DROP TRIGGER IF EXISTS trg_memories_kind_check_insert`);
|
|
319
|
-
db.exec(`DROP TRIGGER IF EXISTS trg_memories_kind_check_update`);
|
|
320
|
-
db.exec(`
|
|
321
|
-
CREATE TRIGGER trg_memories_kind_check_insert
|
|
322
|
-
BEFORE INSERT ON memories
|
|
323
|
-
WHEN NEW.kind IS NULL OR NEW.kind NOT IN ('raw','distilled','superseded','archived')
|
|
324
|
-
BEGIN
|
|
325
|
-
SELECT RAISE(ABORT, 'invalid kind: must be raw|distilled|superseded|archived (not null)');
|
|
326
|
-
END
|
|
327
|
-
`);
|
|
328
|
-
db.exec(`
|
|
329
|
-
CREATE TRIGGER trg_memories_kind_check_update
|
|
330
|
-
BEFORE UPDATE ON memories
|
|
331
|
-
WHEN NEW.kind IS NULL OR NEW.kind NOT IN ('raw','distilled','superseded','archived')
|
|
332
|
-
BEGIN
|
|
333
|
-
SELECT RAISE(ABORT, 'invalid kind: must be raw|distilled|superseded|archived (not null)');
|
|
334
|
-
END
|
|
335
|
-
`);
|
|
336
|
-
// Defensive: any rows that somehow have NULL kind get fixed (shouldn't exist post-v14
|
|
337
|
-
// backfill, but cheap insurance).
|
|
338
|
-
db.exec(`UPDATE memories SET kind = 'distilled' WHERE kind IS NULL`);
|
|
339
|
-
// raw_archive uniqueness. SQLite cannot ADD CONSTRAINT, but a partial unique index
|
|
340
|
-
// on (memory_id, archived_at) is equivalent for INSERT-time enforcement.
|
|
341
|
-
db.exec(`
|
|
342
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_raw_archive_id_at
|
|
343
|
-
ON raw_archive(memory_id, archived_at)
|
|
344
|
-
`);
|
|
345
|
-
},
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
version: 16,
|
|
349
|
-
up: (db) => {
|
|
350
|
-
// A5 stub auth: add tenant_id to all data tables. Single-tenant per deployment;
|
|
351
|
-
// multi-tenant enforcement deferred to v2 (full A5). The columns are needed now
|
|
352
|
-
// so future B-track tables don't have to backfill.
|
|
353
|
-
if (!tableHasColumn(db, 'memories', 'tenant_id')) {
|
|
354
|
-
db.exec(`ALTER TABLE memories ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
|
|
355
|
-
}
|
|
356
|
-
if (!tableHasColumn(db, 'working_memory', 'tenant_id')) {
|
|
357
|
-
db.exec(`ALTER TABLE working_memory ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
|
|
358
|
-
}
|
|
359
|
-
if (!tableHasColumn(db, 'consolidation_runs', 'tenant_id')) {
|
|
360
|
-
db.exec(`ALTER TABLE consolidation_runs ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
|
|
361
|
-
}
|
|
362
|
-
if (!tableHasColumn(db, 'task_snapshots', 'tenant_id')) {
|
|
363
|
-
db.exec(`ALTER TABLE task_snapshots ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
|
|
364
|
-
}
|
|
365
|
-
if (!tableHasColumn(db, 'memory_conflicts', 'tenant_id')) {
|
|
366
|
-
db.exec(`ALTER TABLE memory_conflicts ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
|
|
367
|
-
}
|
|
368
|
-
// Composite indexes for recall hot paths. Leading column is tenant_id so
|
|
369
|
-
// single-tenant lookups are O(log n).
|
|
370
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_tenant_created ON memories(tenant_id, created)`);
|
|
371
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_working_memory_tenant ON working_memory(tenant_id, importance DESC, created_at DESC)`);
|
|
372
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_consolidation_runs_tenant_ts ON consolidation_runs(tenant_id, timestamp DESC)`);
|
|
373
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_task_snapshots_tenant_status ON task_snapshots(tenant_id, status, updated_at DESC)`);
|
|
374
|
-
// A5 stub auth: api_keys (scrypt-hashed; plaintext returned to caller exactly once)
|
|
375
|
-
// and audit_log (append-only mutation trail). Both carry tenant_id from day 1 so
|
|
376
|
-
// future multi-tenant enforcement is a config flip, not a re-migration.
|
|
377
|
-
db.exec(`
|
|
378
|
-
CREATE TABLE IF NOT EXISTS api_keys (
|
|
379
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
380
|
-
key_id TEXT UNIQUE NOT NULL,
|
|
381
|
-
key_hash TEXT NOT NULL,
|
|
382
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
383
|
-
label TEXT,
|
|
384
|
-
created_at TEXT NOT NULL,
|
|
385
|
-
revoked_at TEXT
|
|
386
|
-
)
|
|
387
|
-
`);
|
|
388
|
-
db.exec(`
|
|
389
|
-
CREATE INDEX IF NOT EXISTS idx_api_keys_tenant_active
|
|
390
|
-
ON api_keys(tenant_id) WHERE revoked_at IS NULL
|
|
391
|
-
`);
|
|
392
|
-
db.exec(`
|
|
393
|
-
CREATE TABLE IF NOT EXISTS audit_log (
|
|
394
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
395
|
-
ts TEXT NOT NULL,
|
|
396
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
397
|
-
actor TEXT NOT NULL,
|
|
398
|
-
op TEXT NOT NULL,
|
|
399
|
-
target_id TEXT,
|
|
400
|
-
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
401
|
-
)
|
|
402
|
-
`);
|
|
403
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_tenant_ts ON audit_log(tenant_id, ts DESC)`);
|
|
404
|
-
},
|
|
405
|
-
},
|
|
406
|
-
{
|
|
407
|
-
version: 17,
|
|
408
|
-
up: (db) => {
|
|
409
|
-
// E1.3 Slack ingestion: idempotency log, per-channel backfill cursors, DLQ.
|
|
410
|
-
// See docs/plans/2026-04-29-e1.3-slack-ingestion.md.
|
|
411
|
-
db.exec(`
|
|
412
|
-
CREATE TABLE IF NOT EXISTS slack_event_log (
|
|
413
|
-
event_id TEXT PRIMARY KEY,
|
|
414
|
-
ingested_at TEXT NOT NULL,
|
|
415
|
-
memory_id TEXT
|
|
416
|
-
)
|
|
417
|
-
`);
|
|
418
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_slack_event_log_memory ON slack_event_log(memory_id) WHERE memory_id IS NOT NULL`);
|
|
419
|
-
db.exec(`
|
|
420
|
-
CREATE TABLE IF NOT EXISTS slack_cursors (
|
|
421
|
-
tenant_id TEXT NOT NULL,
|
|
422
|
-
channel_id TEXT NOT NULL,
|
|
423
|
-
latest_ts TEXT NOT NULL,
|
|
424
|
-
updated_at TEXT NOT NULL,
|
|
425
|
-
PRIMARY KEY (tenant_id, channel_id)
|
|
426
|
-
)
|
|
427
|
-
`);
|
|
428
|
-
db.exec(`
|
|
429
|
-
CREATE TABLE IF NOT EXISTS slack_dlq (
|
|
430
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
431
|
-
tenant_id TEXT NOT NULL,
|
|
432
|
-
raw_payload TEXT NOT NULL,
|
|
433
|
-
error TEXT NOT NULL,
|
|
434
|
-
received_at TEXT NOT NULL,
|
|
435
|
-
retried_at TEXT
|
|
436
|
-
)
|
|
437
|
-
`);
|
|
438
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_slack_dlq_tenant_received ON slack_dlq(tenant_id, received_at)`);
|
|
439
|
-
// Multi-tenant routing seam (review patch #6). Empty by default — single-
|
|
440
|
-
// tenant deployments resolve via HIPPO_TENANT fallback. Multi-workspace
|
|
441
|
-
// deployments populate this table to map team_id → tenant_id.
|
|
442
|
-
db.exec(`
|
|
443
|
-
CREATE TABLE IF NOT EXISTS slack_workspaces (
|
|
444
|
-
team_id TEXT PRIMARY KEY,
|
|
445
|
-
tenant_id TEXT NOT NULL,
|
|
446
|
-
added_at TEXT NOT NULL
|
|
447
|
-
)
|
|
448
|
-
`);
|
|
449
|
-
},
|
|
450
|
-
},
|
|
451
|
-
{
|
|
452
|
-
version: 18,
|
|
453
|
-
up: (db) => {
|
|
454
|
-
db.exec(`
|
|
455
|
-
CREATE TABLE IF NOT EXISTS goal_stack (
|
|
456
|
-
id TEXT PRIMARY KEY,
|
|
457
|
-
session_id TEXT NOT NULL,
|
|
458
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
459
|
-
goal_name TEXT NOT NULL,
|
|
460
|
-
level INTEGER NOT NULL DEFAULT 0
|
|
461
|
-
CHECK (level BETWEEN 0 AND 2),
|
|
462
|
-
parent_goal_id TEXT REFERENCES goal_stack(id) ON DELETE SET NULL,
|
|
463
|
-
status TEXT NOT NULL CHECK (status IN ('active','suspended','completed')),
|
|
464
|
-
success_condition TEXT,
|
|
465
|
-
retrieval_policy_id TEXT,
|
|
466
|
-
created_at TEXT NOT NULL,
|
|
467
|
-
completed_at TEXT,
|
|
468
|
-
outcome_score REAL
|
|
469
|
-
CHECK (outcome_score IS NULL OR (outcome_score >= 0 AND outcome_score <= 1))
|
|
470
|
-
);
|
|
471
|
-
|
|
472
|
-
CREATE INDEX IF NOT EXISTS idx_goal_stack_tenant_session_status
|
|
473
|
-
ON goal_stack(tenant_id, session_id, status, created_at);
|
|
474
|
-
|
|
475
|
-
CREATE TABLE IF NOT EXISTS retrieval_policy (
|
|
476
|
-
id TEXT PRIMARY KEY,
|
|
477
|
-
goal_id TEXT NOT NULL REFERENCES goal_stack(id) ON DELETE CASCADE,
|
|
478
|
-
policy_type TEXT NOT NULL CHECK (policy_type IN
|
|
479
|
-
('schema-fit-biased','error-prioritized','recency-first','hybrid')),
|
|
480
|
-
weight_schema_fit REAL NOT NULL DEFAULT 1.0
|
|
481
|
-
CHECK (weight_schema_fit >= 0 AND weight_schema_fit <= 100),
|
|
482
|
-
weight_recency REAL NOT NULL DEFAULT 1.0
|
|
483
|
-
CHECK (weight_recency >= 0 AND weight_recency <= 100),
|
|
484
|
-
weight_outcome REAL NOT NULL DEFAULT 1.0
|
|
485
|
-
CHECK (weight_outcome >= 0 AND weight_outcome <= 100),
|
|
486
|
-
error_priority REAL NOT NULL DEFAULT 1.0
|
|
487
|
-
CHECK (error_priority >= 0 AND error_priority <= 100)
|
|
488
|
-
);
|
|
489
|
-
|
|
490
|
-
CREATE INDEX IF NOT EXISTS idx_retrieval_policy_goal
|
|
491
|
-
ON retrieval_policy(goal_id);
|
|
492
|
-
|
|
493
|
-
CREATE TABLE IF NOT EXISTS goal_recall_log (
|
|
494
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
495
|
-
goal_id TEXT NOT NULL REFERENCES goal_stack(id) ON DELETE CASCADE,
|
|
496
|
-
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
497
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
498
|
-
session_id TEXT NOT NULL,
|
|
499
|
-
recalled_at TEXT NOT NULL,
|
|
500
|
-
score REAL NOT NULL
|
|
501
|
-
);
|
|
502
|
-
|
|
503
|
-
CREATE INDEX IF NOT EXISTS idx_goal_recall_log_goal
|
|
504
|
-
ON goal_recall_log(goal_id);
|
|
505
|
-
CREATE UNIQUE INDEX IF NOT EXISTS uniq_goal_recall_log_memory_goal
|
|
506
|
-
ON goal_recall_log(memory_id, goal_id);
|
|
507
|
-
`);
|
|
508
|
-
},
|
|
509
|
-
},
|
|
510
|
-
{
|
|
511
|
-
version: 19,
|
|
512
|
-
up: (db) => {
|
|
513
|
-
// v0.39 commit 3 (Slack hardening): widen slack_dlq with bucketing,
|
|
514
|
-
// retry tracking, and the signature/timestamp pair that lets `hippo
|
|
515
|
-
// slack dlq replay` re-verify before re-running ingest. ALTER ADD
|
|
516
|
-
// COLUMN with DEFAULT is non-destructive — legacy rows take the
|
|
517
|
-
// default values. Idempotent via tableHasColumn().
|
|
518
|
-
if (!tableHasColumn(db, 'slack_dlq', 'team_id')) {
|
|
519
|
-
db.exec(`ALTER TABLE slack_dlq ADD COLUMN team_id TEXT`);
|
|
520
|
-
}
|
|
521
|
-
if (!tableHasColumn(db, 'slack_dlq', 'bucket')) {
|
|
522
|
-
db.exec(`ALTER TABLE slack_dlq ADD COLUMN bucket TEXT NOT NULL DEFAULT 'parse_error'`);
|
|
523
|
-
// SQLite ALTER TABLE cannot add CHECK; bucket value enforcement is app-level.
|
|
524
|
-
}
|
|
525
|
-
if (!tableHasColumn(db, 'slack_dlq', 'retry_count')) {
|
|
526
|
-
db.exec(`ALTER TABLE slack_dlq ADD COLUMN retry_count INTEGER NOT NULL DEFAULT 0`);
|
|
527
|
-
}
|
|
528
|
-
if (!tableHasColumn(db, 'slack_dlq', 'signature')) {
|
|
529
|
-
db.exec(`ALTER TABLE slack_dlq ADD COLUMN signature TEXT`);
|
|
530
|
-
}
|
|
531
|
-
if (!tableHasColumn(db, 'slack_dlq', 'slack_timestamp')) {
|
|
532
|
-
db.exec(`ALTER TABLE slack_dlq ADD COLUMN slack_timestamp TEXT`);
|
|
533
|
-
}
|
|
534
|
-
},
|
|
535
|
-
},
|
|
536
|
-
{
|
|
537
|
-
version: 20,
|
|
538
|
-
up: (db) => {
|
|
539
|
-
// v0.39 commit 4 (GDPR Path A backfill): redact every existing
|
|
540
|
-
// raw_archive.payload_json so historical archives match the new
|
|
541
|
-
// metadata-only contract from src/raw-archive.ts. Read each row, parse
|
|
542
|
-
// the existing JSON to extract tenant_id and kind (best effort), then
|
|
543
|
-
// UPDATE with the redacted shape. Rows with unparseable legacy JSON get
|
|
544
|
-
// redacted with tenant_id='unknown', kind='unknown'. The audit_log
|
|
545
|
-
// remains the compliance record.
|
|
546
|
-
// SAFETY: rows' shape matches the four columns named in the SELECT above.
|
|
547
|
-
const rows = db
|
|
548
|
-
.prepare(`SELECT id, archived_at, reason, payload_json FROM raw_archive`)
|
|
549
|
-
.all();
|
|
550
|
-
const update = db.prepare(`UPDATE raw_archive SET payload_json = ? WHERE id = ?`);
|
|
551
|
-
for (const row of rows) {
|
|
552
|
-
let tenant = 'unknown';
|
|
553
|
-
let kind = 'unknown';
|
|
554
|
-
try {
|
|
555
|
-
// SAFETY: parsed is a best-effort JSON.parse of legacy payload_json;
|
|
556
|
-
// an unexpected shape only yields undefined fields (falling back to
|
|
557
|
-
// 'unknown' below), and a parse failure is caught, so this never crashes.
|
|
558
|
-
const parsed = JSON.parse(row.payload_json);
|
|
559
|
-
tenant = parsed.tenant_id ?? 'unknown';
|
|
560
|
-
kind = parsed.kind ?? 'unknown';
|
|
561
|
-
}
|
|
562
|
-
catch {
|
|
563
|
-
// Unparseable legacy payload — redact with unknowns.
|
|
564
|
-
}
|
|
565
|
-
const redacted = JSON.stringify({
|
|
566
|
-
redacted: true,
|
|
567
|
-
archived_at: row.archived_at,
|
|
568
|
-
tenant_id: tenant,
|
|
569
|
-
kind,
|
|
570
|
-
reason: row.reason,
|
|
571
|
-
migration: 'v20_redact',
|
|
572
|
-
});
|
|
573
|
-
update.run(redacted, row.id);
|
|
574
|
-
}
|
|
575
|
-
},
|
|
576
|
-
},
|
|
577
|
-
{
|
|
578
|
-
version: 21,
|
|
579
|
-
up: (db) => {
|
|
580
|
-
// v0.39 codex round 3: per-row mirror cleanup tracking. Replaces the
|
|
581
|
-
// global gdpr_v20_mirror_cleanup meta gate (which made the reaper
|
|
582
|
-
// one-shot and silently swallowed failed unlinks). With this column the
|
|
583
|
-
// reaper processes only rows WHERE mirror_cleaned_at IS NULL, sets the
|
|
584
|
-
// timestamp on success, and leaves it NULL on any unlink failure so the
|
|
585
|
-
// next openHippoDb retries automatically.
|
|
586
|
-
if (!tableHasColumn(db, 'raw_archive', 'mirror_cleaned_at')) {
|
|
587
|
-
db.exec(`ALTER TABLE raw_archive ADD COLUMN mirror_cleaned_at TEXT`);
|
|
588
|
-
}
|
|
589
|
-
},
|
|
590
|
-
},
|
|
591
|
-
{
|
|
592
|
-
version: 22,
|
|
593
|
-
up: (db) => {
|
|
594
|
-
// Tenant-isolation gap on continuity tables (codex review 2026-05-02).
|
|
595
|
-
// session_events and session_handoffs predate the v16 tenant migration
|
|
596
|
-
// and were never added to it, so the v0.40.0 provenance gate work
|
|
597
|
-
// exposed a real cross-tenant leak when continuity primitives are used.
|
|
598
|
-
// Adds tenant_id (NOT NULL DEFAULT 'default') with smart backfill from
|
|
599
|
-
// task_snapshots.session_id when unambiguous, plus an optional scope
|
|
600
|
-
// column so a private-channel-derived handoff can default-deny via the
|
|
601
|
-
// same rule recall already enforces.
|
|
602
|
-
//
|
|
603
|
-
// Self-heal partial-init stores: re-run the v4/v5 CREATE TABLE IF NOT
|
|
604
|
-
// EXISTS bodies before ALTERing. A silent skip would otherwise stamp
|
|
605
|
-
// schema_version=22 on a DB missing the underlying tables, leaving
|
|
606
|
-
// them permanently absent.
|
|
607
|
-
db.exec(`
|
|
608
|
-
CREATE TABLE IF NOT EXISTS session_events (
|
|
609
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
610
|
-
session_id TEXT NOT NULL,
|
|
611
|
-
task TEXT,
|
|
612
|
-
event_type TEXT NOT NULL,
|
|
613
|
-
content TEXT NOT NULL,
|
|
614
|
-
source TEXT NOT NULL,
|
|
615
|
-
metadata_json TEXT NOT NULL,
|
|
616
|
-
created_at TEXT NOT NULL
|
|
617
|
-
);
|
|
618
|
-
CREATE TABLE IF NOT EXISTS session_handoffs (
|
|
619
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
620
|
-
session_id TEXT NOT NULL,
|
|
621
|
-
repo_root TEXT,
|
|
622
|
-
task_id TEXT,
|
|
623
|
-
summary TEXT NOT NULL,
|
|
624
|
-
next_action TEXT,
|
|
625
|
-
artifacts_json TEXT NOT NULL DEFAULT '[]',
|
|
626
|
-
created_at TEXT NOT NULL
|
|
627
|
-
);
|
|
628
|
-
`);
|
|
629
|
-
if (!tableHasColumn(db, 'session_events', 'tenant_id')) {
|
|
630
|
-
db.exec(`ALTER TABLE session_events ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
|
|
631
|
-
}
|
|
632
|
-
if (!tableHasColumn(db, 'session_events', 'scope')) {
|
|
633
|
-
db.exec(`ALTER TABLE session_events ADD COLUMN scope TEXT`);
|
|
634
|
-
}
|
|
635
|
-
if (!tableHasColumn(db, 'session_handoffs', 'tenant_id')) {
|
|
636
|
-
db.exec(`ALTER TABLE session_handoffs ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
|
|
637
|
-
}
|
|
638
|
-
if (!tableHasColumn(db, 'session_handoffs', 'scope')) {
|
|
639
|
-
db.exec(`ALTER TABLE session_handoffs ADD COLUMN scope TEXT`);
|
|
640
|
-
}
|
|
641
|
-
// Smart backfill: rows whose session_id maps to exactly one tenant in
|
|
642
|
-
// task_snapshots inherit that tenant. Ambiguous or unmapped rows stay
|
|
643
|
-
// at the column default ('default'). Conservative: never crosses
|
|
644
|
-
// tenant boundaries on guesses. The COUNT(DISTINCT) gate is the load-
|
|
645
|
-
// bearing check; without it, rows with multiple tenants under the same
|
|
646
|
-
// session_id would silently pick whichever group came first.
|
|
647
|
-
db.exec(`
|
|
648
|
-
UPDATE session_events
|
|
649
|
-
SET tenant_id = (
|
|
650
|
-
SELECT MAX(t.tenant_id) FROM task_snapshots t
|
|
651
|
-
WHERE t.session_id = session_events.session_id
|
|
652
|
-
)
|
|
653
|
-
WHERE tenant_id = 'default'
|
|
654
|
-
AND (SELECT COUNT(DISTINCT t.tenant_id) FROM task_snapshots t
|
|
655
|
-
WHERE t.session_id = session_events.session_id) = 1
|
|
656
|
-
`);
|
|
657
|
-
db.exec(`
|
|
658
|
-
UPDATE session_handoffs
|
|
659
|
-
SET tenant_id = (
|
|
660
|
-
SELECT MAX(t.tenant_id) FROM task_snapshots t
|
|
661
|
-
WHERE t.session_id = session_handoffs.session_id
|
|
662
|
-
)
|
|
663
|
-
WHERE tenant_id = 'default'
|
|
664
|
-
AND (SELECT COUNT(DISTINCT t.tenant_id) FROM task_snapshots t
|
|
665
|
-
WHERE t.session_id = session_handoffs.session_id) = 1
|
|
666
|
-
`);
|
|
667
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_events_tenant_session ON session_events(tenant_id, session_id, created_at DESC, id DESC)`);
|
|
668
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_session ON session_handoffs(tenant_id, session_id, created_at DESC)`);
|
|
669
|
-
},
|
|
670
|
-
},
|
|
671
|
-
{
|
|
672
|
-
version: 23,
|
|
673
|
-
up: (db) => {
|
|
674
|
-
// task_snapshots: add scope so all three continuity tables carry it.
|
|
675
|
-
// Self-heal partial-init stores via CREATE TABLE IF NOT EXISTS (the v22
|
|
676
|
-
// session_events / session_handoffs healing is upstream).
|
|
677
|
-
db.exec(`
|
|
678
|
-
CREATE TABLE IF NOT EXISTS task_snapshots (
|
|
679
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
680
|
-
task TEXT NOT NULL,
|
|
681
|
-
summary TEXT NOT NULL,
|
|
682
|
-
next_step TEXT NOT NULL,
|
|
683
|
-
status TEXT NOT NULL,
|
|
684
|
-
source TEXT NOT NULL,
|
|
685
|
-
session_id TEXT,
|
|
686
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
687
|
-
created_at TEXT NOT NULL,
|
|
688
|
-
updated_at TEXT NOT NULL
|
|
689
|
-
)
|
|
690
|
-
`);
|
|
691
|
-
if (!tableHasColumn(db, 'task_snapshots', 'scope')) {
|
|
692
|
-
db.exec(`ALTER TABLE task_snapshots ADD COLUMN scope TEXT`);
|
|
693
|
-
}
|
|
694
|
-
// Quarantine policy (codex round 1 P1): pre-existing continuity rows
|
|
695
|
-
// with NULL scope cannot be safely classified as public after the fact.
|
|
696
|
-
// Mark them 'unknown:legacy' so the api.recall + cmdRecall default-deny
|
|
697
|
-
// filter excludes them for no-scope callers. Fresh rows from new
|
|
698
|
-
// writers carry NULL when scope is unspecified (legitimate non-Slack
|
|
699
|
-
// writes); the 'unknown:legacy' marker is a v23-only one-shot for
|
|
700
|
-
// pre-upgrade rows.
|
|
701
|
-
//
|
|
702
|
-
// Run UPDATEs only on tables that exist (some test paths and edge stores
|
|
703
|
-
// skip v22's table healing). The UPDATEs are themselves idempotent via
|
|
704
|
-
// the WHERE scope IS NULL clause, so re-running is a no-op.
|
|
705
|
-
if (tableExists(db, 'task_snapshots')) {
|
|
706
|
-
db.exec(`UPDATE task_snapshots SET scope = 'unknown:legacy' WHERE scope IS NULL`);
|
|
707
|
-
}
|
|
708
|
-
if (tableExists(db, 'session_events')) {
|
|
709
|
-
db.exec(`UPDATE session_events SET scope = 'unknown:legacy' WHERE scope IS NULL`);
|
|
710
|
-
}
|
|
711
|
-
if (tableExists(db, 'session_handoffs')) {
|
|
712
|
-
db.exec(`UPDATE session_handoffs SET scope = 'unknown:legacy' WHERE scope IS NULL`);
|
|
713
|
-
}
|
|
714
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_task_snapshots_tenant_scope ON task_snapshots(tenant_id, scope, status)`);
|
|
715
|
-
},
|
|
716
|
-
},
|
|
717
|
-
{
|
|
718
|
-
version: 24,
|
|
719
|
-
up: (db) => {
|
|
720
|
-
// v1.3.0 GitHub connector schema (codex round 1, 2026-05-04).
|
|
721
|
-
// Six tables + a min_compatible_binary meta row for rollback safety.
|
|
722
|
-
db.exec(`
|
|
723
|
-
CREATE TABLE IF NOT EXISTS github_event_log (
|
|
724
|
-
idempotency_key TEXT PRIMARY KEY,
|
|
725
|
-
delivery_id TEXT NOT NULL,
|
|
726
|
-
event_name TEXT NOT NULL,
|
|
727
|
-
ingested_at TEXT NOT NULL,
|
|
728
|
-
memory_id TEXT
|
|
729
|
-
)
|
|
730
|
-
`);
|
|
731
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_github_event_log_memory ON github_event_log(memory_id) WHERE memory_id IS NOT NULL`);
|
|
732
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_github_event_log_delivery ON github_event_log(delivery_id)`);
|
|
733
|
-
db.exec(`
|
|
734
|
-
CREATE TABLE IF NOT EXISTS github_cursors (
|
|
735
|
-
tenant_id TEXT NOT NULL,
|
|
736
|
-
repo_full_name TEXT NOT NULL,
|
|
737
|
-
issues_hwm TEXT,
|
|
738
|
-
issue_comments_hwm TEXT,
|
|
739
|
-
pr_review_comments_hwm TEXT,
|
|
740
|
-
updated_at TEXT NOT NULL,
|
|
741
|
-
PRIMARY KEY (tenant_id, repo_full_name)
|
|
742
|
-
)
|
|
743
|
-
`);
|
|
744
|
-
db.exec(`
|
|
745
|
-
CREATE TABLE IF NOT EXISTS github_dlq (
|
|
746
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
747
|
-
tenant_id TEXT NOT NULL,
|
|
748
|
-
raw_payload TEXT NOT NULL,
|
|
749
|
-
error TEXT NOT NULL,
|
|
750
|
-
event_name TEXT,
|
|
751
|
-
delivery_id TEXT,
|
|
752
|
-
signature TEXT,
|
|
753
|
-
installation_id TEXT,
|
|
754
|
-
repo_full_name TEXT,
|
|
755
|
-
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
756
|
-
received_at TEXT NOT NULL,
|
|
757
|
-
retried_at TEXT,
|
|
758
|
-
bucket TEXT NOT NULL DEFAULT 'parse_error'
|
|
759
|
-
)
|
|
760
|
-
`);
|
|
761
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_github_dlq_tenant_received ON github_dlq(tenant_id, received_at)`);
|
|
762
|
-
db.exec(`
|
|
763
|
-
CREATE TABLE IF NOT EXISTS github_installations (
|
|
764
|
-
installation_id TEXT PRIMARY KEY,
|
|
765
|
-
tenant_id TEXT NOT NULL,
|
|
766
|
-
added_at TEXT NOT NULL
|
|
767
|
-
)
|
|
768
|
-
`);
|
|
769
|
-
// PAT-mode multi-tenant routing (codex P0 #4). Maps repo_full_name to
|
|
770
|
-
// tenant when the webhook envelope has no `installation` field. Composite
|
|
771
|
-
// PK so the same repo can intentionally be visible to multiple tenants
|
|
772
|
-
// (e.g., shared tooling accounts) — collision is on (repo, tenant) pair.
|
|
773
|
-
db.exec(`
|
|
774
|
-
CREATE TABLE IF NOT EXISTS github_repositories (
|
|
775
|
-
repo_full_name TEXT NOT NULL,
|
|
776
|
-
tenant_id TEXT NOT NULL,
|
|
777
|
-
added_at TEXT NOT NULL,
|
|
778
|
-
PRIMARY KEY (repo_full_name, tenant_id)
|
|
779
|
-
)
|
|
780
|
-
`);
|
|
781
|
-
// Rollback-safety guard (codex P0 #2). Any binary < 1.2.1 lacks the
|
|
782
|
-
// generic *:private:* default-deny and would leak github:private:* rows
|
|
783
|
-
// if it opened this DB. The startup guard in v1.2.1+ refuses to open a
|
|
784
|
-
// DB whose min_compatible_binary is newer than its own version.
|
|
785
|
-
db.prepare(`INSERT INTO meta(key, value) VALUES('min_compatible_binary', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run('1.2.1');
|
|
786
|
-
},
|
|
787
|
-
},
|
|
788
|
-
{
|
|
789
|
-
version: 25,
|
|
790
|
-
up: (db) => {
|
|
791
|
-
// v1.5.0 DAG-aware recall — cache summary metadata so the assembler can
|
|
792
|
-
// reason about scope without re-walking the DAG. Three additive,
|
|
793
|
-
// optional columns. No min_compatible_binary bump: these columns are
|
|
794
|
-
// pure metadata; older binaries opening this DB will see them as NULL
|
|
795
|
-
// / 0 and behave as before. See docs/plans/2026-05-05-dag-recall.md
|
|
796
|
-
// Task 1.
|
|
797
|
-
if (!tableHasColumn(db, 'memories', 'descendant_count')) {
|
|
798
|
-
db.exec(`ALTER TABLE memories ADD COLUMN descendant_count INTEGER NOT NULL DEFAULT 0`);
|
|
799
|
-
}
|
|
800
|
-
if (!tableHasColumn(db, 'memories', 'earliest_at')) {
|
|
801
|
-
db.exec(`ALTER TABLE memories ADD COLUMN earliest_at TEXT`);
|
|
802
|
-
}
|
|
803
|
-
if (!tableHasColumn(db, 'memories', 'latest_at')) {
|
|
804
|
-
db.exec(`ALTER TABLE memories ADD COLUMN latest_at TEXT`);
|
|
805
|
-
}
|
|
806
|
-
// Backfill descendant_count for existing level-2 summary rows. Use
|
|
807
|
-
// dag_parent_id pointing at the summary id. Level-3 (entity profiles)
|
|
808
|
-
// not built today; their descendant_count stays at default 0.
|
|
809
|
-
db.exec(`
|
|
810
|
-
UPDATE memories
|
|
811
|
-
SET descendant_count = (
|
|
812
|
-
SELECT COUNT(*) FROM memories AS c WHERE c.dag_parent_id = memories.id
|
|
813
|
-
)
|
|
814
|
-
WHERE dag_level >= 2
|
|
815
|
-
AND descendant_count = 0
|
|
816
|
-
`);
|
|
817
|
-
// Backfill earliest_at / latest_at from child created timestamps for
|
|
818
|
-
// existing summaries. Children of a level-2 summary are level-1 facts.
|
|
819
|
-
db.exec(`
|
|
820
|
-
UPDATE memories
|
|
821
|
-
SET earliest_at = (
|
|
822
|
-
SELECT MIN(c.created) FROM memories AS c WHERE c.dag_parent_id = memories.id
|
|
823
|
-
),
|
|
824
|
-
latest_at = (
|
|
825
|
-
SELECT MAX(c.created) FROM memories AS c WHERE c.dag_parent_id = memories.id
|
|
826
|
-
)
|
|
827
|
-
WHERE dag_level >= 2
|
|
828
|
-
AND earliest_at IS NULL
|
|
829
|
-
`);
|
|
830
|
-
},
|
|
831
|
-
},
|
|
832
|
-
{
|
|
833
|
-
version: 26,
|
|
834
|
-
up: (db) => {
|
|
835
|
-
// v1.12.0 A5 v2 sub-1: add role column to api_keys for the admin/member
|
|
836
|
-
// distinction that gates /v1/sleep. Additive — existing keys backfill to
|
|
837
|
-
// 'admin' via DEFAULT (single-tenant operator = admin by definition).
|
|
838
|
-
// No min_compatible_binary bump: old binaries (v1.11.x) ignore the
|
|
839
|
-
// column on SELECTs that don't name it; new binaries on old data run
|
|
840
|
-
// this migration at openHippoDb time before any createApiKey call.
|
|
841
|
-
//
|
|
842
|
-
// v1.12.7 defensive: also guard on tableExists. If a DB landed in the
|
|
843
|
-
// partial-v16-state (api_keys table missing despite schema_version >= 16),
|
|
844
|
-
// running ALTER TABLE here would crash and block all later migrations.
|
|
845
|
-
// The v27 heal below recreates api_keys with the role column already
|
|
846
|
-
// present, so this ALTER becomes a no-op anyway for that path. Defense
|
|
847
|
-
// in depth: guard so v26 never crashes on the partial-apply state.
|
|
848
|
-
if (tableExists(db, 'api_keys') && !tableHasColumn(db, 'api_keys', 'role')) {
|
|
849
|
-
db.exec(`ALTER TABLE api_keys ADD COLUMN role TEXT NOT NULL DEFAULT 'admin'`);
|
|
850
|
-
}
|
|
851
|
-
},
|
|
852
|
-
},
|
|
853
|
-
{
|
|
854
|
-
version: 27,
|
|
855
|
-
up: (db) => {
|
|
856
|
-
// v1.12.7 self-heal — re-assert the v16 schema (api_keys + audit_log).
|
|
857
|
-
//
|
|
858
|
-
// Surfaced 2026-05-24 on Keith's ~/.hippo/hippo.db: schema_version
|
|
859
|
-
// recorded as 25 but api_keys and audit_log tables were missing from
|
|
860
|
-
// migration v16. Root cause unknown — the migration runner has wrapped
|
|
861
|
-
// each migration in BEGIN/COMMIT since the first SQLite commit, so
|
|
862
|
-
// atomicity isn't the bug. Possible causes: DROP TABLE post-migration,
|
|
863
|
-
// SQL import / restore from a pre-v16 backup over a v16+ schema_version,
|
|
864
|
-
// or some edge case the wrapping doesn't catch. Cause may be operator
|
|
865
|
-
// action; either way the practical fix is the same.
|
|
866
|
-
//
|
|
867
|
-
// All CREATE IF NOT EXISTS — zero-cost no-op for users without the
|
|
868
|
-
// bug, fixes anyone who has it. Includes the role column from the
|
|
869
|
-
// start so it matches v26's intent without needing v26 to ALTER on
|
|
870
|
-
// this heal path.
|
|
871
|
-
db.exec(`
|
|
872
|
-
CREATE TABLE IF NOT EXISTS api_keys (
|
|
873
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
874
|
-
key_id TEXT UNIQUE NOT NULL,
|
|
875
|
-
key_hash TEXT NOT NULL,
|
|
876
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
877
|
-
label TEXT,
|
|
878
|
-
created_at TEXT NOT NULL,
|
|
879
|
-
revoked_at TEXT,
|
|
880
|
-
role TEXT NOT NULL DEFAULT 'admin'
|
|
881
|
-
)
|
|
882
|
-
`);
|
|
883
|
-
db.exec(`
|
|
884
|
-
CREATE INDEX IF NOT EXISTS idx_api_keys_tenant_active
|
|
885
|
-
ON api_keys(tenant_id) WHERE revoked_at IS NULL
|
|
886
|
-
`);
|
|
887
|
-
db.exec(`
|
|
888
|
-
CREATE TABLE IF NOT EXISTS audit_log (
|
|
889
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
890
|
-
ts TEXT NOT NULL,
|
|
891
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
892
|
-
actor TEXT NOT NULL,
|
|
893
|
-
op TEXT NOT NULL,
|
|
894
|
-
target_id TEXT,
|
|
895
|
-
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
896
|
-
)
|
|
897
|
-
`);
|
|
898
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_tenant_ts ON audit_log(tenant_id, ts DESC)`);
|
|
899
|
-
// Belt-and-braces: if api_keys existed before this migration WITHOUT
|
|
900
|
-
// the role column (i.e. v16-shape table that v26's ALTER skipped due
|
|
901
|
-
// to tableExists=false on an earlier broken run, then someone manually
|
|
902
|
-
// CREATEd it without role), backfill role.
|
|
903
|
-
if (tableExists(db, 'api_keys') && !tableHasColumn(db, 'api_keys', 'role')) {
|
|
904
|
-
db.exec(`ALTER TABLE api_keys ADD COLUMN role TEXT NOT NULL DEFAULT 'admin'`);
|
|
905
|
-
}
|
|
906
|
-
},
|
|
907
|
-
},
|
|
908
|
-
{
|
|
909
|
-
version: 28,
|
|
910
|
-
up: (db) => {
|
|
911
|
-
// E1 of 5-episode DAG live-coupling arc (docs/plans/2026-05-25-dag-e1-schema-v28.md).
|
|
912
|
-
// Adds dirty-flag persistence for the existing DAG layer's level-2
|
|
913
|
-
// summaries so E2 (child-write propagation) and E3 (sleep-cycle
|
|
914
|
-
// rebuild) have somewhere to write + read the staleness signal. Adds
|
|
915
|
-
// dag_level_3_built_at as a column (Keith Q5 pick: lets E5 entity-
|
|
916
|
-
// profile build path land without a second migration).
|
|
917
|
-
//
|
|
918
|
-
// All columns are additive + DEFAULTed/nullable, so backfill is
|
|
919
|
-
// automatic for existing rows. No min_compatible_binary bump: old
|
|
920
|
-
// binaries (v1.12.x) ignore the columns on SELECTs that don't name
|
|
921
|
-
// them; new binaries on old data hit this migration at openHippoDb
|
|
922
|
-
// time before any DAG path touches summary_dirty.
|
|
923
|
-
//
|
|
924
|
-
// Precedent for column-only guards on memories: v25 (db.ts:827),
|
|
925
|
-
// which added the DAG cache columns the same way. memories table
|
|
926
|
-
// itself comes from v1 and is always present, so the tableExists
|
|
927
|
-
// half of the v26/v27 guard pattern isn't load-bearing here.
|
|
928
|
-
if (!tableHasColumn(db, 'memories', 'summary_dirty')) {
|
|
929
|
-
db.exec(`ALTER TABLE memories ADD COLUMN summary_dirty INTEGER NOT NULL DEFAULT 0`);
|
|
930
|
-
}
|
|
931
|
-
if (!tableHasColumn(db, 'memories', 'last_rebuilt_at')) {
|
|
932
|
-
db.exec(`ALTER TABLE memories ADD COLUMN last_rebuilt_at TEXT`);
|
|
933
|
-
}
|
|
934
|
-
if (!tableHasColumn(db, 'memories', 'rebuild_count')) {
|
|
935
|
-
db.exec(`ALTER TABLE memories ADD COLUMN rebuild_count INTEGER NOT NULL DEFAULT 0`);
|
|
936
|
-
}
|
|
937
|
-
if (!tableHasColumn(db, 'memories', 'dag_level_3_built_at')) {
|
|
938
|
-
// Reserved for E5: buildEntityProfiles will set this on level-3
|
|
939
|
-
// rows when they're created. Always NULL for level 0/1/2 rows.
|
|
940
|
-
db.exec(`ALTER TABLE memories ADD COLUMN dag_level_3_built_at TEXT`);
|
|
941
|
-
}
|
|
942
|
-
},
|
|
943
|
-
},
|
|
944
|
-
{
|
|
945
|
-
version: 29,
|
|
946
|
-
up: (db) => {
|
|
947
|
-
// E2 prediction first-class object (docs/plans/2026-05-26-e2-prediction-object.md).
|
|
948
|
-
// Adds a canonical predictions table for J3 reference-class /
|
|
949
|
-
// planning-fallacy detector (a follow-up episode). Predictions
|
|
950
|
-
// duplicate claim_text in the table itself so memory deletion
|
|
951
|
-
// (forget/consolidate/archive) does not lose prediction data; FK
|
|
952
|
-
// memory_id is NULLABLE with ON DELETE SET NULL.
|
|
953
|
-
//
|
|
954
|
-
// Cross-tenant safety: BEFORE INSERT + BEFORE UPDATE triggers
|
|
955
|
-
// enforce tenant_id match against the referenced memory. SQLite's
|
|
956
|
-
// ON DELETE SET NULL is incompatible with composite FK where one
|
|
957
|
-
// side is NOT NULL, so the trigger pattern replaces a composite
|
|
958
|
-
// FK target. Precedent: v14 memories.kind trigger pair at db.ts:298-322.
|
|
959
|
-
//
|
|
960
|
-
// CHECK constraint pins closure_state to (open|closed|closed-unknown).
|
|
961
|
-
// J3 computes accuracy (clean vs regressed) from (estimate_value,
|
|
962
|
-
// actual_value) at query time.
|
|
963
|
-
if (!tableExists(db, 'predictions')) {
|
|
964
|
-
db.exec(`
|
|
965
|
-
CREATE TABLE predictions (
|
|
966
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
967
|
-
memory_id TEXT,
|
|
968
|
-
tenant_id TEXT NOT NULL,
|
|
969
|
-
class_tag TEXT NOT NULL,
|
|
970
|
-
claim_text TEXT NOT NULL,
|
|
971
|
-
estimate_value REAL,
|
|
972
|
-
estimate_unit TEXT,
|
|
973
|
-
target_date TEXT,
|
|
974
|
-
actual_value REAL,
|
|
975
|
-
closure_state TEXT NOT NULL DEFAULT 'open'
|
|
976
|
-
CHECK (closure_state IN ('open', 'closed', 'closed-unknown')),
|
|
977
|
-
closed_at TEXT,
|
|
978
|
-
closure_note TEXT,
|
|
979
|
-
created_at TEXT NOT NULL,
|
|
980
|
-
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL
|
|
981
|
-
)
|
|
982
|
-
`);
|
|
983
|
-
db.exec(`
|
|
984
|
-
CREATE INDEX IF NOT EXISTS idx_predictions_tenant_class
|
|
985
|
-
ON predictions(tenant_id, class_tag, closure_state)
|
|
986
|
-
`);
|
|
987
|
-
db.exec(`
|
|
988
|
-
CREATE INDEX IF NOT EXISTS idx_predictions_memory
|
|
989
|
-
ON predictions(memory_id) WHERE memory_id IS NOT NULL
|
|
990
|
-
`);
|
|
991
|
-
// Cross-tenant safety: tenant_id must match the referenced memory's
|
|
992
|
-
// tenant_id when memory_id IS NOT NULL. INSERT + UPDATE pair, mirroring
|
|
993
|
-
// v14 memories.kind enforcement (db.ts:298-322).
|
|
994
|
-
db.exec(`
|
|
995
|
-
CREATE TRIGGER IF NOT EXISTS trg_predictions_tenant_match_insert
|
|
996
|
-
BEFORE INSERT ON predictions
|
|
997
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
998
|
-
BEGIN
|
|
999
|
-
SELECT CASE
|
|
1000
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1001
|
-
THEN RAISE(ABORT, 'predictions.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1002
|
-
END;
|
|
1003
|
-
END
|
|
1004
|
-
`);
|
|
1005
|
-
db.exec(`
|
|
1006
|
-
CREATE TRIGGER IF NOT EXISTS trg_predictions_tenant_match_update
|
|
1007
|
-
BEFORE UPDATE ON predictions
|
|
1008
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
1009
|
-
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1010
|
-
BEGIN
|
|
1011
|
-
SELECT CASE
|
|
1012
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1013
|
-
THEN RAISE(ABORT, 'predictions.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1014
|
-
END;
|
|
1015
|
-
END
|
|
1016
|
-
`);
|
|
1017
|
-
}
|
|
1018
|
-
},
|
|
1019
|
-
},
|
|
1020
|
-
{
|
|
1021
|
-
version: 30,
|
|
1022
|
-
up: (db) => {
|
|
1023
|
-
// E2 decision first-class object (docs/plans/2026-05-28-e2-decision-object.md).
|
|
1024
|
-
// Promotes `hippo decide` from a tagged memory (which decayed on a 90-day
|
|
1025
|
-
// half-life even while the decision was still in force) to a canonical
|
|
1026
|
-
// decisions table that is the source of truth. The memory mirror is kept
|
|
1027
|
-
// for recall but is no longer authoritative; memory_id is NULLABLE with
|
|
1028
|
-
// ON DELETE SET NULL so forget/consolidate/archive does not lose a
|
|
1029
|
-
// decision. Mirrors the v29 predictions tenant-match trigger pattern.
|
|
1030
|
-
//
|
|
1031
|
-
// status (active|superseded|closed): superseded carries a self-FK
|
|
1032
|
-
// superseded_by to the successor decision; closed is a terminal
|
|
1033
|
-
// retire-without-successor. A superseded_by same-tenant trigger makes
|
|
1034
|
-
// cross-tenant supersession unrepresentable at the schema level.
|
|
1035
|
-
if (!tableExists(db, 'decisions')) {
|
|
1036
|
-
db.exec(`
|
|
1037
|
-
CREATE TABLE decisions (
|
|
1038
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1039
|
-
memory_id TEXT,
|
|
1040
|
-
tenant_id TEXT NOT NULL,
|
|
1041
|
-
decision_text TEXT NOT NULL,
|
|
1042
|
-
context TEXT,
|
|
1043
|
-
status TEXT NOT NULL DEFAULT 'active'
|
|
1044
|
-
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1045
|
-
superseded_by INTEGER,
|
|
1046
|
-
superseded_at TEXT,
|
|
1047
|
-
closed_at TEXT,
|
|
1048
|
-
created_at TEXT NOT NULL,
|
|
1049
|
-
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1050
|
-
FOREIGN KEY (superseded_by) REFERENCES decisions(id) ON DELETE SET NULL
|
|
1051
|
-
)
|
|
1052
|
-
`);
|
|
1053
|
-
db.exec(`
|
|
1054
|
-
CREATE INDEX IF NOT EXISTS idx_decisions_tenant_status
|
|
1055
|
-
ON decisions(tenant_id, status)
|
|
1056
|
-
`);
|
|
1057
|
-
db.exec(`
|
|
1058
|
-
CREATE INDEX IF NOT EXISTS idx_decisions_memory
|
|
1059
|
-
ON decisions(memory_id) WHERE memory_id IS NOT NULL
|
|
1060
|
-
`);
|
|
1061
|
-
// Cross-tenant safety vs the referenced memory (mirrors predictions v29).
|
|
1062
|
-
db.exec(`
|
|
1063
|
-
CREATE TRIGGER IF NOT EXISTS trg_decisions_tenant_match_insert
|
|
1064
|
-
BEFORE INSERT ON decisions
|
|
1065
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
1066
|
-
BEGIN
|
|
1067
|
-
SELECT CASE
|
|
1068
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1069
|
-
THEN RAISE(ABORT, 'decisions.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1070
|
-
END;
|
|
1071
|
-
END
|
|
1072
|
-
`);
|
|
1073
|
-
db.exec(`
|
|
1074
|
-
CREATE TRIGGER IF NOT EXISTS trg_decisions_tenant_match_update
|
|
1075
|
-
BEFORE UPDATE ON decisions
|
|
1076
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
1077
|
-
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1078
|
-
BEGIN
|
|
1079
|
-
SELECT CASE
|
|
1080
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1081
|
-
THEN RAISE(ABORT, 'decisions.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1082
|
-
END;
|
|
1083
|
-
END
|
|
1084
|
-
`);
|
|
1085
|
-
// Cross-tenant safety vs the successor decision (self-FK). superseded_by
|
|
1086
|
-
// is set only via UPDATE (the supersede path); the successor must share
|
|
1087
|
-
// the tenant. The successor row already exists in the same transaction
|
|
1088
|
-
// when this fires, so the subquery resolves.
|
|
1089
|
-
db.exec(`
|
|
1090
|
-
CREATE TRIGGER IF NOT EXISTS trg_decisions_supersede_tenant_match_update
|
|
1091
|
-
BEFORE UPDATE ON decisions
|
|
1092
|
-
WHEN NEW.superseded_by IS NOT NULL
|
|
1093
|
-
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1094
|
-
BEGIN
|
|
1095
|
-
SELECT CASE
|
|
1096
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM decisions WHERE id = NEW.superseded_by)
|
|
1097
|
-
THEN RAISE(ABORT, 'decisions.superseded_by must reference a decision in the same tenant')
|
|
1098
|
-
END;
|
|
1099
|
-
END
|
|
1100
|
-
`);
|
|
1101
|
-
}
|
|
1102
|
-
},
|
|
1103
|
-
},
|
|
1104
|
-
{
|
|
1105
|
-
version: 31,
|
|
1106
|
-
up: (db) => {
|
|
1107
|
-
// E2 incident first-class object (docs/plans/2026-05-29-e2-incident-object.md).
|
|
1108
|
-
// Mirrors the v30 decisions block but for an open -> resolved -> closed
|
|
1109
|
-
// lifecycle (NOT supersede): there is no superseded_by self-FK and no
|
|
1110
|
-
// supersede trigger. An incident is a postmortem capsule: a recorded
|
|
1111
|
-
// operational event with a lifecycle and optional linked receipts (the
|
|
1112
|
-
// memories that are its evidence, stored as a JSON array of ids in
|
|
1113
|
-
// linked_memory_ids). The memory mirror is kept for recall but is not
|
|
1114
|
-
// authoritative; memory_id is NULLABLE with ON DELETE SET NULL so
|
|
1115
|
-
// forget/consolidate/archive does not lose an incident.
|
|
1116
|
-
//
|
|
1117
|
-
// status (open|resolved|closed): resolved records a resolution_text +
|
|
1118
|
-
// resolved_at and stays on record; closed is a terminal retire reachable
|
|
1119
|
-
// from open or resolved. Cross-tenant safety: BEFORE INSERT + BEFORE
|
|
1120
|
-
// UPDATE triggers enforce incidents.tenant_id == the referenced memory's
|
|
1121
|
-
// tenant_id (verbatim mirror of the v30 decisions tenant-match triggers).
|
|
1122
|
-
if (!tableExists(db, 'incidents')) {
|
|
1123
|
-
db.exec(`
|
|
1124
|
-
CREATE TABLE incidents (
|
|
1125
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1126
|
-
memory_id TEXT,
|
|
1127
|
-
tenant_id TEXT NOT NULL,
|
|
1128
|
-
incident_text TEXT NOT NULL,
|
|
1129
|
-
context TEXT,
|
|
1130
|
-
status TEXT NOT NULL DEFAULT 'open'
|
|
1131
|
-
CHECK (status IN ('open', 'resolved', 'closed')),
|
|
1132
|
-
resolution_text TEXT,
|
|
1133
|
-
resolved_at TEXT,
|
|
1134
|
-
closed_at TEXT,
|
|
1135
|
-
linked_memory_ids TEXT NOT NULL DEFAULT '[]',
|
|
1136
|
-
created_at TEXT NOT NULL,
|
|
1137
|
-
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL
|
|
1138
|
-
)
|
|
1139
|
-
`);
|
|
1140
|
-
db.exec(`
|
|
1141
|
-
CREATE INDEX IF NOT EXISTS idx_incidents_tenant_status
|
|
1142
|
-
ON incidents(tenant_id, status)
|
|
1143
|
-
`);
|
|
1144
|
-
db.exec(`
|
|
1145
|
-
CREATE INDEX IF NOT EXISTS idx_incidents_memory
|
|
1146
|
-
ON incidents(memory_id) WHERE memory_id IS NOT NULL
|
|
1147
|
-
`);
|
|
1148
|
-
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1149
|
-
// v30 decisions tenant-match triggers; no supersede trigger).
|
|
1150
|
-
db.exec(`
|
|
1151
|
-
CREATE TRIGGER IF NOT EXISTS trg_incidents_tenant_match_insert
|
|
1152
|
-
BEFORE INSERT ON incidents
|
|
1153
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
1154
|
-
BEGIN
|
|
1155
|
-
SELECT CASE
|
|
1156
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1157
|
-
THEN RAISE(ABORT, 'incidents.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1158
|
-
END;
|
|
1159
|
-
END
|
|
1160
|
-
`);
|
|
1161
|
-
db.exec(`
|
|
1162
|
-
CREATE TRIGGER IF NOT EXISTS trg_incidents_tenant_match_update
|
|
1163
|
-
BEFORE UPDATE ON incidents
|
|
1164
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
1165
|
-
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1166
|
-
BEGIN
|
|
1167
|
-
SELECT CASE
|
|
1168
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1169
|
-
THEN RAISE(ABORT, 'incidents.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1170
|
-
END;
|
|
1171
|
-
END
|
|
1172
|
-
`);
|
|
1173
|
-
}
|
|
1174
|
-
},
|
|
1175
|
-
},
|
|
1176
|
-
{
|
|
1177
|
-
version: 32,
|
|
1178
|
-
up: (db) => {
|
|
1179
|
-
// E2 process first-class object (docs/plans/2026-05-29-e2-process-object.md).
|
|
1180
|
-
// A process is a "living process map": a named, ordered list of steps that
|
|
1181
|
-
// evolves. Unlike incident (open->resolved->closed, no supersede), process
|
|
1182
|
-
// REUSES the v30 decisions supersede path as its delta mechanism: a process
|
|
1183
|
-
// evolves by being superseded by a NEW VERSION that records what changed
|
|
1184
|
-
// (change_summary) and the full new state (steps), carrying a derived
|
|
1185
|
-
// version counter. So this table combines the v31 incidents tenant-match
|
|
1186
|
-
// trigger pair (vs the referenced memory) WITH the v30 decisions
|
|
1187
|
-
// superseded_by self-FK + supersede tenant-match trigger.
|
|
1188
|
-
//
|
|
1189
|
-
// status (active|superseded|closed): superseded carries a self-FK
|
|
1190
|
-
// superseded_by to the successor version; closed is a terminal
|
|
1191
|
-
// retire-without-successor (only an active head closes). The memory mirror
|
|
1192
|
-
// is kept for recall but is not authoritative; memory_id is NULLABLE with
|
|
1193
|
-
// ON DELETE SET NULL so forget/consolidate/archive does not lose a process.
|
|
1194
|
-
// steps is a JSON-encoded array of step strings (scoped v1; a normalized
|
|
1195
|
-
// process_steps table is deferred). version is server-derived
|
|
1196
|
-
// (predecessor.version + 1); change_summary is set on a successor row only.
|
|
1197
|
-
if (!tableExists(db, 'processes')) {
|
|
1198
|
-
db.exec(`
|
|
1199
|
-
CREATE TABLE processes (
|
|
1200
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1201
|
-
memory_id TEXT,
|
|
1202
|
-
tenant_id TEXT NOT NULL,
|
|
1203
|
-
process_name TEXT NOT NULL,
|
|
1204
|
-
description TEXT,
|
|
1205
|
-
steps TEXT NOT NULL DEFAULT '[]',
|
|
1206
|
-
version INTEGER NOT NULL DEFAULT 1,
|
|
1207
|
-
status TEXT NOT NULL DEFAULT 'active'
|
|
1208
|
-
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1209
|
-
superseded_by INTEGER,
|
|
1210
|
-
superseded_at TEXT,
|
|
1211
|
-
change_summary TEXT,
|
|
1212
|
-
closed_at TEXT,
|
|
1213
|
-
created_at TEXT NOT NULL,
|
|
1214
|
-
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1215
|
-
FOREIGN KEY (superseded_by) REFERENCES processes(id) ON DELETE SET NULL
|
|
1216
|
-
)
|
|
1217
|
-
`);
|
|
1218
|
-
db.exec(`
|
|
1219
|
-
CREATE INDEX IF NOT EXISTS idx_processes_tenant_status
|
|
1220
|
-
ON processes(tenant_id, status)
|
|
1221
|
-
`);
|
|
1222
|
-
db.exec(`
|
|
1223
|
-
CREATE INDEX IF NOT EXISTS idx_processes_memory
|
|
1224
|
-
ON processes(memory_id) WHERE memory_id IS NOT NULL
|
|
1225
|
-
`);
|
|
1226
|
-
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1227
|
-
// v31 incidents / v30 decisions tenant-match triggers).
|
|
1228
|
-
db.exec(`
|
|
1229
|
-
CREATE TRIGGER IF NOT EXISTS trg_processes_tenant_match_insert
|
|
1230
|
-
BEFORE INSERT ON processes
|
|
1231
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
1232
|
-
BEGIN
|
|
1233
|
-
SELECT CASE
|
|
1234
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1235
|
-
THEN RAISE(ABORT, 'processes.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1236
|
-
END;
|
|
1237
|
-
END
|
|
1238
|
-
`);
|
|
1239
|
-
db.exec(`
|
|
1240
|
-
CREATE TRIGGER IF NOT EXISTS trg_processes_tenant_match_update
|
|
1241
|
-
BEFORE UPDATE ON processes
|
|
1242
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
1243
|
-
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1244
|
-
BEGIN
|
|
1245
|
-
SELECT CASE
|
|
1246
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1247
|
-
THEN RAISE(ABORT, 'processes.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1248
|
-
END;
|
|
1249
|
-
END
|
|
1250
|
-
`);
|
|
1251
|
-
// Cross-tenant safety vs the successor process (self-FK; verbatim mirror
|
|
1252
|
-
// of the v30 decisions supersede trigger). superseded_by is set only via
|
|
1253
|
-
// the supersede UPDATE; the successor must share the tenant. The successor
|
|
1254
|
-
// row already exists in the same transaction when this fires.
|
|
1255
|
-
db.exec(`
|
|
1256
|
-
CREATE TRIGGER IF NOT EXISTS trg_processes_supersede_tenant_match_update
|
|
1257
|
-
BEFORE UPDATE ON processes
|
|
1258
|
-
WHEN NEW.superseded_by IS NOT NULL
|
|
1259
|
-
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1260
|
-
BEGIN
|
|
1261
|
-
SELECT CASE
|
|
1262
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM processes WHERE id = NEW.superseded_by)
|
|
1263
|
-
THEN RAISE(ABORT, 'processes.superseded_by must reference a process in the same tenant')
|
|
1264
|
-
END;
|
|
1265
|
-
END
|
|
1266
|
-
`);
|
|
1267
|
-
}
|
|
1268
|
-
},
|
|
1269
|
-
},
|
|
1270
|
-
{
|
|
1271
|
-
version: 33,
|
|
1272
|
-
up: (db) => {
|
|
1273
|
-
// E2 policy first-class object (docs/plans/2026-05-30-e2-policy-object.md).
|
|
1274
|
-
// The "bi-temporal-first" object type: a named rule/statement that is in
|
|
1275
|
-
// force over an EFFECTIVE-TIME range (valid_from required, valid_to nullable
|
|
1276
|
-
// = open-ended) and evolves via the v32 processes supersede machinery
|
|
1277
|
-
// (superseded_by self-FK + supersede tenant-match trigger + version +
|
|
1278
|
-
// change_summary). This table = the v32 processes table MINUS `steps`
|
|
1279
|
-
// (a policy has policy_text, not an ordered step list) PLUS the first-class
|
|
1280
|
-
// effective-time columns valid_from/valid_to. Valid-time is the queryable
|
|
1281
|
-
// axis (the as-of query loadPoliciesAsOf); transaction-time is present via
|
|
1282
|
-
// created_at + the supersede chain's superseded_at (time-travel deferred).
|
|
1283
|
-
//
|
|
1284
|
-
// All date inputs are normalized to canonical ISO-8601 datetime
|
|
1285
|
-
// (toISOString) at the store boundary before persist/compare, so the
|
|
1286
|
-
// fixed-width values sort lexically and the half-open [valid_from, valid_to)
|
|
1287
|
-
// as-of comparison is correct (plan-eng-critic round-1 CRIT fix).
|
|
1288
|
-
if (!tableExists(db, 'policies')) {
|
|
1289
|
-
db.exec(`
|
|
1290
|
-
CREATE TABLE policies (
|
|
1291
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1292
|
-
memory_id TEXT,
|
|
1293
|
-
tenant_id TEXT NOT NULL,
|
|
1294
|
-
policy_name TEXT NOT NULL,
|
|
1295
|
-
policy_text TEXT NOT NULL,
|
|
1296
|
-
valid_from TEXT NOT NULL,
|
|
1297
|
-
valid_to TEXT,
|
|
1298
|
-
version INTEGER NOT NULL DEFAULT 1,
|
|
1299
|
-
status TEXT NOT NULL DEFAULT 'active'
|
|
1300
|
-
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1301
|
-
superseded_by INTEGER,
|
|
1302
|
-
superseded_at TEXT,
|
|
1303
|
-
change_summary TEXT,
|
|
1304
|
-
closed_at TEXT,
|
|
1305
|
-
created_at TEXT NOT NULL,
|
|
1306
|
-
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1307
|
-
FOREIGN KEY (superseded_by) REFERENCES policies(id) ON DELETE SET NULL
|
|
1308
|
-
)
|
|
1309
|
-
`);
|
|
1310
|
-
db.exec(`
|
|
1311
|
-
CREATE INDEX IF NOT EXISTS idx_policies_tenant_status
|
|
1312
|
-
ON policies(tenant_id, status)
|
|
1313
|
-
`);
|
|
1314
|
-
db.exec(`
|
|
1315
|
-
CREATE INDEX IF NOT EXISTS idx_policies_memory
|
|
1316
|
-
ON policies(memory_id) WHERE memory_id IS NOT NULL
|
|
1317
|
-
`);
|
|
1318
|
-
// Supports the as-of query (active policies in force at a valid-time).
|
|
1319
|
-
db.exec(`
|
|
1320
|
-
CREATE INDEX IF NOT EXISTS idx_policies_asof
|
|
1321
|
-
ON policies(tenant_id, valid_from)
|
|
1322
|
-
`);
|
|
1323
|
-
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1324
|
-
// v32 processes tenant-match triggers).
|
|
1325
|
-
db.exec(`
|
|
1326
|
-
CREATE TRIGGER IF NOT EXISTS trg_policies_tenant_match_insert
|
|
1327
|
-
BEFORE INSERT ON policies
|
|
1328
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
1329
|
-
BEGIN
|
|
1330
|
-
SELECT CASE
|
|
1331
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1332
|
-
THEN RAISE(ABORT, 'policies.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1333
|
-
END;
|
|
1334
|
-
END
|
|
1335
|
-
`);
|
|
1336
|
-
db.exec(`
|
|
1337
|
-
CREATE TRIGGER IF NOT EXISTS trg_policies_tenant_match_update
|
|
1338
|
-
BEFORE UPDATE ON policies
|
|
1339
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
1340
|
-
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1341
|
-
BEGIN
|
|
1342
|
-
SELECT CASE
|
|
1343
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1344
|
-
THEN RAISE(ABORT, 'policies.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1345
|
-
END;
|
|
1346
|
-
END
|
|
1347
|
-
`);
|
|
1348
|
-
// Cross-tenant safety vs the successor policy (self-FK; verbatim mirror of
|
|
1349
|
-
// the v32 processes supersede trigger).
|
|
1350
|
-
db.exec(`
|
|
1351
|
-
CREATE TRIGGER IF NOT EXISTS trg_policies_supersede_tenant_match_update
|
|
1352
|
-
BEFORE UPDATE ON policies
|
|
1353
|
-
WHEN NEW.superseded_by IS NOT NULL
|
|
1354
|
-
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1355
|
-
BEGIN
|
|
1356
|
-
SELECT CASE
|
|
1357
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM policies WHERE id = NEW.superseded_by)
|
|
1358
|
-
THEN RAISE(ABORT, 'policies.superseded_by must reference a policy in the same tenant')
|
|
1359
|
-
END;
|
|
1360
|
-
END
|
|
1361
|
-
`);
|
|
1362
|
-
}
|
|
1363
|
-
},
|
|
1364
|
-
},
|
|
1365
|
-
{
|
|
1366
|
-
version: 34,
|
|
1367
|
-
up: (db) => {
|
|
1368
|
-
// E2 skill first-class object (docs/plans/2026-05-30-e2-skill-object.md).
|
|
1369
|
-
// A skill is a reusable, agent-followable capability: an `instructions` body
|
|
1370
|
-
// + an optional `trigger_text` (when to apply), evolving via the v32
|
|
1371
|
-
// processes supersede machinery (superseded_by self-FK + supersede
|
|
1372
|
-
// tenant-match trigger + version + change_summary). This table = the v32
|
|
1373
|
-
// processes table MINUS `steps` (a skill's content is a single instructions
|
|
1374
|
-
// body) PLUS `instructions` (NOT NULL) and `trigger_text`. "Executable" is
|
|
1375
|
-
// scoped to an agent-followable instruction that EXPORTS into the agent's
|
|
1376
|
-
// in-force rules (AGENTS.md / CLAUDE.md) via exportSkills; literal code
|
|
1377
|
-
// execution is deferred. NOTE: the column is `trigger_text`, NOT `trigger`,
|
|
1378
|
-
// because TRIGGER is a SQLite reserved keyword.
|
|
1379
|
-
if (!tableExists(db, 'skills')) {
|
|
1380
|
-
db.exec(`
|
|
1381
|
-
CREATE TABLE skills (
|
|
1382
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1383
|
-
memory_id TEXT,
|
|
1384
|
-
tenant_id TEXT NOT NULL,
|
|
1385
|
-
skill_name TEXT NOT NULL,
|
|
1386
|
-
instructions TEXT NOT NULL,
|
|
1387
|
-
trigger_text TEXT,
|
|
1388
|
-
version INTEGER NOT NULL DEFAULT 1,
|
|
1389
|
-
status TEXT NOT NULL DEFAULT 'active'
|
|
1390
|
-
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1391
|
-
superseded_by INTEGER,
|
|
1392
|
-
superseded_at TEXT,
|
|
1393
|
-
change_summary TEXT,
|
|
1394
|
-
closed_at TEXT,
|
|
1395
|
-
created_at TEXT NOT NULL,
|
|
1396
|
-
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1397
|
-
FOREIGN KEY (superseded_by) REFERENCES skills(id) ON DELETE SET NULL
|
|
1398
|
-
)
|
|
1399
|
-
`);
|
|
1400
|
-
db.exec(`
|
|
1401
|
-
CREATE INDEX IF NOT EXISTS idx_skills_tenant_status
|
|
1402
|
-
ON skills(tenant_id, status)
|
|
1403
|
-
`);
|
|
1404
|
-
db.exec(`
|
|
1405
|
-
CREATE INDEX IF NOT EXISTS idx_skills_memory
|
|
1406
|
-
ON skills(memory_id) WHERE memory_id IS NOT NULL
|
|
1407
|
-
`);
|
|
1408
|
-
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1409
|
-
// v32 processes tenant-match triggers).
|
|
1410
|
-
db.exec(`
|
|
1411
|
-
CREATE TRIGGER IF NOT EXISTS trg_skills_tenant_match_insert
|
|
1412
|
-
BEFORE INSERT ON skills
|
|
1413
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
1414
|
-
BEGIN
|
|
1415
|
-
SELECT CASE
|
|
1416
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1417
|
-
THEN RAISE(ABORT, 'skills.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1418
|
-
END;
|
|
1419
|
-
END
|
|
1420
|
-
`);
|
|
1421
|
-
db.exec(`
|
|
1422
|
-
CREATE TRIGGER IF NOT EXISTS trg_skills_tenant_match_update
|
|
1423
|
-
BEFORE UPDATE ON skills
|
|
1424
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
1425
|
-
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1426
|
-
BEGIN
|
|
1427
|
-
SELECT CASE
|
|
1428
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1429
|
-
THEN RAISE(ABORT, 'skills.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1430
|
-
END;
|
|
1431
|
-
END
|
|
1432
|
-
`);
|
|
1433
|
-
// Cross-tenant safety vs the successor skill (self-FK; verbatim mirror of
|
|
1434
|
-
// the v32 processes supersede trigger).
|
|
1435
|
-
db.exec(`
|
|
1436
|
-
CREATE TRIGGER IF NOT EXISTS trg_skills_supersede_tenant_match_update
|
|
1437
|
-
BEFORE UPDATE ON skills
|
|
1438
|
-
WHEN NEW.superseded_by IS NOT NULL
|
|
1439
|
-
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1440
|
-
BEGIN
|
|
1441
|
-
SELECT CASE
|
|
1442
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM skills WHERE id = NEW.superseded_by)
|
|
1443
|
-
THEN RAISE(ABORT, 'skills.superseded_by must reference a skill in the same tenant')
|
|
1444
|
-
END;
|
|
1445
|
-
END
|
|
1446
|
-
`);
|
|
1447
|
-
}
|
|
1448
|
-
},
|
|
1449
|
-
},
|
|
1450
|
-
{
|
|
1451
|
-
version: 35,
|
|
1452
|
-
up: (db) => {
|
|
1453
|
-
// E2 project_brief first-class object
|
|
1454
|
-
// (docs/plans/2026-05-30-e2-project-brief-object.md). A project_brief is the
|
|
1455
|
-
// living, repo-scoped summary of a repository's state: a `summary` body
|
|
1456
|
-
// scoped to a `repo`, evolving via the v34 skills supersede machinery
|
|
1457
|
-
// (superseded_by self-FK + supersede tenant-match trigger + version +
|
|
1458
|
-
// change_summary). This table = the v34 skills table with
|
|
1459
|
-
// skill_name/trigger_text replaced by `repo` (the repo-scoping dimension)
|
|
1460
|
-
// PLUS `summary` (the brief body). The distinguishing op (refreshBrief, in
|
|
1461
|
-
// src/project-briefs.ts) auto-assembles the summary from the repo's receipts
|
|
1462
|
-
// (memory rows tagged path:<repo>); it needs no schema support beyond `repo`.
|
|
1463
|
-
// All column names were checked against SQLite reserved words (skill-episode
|
|
1464
|
-
// lesson re: `trigger`): repo/summary/version/status/etc. are non-reserved.
|
|
1465
|
-
if (!tableExists(db, 'project_briefs')) {
|
|
1466
|
-
db.exec(`
|
|
1467
|
-
CREATE TABLE project_briefs (
|
|
1468
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1469
|
-
memory_id TEXT,
|
|
1470
|
-
tenant_id TEXT NOT NULL,
|
|
1471
|
-
repo TEXT NOT NULL,
|
|
1472
|
-
summary TEXT NOT NULL,
|
|
1473
|
-
version INTEGER NOT NULL DEFAULT 1,
|
|
1474
|
-
status TEXT NOT NULL DEFAULT 'active'
|
|
1475
|
-
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1476
|
-
superseded_by INTEGER,
|
|
1477
|
-
superseded_at TEXT,
|
|
1478
|
-
change_summary TEXT,
|
|
1479
|
-
closed_at TEXT,
|
|
1480
|
-
created_at TEXT NOT NULL,
|
|
1481
|
-
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1482
|
-
FOREIGN KEY (superseded_by) REFERENCES project_briefs(id) ON DELETE SET NULL
|
|
1483
|
-
)
|
|
1484
|
-
`);
|
|
1485
|
-
db.exec(`
|
|
1486
|
-
CREATE INDEX IF NOT EXISTS idx_project_briefs_tenant_status
|
|
1487
|
-
ON project_briefs(tenant_id, status)
|
|
1488
|
-
`);
|
|
1489
|
-
db.exec(`
|
|
1490
|
-
CREATE INDEX IF NOT EXISTS idx_project_briefs_memory
|
|
1491
|
-
ON project_briefs(memory_id) WHERE memory_id IS NOT NULL
|
|
1492
|
-
`);
|
|
1493
|
-
db.exec(`
|
|
1494
|
-
CREATE INDEX IF NOT EXISTS idx_project_briefs_repo
|
|
1495
|
-
ON project_briefs(tenant_id, repo, status)
|
|
1496
|
-
`);
|
|
1497
|
-
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1498
|
-
// v34 skills tenant-match triggers).
|
|
1499
|
-
db.exec(`
|
|
1500
|
-
CREATE TRIGGER IF NOT EXISTS trg_project_briefs_tenant_match_insert
|
|
1501
|
-
BEFORE INSERT ON project_briefs
|
|
1502
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
1503
|
-
BEGIN
|
|
1504
|
-
SELECT CASE
|
|
1505
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1506
|
-
THEN RAISE(ABORT, 'project_briefs.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1507
|
-
END;
|
|
1508
|
-
END
|
|
1509
|
-
`);
|
|
1510
|
-
db.exec(`
|
|
1511
|
-
CREATE TRIGGER IF NOT EXISTS trg_project_briefs_tenant_match_update
|
|
1512
|
-
BEFORE UPDATE ON project_briefs
|
|
1513
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
1514
|
-
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1515
|
-
BEGIN
|
|
1516
|
-
SELECT CASE
|
|
1517
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1518
|
-
THEN RAISE(ABORT, 'project_briefs.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1519
|
-
END;
|
|
1520
|
-
END
|
|
1521
|
-
`);
|
|
1522
|
-
// Cross-tenant safety vs the successor brief (self-FK; verbatim mirror of
|
|
1523
|
-
// the v34 skills supersede trigger).
|
|
1524
|
-
db.exec(`
|
|
1525
|
-
CREATE TRIGGER IF NOT EXISTS trg_project_briefs_supersede_tenant_match_update
|
|
1526
|
-
BEFORE UPDATE ON project_briefs
|
|
1527
|
-
WHEN NEW.superseded_by IS NOT NULL
|
|
1528
|
-
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1529
|
-
BEGIN
|
|
1530
|
-
SELECT CASE
|
|
1531
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM project_briefs WHERE id = NEW.superseded_by)
|
|
1532
|
-
THEN RAISE(ABORT, 'project_briefs.superseded_by must reference a project_brief in the same tenant')
|
|
1533
|
-
END;
|
|
1534
|
-
END
|
|
1535
|
-
`);
|
|
1536
|
-
}
|
|
1537
|
-
},
|
|
1538
|
-
},
|
|
1539
|
-
{
|
|
1540
|
-
version: 36,
|
|
1541
|
-
up: (db) => {
|
|
1542
|
-
// E2 customer_note first-class object (the LAST E2 object)
|
|
1543
|
-
// (docs/plans/2026-06-01-e2-customer-note-object.md). A customer_note is a
|
|
1544
|
-
// discrete note recorded against an account/customer entity, evolving via the
|
|
1545
|
-
// v35 project_briefs supersede machinery (superseded_by self-FK + supersede
|
|
1546
|
-
// tenant-match trigger + version + change_summary). This table = the v35
|
|
1547
|
-
// project_briefs table with repo/summary replaced by `customer` (the
|
|
1548
|
-
// entity-scoping dimension; a free-form account/customer id - the entities
|
|
1549
|
-
// table is unbuilt E3.1, so a FK is deferred) PLUS `note` (the note body).
|
|
1550
|
-
// MANY notes per customer (each its own supersede chain), unlike the
|
|
1551
|
-
// one-summary-per-repo project_brief. All column names checked against SQLite
|
|
1552
|
-
// reserved words (skill-episode lesson, codebase-audit rule 10): customer/note/
|
|
1553
|
-
// version/status/etc. are non-reserved.
|
|
1554
|
-
if (!tableExists(db, 'customer_notes')) {
|
|
1555
|
-
db.exec(`
|
|
1556
|
-
CREATE TABLE customer_notes (
|
|
1557
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1558
|
-
memory_id TEXT,
|
|
1559
|
-
tenant_id TEXT NOT NULL,
|
|
1560
|
-
customer TEXT NOT NULL,
|
|
1561
|
-
note TEXT NOT NULL,
|
|
1562
|
-
version INTEGER NOT NULL DEFAULT 1,
|
|
1563
|
-
status TEXT NOT NULL DEFAULT 'active'
|
|
1564
|
-
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1565
|
-
superseded_by INTEGER,
|
|
1566
|
-
superseded_at TEXT,
|
|
1567
|
-
change_summary TEXT,
|
|
1568
|
-
closed_at TEXT,
|
|
1569
|
-
created_at TEXT NOT NULL,
|
|
1570
|
-
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1571
|
-
FOREIGN KEY (superseded_by) REFERENCES customer_notes(id) ON DELETE SET NULL
|
|
1572
|
-
)
|
|
1573
|
-
`);
|
|
1574
|
-
db.exec(`
|
|
1575
|
-
CREATE INDEX IF NOT EXISTS idx_customer_notes_tenant_status
|
|
1576
|
-
ON customer_notes(tenant_id, status)
|
|
1577
|
-
`);
|
|
1578
|
-
db.exec(`
|
|
1579
|
-
CREATE INDEX IF NOT EXISTS idx_customer_notes_memory
|
|
1580
|
-
ON customer_notes(memory_id) WHERE memory_id IS NOT NULL
|
|
1581
|
-
`);
|
|
1582
|
-
db.exec(`
|
|
1583
|
-
CREATE INDEX IF NOT EXISTS idx_customer_notes_customer
|
|
1584
|
-
ON customer_notes(tenant_id, customer, status)
|
|
1585
|
-
`);
|
|
1586
|
-
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1587
|
-
// v35 project_briefs tenant-match triggers).
|
|
1588
|
-
db.exec(`
|
|
1589
|
-
CREATE TRIGGER IF NOT EXISTS trg_customer_notes_tenant_match_insert
|
|
1590
|
-
BEFORE INSERT ON customer_notes
|
|
1591
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
1592
|
-
BEGIN
|
|
1593
|
-
SELECT CASE
|
|
1594
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1595
|
-
THEN RAISE(ABORT, 'customer_notes.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1596
|
-
END;
|
|
1597
|
-
END
|
|
1598
|
-
`);
|
|
1599
|
-
db.exec(`
|
|
1600
|
-
CREATE TRIGGER IF NOT EXISTS trg_customer_notes_tenant_match_update
|
|
1601
|
-
BEFORE UPDATE ON customer_notes
|
|
1602
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
1603
|
-
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1604
|
-
BEGIN
|
|
1605
|
-
SELECT CASE
|
|
1606
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1607
|
-
THEN RAISE(ABORT, 'customer_notes.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1608
|
-
END;
|
|
1609
|
-
END
|
|
1610
|
-
`);
|
|
1611
|
-
// Cross-tenant safety vs the successor note (self-FK; verbatim mirror of the
|
|
1612
|
-
// v35 project_briefs supersede trigger).
|
|
1613
|
-
db.exec(`
|
|
1614
|
-
CREATE TRIGGER IF NOT EXISTS trg_customer_notes_supersede_tenant_match_update
|
|
1615
|
-
BEFORE UPDATE ON customer_notes
|
|
1616
|
-
WHEN NEW.superseded_by IS NOT NULL
|
|
1617
|
-
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1618
|
-
BEGIN
|
|
1619
|
-
SELECT CASE
|
|
1620
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM customer_notes WHERE id = NEW.superseded_by)
|
|
1621
|
-
THEN RAISE(ABORT, 'customer_notes.superseded_by must reference a customer_note in the same tenant')
|
|
1622
|
-
END;
|
|
1623
|
-
END
|
|
1624
|
-
`);
|
|
1625
|
-
}
|
|
1626
|
-
},
|
|
1627
|
-
},
|
|
1628
|
-
{
|
|
1629
|
-
version: 37,
|
|
1630
|
-
up: (db) => {
|
|
1631
|
-
// E3.3 graph-on-consolidated guard (docs/plans/2026-06-01-e3-graph-guard.md).
|
|
1632
|
-
// The graph layer (entities + relations) sits ON TOP OF consolidated state and
|
|
1633
|
-
// must NEVER index the raw layer. The substrate: entities + relations +
|
|
1634
|
-
// graph_extraction_queue, each FK-ing to memories and guarded so they can only
|
|
1635
|
-
// reference CONSOLIDATED memories (kind IN ('distilled','superseded')), never
|
|
1636
|
-
// kind='raw'. New tables -> real CHECK constraints (unlike the ALTER'd memories,
|
|
1637
|
-
// whose kind CHECK lives in triggers). The kind/source MATCH (source_kind ==
|
|
1638
|
-
// the FK'd memory's actual kind) cannot be a CHECK (CHECK can't subquery), so it
|
|
1639
|
-
// is a BEFORE INSERT *and* BEFORE UPDATE trigger (the subquery-capable pattern
|
|
1640
|
-
// from the v30 decisions / predictions tenant-match triggers). Both INSERT and
|
|
1641
|
-
// UPDATE are guarded: an INSERT-only guard is bypassable via a raw SQL UPDATE
|
|
1642
|
-
// that moves a row onto a raw memory (plan-eng-critic 2026-06-01). All column
|
|
1643
|
-
// names checked vs SQL reserved words (rule 10): rel_type avoids REFERENCES.
|
|
1644
|
-
if (!tableExists(db, 'entities')) {
|
|
1645
|
-
db.exec(`
|
|
1646
|
-
CREATE TABLE entities (
|
|
1647
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1648
|
-
tenant_id TEXT NOT NULL,
|
|
1649
|
-
entity_type TEXT NOT NULL
|
|
1650
|
-
CHECK (entity_type IN ('person', 'project', 'customer', 'system', 'policy', 'decision')),
|
|
1651
|
-
name TEXT NOT NULL,
|
|
1652
|
-
memory_id TEXT NOT NULL,
|
|
1653
|
-
source_kind TEXT NOT NULL CHECK (source_kind IN ('distilled', 'superseded')),
|
|
1654
|
-
created_at TEXT NOT NULL,
|
|
1655
|
-
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
1656
|
-
)
|
|
1657
|
-
`);
|
|
1658
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_tenant ON entities(tenant_id)`);
|
|
1659
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_memory ON entities(memory_id)`);
|
|
1660
|
-
db.exec(`
|
|
1661
|
-
CREATE TABLE relations (
|
|
1662
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1663
|
-
tenant_id TEXT NOT NULL,
|
|
1664
|
-
from_entity_id INTEGER NOT NULL,
|
|
1665
|
-
to_entity_id INTEGER NOT NULL,
|
|
1666
|
-
rel_type TEXT NOT NULL
|
|
1667
|
-
CHECK (rel_type IN ('owns', 'supersedes', 'depends-on', 'blocked-by', 'references')),
|
|
1668
|
-
memory_id TEXT NOT NULL,
|
|
1669
|
-
source_kind TEXT NOT NULL CHECK (source_kind IN ('distilled', 'superseded')),
|
|
1670
|
-
created_at TEXT NOT NULL,
|
|
1671
|
-
FOREIGN KEY (from_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
1672
|
-
FOREIGN KEY (to_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
1673
|
-
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
1674
|
-
)
|
|
1675
|
-
`);
|
|
1676
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_tenant ON relations(tenant_id)`);
|
|
1677
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id)`);
|
|
1678
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id)`);
|
|
1679
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_memory ON relations(memory_id)`);
|
|
1680
|
-
db.exec(`
|
|
1681
|
-
CREATE TABLE graph_extraction_queue (
|
|
1682
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1683
|
-
tenant_id TEXT NOT NULL,
|
|
1684
|
-
memory_id TEXT NOT NULL,
|
|
1685
|
-
kind TEXT NOT NULL CHECK (kind IN ('distilled', 'superseded')),
|
|
1686
|
-
status TEXT NOT NULL DEFAULT 'pending'
|
|
1687
|
-
CHECK (status IN ('pending', 'processed', 'skipped')),
|
|
1688
|
-
enqueued_at TEXT NOT NULL,
|
|
1689
|
-
processed_at TEXT,
|
|
1690
|
-
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
1691
|
-
)
|
|
1692
|
-
`);
|
|
1693
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_graph_queue_status ON graph_extraction_queue(tenant_id, status)`);
|
|
1694
|
-
// entities guard: source_kind must equal the FK'd memory's actual kind (so a
|
|
1695
|
-
// raw memory or a lying source_kind both ABORT), and tenant must match. Both
|
|
1696
|
-
// INSERT and UPDATE (UPDATE fires when memory_id/source_kind/tenant_id change).
|
|
1697
|
-
db.exec(`
|
|
1698
|
-
CREATE TRIGGER IF NOT EXISTS trg_entities_consolidated_only_insert
|
|
1699
|
-
BEFORE INSERT ON entities
|
|
1700
|
-
BEGIN
|
|
1701
|
-
SELECT CASE
|
|
1702
|
-
WHEN NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1703
|
-
THEN RAISE(ABORT, 'entities.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
|
|
1704
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1705
|
-
THEN RAISE(ABORT, 'entities.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1706
|
-
END;
|
|
1707
|
-
END
|
|
1708
|
-
`);
|
|
1709
|
-
db.exec(`
|
|
1710
|
-
CREATE TRIGGER IF NOT EXISTS trg_entities_consolidated_only_update
|
|
1711
|
-
BEFORE UPDATE ON entities
|
|
1712
|
-
WHEN NEW.memory_id IS NOT OLD.memory_id
|
|
1713
|
-
OR NEW.source_kind IS NOT OLD.source_kind
|
|
1714
|
-
OR NEW.tenant_id IS NOT OLD.tenant_id
|
|
1715
|
-
BEGIN
|
|
1716
|
-
SELECT CASE
|
|
1717
|
-
WHEN NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1718
|
-
THEN RAISE(ABORT, 'entities.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
|
|
1719
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1720
|
-
THEN RAISE(ABORT, 'entities.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1721
|
-
END;
|
|
1722
|
-
END
|
|
1723
|
-
`);
|
|
1724
|
-
// relations guard: source_kind must equal the FK'd memory's kind; tenant must
|
|
1725
|
-
// match the memory AND both endpoint entities (no cross-tenant edges).
|
|
1726
|
-
db.exec(`
|
|
1727
|
-
CREATE TRIGGER IF NOT EXISTS trg_relations_consolidated_only_insert
|
|
1728
|
-
BEFORE INSERT ON relations
|
|
1729
|
-
BEGIN
|
|
1730
|
-
SELECT CASE
|
|
1731
|
-
WHEN NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1732
|
-
THEN RAISE(ABORT, 'relations.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
|
|
1733
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1734
|
-
THEN RAISE(ABORT, 'relations.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1735
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.from_entity_id)
|
|
1736
|
-
THEN RAISE(ABORT, 'relations.tenant_id must match the from_entity tenant (no cross-tenant edges)')
|
|
1737
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.to_entity_id)
|
|
1738
|
-
THEN RAISE(ABORT, 'relations.tenant_id must match the to_entity tenant (no cross-tenant edges)')
|
|
1739
|
-
END;
|
|
1740
|
-
END
|
|
1741
|
-
`);
|
|
1742
|
-
db.exec(`
|
|
1743
|
-
CREATE TRIGGER IF NOT EXISTS trg_relations_consolidated_only_update
|
|
1744
|
-
BEFORE UPDATE ON relations
|
|
1745
|
-
WHEN NEW.memory_id IS NOT OLD.memory_id
|
|
1746
|
-
OR NEW.source_kind IS NOT OLD.source_kind
|
|
1747
|
-
OR NEW.tenant_id IS NOT OLD.tenant_id
|
|
1748
|
-
OR NEW.from_entity_id IS NOT OLD.from_entity_id
|
|
1749
|
-
OR NEW.to_entity_id IS NOT OLD.to_entity_id
|
|
1750
|
-
BEGIN
|
|
1751
|
-
SELECT CASE
|
|
1752
|
-
WHEN NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1753
|
-
THEN RAISE(ABORT, 'relations.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
|
|
1754
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1755
|
-
THEN RAISE(ABORT, 'relations.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1756
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.from_entity_id)
|
|
1757
|
-
THEN RAISE(ABORT, 'relations.tenant_id must match the from_entity tenant (no cross-tenant edges)')
|
|
1758
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.to_entity_id)
|
|
1759
|
-
THEN RAISE(ABORT, 'relations.tenant_id must match the to_entity tenant (no cross-tenant edges)')
|
|
1760
|
-
END;
|
|
1761
|
-
END
|
|
1762
|
-
`);
|
|
1763
|
-
// graph_extraction_queue guard: kind must equal the FK'd memory's actual kind
|
|
1764
|
-
// (so a raw memory ABORTs), and tenant must match. INSERT and UPDATE.
|
|
1765
|
-
db.exec(`
|
|
1766
|
-
CREATE TRIGGER IF NOT EXISTS trg_graph_queue_consolidated_only_insert
|
|
1767
|
-
BEFORE INSERT ON graph_extraction_queue
|
|
1768
|
-
BEGIN
|
|
1769
|
-
SELECT CASE
|
|
1770
|
-
WHEN NEW.kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1771
|
-
THEN RAISE(ABORT, 'graph_extraction_queue.kind must equal the referenced memory kind; only consolidated memories are queued (no raw)')
|
|
1772
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1773
|
-
THEN RAISE(ABORT, 'graph_extraction_queue.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1774
|
-
END;
|
|
1775
|
-
END
|
|
1776
|
-
`);
|
|
1777
|
-
db.exec(`
|
|
1778
|
-
CREATE TRIGGER IF NOT EXISTS trg_graph_queue_consolidated_only_update
|
|
1779
|
-
BEFORE UPDATE ON graph_extraction_queue
|
|
1780
|
-
WHEN NEW.memory_id IS NOT OLD.memory_id
|
|
1781
|
-
OR NEW.kind IS NOT OLD.kind
|
|
1782
|
-
OR NEW.tenant_id IS NOT OLD.tenant_id
|
|
1783
|
-
BEGIN
|
|
1784
|
-
SELECT CASE
|
|
1785
|
-
WHEN NEW.kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1786
|
-
THEN RAISE(ABORT, 'graph_extraction_queue.kind must equal the referenced memory kind; only consolidated memories are queued (no raw)')
|
|
1787
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1788
|
-
THEN RAISE(ABORT, 'graph_extraction_queue.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1789
|
-
END;
|
|
1790
|
-
END
|
|
1791
|
-
`);
|
|
1792
|
-
// Reverse guard (codex-review-critic 2026-06-01, P1): the graph-table triggers
|
|
1793
|
-
// only fire on writes to the GRAPH tables. They do NOT fire when an
|
|
1794
|
-
// already-indexed memory is later mutated. So 'UPDATE memories SET kind=raw'
|
|
1795
|
-
// (or a tenant change) on a memory the graph references would silently leave
|
|
1796
|
-
// entity/relation/queue rows pointing at a raw / cross-tenant memory while
|
|
1797
|
-
// their source_kind stays 'distilled' - bypassing the central 'graph never
|
|
1798
|
-
// indexes raw' invariant after insertion. This trigger closes that direction:
|
|
1799
|
-
// a memory cannot be reclassified to raw, nor moved cross-tenant, WHILE the
|
|
1800
|
-
// graph references it (rebuild/remove the graph rows first). Cheap: the EXISTS
|
|
1801
|
-
// checks are only evaluated when kind actually becomes raw or tenant changes.
|
|
1802
|
-
db.exec(`
|
|
1803
|
-
CREATE TRIGGER IF NOT EXISTS trg_memories_graph_referenced_guard
|
|
1804
|
-
BEFORE UPDATE ON memories
|
|
1805
|
-
WHEN (NEW.kind IS NOT OLD.kind OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1806
|
-
AND (
|
|
1807
|
-
EXISTS (SELECT 1 FROM entities WHERE memory_id = OLD.id)
|
|
1808
|
-
OR EXISTS (SELECT 1 FROM relations WHERE memory_id = OLD.id)
|
|
1809
|
-
OR EXISTS (SELECT 1 FROM graph_extraction_queue WHERE memory_id = OLD.id)
|
|
1810
|
-
)
|
|
1811
|
-
BEGIN
|
|
1812
|
-
SELECT RAISE(ABORT, 'cannot change the kind or tenant of a memory while the graph references it (E3.3 graph-on-consolidated guard); a graph-referenced memory is immutable in kind/tenant - rebuild/remove the graph rows first, or rebuild them after supersession');
|
|
1813
|
-
END
|
|
1814
|
-
`);
|
|
1815
|
-
// Reverse guard #2 (codex-review-critic 2026-06-01 retry, P2): an entity that is
|
|
1816
|
-
// a relation endpoint cannot be moved cross-tenant. The entity UPDATE trigger
|
|
1817
|
-
// validates the entity against its source memory, but an existing relation
|
|
1818
|
-
// pointing at the entity is NOT re-validated, so a raw 'UPDATE entities SET
|
|
1819
|
-
// tenant_id=?, memory_id=?' to another tenant would leave a tenant-A relation
|
|
1820
|
-
// pointing at a tenant-B entity. Block the tenant move while the entity is
|
|
1821
|
-
// referenced by any relation (rebuild the relations first).
|
|
1822
|
-
db.exec(`
|
|
1823
|
-
CREATE TRIGGER IF NOT EXISTS trg_entities_no_tenant_move_when_referenced
|
|
1824
|
-
BEFORE UPDATE ON entities
|
|
1825
|
-
WHEN NEW.tenant_id IS NOT OLD.tenant_id
|
|
1826
|
-
AND EXISTS (SELECT 1 FROM relations WHERE from_entity_id = OLD.id OR to_entity_id = OLD.id)
|
|
1827
|
-
BEGIN
|
|
1828
|
-
SELECT RAISE(ABORT, 'cannot move an entity cross-tenant while a relation references it as an endpoint (E3.3 graph-on-consolidated guard); rebuild/remove the relations first');
|
|
1829
|
-
END
|
|
1830
|
-
`);
|
|
1831
|
-
}
|
|
1832
|
-
},
|
|
1833
|
-
},
|
|
1834
|
-
{
|
|
1835
|
-
version: 38,
|
|
1836
|
-
up: (db) => {
|
|
1837
|
-
// E2-provenance: anchor graph entity/relation provenance to the authoritative E2
|
|
1838
|
-
// object (decision/policy/customer-note/project-brief) instead of the decaying
|
|
1839
|
-
// memory mirror (docs/plans/2026-06-03-graph-e2-provenance.md). An in-force E2
|
|
1840
|
-
// object must STAY in the graph after its mirror memory is forgotten or
|
|
1841
|
-
// consolidation-pruned. The graph is a PURE DERIVED CACHE (clearGraph + rebuild on
|
|
1842
|
-
// every `graph extract` / `sleep`), so v38 DROPs+recreates entities/relations (no
|
|
1843
|
-
// data copy) and the next extract repopulates. graph_extraction_queue is untouched.
|
|
1844
|
-
//
|
|
1845
|
-
// Two provenance paths, "at least one, no raw":
|
|
1846
|
-
// - memory path (memory_id NOT NULL): source_kind must equal the FK'd memory's live
|
|
1847
|
-
// kind and that kind is distilled|superseded (raw still ABORTs) + tenant-match.
|
|
1848
|
-
// - object path (memory_id NULL): source_object_type/id must reference an EXISTING
|
|
1849
|
-
// same-tenant E2 row whose status is active|superseded (not closed). E2 objects
|
|
1850
|
-
// are consolidated BY CONSTRUCTION, so the no-raw invariant still holds.
|
|
1851
|
-
// - all-null is rejected.
|
|
1852
|
-
//
|
|
1853
|
-
// memory_id is now NULLABLE with ON DELETE SET NULL (was NOT NULL / CASCADE), so a
|
|
1854
|
-
// mirror forget/consolidate nulls the recall pointer without dropping the row. NOTE
|
|
1855
|
-
// (empirically verified, contradicts the SQLite docs): node:sqlite DOES fire the
|
|
1856
|
-
// BEFORE UPDATE guard from the FK SET NULL action even with recursive_triggers OFF.
|
|
1857
|
-
// So the *_consolidated_only_UPDATE triggers deliberately OMIT the all-null ABORT
|
|
1858
|
-
// (kept on INSERT) - otherwise a mirror delete of a memory-only row would be blocked.
|
|
1859
|
-
// See the per-trigger comments below. A SET NULL leaves source_kind at its old value
|
|
1860
|
-
// by design (the object path is distilled-by-construction; source_kind is only
|
|
1861
|
-
// re-checked when memory_id NOT NULL).
|
|
1862
|
-
// source_object_id is a SOFT (type,id) pointer (no hard FK) the rebuild re-validates,
|
|
1863
|
-
// so a legitimate E2 hard-delete is never blocked; a `closed` E2 row drops at next
|
|
1864
|
-
// extract. SQLite cannot parametrize a table name in a trigger, so the object-path
|
|
1865
|
-
// validation is an explicit 4-way CASE (one arm per E2 table).
|
|
1866
|
-
// Drop child (relations FK entities) first, then parent.
|
|
1867
|
-
db.exec('DROP TABLE IF EXISTS relations');
|
|
1868
|
-
db.exec('DROP TABLE IF EXISTS entities');
|
|
1869
|
-
db.exec(`
|
|
1870
|
-
CREATE TABLE entities (
|
|
1871
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1872
|
-
tenant_id TEXT NOT NULL,
|
|
1873
|
-
entity_type TEXT NOT NULL
|
|
1874
|
-
CHECK (entity_type IN ('person', 'project', 'customer', 'system', 'policy', 'decision')),
|
|
1875
|
-
name TEXT NOT NULL,
|
|
1876
|
-
memory_id TEXT,
|
|
1877
|
-
source_kind TEXT NOT NULL CHECK (source_kind IN ('distilled', 'superseded')),
|
|
1878
|
-
source_object_type TEXT
|
|
1879
|
-
CHECK (source_object_type IS NULL OR source_object_type IN ('decision', 'policy', 'customer', 'project')),
|
|
1880
|
-
source_object_id INTEGER,
|
|
1881
|
-
created_at TEXT NOT NULL,
|
|
1882
|
-
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL
|
|
1883
|
-
)
|
|
1884
|
-
`);
|
|
1885
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_tenant ON entities(tenant_id)`);
|
|
1886
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_memory ON entities(memory_id)`);
|
|
1887
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_source_object ON entities(source_object_type, source_object_id) WHERE source_object_id IS NOT NULL`);
|
|
1888
|
-
db.exec(`
|
|
1889
|
-
CREATE TABLE relations (
|
|
1890
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1891
|
-
tenant_id TEXT NOT NULL,
|
|
1892
|
-
from_entity_id INTEGER NOT NULL,
|
|
1893
|
-
to_entity_id INTEGER NOT NULL,
|
|
1894
|
-
rel_type TEXT NOT NULL
|
|
1895
|
-
CHECK (rel_type IN ('owns', 'supersedes', 'depends-on', 'blocked-by', 'references')),
|
|
1896
|
-
memory_id TEXT,
|
|
1897
|
-
source_kind TEXT NOT NULL CHECK (source_kind IN ('distilled', 'superseded')),
|
|
1898
|
-
source_object_type TEXT
|
|
1899
|
-
CHECK (source_object_type IS NULL OR source_object_type IN ('decision', 'policy', 'customer', 'project')),
|
|
1900
|
-
source_object_id INTEGER,
|
|
1901
|
-
created_at TEXT NOT NULL,
|
|
1902
|
-
FOREIGN KEY (from_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
1903
|
-
FOREIGN KEY (to_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
1904
|
-
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL
|
|
1905
|
-
)
|
|
1906
|
-
`);
|
|
1907
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_tenant ON relations(tenant_id)`);
|
|
1908
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id)`);
|
|
1909
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id)`);
|
|
1910
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_memory ON relations(memory_id)`);
|
|
1911
|
-
// Mirrors idx_entities_source_object: removeGraphEntitiesForObject (close-time cleanup)
|
|
1912
|
-
// deletes relations by (source_object_type, source_object_id), so index that pair.
|
|
1913
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_source_object ON relations(source_object_type, source_object_id) WHERE source_object_id IS NOT NULL`);
|
|
1914
|
-
// entities guard (dual-provenance): at least one valid provenance, no raw.
|
|
1915
|
-
// - all-null -> ABORT.
|
|
1916
|
-
// - memory path (memory_id NOT NULL): source_kind == the FK'd memory's kind
|
|
1917
|
-
// (raw / lying source_kind ABORT) AND tenant-match.
|
|
1918
|
-
// - object path (memory_id NULL): the (type,id) points at an EXISTING same-tenant
|
|
1919
|
-
// E2 row whose status is active|superseded (explicit 4-way CASE per table).
|
|
1920
|
-
db.exec(`
|
|
1921
|
-
CREATE TRIGGER IF NOT EXISTS trg_entities_consolidated_only_insert
|
|
1922
|
-
BEFORE INSERT ON entities
|
|
1923
|
-
BEGIN
|
|
1924
|
-
SELECT CASE
|
|
1925
|
-
WHEN (NEW.source_object_type IS NULL AND NEW.source_object_id IS NOT NULL)
|
|
1926
|
-
OR (NEW.source_object_type IS NOT NULL AND NEW.source_object_id IS NULL)
|
|
1927
|
-
THEN RAISE(ABORT, 'source_object_type and source_object_id must be set together (all-or-none)')
|
|
1928
|
-
WHEN NEW.memory_id IS NULL AND NEW.source_object_id IS NULL
|
|
1929
|
-
THEN RAISE(ABORT, 'graph row needs a memory or a source object')
|
|
1930
|
-
WHEN NEW.memory_id IS NOT NULL AND NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1931
|
-
THEN RAISE(ABORT, 'entities.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
|
|
1932
|
-
WHEN NEW.memory_id IS NOT NULL AND NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1933
|
-
THEN RAISE(ABORT, 'entities.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1934
|
-
WHEN NEW.source_object_type ='decision'
|
|
1935
|
-
AND NOT EXISTS (SELECT 1 FROM decisions WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
|
|
1936
|
-
THEN RAISE(ABORT, 'entities.source_object must reference an active/superseded decision in the same tenant')
|
|
1937
|
-
WHEN NEW.source_object_type ='policy'
|
|
1938
|
-
AND NOT EXISTS (SELECT 1 FROM policies WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
|
|
1939
|
-
THEN RAISE(ABORT, 'entities.source_object must reference an active/superseded policy in the same tenant')
|
|
1940
|
-
WHEN NEW.source_object_type ='customer'
|
|
1941
|
-
AND NOT EXISTS (SELECT 1 FROM customer_notes WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
|
|
1942
|
-
THEN RAISE(ABORT, 'entities.source_object must reference an active/superseded customer_note in the same tenant')
|
|
1943
|
-
WHEN NEW.source_object_type ='project'
|
|
1944
|
-
AND NOT EXISTS (SELECT 1 FROM project_briefs WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
|
|
1945
|
-
THEN RAISE(ABORT, 'entities.source_object must reference an active/superseded project_brief in the same tenant')
|
|
1946
|
-
END;
|
|
1947
|
-
END
|
|
1948
|
-
`);
|
|
1949
|
-
db.exec(`
|
|
1950
|
-
CREATE TRIGGER IF NOT EXISTS trg_entities_consolidated_only_update
|
|
1951
|
-
BEFORE UPDATE ON entities
|
|
1952
|
-
WHEN NEW.memory_id IS NOT OLD.memory_id
|
|
1953
|
-
OR NEW.source_kind IS NOT OLD.source_kind
|
|
1954
|
-
OR NEW.tenant_id IS NOT OLD.tenant_id
|
|
1955
|
-
OR NEW.source_object_type IS NOT OLD.source_object_type
|
|
1956
|
-
OR NEW.source_object_id IS NOT OLD.source_object_id
|
|
1957
|
-
BEGIN
|
|
1958
|
-
-- NO all-null ABORT on UPDATE: node:sqlite fires this BEFORE UPDATE trigger from
|
|
1959
|
-
-- the FK ON DELETE SET NULL action even with recursive_triggers OFF (empirically
|
|
1960
|
-
-- verified - the plan's "SET NULL does not fire the guard" premise is false for
|
|
1961
|
-
-- this build). A mirror forget on a memory-only entity legitimately produces an
|
|
1962
|
-
-- all-null row; aborting here would block the memory delete. The INSERT guard
|
|
1963
|
-
-- still forbids CREATING a provenance-less row, and the next graph rebuild clears
|
|
1964
|
-
-- any all-null orphan. A raw UPDATE to all-null is harmless (not recall-surfaced).
|
|
1965
|
-
-- Object-path STATUS is validated only when the object columns THEMSELVES change
|
|
1966
|
-
-- (an explicit re-point), NOT on the FK ON DELETE SET NULL transition (which changes
|
|
1967
|
-
-- only memory_id). This threads the needle (codex P1 + P2 round 4): the SET NULL of a
|
|
1968
|
-
-- since-closed object's mirror is NOT blocked (object cols unchanged -> object checks
|
|
1969
|
-
-- skip), while an explicit raw UPDATE that re-points the row to a bad/closed/cross-
|
|
1970
|
-
-- tenant object IS rejected. all-or-none + memory-path (raw/tenant) always apply.
|
|
1971
|
-
SELECT CASE
|
|
1972
|
-
WHEN (NEW.source_object_type IS NULL AND NEW.source_object_id IS NOT NULL)
|
|
1973
|
-
OR (NEW.source_object_type IS NOT NULL AND NEW.source_object_id IS NULL)
|
|
1974
|
-
THEN RAISE(ABORT, 'entities.source_object_type and source_object_id must be set together (all-or-none)')
|
|
1975
|
-
WHEN NEW.memory_id IS NOT NULL AND NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1976
|
-
THEN RAISE(ABORT, 'entities.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
|
|
1977
|
-
WHEN NEW.memory_id IS NOT NULL AND NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1978
|
-
THEN RAISE(ABORT, 'entities.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1979
|
-
WHEN (NEW.source_object_type IS NOT OLD.source_object_type OR NEW.source_object_id IS NOT OLD.source_object_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1980
|
-
AND NEW.source_object_type ='decision'
|
|
1981
|
-
AND NOT EXISTS (SELECT 1 FROM decisions WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
|
|
1982
|
-
THEN RAISE(ABORT, 'entities.source_object must reference an active/superseded decision in the same tenant')
|
|
1983
|
-
WHEN (NEW.source_object_type IS NOT OLD.source_object_type OR NEW.source_object_id IS NOT OLD.source_object_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1984
|
-
AND NEW.source_object_type ='policy'
|
|
1985
|
-
AND NOT EXISTS (SELECT 1 FROM policies WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
|
|
1986
|
-
THEN RAISE(ABORT, 'entities.source_object must reference an active/superseded policy in the same tenant')
|
|
1987
|
-
WHEN (NEW.source_object_type IS NOT OLD.source_object_type OR NEW.source_object_id IS NOT OLD.source_object_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1988
|
-
AND NEW.source_object_type ='customer'
|
|
1989
|
-
AND NOT EXISTS (SELECT 1 FROM customer_notes WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
|
|
1990
|
-
THEN RAISE(ABORT, 'entities.source_object must reference an active/superseded customer_note in the same tenant')
|
|
1991
|
-
WHEN (NEW.source_object_type IS NOT OLD.source_object_type OR NEW.source_object_id IS NOT OLD.source_object_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1992
|
-
AND NEW.source_object_type ='project'
|
|
1993
|
-
AND NOT EXISTS (SELECT 1 FROM project_briefs WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
|
|
1994
|
-
THEN RAISE(ABORT, 'entities.source_object must reference an active/superseded project_brief in the same tenant')
|
|
1995
|
-
END;
|
|
1996
|
-
END
|
|
1997
|
-
`);
|
|
1998
|
-
// relations guard (dual-provenance) + the existing from/to endpoint same-tenant checks.
|
|
1999
|
-
db.exec(`
|
|
2000
|
-
CREATE TRIGGER IF NOT EXISTS trg_relations_consolidated_only_insert
|
|
2001
|
-
BEFORE INSERT ON relations
|
|
2002
|
-
BEGIN
|
|
2003
|
-
SELECT CASE
|
|
2004
|
-
WHEN (NEW.source_object_type IS NULL AND NEW.source_object_id IS NOT NULL)
|
|
2005
|
-
OR (NEW.source_object_type IS NOT NULL AND NEW.source_object_id IS NULL)
|
|
2006
|
-
THEN RAISE(ABORT, 'source_object_type and source_object_id must be set together (all-or-none)')
|
|
2007
|
-
WHEN NEW.memory_id IS NULL AND NEW.source_object_id IS NULL
|
|
2008
|
-
THEN RAISE(ABORT, 'graph row needs a memory or a source object')
|
|
2009
|
-
WHEN NEW.memory_id IS NOT NULL AND NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
2010
|
-
THEN RAISE(ABORT, 'relations.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
|
|
2011
|
-
WHEN NEW.memory_id IS NOT NULL AND NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
2012
|
-
THEN RAISE(ABORT, 'relations.tenant_id must match memories.tenant_id for the referenced memory')
|
|
2013
|
-
WHEN NEW.source_object_type ='decision'
|
|
2014
|
-
AND NOT EXISTS (SELECT 1 FROM decisions WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
|
|
2015
|
-
THEN RAISE(ABORT, 'relations.source_object must reference an active/superseded decision in the same tenant')
|
|
2016
|
-
WHEN NEW.source_object_type ='policy'
|
|
2017
|
-
AND NOT EXISTS (SELECT 1 FROM policies WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
|
|
2018
|
-
THEN RAISE(ABORT, 'relations.source_object must reference an active/superseded policy in the same tenant')
|
|
2019
|
-
WHEN NEW.source_object_type ='customer'
|
|
2020
|
-
AND NOT EXISTS (SELECT 1 FROM customer_notes WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
|
|
2021
|
-
THEN RAISE(ABORT, 'relations.source_object must reference an active/superseded customer_note in the same tenant')
|
|
2022
|
-
WHEN NEW.source_object_type ='project'
|
|
2023
|
-
AND NOT EXISTS (SELECT 1 FROM project_briefs WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
|
|
2024
|
-
THEN RAISE(ABORT, 'relations.source_object must reference an active/superseded project_brief in the same tenant')
|
|
2025
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.from_entity_id)
|
|
2026
|
-
THEN RAISE(ABORT, 'relations.tenant_id must match the from_entity tenant (no cross-tenant edges)')
|
|
2027
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.to_entity_id)
|
|
2028
|
-
THEN RAISE(ABORT, 'relations.tenant_id must match the to_entity tenant (no cross-tenant edges)')
|
|
2029
|
-
END;
|
|
2030
|
-
END
|
|
2031
|
-
`);
|
|
2032
|
-
db.exec(`
|
|
2033
|
-
CREATE TRIGGER IF NOT EXISTS trg_relations_consolidated_only_update
|
|
2034
|
-
BEFORE UPDATE ON relations
|
|
2035
|
-
WHEN NEW.memory_id IS NOT OLD.memory_id
|
|
2036
|
-
OR NEW.source_kind IS NOT OLD.source_kind
|
|
2037
|
-
OR NEW.tenant_id IS NOT OLD.tenant_id
|
|
2038
|
-
OR NEW.source_object_type IS NOT OLD.source_object_type
|
|
2039
|
-
OR NEW.source_object_id IS NOT OLD.source_object_id
|
|
2040
|
-
OR NEW.from_entity_id IS NOT OLD.from_entity_id
|
|
2041
|
-
OR NEW.to_entity_id IS NOT OLD.to_entity_id
|
|
2042
|
-
BEGIN
|
|
2043
|
-
-- NO all-null ABORT on UPDATE (same reason as the entities UPDATE trigger): the FK
|
|
2044
|
-
-- ON DELETE SET NULL fires this trigger in node:sqlite, and a mirror forget on a
|
|
2045
|
-
-- memory-only relation legitimately nulls memory_id. The INSERT guard still forbids
|
|
2046
|
-
-- creating a provenance-less relation; the rebuild clears any orphan.
|
|
2047
|
-
-- Object-path STATUS validated only on an explicit object-column change, NOT on the
|
|
2048
|
-
-- FK SET NULL transition (see the entities UPDATE trigger): SET NULL of a since-closed
|
|
2049
|
-
-- object's mirror is not blocked (object cols unchanged), while an explicit re-point to
|
|
2050
|
-
-- a bad object is rejected. all-or-none + memory-path + endpoint checks always apply.
|
|
2051
|
-
SELECT CASE
|
|
2052
|
-
WHEN (NEW.source_object_type IS NULL AND NEW.source_object_id IS NOT NULL)
|
|
2053
|
-
OR (NEW.source_object_type IS NOT NULL AND NEW.source_object_id IS NULL)
|
|
2054
|
-
THEN RAISE(ABORT, 'relations.source_object_type and source_object_id must be set together (all-or-none)')
|
|
2055
|
-
WHEN NEW.memory_id IS NOT NULL AND NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
2056
|
-
THEN RAISE(ABORT, 'relations.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
|
|
2057
|
-
WHEN NEW.memory_id IS NOT NULL AND NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
2058
|
-
THEN RAISE(ABORT, 'relations.tenant_id must match memories.tenant_id for the referenced memory')
|
|
2059
|
-
WHEN (NEW.source_object_type IS NOT OLD.source_object_type OR NEW.source_object_id IS NOT OLD.source_object_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
2060
|
-
AND NEW.source_object_type ='decision'
|
|
2061
|
-
AND NOT EXISTS (SELECT 1 FROM decisions WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
|
|
2062
|
-
THEN RAISE(ABORT, 'relations.source_object must reference an active/superseded decision in the same tenant')
|
|
2063
|
-
WHEN (NEW.source_object_type IS NOT OLD.source_object_type OR NEW.source_object_id IS NOT OLD.source_object_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
2064
|
-
AND NEW.source_object_type ='policy'
|
|
2065
|
-
AND NOT EXISTS (SELECT 1 FROM policies WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
|
|
2066
|
-
THEN RAISE(ABORT, 'relations.source_object must reference an active/superseded policy in the same tenant')
|
|
2067
|
-
WHEN (NEW.source_object_type IS NOT OLD.source_object_type OR NEW.source_object_id IS NOT OLD.source_object_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
2068
|
-
AND NEW.source_object_type ='customer'
|
|
2069
|
-
AND NOT EXISTS (SELECT 1 FROM customer_notes WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
|
|
2070
|
-
THEN RAISE(ABORT, 'relations.source_object must reference an active/superseded customer_note in the same tenant')
|
|
2071
|
-
WHEN (NEW.source_object_type IS NOT OLD.source_object_type OR NEW.source_object_id IS NOT OLD.source_object_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
2072
|
-
AND NEW.source_object_type ='project'
|
|
2073
|
-
AND NOT EXISTS (SELECT 1 FROM project_briefs WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
|
|
2074
|
-
THEN RAISE(ABORT, 'relations.source_object must reference an active/superseded project_brief in the same tenant')
|
|
2075
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.from_entity_id)
|
|
2076
|
-
THEN RAISE(ABORT, 'relations.tenant_id must match the from_entity tenant (no cross-tenant edges)')
|
|
2077
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.to_entity_id)
|
|
2078
|
-
THEN RAISE(ABORT, 'relations.tenant_id must match the to_entity tenant (no cross-tenant edges)')
|
|
2079
|
-
END;
|
|
2080
|
-
END
|
|
2081
|
-
`);
|
|
2082
|
-
// Recreated VERBATIM from v37 (logic unchanged): an entity that is a relation
|
|
2083
|
-
// endpoint cannot be moved cross-tenant while referenced. trg_memories_graph_referenced_guard
|
|
2084
|
-
// (on memories) and trg_graph_queue_* (on graph_extraction_queue) are NOT recreated
|
|
2085
|
-
// here: those tables are not dropped by v38, so the triggers survive.
|
|
2086
|
-
db.exec(`
|
|
2087
|
-
CREATE TRIGGER IF NOT EXISTS trg_entities_no_tenant_move_when_referenced
|
|
2088
|
-
BEFORE UPDATE ON entities
|
|
2089
|
-
WHEN NEW.tenant_id IS NOT OLD.tenant_id
|
|
2090
|
-
AND EXISTS (SELECT 1 FROM relations WHERE from_entity_id = OLD.id OR to_entity_id = OLD.id)
|
|
2091
|
-
BEGIN
|
|
2092
|
-
SELECT RAISE(ABORT, 'cannot move an entity cross-tenant while a relation references it as an endpoint (E3.3 graph-on-consolidated guard); rebuild/remove the relations first');
|
|
2093
|
-
END
|
|
2094
|
-
`);
|
|
2095
|
-
},
|
|
2096
|
-
},
|
|
2097
|
-
{
|
|
2098
|
-
version: 39,
|
|
2099
|
-
up: (db, ctx) => {
|
|
2100
|
-
// Memory scope isolation (docs/plans/2026-07-01-memory-scope-isolation.md).
|
|
2101
|
-
// origin_project: '<name>' = owned by that project, '' = user-global,
|
|
2102
|
-
// NULL = legacy/unknown (ambient context treats NULL as deny).
|
|
2103
|
-
if (!tableHasColumn(db, 'memories', 'origin_project')) {
|
|
2104
|
-
db.exec(`ALTER TABLE memories ADD COLUMN origin_project TEXT`);
|
|
2105
|
-
}
|
|
2106
|
-
// Backfill on store-location evidence only (no path-tag guessing):
|
|
2107
|
-
// 1. Rows shared from a project carry source 'shared:<project>:<ts>' -
|
|
2108
|
-
// take <project>; a share whose <project> is the home dir basename
|
|
2109
|
-
// maps to '' (user-global).
|
|
2110
|
-
// 2. Every other row was written into THIS store, so it takes the
|
|
2111
|
-
// store's own origin: `<project>/.hippo` -> '<project>', the
|
|
2112
|
-
// home/global store -> '' (user-global).
|
|
2113
|
-
// Rows stay NULL only when no hippoRoot was provided.
|
|
2114
|
-
const hippoRoot = ctx?.hippoRoot;
|
|
2115
|
-
if (hippoRoot) {
|
|
2116
|
-
// Provenance-source evidence first (shared:<project>: / promoted:<localRoot>,
|
|
2117
|
-
// parsed by the same helper the markdown-import stamp uses), then the
|
|
2118
|
-
// store's own location for everything else.
|
|
2119
|
-
const homeName = path.basename(os.homedir()).toLowerCase();
|
|
2120
|
-
// SAFETY: sourcedRows' shape matches the two columns (id, source)
|
|
2121
|
-
// named in the SELECT above.
|
|
2122
|
-
const sourcedRows = db.prepare(`SELECT id, source FROM memories WHERE origin_project IS NULL AND (source LIKE 'shared:%' OR source LIKE 'promoted:%')`).all();
|
|
2123
|
-
const setOrigin = db.prepare(`UPDATE memories SET origin_project = ? WHERE id = ?`);
|
|
2124
|
-
for (const row of sourcedRows) {
|
|
2125
|
-
const origin = originFromSource(row.source, homeName);
|
|
2126
|
-
if (origin === null)
|
|
2127
|
-
continue;
|
|
2128
|
-
setOrigin.run(origin, row.id);
|
|
2129
|
-
}
|
|
2130
|
-
// The global root itself is ALWAYS user-global (''), regardless of
|
|
2131
|
-
// what surrounds it on disk - a HIPPO_HOME inside a dotfiles git
|
|
2132
|
-
// repo must not stamp the whole corpus with that repo's name.
|
|
2133
|
-
const storeOrigin = isGlobalStoreRoot(hippoRoot)
|
|
2134
|
-
? ''
|
|
2135
|
-
: deriveOriginProject(path.dirname(hippoRoot));
|
|
2136
|
-
db.prepare(`UPDATE memories SET origin_project = ? WHERE origin_project IS NULL`).run(storeOrigin);
|
|
2137
|
-
}
|
|
2138
|
-
// Rollback-safety guard (v24 precedent): a pre-isolation binary opening
|
|
2139
|
-
// this DB would ignore origin_project and the secret veto and resume
|
|
2140
|
-
// injecting cross-project rows. 1.24.0 is the first version with the
|
|
2141
|
-
// isolation behavior. Forward-only - never lower an existing minimum.
|
|
2142
|
-
// SAFETY: this get() result's shape matches the single `value` column
|
|
2143
|
-
// named in the SELECT above.
|
|
2144
|
-
const existingMin = db.prepare(`SELECT value FROM meta WHERE key = 'min_compatible_binary'`).get()?.value;
|
|
2145
|
-
if (!existingMin || compareSemver('1.24.0', existingMin) > 0) {
|
|
2146
|
-
db.prepare(`INSERT INTO meta(key, value) VALUES('min_compatible_binary', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run('1.24.0');
|
|
2147
|
-
}
|
|
2148
|
-
},
|
|
2149
|
-
},
|
|
2150
|
-
{
|
|
2151
|
-
version: 40,
|
|
2152
|
-
up: (db) => {
|
|
2153
|
-
// LC1 retrieval-trace persistence (docs/plans/2026-08-02-lc1-recall-trace-persistence.md).
|
|
2154
|
-
// Follows the v18 goal_recall_log style: one parent trace row per
|
|
2155
|
-
// recall, a WITHOUT ROWID child table for the ranked results, and a
|
|
2156
|
-
// separate append-only outcomes table so audit_log pruning can never
|
|
2157
|
-
// erase training data.
|
|
2158
|
-
//
|
|
2159
|
-
// F6 (deliberate, not an oversight): unlike v39, this migration does
|
|
2160
|
-
// NOT bump `min_compatible_binary`. A pre-v40 binary opening this DB
|
|
2161
|
-
// ignores the three new tables and keeps writing `last_retrieval_ids`
|
|
2162
|
-
// exactly as before — it never touches `last_trace_id` (that key
|
|
2163
|
-
// simply stays whatever it was). recordTraceOutcome's F4 consumer-
|
|
2164
|
-
// side validation (recall-trace.ts) makes any resulting staleness
|
|
2165
|
-
// harmless to linkage: it re-validates the named trace's tenant AND
|
|
2166
|
-
// intersects credited ids against the trace's OWN result set before
|
|
2167
|
-
// inserting, so a stale/mismatched trace id from an old-binary write
|
|
2168
|
-
// gets silently skipped rather than mislinked. That preserves the
|
|
2169
|
-
// plan's "drop the three tables + last_trace_id restores v39 behavior
|
|
2170
|
-
// exactly" rollback promise — a min_compatible_binary bump would
|
|
2171
|
-
// additionally lock old binaries out of the WHOLE store for what is
|
|
2172
|
-
// purely an observability/training feature, which the rollback
|
|
2173
|
-
// promise does not require.
|
|
2174
|
-
db.exec(`
|
|
2175
|
-
CREATE TABLE IF NOT EXISTS recall_traces (
|
|
2176
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2177
|
-
ts TEXT NOT NULL,
|
|
2178
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
2179
|
-
session_id TEXT,
|
|
2180
|
-
pipeline TEXT NOT NULL CHECK (pipeline IN ('api','cli','context','mcp')),
|
|
2181
|
-
-- 'mcp' reserved for the deferred MCP wire-up. Deliberate: SQLite
|
|
2182
|
-
-- cannot ALTER a CHECK constraint, so extending it later means a
|
|
2183
|
-
-- full table-rebuild migration. One unused enum value now is
|
|
2184
|
-
-- cheaper than that rebuild.
|
|
2185
|
-
query_hash TEXT NOT NULL, -- sha256/16, NEVER raw query (audit convention, cli.ts:1532)
|
|
2186
|
-
query_length INTEGER NOT NULL,
|
|
2187
|
-
result_count INTEGER NOT NULL,
|
|
2188
|
-
explain_mode INTEGER NOT NULL DEFAULT 0
|
|
2189
|
-
);
|
|
2190
|
-
CREATE INDEX IF NOT EXISTS idx_recall_traces_tenant_ts ON recall_traces(tenant_id, ts DESC);
|
|
2191
|
-
|
|
2192
|
-
CREATE TABLE IF NOT EXISTS recall_trace_results (
|
|
2193
|
-
trace_id INTEGER NOT NULL REFERENCES recall_traces(id) ON DELETE CASCADE,
|
|
2194
|
-
tenant_id TEXT NOT NULL DEFAULT 'default', -- denormalized like goal_recall_log (v18
|
|
2195
|
-
-- precedent): tenant-scoped training queries
|
|
2196
|
-
-- over the memory index must not need a join
|
|
2197
|
-
-- back through recall_traces
|
|
2198
|
-
memory_id TEXT NOT NULL, -- NO FK to memories: traces must OUTLIVE forgotten
|
|
2199
|
-
-- memories (they are LC2's negative class). PRAGMA
|
|
2200
|
-
-- foreign_keys=ON is real, so an FK would either
|
|
2201
|
-
-- block inserts or cascade-delete exactly the rows
|
|
2202
|
-
-- training needs. Deliberate.
|
|
2203
|
-
result_rank INTEGER NOT NULL, -- NOT "rank" (SQL keyword; audit rule 10)
|
|
2204
|
-
score REAL NOT NULL,
|
|
2205
|
-
rerank_json TEXT, -- compact RerankStep[] when explain/trace present; else NULL
|
|
2206
|
-
PRIMARY KEY (trace_id, result_rank)
|
|
2207
|
-
) WITHOUT ROWID;
|
|
2208
|
-
CREATE INDEX IF NOT EXISTS idx_recall_trace_results_tenant_memory
|
|
2209
|
-
ON recall_trace_results(tenant_id, memory_id);
|
|
2210
|
-
|
|
2211
|
-
CREATE TABLE IF NOT EXISTS recall_trace_outcomes (
|
|
2212
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2213
|
-
trace_id INTEGER NOT NULL REFERENCES recall_traces(id) ON DELETE CASCADE,
|
|
2214
|
-
ts TEXT NOT NULL,
|
|
2215
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
2216
|
-
outcome TEXT NOT NULL CHECK (outcome IN ('positive','negative')),
|
|
2217
|
-
memory_ids_json TEXT NOT NULL -- ids actually credited by this outcome event
|
|
2218
|
-
);
|
|
2219
|
-
CREATE INDEX IF NOT EXISTS idx_recall_trace_outcomes_trace ON recall_trace_outcomes(trace_id);
|
|
2220
|
-
`);
|
|
2221
|
-
},
|
|
2222
|
-
},
|
|
2223
|
-
{
|
|
2224
|
-
version: 41,
|
|
2225
|
-
up: (db) => {
|
|
2226
|
-
// AT1 rejected-value tombstone (docs/plans/2026-08-15-at1-rejected-value-tombstone.md).
|
|
2227
|
-
// Additive table, template = v40 above. A human who rejects a fact gets
|
|
2228
|
-
// a durable say: the write-path guard in upsertEntryRow refuses any
|
|
2229
|
-
// write that would re-introduce a value whose normalized digest
|
|
2230
|
-
// matches a row here.
|
|
2231
|
-
//
|
|
2232
|
-
// No raw content and no preview stored. Follows the raw_archive
|
|
2233
|
-
// redaction precedent (raw-archive.ts payload is {redacted:true, ...}):
|
|
2234
|
-
// a rejected value may itself be a secret or PII ("never store my key
|
|
2235
|
-
// again") — persisting it in the tombstone would defeat the point. The
|
|
2236
|
-
// human sees the content at reject time (CLI echoes it); afterwards
|
|
2237
|
-
// `reason` is the human-readable identity.
|
|
2238
|
-
//
|
|
2239
|
-
// No FK on source_memory_id (v40 precedent above: tombstone outlives
|
|
2240
|
-
// the row it was sourced from) — provenance only.
|
|
2241
|
-
//
|
|
2242
|
-
// Reserved-word check on column names (skill-episode lesson, rule 10):
|
|
2243
|
-
// tenant/digest/reason/rejected/source/normalized/chars are non-reserved.
|
|
2244
|
-
//
|
|
2245
|
-
// No min_compatible_binary bump — a deliberate tradeoff (plan §2,
|
|
2246
|
-
// flagged to the ship gate). An old binary sharing a synced store
|
|
2247
|
-
// writes WITHOUT the guard until upgraded; documented in
|
|
2248
|
-
// MEMORY_ENVELOPE.md rather than hard-locking every old binary out of
|
|
2249
|
-
// the store, which is disproportionate for the dominant single-user
|
|
2250
|
-
// single-binary deployment.
|
|
2251
|
-
db.exec(`
|
|
2252
|
-
CREATE TABLE IF NOT EXISTS rejected_values (
|
|
2253
|
-
tenant_id TEXT NOT NULL,
|
|
2254
|
-
digest TEXT NOT NULL, -- sha256/64 of normalized content
|
|
2255
|
-
reason TEXT, -- human-supplied; the only description stored
|
|
2256
|
-
rejected_by TEXT, -- actor ('user:<id>' / 'cli' / ...)
|
|
2257
|
-
rejected_at TEXT NOT NULL,
|
|
2258
|
-
source_memory_id TEXT, -- provenance only; NO FK (tombstone outlives the row)
|
|
2259
|
-
normalized_chars INTEGER, -- weak sanity aid for humans listing tombstones
|
|
2260
|
-
PRIMARY KEY (tenant_id, digest)
|
|
2261
|
-
) WITHOUT ROWID;
|
|
2262
|
-
`);
|
|
2263
|
-
},
|
|
2264
|
-
},
|
|
2265
|
-
{
|
|
2266
|
-
version: 42,
|
|
2267
|
-
up: (db) => {
|
|
2268
|
-
// W1 handoff envelope (trajectories/01M2BQTM4AGFVMYY7G2XV5G7WY/plan.md).
|
|
2269
|
-
// Five nullable columns so the envelope carries evidence and outcome
|
|
2270
|
-
// and W2/W5 can filter on them without parsing JSON.
|
|
2271
|
-
if (!tableHasColumn(db, 'session_handoffs', 'constraints_json')) {
|
|
2272
|
-
db.exec(`ALTER TABLE session_handoffs ADD COLUMN constraints_json TEXT`);
|
|
2273
|
-
}
|
|
2274
|
-
if (!tableHasColumn(db, 'session_handoffs', 'evidence_json')) {
|
|
2275
|
-
db.exec(`ALTER TABLE session_handoffs ADD COLUMN evidence_json TEXT`);
|
|
2276
|
-
}
|
|
2277
|
-
if (!tableHasColumn(db, 'session_handoffs', 'outcome')) {
|
|
2278
|
-
db.exec(`ALTER TABLE session_handoffs ADD COLUMN outcome TEXT`);
|
|
2279
|
-
// codex P2: backfill from session_complete so pre-existing handoffs don't
|
|
2280
|
-
// all read as unfinished and get injected by the new 72h ambient fallback.
|
|
2281
|
-
db.exec(`
|
|
2282
|
-
UPDATE session_handoffs SET outcome = (
|
|
2283
|
-
SELECT e.content FROM session_events e
|
|
2284
|
-
WHERE e.tenant_id = session_handoffs.tenant_id AND e.session_id = session_handoffs.session_id
|
|
2285
|
-
AND e.event_type = 'session_complete' AND e.content IN ('success','partial','failure')
|
|
2286
|
-
ORDER BY e.created_at DESC, e.id DESC LIMIT 1
|
|
2287
|
-
)
|
|
2288
|
-
WHERE outcome IS NULL AND EXISTS (
|
|
2289
|
-
SELECT 1 FROM session_events e
|
|
2290
|
-
WHERE e.tenant_id = session_handoffs.tenant_id AND e.session_id = session_handoffs.session_id
|
|
2291
|
-
AND e.event_type = 'session_complete' AND e.content IN ('success','partial','failure')
|
|
2292
|
-
)
|
|
2293
|
-
`);
|
|
2294
|
-
}
|
|
2295
|
-
if (!tableHasColumn(db, 'session_handoffs', 'target_runtime')) {
|
|
2296
|
-
db.exec(`ALTER TABLE session_handoffs ADD COLUMN target_runtime TEXT`);
|
|
2297
|
-
}
|
|
2298
|
-
if (!tableHasColumn(db, 'session_handoffs', 'card_id')) {
|
|
2299
|
-
db.exec(`ALTER TABLE session_handoffs ADD COLUMN card_id TEXT`);
|
|
2300
|
-
}
|
|
2301
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_outcome ON session_handoffs(tenant_id, outcome, created_at DESC)`);
|
|
2302
|
-
},
|
|
2303
|
-
},
|
|
2304
|
-
{
|
|
2305
|
-
version: 43,
|
|
2306
|
-
up: (db) => {
|
|
2307
|
-
// W2a work-queue cards (trajectories/01M2D5VSYJFK4YXQ0RG2NGCPYJ/plan.md). Additive only, v41 precedent.
|
|
2308
|
-
db.exec(`
|
|
2309
|
-
CREATE TABLE IF NOT EXISTS cards (
|
|
2310
|
-
id TEXT PRIMARY KEY,
|
|
2311
|
-
title TEXT NOT NULL,
|
|
2312
|
-
status TEXT NOT NULL DEFAULT 'backlog',
|
|
2313
|
-
assignee_runtime TEXT,
|
|
2314
|
-
repo TEXT,
|
|
2315
|
-
contract TEXT,
|
|
2316
|
-
budget INTEGER,
|
|
2317
|
-
lease_until TEXT,
|
|
2318
|
-
heartbeat_at TEXT,
|
|
2319
|
-
created_at TEXT NOT NULL,
|
|
2320
|
-
updated_at TEXT NOT NULL,
|
|
2321
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
2322
|
-
scope TEXT
|
|
2323
|
-
) WITHOUT ROWID;
|
|
2324
|
-
CREATE INDEX IF NOT EXISTS idx_cards_tenant_status
|
|
2325
|
-
ON cards(tenant_id, status, updated_at DESC);
|
|
2326
|
-
|
|
2327
|
-
CREATE TABLE IF NOT EXISTS card_deps (
|
|
2328
|
-
parent TEXT NOT NULL REFERENCES cards(id),
|
|
2329
|
-
child TEXT NOT NULL REFERENCES cards(id),
|
|
2330
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
2331
|
-
created_at TEXT NOT NULL,
|
|
2332
|
-
PRIMARY KEY (parent, child)
|
|
2333
|
-
) WITHOUT ROWID;
|
|
2334
|
-
CREATE INDEX IF NOT EXISTS idx_card_deps_tenant_child
|
|
2335
|
-
ON card_deps(tenant_id, child);
|
|
2336
|
-
CREATE INDEX IF NOT EXISTS idx_card_deps_tenant_parent
|
|
2337
|
-
ON card_deps(tenant_id, parent);
|
|
2338
|
-
|
|
2339
|
-
CREATE TABLE IF NOT EXISTS card_runs (
|
|
2340
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2341
|
-
card TEXT NOT NULL REFERENCES cards(id),
|
|
2342
|
-
runtime TEXT NOT NULL,
|
|
2343
|
-
session_id TEXT,
|
|
2344
|
-
started TEXT NOT NULL,
|
|
2345
|
-
ended TEXT,
|
|
2346
|
-
outcome TEXT,
|
|
2347
|
-
created_at TEXT NOT NULL,
|
|
2348
|
-
updated_at TEXT NOT NULL,
|
|
2349
|
-
tenant_id TEXT NOT NULL DEFAULT 'default'
|
|
2350
|
-
);
|
|
2351
|
-
CREATE INDEX IF NOT EXISTS idx_card_runs_tenant_card
|
|
2352
|
-
ON card_runs(tenant_id, card, started DESC);
|
|
2353
|
-
|
|
2354
|
-
CREATE TABLE IF NOT EXISTS card_comments (
|
|
2355
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2356
|
-
card_id TEXT NOT NULL REFERENCES cards(id),
|
|
2357
|
-
author TEXT NOT NULL,
|
|
2358
|
-
body TEXT NOT NULL,
|
|
2359
|
-
created_at TEXT NOT NULL,
|
|
2360
|
-
tenant_id TEXT NOT NULL DEFAULT 'default'
|
|
2361
|
-
);
|
|
2362
|
-
CREATE INDEX IF NOT EXISTS idx_card_comments_tenant_card
|
|
2363
|
-
ON card_comments(tenant_id, card_id, created_at DESC);
|
|
2364
|
-
|
|
2365
|
-
CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_card
|
|
2366
|
-
ON session_handoffs(tenant_id, card_id, created_at DESC);
|
|
2367
|
-
`);
|
|
2368
|
-
},
|
|
2369
|
-
},
|
|
2370
|
-
];
|
|
2371
|
-
function tableHasColumn(db, tableName, columnName) {
|
|
2372
|
-
if (!/^[a-z_]+$/i.test(tableName))
|
|
2373
|
-
throw new Error(`Invalid table name: ${tableName}`);
|
|
2374
|
-
// SAFETY: rows' shape matches PRAGMA table_info's documented `name` column.
|
|
2375
|
-
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
2376
|
-
return rows.some((row) => row.name === columnName);
|
|
2377
|
-
}
|
|
2378
|
-
function tableExists(db, tableName) {
|
|
2379
|
-
if (!/^[a-z_]+$/i.test(tableName))
|
|
2380
|
-
throw new Error(`Invalid table name: ${tableName}`);
|
|
2381
|
-
// SAFETY: row's shape matches the single `name` column named in the
|
|
2382
|
-
// SELECT above.
|
|
2383
|
-
const row = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`).get(tableName);
|
|
2384
|
-
return !!row?.name;
|
|
2385
|
-
}
|
|
2386
|
-
export function getHippoDbPath(hippoRoot) {
|
|
2387
|
-
return path.join(hippoRoot, 'hippo.db');
|
|
2388
|
-
}
|
|
2389
|
-
export function getCurrentSchemaVersion() {
|
|
2390
|
-
return CURRENT_SCHEMA_VERSION;
|
|
2391
|
-
}
|
|
2392
|
-
function readMinCompatibleBinary(db) {
|
|
2393
|
-
// Tolerate the meta table not yet existing on a fresh DB.
|
|
2394
|
-
try {
|
|
2395
|
-
// SAFETY: row's shape matches the single `value` column named in the
|
|
2396
|
-
// SELECT above.
|
|
2397
|
-
const row = db.prepare(`SELECT value FROM meta WHERE key = 'min_compatible_binary'`).get();
|
|
2398
|
-
return row?.value ?? null;
|
|
2399
|
-
}
|
|
2400
|
-
catch {
|
|
2401
|
-
return null;
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2404
|
-
function isSqliteBusy(error) {
|
|
2405
|
-
const code = error?.errcode;
|
|
2406
|
-
return code === 5 || code === 6 || code === 517;
|
|
2407
|
-
}
|
|
2408
|
-
// busy_timeout covers neither of this file's two contended statements: SQLite
|
|
2409
|
-
// skips the busy handler for `PRAGMA journal_mode` and for a write that upgrades
|
|
2410
|
-
// a deferred read snapshot. Both need an explicit wait instead.
|
|
2411
|
-
function execWithBusyRetry(db, sql, timeoutMs = 30000) {
|
|
2412
|
-
const deadline = Date.now() + timeoutMs;
|
|
2413
|
-
const idle = new Int32Array(new SharedArrayBuffer(4));
|
|
2414
|
-
for (;;) {
|
|
2415
|
-
try {
|
|
2416
|
-
db.exec(sql);
|
|
2417
|
-
return;
|
|
2418
|
-
}
|
|
2419
|
-
catch (error) {
|
|
2420
|
-
if (!isSqliteBusy(error) || Date.now() >= deadline)
|
|
2421
|
-
throw error;
|
|
2422
|
-
Atomics.wait(idle, 0, 0, 10 + Math.floor(Math.random() * 20));
|
|
2423
|
-
}
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
export function openHippoDb(hippoRoot) {
|
|
2427
|
-
fs.mkdirSync(hippoRoot, { recursive: true });
|
|
2428
|
-
const db = new DatabaseSync(getHippoDbPath(hippoRoot));
|
|
2429
|
-
try {
|
|
2430
|
-
db.exec('PRAGMA busy_timeout = 5000');
|
|
2431
|
-
execWithBusyRetry(db, 'PRAGMA journal_mode = WAL');
|
|
2432
|
-
db.exec('PRAGMA synchronous = NORMAL');
|
|
2433
|
-
db.exec('PRAGMA wal_autocheckpoint = 100');
|
|
2434
|
-
db.exec('PRAGMA foreign_keys = ON');
|
|
2435
|
-
runMigrations(db, hippoRoot);
|
|
2436
|
-
// Path A backfill: delete any orphan markdown mirrors for already-archived
|
|
2437
|
-
// raw_archive rows. Idempotent via per-row raw_archive.mirror_cleaned_at
|
|
2438
|
-
// (v21). Wrapped in try/catch — a filesystem failure must not prevent DB open.
|
|
2439
|
-
try {
|
|
2440
|
-
cleanupArchivedMirrors(hippoRoot, db);
|
|
2441
|
-
}
|
|
2442
|
-
catch (cleanupErr) {
|
|
2443
|
-
console.error('openHippoDb: cleanupArchivedMirrors failed (non-fatal):', cleanupErr);
|
|
2444
|
-
}
|
|
2445
|
-
return db;
|
|
2446
|
-
}
|
|
2447
|
-
catch (error) {
|
|
2448
|
-
try {
|
|
2449
|
-
db.close();
|
|
2450
|
-
}
|
|
2451
|
-
catch {
|
|
2452
|
-
// Best effort only.
|
|
2453
|
-
}
|
|
2454
|
-
throw error;
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
function runMigrations(db, hippoRoot) {
|
|
2458
|
-
ensureMetaTable(db);
|
|
2459
|
-
// v1.3.1 rollback-safety guard. Schema v24 stamped meta.min_compatible_binary;
|
|
2460
|
-
// older binaries that lack the generic *:private:* default-deny filter would
|
|
2461
|
-
// leak github:private:* rows on no-scope recall. Refuse to open a DB stamped
|
|
2462
|
-
// with a min newer than this binary's version. Read BEFORE migrations so a
|
|
2463
|
-
// stale v1.2.0 binary cannot apply unknown future migrations either.
|
|
2464
|
-
const minRequired = readMinCompatibleBinary(db);
|
|
2465
|
-
if (minRequired && compareSemver(minRequired, PACKAGE_VERSION) > 0) {
|
|
2466
|
-
throw new Error(`hippo-memory: this database requires hippo-memory >= ${minRequired}, but the running binary is ${PACKAGE_VERSION}. ` +
|
|
2467
|
-
`Upgrade hippo-memory to open this database. Running an older binary against this DB would leak private rows that the ` +
|
|
2468
|
-
`older filter does not recognize.`);
|
|
2469
|
-
}
|
|
2470
|
-
let currentVersion = getSchemaVersion(db);
|
|
2471
|
-
if (currentVersion > 0)
|
|
2472
|
-
ensureContinuityTables(db);
|
|
2473
|
-
for (const migration of MIGRATIONS) {
|
|
2474
|
-
if (migration.version <= currentVersion)
|
|
2475
|
-
continue;
|
|
2476
|
-
execWithBusyRetry(db, 'BEGIN IMMEDIATE');
|
|
2477
|
-
try {
|
|
2478
|
-
// Re-read under the write lock: another process may have applied this
|
|
2479
|
-
// migration while we waited, and re-running one is not idempotent.
|
|
2480
|
-
const applied = getSchemaVersion(db);
|
|
2481
|
-
if (applied >= migration.version) {
|
|
2482
|
-
db.exec('COMMIT');
|
|
2483
|
-
currentVersion = applied;
|
|
2484
|
-
continue;
|
|
2485
|
-
}
|
|
2486
|
-
migration.up(db, { hippoRoot });
|
|
2487
|
-
setSchemaVersion(db, migration.version);
|
|
2488
|
-
db.exec('COMMIT');
|
|
2489
|
-
currentVersion = migration.version;
|
|
2490
|
-
}
|
|
2491
|
-
catch (error) {
|
|
2492
|
-
try {
|
|
2493
|
-
db.exec('ROLLBACK');
|
|
2494
|
-
}
|
|
2495
|
-
catch { /* already rolled back; keep the original error */ }
|
|
2496
|
-
throw error;
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
ensureContinuityIndexes(db);
|
|
2500
|
-
ensureMetaDefaults(db);
|
|
2501
|
-
ensureOptionalFts(db);
|
|
2502
|
-
}
|
|
2503
|
-
function ensureMetaTable(db) {
|
|
2504
|
-
db.exec(`
|
|
2505
|
-
CREATE TABLE IF NOT EXISTS meta (
|
|
2506
|
-
key TEXT PRIMARY KEY,
|
|
2507
|
-
value TEXT NOT NULL
|
|
2508
|
-
);
|
|
2509
|
-
`);
|
|
2510
|
-
}
|
|
2511
|
-
export function getSchemaVersion(db) {
|
|
2512
|
-
// SAFETY: row's shape matches the single `value` column named in the
|
|
2513
|
-
// SELECT above.
|
|
2514
|
-
const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
|
|
2515
|
-
const version = Number(row?.value ?? 0);
|
|
2516
|
-
return Number.isFinite(version) ? version : 0;
|
|
2517
|
-
}
|
|
2518
|
-
function setSchemaVersion(db, version) {
|
|
2519
|
-
db.prepare(`INSERT INTO meta(key, value) VALUES('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run(String(version));
|
|
2520
|
-
db.exec(`PRAGMA user_version = ${Math.max(0, Math.trunc(version))}`);
|
|
2521
|
-
}
|
|
2522
|
-
// Before the loop on stamped stores: a table lost after its migration stamped (2026-08-15
|
|
2523
|
-
// incident) is never re-migrated, and v4/v16/v22 ALTER or read it. Fresh stores use the chain.
|
|
2524
|
-
function ensureContinuityTables(db) {
|
|
2525
|
-
db.exec(`
|
|
2526
|
-
CREATE TABLE IF NOT EXISTS task_snapshots (
|
|
2527
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2528
|
-
task TEXT NOT NULL,
|
|
2529
|
-
summary TEXT NOT NULL,
|
|
2530
|
-
next_step TEXT NOT NULL,
|
|
2531
|
-
status TEXT NOT NULL,
|
|
2532
|
-
source TEXT NOT NULL,
|
|
2533
|
-
created_at TEXT NOT NULL,
|
|
2534
|
-
updated_at TEXT NOT NULL,
|
|
2535
|
-
session_id TEXT,
|
|
2536
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
2537
|
-
scope TEXT
|
|
2538
|
-
)
|
|
2539
|
-
`);
|
|
2540
|
-
db.exec(`
|
|
2541
|
-
CREATE TABLE IF NOT EXISTS session_events (
|
|
2542
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2543
|
-
session_id TEXT NOT NULL,
|
|
2544
|
-
task TEXT,
|
|
2545
|
-
event_type TEXT NOT NULL,
|
|
2546
|
-
content TEXT NOT NULL,
|
|
2547
|
-
source TEXT NOT NULL,
|
|
2548
|
-
metadata_json TEXT NOT NULL,
|
|
2549
|
-
created_at TEXT NOT NULL,
|
|
2550
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
2551
|
-
scope TEXT
|
|
2552
|
-
)
|
|
2553
|
-
`);
|
|
2554
|
-
db.exec(`
|
|
2555
|
-
CREATE TABLE IF NOT EXISTS session_handoffs (
|
|
2556
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2557
|
-
session_id TEXT NOT NULL,
|
|
2558
|
-
repo_root TEXT,
|
|
2559
|
-
task_id TEXT,
|
|
2560
|
-
summary TEXT NOT NULL,
|
|
2561
|
-
next_action TEXT,
|
|
2562
|
-
artifacts_json TEXT NOT NULL DEFAULT '[]',
|
|
2563
|
-
created_at TEXT NOT NULL,
|
|
2564
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
2565
|
-
scope TEXT,
|
|
2566
|
-
constraints_json TEXT,
|
|
2567
|
-
evidence_json TEXT,
|
|
2568
|
-
outcome TEXT,
|
|
2569
|
-
target_runtime TEXT,
|
|
2570
|
-
card_id TEXT
|
|
2571
|
-
)
|
|
2572
|
-
`);
|
|
2573
|
-
db.exec(`
|
|
2574
|
-
CREATE TABLE IF NOT EXISTS cards (
|
|
2575
|
-
id TEXT PRIMARY KEY,
|
|
2576
|
-
title TEXT NOT NULL,
|
|
2577
|
-
status TEXT NOT NULL DEFAULT 'backlog',
|
|
2578
|
-
assignee_runtime TEXT,
|
|
2579
|
-
repo TEXT,
|
|
2580
|
-
contract TEXT,
|
|
2581
|
-
budget INTEGER,
|
|
2582
|
-
lease_until TEXT,
|
|
2583
|
-
heartbeat_at TEXT,
|
|
2584
|
-
created_at TEXT NOT NULL,
|
|
2585
|
-
updated_at TEXT NOT NULL,
|
|
2586
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
2587
|
-
scope TEXT
|
|
2588
|
-
) WITHOUT ROWID
|
|
2589
|
-
`);
|
|
2590
|
-
db.exec(`
|
|
2591
|
-
CREATE TABLE IF NOT EXISTS card_deps (
|
|
2592
|
-
parent TEXT NOT NULL REFERENCES cards(id),
|
|
2593
|
-
child TEXT NOT NULL REFERENCES cards(id),
|
|
2594
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
2595
|
-
created_at TEXT NOT NULL,
|
|
2596
|
-
PRIMARY KEY (parent, child)
|
|
2597
|
-
) WITHOUT ROWID
|
|
2598
|
-
`);
|
|
2599
|
-
db.exec(`
|
|
2600
|
-
CREATE TABLE IF NOT EXISTS card_runs (
|
|
2601
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2602
|
-
card TEXT NOT NULL REFERENCES cards(id),
|
|
2603
|
-
runtime TEXT NOT NULL,
|
|
2604
|
-
session_id TEXT,
|
|
2605
|
-
started TEXT NOT NULL,
|
|
2606
|
-
ended TEXT,
|
|
2607
|
-
outcome TEXT,
|
|
2608
|
-
created_at TEXT NOT NULL,
|
|
2609
|
-
updated_at TEXT NOT NULL,
|
|
2610
|
-
tenant_id TEXT NOT NULL DEFAULT 'default'
|
|
2611
|
-
)
|
|
2612
|
-
`);
|
|
2613
|
-
db.exec(`
|
|
2614
|
-
CREATE TABLE IF NOT EXISTS card_comments (
|
|
2615
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2616
|
-
card_id TEXT NOT NULL REFERENCES cards(id),
|
|
2617
|
-
author TEXT NOT NULL,
|
|
2618
|
-
body TEXT NOT NULL,
|
|
2619
|
-
created_at TEXT NOT NULL,
|
|
2620
|
-
tenant_id TEXT NOT NULL DEFAULT 'default'
|
|
2621
|
-
)
|
|
2622
|
-
`);
|
|
2623
|
-
}
|
|
2624
|
-
// After the loop: tenant_id (v16) and scope (v23) do not exist yet on a genuine old store.
|
|
2625
|
-
function ensureContinuityIndexes(db) {
|
|
2626
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_task_snapshots_status_updated ON task_snapshots(status, updated_at DESC, id DESC)`);
|
|
2627
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_task_snapshots_tenant_status ON task_snapshots(tenant_id, status, updated_at DESC)`);
|
|
2628
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_task_snapshots_tenant_scope ON task_snapshots(tenant_id, scope, status)`);
|
|
2629
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_events_session_created ON session_events(session_id, created_at DESC, id DESC)`);
|
|
2630
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_events_task_created ON session_events(task, created_at DESC, id DESC)`);
|
|
2631
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_events_tenant_session ON session_events(tenant_id, session_id, created_at DESC, id DESC)`);
|
|
2632
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_session ON session_handoffs(session_id, created_at DESC)`);
|
|
2633
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_session ON session_handoffs(tenant_id, session_id, created_at DESC)`);
|
|
2634
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_outcome ON session_handoffs(tenant_id, outcome, created_at DESC)`);
|
|
2635
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_cards_tenant_status ON cards(tenant_id, status, updated_at DESC)`);
|
|
2636
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_card_deps_tenant_child ON card_deps(tenant_id, child)`);
|
|
2637
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_card_deps_tenant_parent ON card_deps(tenant_id, parent)`);
|
|
2638
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_card_runs_tenant_card ON card_runs(tenant_id, card, started DESC)`);
|
|
2639
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_card_comments_tenant_card ON card_comments(tenant_id, card_id, created_at DESC)`);
|
|
2640
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_card ON session_handoffs(tenant_id, card_id, created_at DESC)`);
|
|
2641
|
-
}
|
|
2642
|
-
function ensureMetaDefaults(db) {
|
|
2643
|
-
const defaults = [
|
|
2644
|
-
['schema_version', String(CURRENT_SCHEMA_VERSION)],
|
|
2645
|
-
['last_retrieval_ids', '[]'],
|
|
2646
|
-
['last_trace_id', ''],
|
|
2647
|
-
['total_remembered', '0'],
|
|
2648
|
-
['total_recalled', '0'],
|
|
2649
|
-
['total_forgotten', '0'],
|
|
2650
|
-
['fts5_available', '0'],
|
|
2651
|
-
];
|
|
2652
|
-
// Read-first: an already-current store has all 7 keys, so this is one
|
|
2653
|
-
// SELECT and zero writes instead of 7 no-op INSERT OR IGNOREs, each of
|
|
2654
|
-
// which takes a RESERVED lock even when nothing changes.
|
|
2655
|
-
const keys = defaults.map(([key]) => key);
|
|
2656
|
-
const present = new Set(db
|
|
2657
|
-
.prepare(`SELECT key FROM meta WHERE key IN (${keys.map(() => '?').join(',')})`)
|
|
2658
|
-
.all(...keys)
|
|
2659
|
-
.map((r) => r.key));
|
|
2660
|
-
if (present.size === defaults.length)
|
|
2661
|
-
return;
|
|
2662
|
-
const stmt = db.prepare(`INSERT OR IGNORE INTO meta(key, value) VALUES (?, ?)`);
|
|
2663
|
-
for (const [key, value] of defaults) {
|
|
2664
|
-
if (!present.has(key))
|
|
2665
|
-
stmt.run(key, value);
|
|
2666
|
-
}
|
|
2667
|
-
}
|
|
2668
|
-
function ensureOptionalFts(db) {
|
|
2669
|
-
let available = false;
|
|
2670
|
-
try {
|
|
2671
|
-
db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(id UNINDEXED, content, tags)`);
|
|
2672
|
-
backfillFtsIndex(db);
|
|
2673
|
-
available = true;
|
|
2674
|
-
}
|
|
2675
|
-
catch {
|
|
2676
|
-
available = false;
|
|
2677
|
-
}
|
|
2678
|
-
// Read-first: only write when the flag actually changed, so a healthy
|
|
2679
|
-
// store's open doesn't take a write lock for a same-value upsert.
|
|
2680
|
-
const flag = available ? '1' : '0';
|
|
2681
|
-
if (getMeta(db, 'fts5_available') !== flag)
|
|
2682
|
-
setMeta(db, 'fts5_available', flag);
|
|
2683
|
-
}
|
|
2684
|
-
function backfillFtsIndex(db) {
|
|
2685
|
-
// SAFETY: this get() result's shape matches the single aliased `c` column
|
|
2686
|
-
// named in the SELECT above.
|
|
2687
|
-
const memCount = db.prepare(`SELECT COUNT(*) AS c FROM memories`).get()?.c ?? 0;
|
|
2688
|
-
// SAFETY: this get() result's shape matches the single aliased `c` column
|
|
2689
|
-
// named in the SELECT above.
|
|
2690
|
-
const ftsCount = db.prepare(`SELECT COUNT(*) AS c FROM memories_fts`).get()?.c ?? 0;
|
|
2691
|
-
if (memCount === ftsCount)
|
|
2692
|
-
return;
|
|
2693
|
-
db.exec(`
|
|
2694
|
-
INSERT INTO memories_fts(id, content, tags)
|
|
2695
|
-
SELECT m.id, m.content, m.tags_json
|
|
2696
|
-
FROM memories m
|
|
2697
|
-
WHERE NOT EXISTS (
|
|
2698
|
-
SELECT 1 FROM memories_fts f WHERE f.id = m.id
|
|
2699
|
-
)
|
|
2700
|
-
`);
|
|
2701
|
-
db.exec(`
|
|
2702
|
-
DELETE FROM memories_fts
|
|
2703
|
-
WHERE id NOT IN (SELECT id FROM memories)
|
|
2704
|
-
`);
|
|
2705
|
-
}
|
|
2706
|
-
export function closeHippoDb(db) {
|
|
2707
|
-
db.close();
|
|
2708
|
-
}
|
|
2709
|
-
export function getMeta(db, key, fallback = '') {
|
|
2710
|
-
// SAFETY: row's shape matches the single `value` column named in the
|
|
2711
|
-
// SELECT above.
|
|
2712
|
-
const row = db.prepare(`SELECT value FROM meta WHERE key = ?`).get(key);
|
|
2713
|
-
return row?.value ?? fallback;
|
|
2714
|
-
}
|
|
2715
|
-
export function setMeta(db, key, value) {
|
|
2716
|
-
db.prepare(`INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run(key, value);
|
|
2717
|
-
}
|
|
2718
|
-
export function isFtsAvailable(db) {
|
|
2719
|
-
return getMeta(db, 'fts5_available', '0') === '1';
|
|
2720
|
-
}
|
|
2721
|
-
export function pruneConsolidationRuns(db, keep = 50) {
|
|
2722
|
-
db.prepare(`
|
|
2723
|
-
DELETE FROM consolidation_runs
|
|
2724
|
-
WHERE id NOT IN (
|
|
2725
|
-
SELECT id FROM consolidation_runs
|
|
2726
|
-
ORDER BY timestamp DESC, id DESC
|
|
2727
|
-
LIMIT ?
|
|
2728
|
-
)
|
|
2729
|
-
`).run(keep);
|
|
2730
|
-
}
|
|
2731
|
-
//# sourceMappingURL=db.js.map
|