hippo-memory 1.62.0 → 1.63.1
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 +11 -2
- package/dist/agent-memories/apply.d.ts +2 -0
- package/dist/agent-memories/apply.js +23 -1
- package/dist/agent-memories/claude-code.d.ts +2 -1
- package/dist/agent-memories/claude-code.js +4 -4
- package/dist/agent-memories/report.d.ts +1 -0
- package/dist/agent-memories/report.js +2 -2
- package/dist/agent-memories/sync.js +37 -23
- package/dist/ambient-store.d.ts +4 -4
- package/dist/ambient-store.js +4 -3
- package/dist/api/assemble.d.ts +7 -10
- package/dist/api/assemble.js +7 -12
- package/dist/api/audit.d.ts +2 -2
- package/dist/api/audit.js +2 -2
- package/dist/api/auth.d.ts +6 -6
- package/dist/api/auth.js +5 -7
- package/dist/api/context-select.d.ts +4 -3
- package/dist/api/context-select.js +9 -11
- package/dist/api/context-types.d.ts +12 -17
- package/dist/api/context.d.ts +3 -2
- package/dist/api/context.js +20 -20
- package/dist/api/dormant.js +1 -1
- package/dist/api/drill-down.d.ts +4 -4
- package/dist/api/drill-down.js +22 -10
- package/dist/api/outcome.d.ts +8 -13
- package/dist/api/outcome.js +13 -17
- package/dist/api/promote.d.ts +5 -8
- package/dist/api/quarantine.js +3 -2
- package/dist/api/recall-types.d.ts +43 -55
- package/dist/api/recall.d.ts +3 -3
- package/dist/api/recall.js +7 -18
- package/dist/api/remember.d.ts +2 -2
- package/dist/api/sleep.d.ts +7 -35
- package/dist/api/sleep.js +3 -2
- package/dist/api/tokens.d.ts +2 -2
- package/dist/api/tokens.js +2 -2
- package/dist/api/types.d.ts +6 -10
- package/dist/api/types.js +2 -4
- package/dist/audit-prune.d.ts +4 -6
- package/dist/audit-prune.js +3 -5
- package/dist/audit.js +11 -33
- package/dist/auth.d.ts +8 -9
- package/dist/auth.js +5 -7
- package/dist/autolearn.d.ts +1 -1
- package/dist/autolearn.js +1 -1
- package/dist/availability.js +3 -5
- package/dist/capture/command.d.ts +5 -4
- package/dist/capture/command.js +8 -22
- package/dist/capture/compact.d.ts +1 -1
- package/dist/capture/compact.js +9 -13
- package/dist/capture/extract.js +37 -115
- package/dist/capture-error.d.ts +1 -1
- package/dist/capture-error.js +1 -1
- package/dist/churn-git.d.ts +1 -1
- package/dist/churn-git.js +1 -1
- package/dist/cli/audit.js +3 -4
- package/dist/cli/auth.js +3 -6
- package/dist/cli/curate.d.ts +1 -1
- package/dist/cli/curate.js +8 -24
- package/dist/cli/dag.js +5 -9
- package/dist/cli/decisions.js +10 -21
- package/dist/cli/explain.js +2 -1
- package/dist/cli/goals.js +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/playbooks.js +4 -9
- package/dist/cli/projects.js +3 -1
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.js +6 -18
- package/dist/cli/session-hooks.js +21 -40
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.js +2 -6
- package/dist/cli/shared.js +3 -3
- package/dist/cli/slack.js +1 -1
- package/dist/cli/sleep.js +27 -1
- package/dist/cli/status.d.ts +4 -4
- package/dist/cli/status.js +7 -12
- package/dist/cli/transfer.js +8 -13
- package/dist/cli/usage.d.ts +1 -1
- package/dist/cli/usage.js +10 -7
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -9
- package/dist/compaction-record.d.ts +2 -0
- package/dist/compaction-record.js +89 -68
- package/dist/compare.d.ts +11 -16
- package/dist/compare.js +11 -16
- package/dist/config.d.ts +19 -18
- package/dist/config.js +90 -67
- package/dist/connectors/github/backfill.d.ts +2 -2
- package/dist/connectors/github/backfill.js +8 -15
- package/dist/connectors/github/cli-impl.js +3 -8
- package/dist/connectors/github/deletion.d.ts +5 -12
- package/dist/connectors/github/deletion.js +5 -12
- package/dist/connectors/github/dlq.d.ts +6 -9
- package/dist/connectors/github/dlq.js +2 -3
- package/dist/connectors/github/ingest.d.ts +5 -7
- package/dist/connectors/github/ingest.js +8 -12
- package/dist/connectors/github/octokit-client.d.ts +3 -5
- package/dist/connectors/github/octokit-client.js +5 -6
- package/dist/connectors/github/signature.d.ts +9 -39
- package/dist/connectors/github/signature.js +9 -39
- package/dist/connectors/github/tenant-routing.d.ts +1 -1
- package/dist/connectors/github/tenant-routing.js +1 -1
- package/dist/connectors/github/transform.js +2 -2
- package/dist/connectors/github/types.d.ts +2 -10
- package/dist/connectors/github/types.js +1 -3
- package/dist/connectors/slack/deletion.d.ts +3 -8
- package/dist/connectors/slack/deletion.js +3 -8
- package/dist/connectors/slack/dlq.d.ts +1 -1
- package/dist/connectors/slack/ingest.d.ts +1 -1
- package/dist/connectors/slack/ingest.js +7 -16
- package/dist/connectors/slack/signature.d.ts +1 -1
- package/dist/connectors/slack/tenant-routing.d.ts +3 -5
- package/dist/connectors/slack/tenant-routing.js +3 -5
- package/dist/connectors/slack/transform.d.ts +5 -6
- package/dist/connectors/slack/transform.js +5 -6
- package/dist/connectors/slack/types.d.ts +2 -6
- package/dist/connectors/slack/types.js +1 -3
- package/dist/connectors/slack/web-client.js +10 -3
- package/dist/connectors/slack/workspaces.d.ts +3 -5
- package/dist/connectors/slack/workspaces.js +3 -5
- package/dist/consolidate/conflicts.js +3 -14
- package/dist/consolidate/decay.js +9 -29
- package/dist/consolidate/llm-passes.js +4 -5
- package/dist/consolidate/merge.js +8 -23
- package/dist/consolidate/run.d.ts +3 -8
- package/dist/consolidate/run.js +21 -26
- package/dist/consolidate/sleep.js +7 -18
- package/dist/consolidate/traces.js +9 -21
- package/dist/customer-notes.d.ts +5 -7
- package/dist/customer-notes.js +6 -9
- package/dist/dag.d.ts +10 -21
- package/dist/dag.js +23 -73
- package/dist/db/continuity.d.ts +5 -0
- package/dist/db/continuity.js +43 -25
- package/dist/db/meta.d.ts +1 -0
- package/dist/db/meta.js +4 -3
- package/dist/db/migrate.d.ts +9 -0
- package/dist/db/migrate.js +110 -52
- package/dist/db/migrations/v14.js +1 -1
- package/dist/db/migrations/v15.js +1 -2
- package/dist/db/migrations/v16.js +3 -4
- package/dist/db/migrations/v17.js +2 -3
- package/dist/db/migrations/v19.js +1 -1
- package/dist/db/migrations/v20.js +1 -1
- package/dist/db/migrations/v21.js +2 -6
- package/dist/db/migrations/v22.js +2 -4
- package/dist/db/migrations/v23.js +1 -1
- package/dist/db/migrations/v24.js +4 -6
- package/dist/db/migrations/v25.js +2 -3
- package/dist/db/migrations/v26.js +3 -3
- package/dist/db/migrations/v27.js +2 -10
- package/dist/db/migrations/v28.js +5 -8
- package/dist/db/migrations/v29.js +3 -4
- package/dist/db/migrations/v30.js +2 -2
- package/dist/db/migrations/v31.js +1 -1
- package/dist/db/migrations/v32.js +1 -1
- package/dist/db/migrations/v33.js +3 -3
- package/dist/db/migrations/v34.js +1 -1
- package/dist/db/migrations/v35.js +3 -4
- package/dist/db/migrations/v36.js +3 -4
- package/dist/db/migrations/v37.js +5 -5
- package/dist/db/migrations/v38.js +7 -8
- package/dist/db/migrations/v39.js +1 -1
- package/dist/db/migrations/v40.js +4 -16
- package/dist/db/migrations/v41.js +3 -4
- package/dist/db/migrations/v42.js +3 -4
- package/dist/db/migrations/v45.js +1 -1
- package/dist/db/migrations/v46.js +1 -1
- package/dist/db/migrations/v47.js +1 -1
- package/dist/db/migrations/v48.js +1 -1
- package/dist/db.d.ts +1 -1
- package/dist/db.js +1 -1
- package/dist/decisions.d.ts +2 -2
- package/dist/decisions.js +6 -6
- package/dist/dedupe.js +86 -61
- package/dist/delivery-recorder.js +154 -135
- package/dist/doctor.js +140 -105
- package/dist/dormant.js +1 -4
- package/dist/embedding-provider.d.ts +4 -8
- package/dist/embedding-provider.js +4 -8
- package/dist/embeddings.js +55 -47
- package/dist/env.d.ts +1 -1
- package/dist/env.js +12 -12
- package/dist/escape.d.ts +5 -0
- package/dist/escape.js +10 -0
- package/dist/eval-stats.d.ts +1 -2
- package/dist/eval-stats.js +1 -2
- package/dist/eval-suite.js +27 -21
- package/dist/extract.js +4 -9
- package/dist/failure-log.d.ts +3 -3
- package/dist/failure-log.js +1 -1
- package/dist/forward-claim-detector.d.ts +2 -4
- package/dist/forward-claim-detector.js +6 -11
- package/dist/goals.d.ts +3 -3
- package/dist/goals.js +5 -6
- package/dist/graph/read.d.ts +2 -2
- package/dist/graph/read.js +5 -6
- package/dist/graph/types.d.ts +8 -8
- package/dist/graph/write.d.ts +7 -14
- package/dist/graph/write.js +16 -23
- package/dist/graph-extract.d.ts +7 -8
- package/dist/graph-extract.js +62 -72
- package/dist/graph-recall.d.ts +2 -2
- package/dist/graph-recall.js +55 -49
- package/dist/graph-stream.d.ts +5 -6
- package/dist/graph-stream.js +66 -57
- package/dist/graph-view.d.ts +2 -2
- package/dist/graph-view.js +7 -7
- package/dist/half-life-migration.d.ts +1 -2
- package/dist/half-life-migration.js +2 -3
- package/dist/hooks/codex-session.js +1 -1
- package/dist/hooks/codex-wrapper.d.ts +1 -1
- package/dist/hooks/codex-wrapper.js +3 -2
- package/dist/hooks/json-hooks.d.ts +2 -2
- package/dist/hooks/json-hooks.js +5 -4
- package/dist/hooks/opencode.d.ts +1 -1
- package/dist/hooks/opencode.js +5 -4
- package/dist/hooks/shared.d.ts +3 -7
- package/dist/hooks/shared.js +1 -8
- package/dist/http-util.d.ts +2 -3
- package/dist/http-util.js +3 -0
- package/dist/importers/core.d.ts +2 -9
- package/dist/importers/core.js +15 -30
- package/dist/importers/sources.js +2 -1
- package/dist/importers/vault.js +2 -20
- package/dist/incidents.d.ts +1 -1
- package/dist/incidents.js +1 -1
- package/dist/instruction-detect.d.ts +1 -1
- package/dist/instruction-detect.js +1 -1
- package/dist/invalidation.d.ts +3 -0
- package/dist/invalidation.js +160 -114
- package/dist/json.d.ts +5 -0
- package/dist/json.js +4 -0
- package/dist/judgment.js +1 -2
- package/dist/local-embedding.js +1 -1
- package/dist/mcp/admin-tools.js +7 -17
- package/dist/mcp/format.js +1 -1
- package/dist/mcp/framing.js +3 -6
- package/dist/mcp/protocol.d.ts +2 -5
- package/dist/mcp/protocol.js +1 -3
- package/dist/mcp/recall-tools.js +12 -15
- package/dist/mcp/request.js +4 -3
- package/dist/mcp/session-state.js +2 -3
- package/dist/mcp/stdio.js +2 -1
- package/dist/mcp/tools.js +9 -6
- package/dist/memory-value-weights.d.ts +5 -8
- package/dist/memory-value-weights.js +5 -8
- package/dist/memory-value.d.ts +13 -13
- package/dist/memory-value.js +26 -37
- package/dist/memory.d.ts +20 -22
- package/dist/memory.js +24 -48
- package/dist/multihop.d.ts +1 -1
- package/dist/multihop.js +3 -2
- package/dist/owner-validation.d.ts +4 -5
- package/dist/owner-validation.js +4 -5
- package/dist/physics.d.ts +4 -4
- package/dist/physics.js +7 -9
- package/dist/policies.d.ts +9 -10
- package/dist/policies.js +12 -14
- package/dist/postinstall.js +3 -6
- package/dist/predictions/planning-fallacy.d.ts +9 -14
- package/dist/predictions/planning-fallacy.js +10 -16
- package/dist/predictions/store.d.ts +15 -23
- package/dist/predictions/store.js +36 -33
- package/dist/processes.d.ts +2 -7
- package/dist/processes.js +3 -3
- package/dist/project-briefs.d.ts +2 -3
- package/dist/project-briefs.js +9 -13
- package/dist/project-identity.d.ts +22 -9
- package/dist/project-identity.js +47 -12
- package/dist/project-merge.d.ts +17 -2
- package/dist/project-merge.js +109 -27
- package/dist/project-remote.d.ts +12 -0
- package/dist/project-remote.js +138 -0
- package/dist/prompt-recall.js +1 -2
- package/dist/rate-limit.d.ts +1 -1
- package/dist/rate-limit.js +1 -1
- package/dist/raw-archive.d.ts +9 -0
- package/dist/raw-archive.js +60 -53
- package/dist/recall-history.d.ts +19 -20
- package/dist/recall-history.js +24 -42
- package/dist/recall-pipeline.js +4 -28
- package/dist/recall-scope.d.ts +7 -8
- package/dist/recall-scope.js +7 -8
- package/dist/recall-trace.d.ts +5 -9
- package/dist/recall-trace.js +6 -10
- package/dist/refine-llm.d.ts +1 -1
- package/dist/refine-llm.js +2 -2
- package/dist/reject-flow.d.ts +3 -4
- package/dist/reject-flow.js +122 -117
- package/dist/rejection.d.ts +5 -6
- package/dist/rejection.js +7 -15
- package/dist/rerankers/clef.d.ts +1 -1
- package/dist/rerankers/jev.d.ts +1 -2
- package/dist/rerankers/jev.js +4 -5
- package/dist/rerankers/llm.d.ts +1 -2
- package/dist/rerankers/llm.js +1 -2
- package/dist/rerankers/types.d.ts +1 -2
- package/dist/rrf.d.ts +2 -2
- package/dist/rrf.js +2 -2
- package/dist/search/bm25-search.d.ts +1 -1
- package/dist/search/bm25-search.js +2 -1
- package/dist/search/boosts.js +2 -1
- package/dist/search/hybrid.d.ts +1 -1
- package/dist/search/hybrid.js +2 -1
- package/dist/search/physics-search.d.ts +1 -1
- package/dist/search/physics-search.js +2 -1
- package/dist/search/types.d.ts +2 -0
- package/dist/search/types.js +3 -1
- package/dist/secret-detect.d.ts +4 -5
- package/dist/secret-detect.js +6 -10
- package/dist/server/auth.js +5 -5
- package/dist/server/client-ip.js +1 -1
- package/dist/server/cursor.js +2 -1
- package/dist/server/mcp-http.js +4 -4
- package/dist/server/request.d.ts +3 -6
- package/dist/server/request.js +6 -7
- package/dist/server/routes/admin.js +5 -4
- package/dist/server/routes/customer-notes.js +6 -5
- package/dist/server/routes/decisions.js +4 -3
- package/dist/server/routes/incidents.js +7 -5
- package/dist/server/routes/memories.js +7 -7
- package/dist/server/routes/policies.js +3 -2
- package/dist/server/routes/predictions.js +12 -15
- package/dist/server/routes/processes.js +3 -2
- package/dist/server/routes/project-briefs.js +8 -7
- package/dist/server/routes/recall.js +95 -93
- package/dist/server/routes/skills.js +6 -5
- package/dist/server/types.d.ts +1 -1
- package/dist/server/validation.d.ts +1 -2
- package/dist/server/validation.js +7 -14
- package/dist/server-detect.js +72 -58
- package/dist/server.d.ts +2 -2
- package/dist/server.js +131 -117
- package/dist/shared.d.ts +17 -17
- package/dist/shared.js +93 -97
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +8 -8
- package/dist/store/audit-event.d.ts +2 -2
- package/dist/store/audit-event.js +1 -1
- package/dist/store/candidates.d.ts +2 -2
- package/dist/store/candidates.js +4 -3
- package/dist/store/conflicts.js +30 -22
- package/dist/store/delete-and-batch.d.ts +11 -14
- package/dist/store/delete-and-batch.js +40 -91
- package/dist/store/entry-reads.d.ts +17 -25
- package/dist/store/entry-reads.js +59 -40
- package/dist/store/entry-row.d.ts +6 -24
- package/dist/store/entry-row.js +6 -24
- package/dist/store/entry-writes.d.ts +6 -7
- package/dist/store/entry-writes.js +13 -11
- package/dist/store/handoffs.d.ts +1 -1
- package/dist/store/handoffs.js +7 -10
- package/dist/store/index-and-stats.d.ts +2 -6
- package/dist/store/index-and-stats.js +4 -10
- package/dist/store/mirrors.d.ts +6 -19
- package/dist/store/mirrors.js +14 -39
- package/dist/store/open.js +11 -36
- package/dist/store/rows.d.ts +5 -11
- package/dist/store/rows.js +6 -11
- package/dist/store/search-rows.d.ts +17 -34
- package/dist/store/search-rows.js +31 -59
- package/dist/store/sessions.d.ts +4 -5
- package/dist/store/sessions.js +5 -6
- package/dist/store/summaries.d.ts +13 -17
- package/dist/store/summaries.js +26 -70
- package/dist/support-bundle.js +4 -8
- package/dist/tenant.d.ts +1 -5
- package/dist/token-ledger.d.ts +1 -1
- package/dist/token-ledger.js +3 -5
- package/dist/trace.js +1 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +1 -1
- package/extensions/openclaw-plugin/README.md +3 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +2 -2
- package/extensions/openclaw-plugin/package.json +3 -3
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
package/dist/dag.js
CHANGED
|
@@ -87,13 +87,9 @@ export async function generateDagSummary(label, factContents, opts) {
|
|
|
87
87
|
return null;
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// LLM-synthesized summary. Map preserves insertion order, so
|
|
94
|
-
// single-tenant stores (every row 'default') get exactly one partition
|
|
95
|
-
// and iterate in the same order as before this fix — byte-identical
|
|
96
|
-
// behavior there.
|
|
90
|
+
// Partition BEFORE clustering so one LLM summary never mixes facts from different tenants.
|
|
91
|
+
// Map keeps insertion order, so a single-tenant store iterates in its original order.
|
|
92
|
+
// buildEntityProfiles partitions its L2s with this same key.
|
|
97
93
|
function partitionFactsByTenant(unparented) {
|
|
98
94
|
const unparentedByTenant = new Map();
|
|
99
95
|
for (const fact of unparented) {
|
|
@@ -123,9 +119,8 @@ function createClusterSummaryEntry(summary, cluster, home) {
|
|
|
123
119
|
baseHalfLifeDays: home.baseHalfLifeDays,
|
|
124
120
|
});
|
|
125
121
|
summaryEntry.origin_project = home.originProject;
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// can reason about scope without walking the children.
|
|
122
|
+
// Cache descendant_count + earliest/latest_at on the summary row so
|
|
123
|
+
// DAG-aware recall can reason about scope without walking the children.
|
|
129
124
|
summaryEntry.descendant_count = cluster.members.length;
|
|
130
125
|
summaryEntry.earliest_at = memberCreatedAts[0];
|
|
131
126
|
summaryEntry.latest_at = memberCreatedAts[memberCreatedAts.length - 1];
|
|
@@ -137,21 +132,8 @@ async function summarizeCluster(hippoRoot, cluster, home, opts, result) {
|
|
|
137
132
|
if (!summary)
|
|
138
133
|
return;
|
|
139
134
|
const summaryEntry = createClusterSummaryEntry(summary, cluster, home);
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
// member re-parenting writes below never run for a skipped cluster
|
|
143
|
-
// (there is no summary id to parent them under).
|
|
144
|
-
//
|
|
145
|
-
// The tombstone check itself is tenant-scoped for free: writeEntry ->
|
|
146
|
-
// writeEntryDbOnly -> upsertEntryRow calls
|
|
147
|
-
// checkRejectionGuard(db, entry.tenantId ?? 'default', ...)
|
|
148
|
-
// (store/entry-writes.ts), reading tenantId off the entry being written. Now
|
|
149
|
-
// that summaryEntry carries home.tenantId instead of the implicit
|
|
150
|
-
// 'default', the guard consults that tenant's tombstones — no
|
|
151
|
-
// separate check needed here (unlike consolidate.ts's merge pass,
|
|
152
|
-
// which pre-checks via findRejectedValue because it writes through
|
|
153
|
-
// batchWriteAndDelete's bypassRejectionGuard path instead of
|
|
154
|
-
// writeEntry).
|
|
135
|
+
// A refused summary skips only this cluster. writeEntry's guard reads tenantId off the entry
|
|
136
|
+
// (home.tenantId), so it already checks this tenant's tombstones: no separate check here.
|
|
155
137
|
try {
|
|
156
138
|
writeEntry(hippoRoot, summaryEntry);
|
|
157
139
|
}
|
|
@@ -169,11 +151,8 @@ async function summarizeCluster(hippoRoot, cluster, home, opts, result) {
|
|
|
169
151
|
writeEntry(hippoRoot, updated);
|
|
170
152
|
result.factsLinked++;
|
|
171
153
|
}
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
// The summary we just built IS fresh, no rebuild needed. Without this,
|
|
175
|
-
// E3 in the SAME sleep cycle would re-rebuild every new summary
|
|
176
|
-
// (2x LLM cost). plan-eng-r1 HIGH must-fix.
|
|
154
|
+
// Member writes just marked this fresh summary dirty; clear it, or the same sleep cycle's
|
|
155
|
+
// rebuild pass would re-rebuild every new summary at twice the LLM cost.
|
|
177
156
|
clearSummaryDirtyAfterBuild(hippoRoot, summaryEntry.id, summaryEntry.tenantId, 'buildDag');
|
|
178
157
|
}
|
|
179
158
|
export async function buildDag(hippoRoot, facts, opts) {
|
|
@@ -248,21 +227,17 @@ async function rebuildOneSummary(hippoRoot, summary, opts, result) {
|
|
|
248
227
|
// success, silently skip
|
|
249
228
|
}
|
|
250
229
|
/**
|
|
251
|
-
*
|
|
230
|
+
* Sleep-cycle phase that drains the dirty L2 summary queue.
|
|
252
231
|
* Thin orchestrator; the heavy lifting lives in store.ts (load + apply)
|
|
253
232
|
* and dag.ts:generateDagSummary (LLM call).
|
|
254
233
|
*
|
|
255
|
-
* Per-summary try/catch
|
|
256
|
-
* throwing rebuild does NOT abort the rest of the queue.
|
|
234
|
+
* Per-summary try/catch: one throwing rebuild does NOT abort the rest of the queue.
|
|
257
235
|
*
|
|
258
236
|
* Race-loser handling: applyRebuildResult's UPDATE WHERE includes
|
|
259
237
|
* AND summary_dirty=1, so concurrent sleep's second writer returns
|
|
260
238
|
* changed=false. Silent skip (neither rebuilt++ nor refused++ nor failed++).
|
|
261
239
|
*
|
|
262
|
-
*
|
|
263
|
-
* (rebuild content matches a previously-rejected value) increments
|
|
264
|
-
* `refused`, not `rebuilt`. Dirty still clears either way; only the stat
|
|
265
|
-
* split changed (docs/plans/2026-08-15-hardening-at1-followups.md T4).
|
|
240
|
+
* A tombstone hit counts as `refused`, not `rebuilt`; dirty clears either way.
|
|
266
241
|
*/
|
|
267
242
|
export async function rebuildDirtySummaries(hippoRoot, opts) {
|
|
268
243
|
const cap = opts.cap ?? 20;
|
|
@@ -291,33 +266,14 @@ export async function rebuildDirtySummaries(hippoRoot, opts) {
|
|
|
291
266
|
await rebuildOneSummary(hippoRoot, summary, opts, result);
|
|
292
267
|
}
|
|
293
268
|
catch (err) {
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
// (audit() wraps its own writes try/catch per store/audit-event.ts, so a
|
|
297
|
-
// throw here is exotic: SQLite I/O error, prepare failure, etc).
|
|
269
|
+
// One throw doesn't abort the queue; log enough to triage an exotic
|
|
270
|
+
// failure (SQLite I/O, prepare), since audit() already catches its own.
|
|
298
271
|
result.failed++;
|
|
299
272
|
log.error(`rebuildDirtySummaries: summary ${summary.id} (tenant ${summary.tenantId}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
300
273
|
}
|
|
301
274
|
}
|
|
302
275
|
return result;
|
|
303
276
|
}
|
|
304
|
-
// independent-review HIGH #1 fold: cluster ONLY within-tenant.
|
|
305
|
-
// clusterFacts has no tenant awareness; without this partition step a
|
|
306
|
-
// multi-tenant host could form a cluster spanning tenants and produce
|
|
307
|
-
// a single L3 with tenantId='default' that doesn't belong to either
|
|
308
|
-
// child tenant. Fix: bucket by tenantId, run clusterFacts per-tenant,
|
|
309
|
-
// pass tenantId to createMemory.
|
|
310
|
-
function partitionL2sByTenant(unparented) {
|
|
311
|
-
const byTenant = new Map();
|
|
312
|
-
for (const l2 of unparented) {
|
|
313
|
-
const tid = l2.tenantId ?? 'default';
|
|
314
|
-
const key = derivationPartitionKey(tid, l2.scope, l2.origin_project);
|
|
315
|
-
const list = byTenant.get(key) ?? [];
|
|
316
|
-
list.push(l2);
|
|
317
|
-
byTenant.set(key, list);
|
|
318
|
-
}
|
|
319
|
-
return byTenant;
|
|
320
|
-
}
|
|
321
277
|
/** The L3 profile entry for one cluster of L2 summaries. */
|
|
322
278
|
function createProfileEntry(summary, cluster, home) {
|
|
323
279
|
const memberCreatedAts = cluster.members.map((m) => m.created).sort();
|
|
@@ -327,7 +283,7 @@ function createProfileEntry(summary, cluster, home) {
|
|
|
327
283
|
tags: [...cluster.entityTags, ...neverAutoShareTags(cluster.members), 'dag-entity-profile'],
|
|
328
284
|
confidence: 'inferred',
|
|
329
285
|
dag_level: 3,
|
|
330
|
-
tenantId: home.tenantId,
|
|
286
|
+
tenantId: home.tenantId,
|
|
331
287
|
scope: home.scope,
|
|
332
288
|
baseHalfLifeDays: home.baseHalfLifeDays,
|
|
333
289
|
});
|
|
@@ -346,9 +302,7 @@ async function profileCluster(hippoRoot, cluster, home, opts, result) {
|
|
|
346
302
|
return;
|
|
347
303
|
}
|
|
348
304
|
const profileEntry = createProfileEntry(summary, cluster, home);
|
|
349
|
-
//
|
|
350
|
-
// count, log once. The member re-linking writes below never run for a
|
|
351
|
-
// skipped cluster (mirrors buildDag above).
|
|
305
|
+
// A refused profile skips only this cluster; the re-linking below never runs for it.
|
|
352
306
|
try {
|
|
353
307
|
writeEntry(hippoRoot, profileEntry);
|
|
354
308
|
}
|
|
@@ -366,21 +320,17 @@ async function profileCluster(hippoRoot, cluster, home, opts, result) {
|
|
|
366
320
|
writeEntry(hippoRoot, updated);
|
|
367
321
|
result.l2sLinked++;
|
|
368
322
|
}
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
// Args: (root, id, tenantId, actor, source).
|
|
323
|
+
// Re-linking just marked the fresh L3 dirty; clear it so this cycle's rebuild skips it.
|
|
324
|
+
// The distinct source tags the audit row.
|
|
372
325
|
clearSummaryDirtyAfterBuild(hippoRoot, profileEntry.id, home.tenantId, 'buildEntityProfiles', 'buildEntityProfiles-clean');
|
|
373
326
|
}
|
|
374
327
|
/**
|
|
375
|
-
*
|
|
328
|
+
* Build L3 entity profiles by clustering L2 summaries with
|
|
376
329
|
* shared entity tags. Threshold 2+ L2s per entity. Mirrors buildDag L1->L2
|
|
377
330
|
* pattern, one level up.
|
|
378
331
|
*
|
|
379
|
-
* Born-dirty cancellation
|
|
380
|
-
*
|
|
381
|
-
* call clearSummaryDirtyAfterBuild with source='buildEntityProfiles-clean'
|
|
382
|
-
* so E3 sleep-cycle rebuild doesn't re-rebuild the freshly-built L3 this
|
|
383
|
-
* same cycle.
|
|
332
|
+
* Born-dirty cancellation: each L2 link write marks the new L3 dirty, so clear it
|
|
333
|
+
* or the same sleep cycle's rebuild re-rebuilds the freshly built L3.
|
|
384
334
|
*/
|
|
385
335
|
export async function buildEntityProfiles(hippoRoot, l2Summaries, opts) {
|
|
386
336
|
const result = {
|
|
@@ -393,9 +343,9 @@ export async function buildEntityProfiles(hippoRoot, l2Summaries, opts) {
|
|
|
393
343
|
const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
|
|
394
344
|
// Only L2 with no L3 parent yet (avoid re-clustering already-profiled L2s).
|
|
395
345
|
const unparented = l2Summaries.filter((s) => s.dag_level === 2 && !s.dag_parent_id);
|
|
396
|
-
for (const [, tenantL2s] of
|
|
346
|
+
for (const [, tenantL2s] of partitionFactsByTenant(unparented)) {
|
|
397
347
|
const home = {
|
|
398
|
-
tenantId: tenantL2s[0].tenantId
|
|
348
|
+
tenantId: tenantL2s[0].tenantId,
|
|
399
349
|
scope: derivationScope(tenantL2s[0].scope),
|
|
400
350
|
originProject: tenantL2s[0].origin_project,
|
|
401
351
|
baseHalfLifeDays,
|
package/dist/db/continuity.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { DatabaseSyncLike } from './sqlite.js';
|
|
2
2
|
export declare const MEMORY_QUARANTINE_DDL = "\n CREATE TABLE IF NOT EXISTS memory_quarantine (\n tenant_id TEXT NOT NULL DEFAULT 'default',\n memory_id TEXT NOT NULL,\n original_scope TEXT,\n reason TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','approved','rejected')),\n quarantined_at TEXT NOT NULL,\n decided_at TEXT,\n decided_by TEXT,\n PRIMARY KEY (tenant_id, memory_id)\n ) WITHOUT ROWID;\n CREATE INDEX IF NOT EXISTS idx_memory_quarantine_status\n ON memory_quarantine(tenant_id, status, quarantined_at DESC);\n";
|
|
3
3
|
export declare function ensureContinuityTables(db: DatabaseSyncLike): void;
|
|
4
|
+
export declare const MEMORIES_FTS_DDL = "CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(id UNINDEXED, content, tags)";
|
|
5
|
+
/** Names of every table and index a `CREATE ... IF NOT EXISTS` in `ddl` makes. */
|
|
6
|
+
export declare function createdObjectNames(ddl: string): string[];
|
|
7
|
+
/** Every object runMigrations re-asserts on a stamped store; the open fast path requires all of them. */
|
|
8
|
+
export declare const REQUIRED_SCHEMA_OBJECTS: readonly string[];
|
|
4
9
|
export declare function ensureContinuityIndexes(db: DatabaseSyncLike): void;
|
|
5
10
|
//# sourceMappingURL=continuity.d.ts.map
|
package/dist/db/continuity.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { META_TABLE_DDL } from './meta.js';
|
|
1
2
|
// Shared by migration v48 and the stamped-store self-heal below.
|
|
2
3
|
export const MEMORY_QUARANTINE_DDL = `
|
|
3
4
|
CREATE TABLE IF NOT EXISTS memory_quarantine (
|
|
@@ -112,34 +113,51 @@ const CARD_COMMENTS_DDL = `
|
|
|
112
113
|
tenant_id TEXT NOT NULL DEFAULT 'default'
|
|
113
114
|
)
|
|
114
115
|
`;
|
|
115
|
-
// Before the loop on stamped stores: a table lost after its migration stamped
|
|
116
|
-
//
|
|
116
|
+
// Before the loop on stamped stores: a table lost after its migration stamped
|
|
117
|
+
// is never re-migrated, and v4/v16/v22 ALTER or read it. Fresh stores use the chain.
|
|
117
118
|
export function ensureContinuityTables(db) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
db.exec(SESSION_HANDOFFS_DDL);
|
|
121
|
-
db.exec(CARDS_DDL);
|
|
122
|
-
db.exec(CARD_DEPS_DDL);
|
|
123
|
-
db.exec(CARD_RUNS_DDL);
|
|
124
|
-
db.exec(CARD_COMMENTS_DDL);
|
|
125
|
-
db.exec(MEMORY_QUARANTINE_DDL);
|
|
119
|
+
for (const ddl of CONTINUITY_TABLE_DDL)
|
|
120
|
+
db.exec(ddl);
|
|
126
121
|
}
|
|
122
|
+
const CONTINUITY_TABLE_DDL = [
|
|
123
|
+
TASK_SNAPSHOTS_DDL,
|
|
124
|
+
SESSION_EVENTS_DDL,
|
|
125
|
+
SESSION_HANDOFFS_DDL,
|
|
126
|
+
CARDS_DDL,
|
|
127
|
+
CARD_DEPS_DDL,
|
|
128
|
+
CARD_RUNS_DDL,
|
|
129
|
+
CARD_COMMENTS_DDL,
|
|
130
|
+
MEMORY_QUARANTINE_DDL,
|
|
131
|
+
];
|
|
132
|
+
const CONTINUITY_INDEX_DDL = [
|
|
133
|
+
`CREATE INDEX IF NOT EXISTS idx_task_snapshots_status_updated ON task_snapshots(status, updated_at DESC, id DESC)`,
|
|
134
|
+
`CREATE INDEX IF NOT EXISTS idx_task_snapshots_tenant_status ON task_snapshots(tenant_id, status, updated_at DESC)`,
|
|
135
|
+
`CREATE INDEX IF NOT EXISTS idx_task_snapshots_tenant_scope ON task_snapshots(tenant_id, scope, status)`,
|
|
136
|
+
`CREATE INDEX IF NOT EXISTS idx_session_events_session_created ON session_events(session_id, created_at DESC, id DESC)`,
|
|
137
|
+
`CREATE INDEX IF NOT EXISTS idx_session_events_task_created ON session_events(task, created_at DESC, id DESC)`,
|
|
138
|
+
`CREATE INDEX IF NOT EXISTS idx_session_events_tenant_session ON session_events(tenant_id, session_id, created_at DESC, id DESC)`,
|
|
139
|
+
`CREATE INDEX IF NOT EXISTS idx_session_handoffs_session ON session_handoffs(session_id, created_at DESC)`,
|
|
140
|
+
`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_session ON session_handoffs(tenant_id, session_id, created_at DESC)`,
|
|
141
|
+
`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_outcome ON session_handoffs(tenant_id, outcome, created_at DESC)`,
|
|
142
|
+
`CREATE INDEX IF NOT EXISTS idx_cards_tenant_status ON cards(tenant_id, status, updated_at DESC)`,
|
|
143
|
+
`CREATE INDEX IF NOT EXISTS idx_card_deps_tenant_child ON card_deps(tenant_id, child)`,
|
|
144
|
+
`CREATE INDEX IF NOT EXISTS idx_card_deps_tenant_parent ON card_deps(tenant_id, parent)`,
|
|
145
|
+
`CREATE INDEX IF NOT EXISTS idx_card_runs_tenant_card ON card_runs(tenant_id, card, started DESC)`,
|
|
146
|
+
`CREATE INDEX IF NOT EXISTS idx_card_comments_tenant_card ON card_comments(tenant_id, card_id, created_at DESC)`,
|
|
147
|
+
`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_card ON session_handoffs(tenant_id, card_id, created_at DESC)`,
|
|
148
|
+
];
|
|
149
|
+
// Lives with the other re-asserted DDL so the required-object list below derives from one place.
|
|
150
|
+
export const MEMORIES_FTS_DDL = `CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(id UNINDEXED, content, tags)`;
|
|
151
|
+
const CREATED_NAME = /CREATE\s+(?:VIRTUAL\s+)?(?:TABLE|INDEX)\s+IF\s+NOT\s+EXISTS\s+(\w+)/gi;
|
|
152
|
+
/** Names of every table and index a `CREATE ... IF NOT EXISTS` in `ddl` makes. */
|
|
153
|
+
export function createdObjectNames(ddl) {
|
|
154
|
+
return [...ddl.matchAll(CREATED_NAME)].map((m) => m[1]);
|
|
155
|
+
}
|
|
156
|
+
/** Every object runMigrations re-asserts on a stamped store; the open fast path requires all of them. */
|
|
157
|
+
export const REQUIRED_SCHEMA_OBJECTS = Object.freeze([META_TABLE_DDL, ...CONTINUITY_TABLE_DDL, ...CONTINUITY_INDEX_DDL, MEMORIES_FTS_DDL].flatMap(createdObjectNames));
|
|
127
158
|
// After the loop: tenant_id (v16) and scope (v23) do not exist yet on a genuine old store.
|
|
128
159
|
export function ensureContinuityIndexes(db) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_task_snapshots_tenant_scope ON task_snapshots(tenant_id, scope, status)`);
|
|
132
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_events_session_created ON session_events(session_id, created_at DESC, id DESC)`);
|
|
133
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_events_task_created ON session_events(task, created_at DESC, id DESC)`);
|
|
134
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_events_tenant_session ON session_events(tenant_id, session_id, created_at DESC, id DESC)`);
|
|
135
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_session ON session_handoffs(session_id, created_at DESC)`);
|
|
136
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_session ON session_handoffs(tenant_id, session_id, created_at DESC)`);
|
|
137
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_outcome ON session_handoffs(tenant_id, outcome, created_at DESC)`);
|
|
138
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_cards_tenant_status ON cards(tenant_id, status, updated_at DESC)`);
|
|
139
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_card_deps_tenant_child ON card_deps(tenant_id, child)`);
|
|
140
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_card_deps_tenant_parent ON card_deps(tenant_id, parent)`);
|
|
141
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_card_runs_tenant_card ON card_runs(tenant_id, card, started DESC)`);
|
|
142
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_card_comments_tenant_card ON card_comments(tenant_id, card_id, created_at DESC)`);
|
|
143
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_card ON session_handoffs(tenant_id, card_id, created_at DESC)`);
|
|
160
|
+
for (const ddl of CONTINUITY_INDEX_DDL)
|
|
161
|
+
db.exec(ddl);
|
|
144
162
|
}
|
|
145
163
|
//# sourceMappingURL=continuity.js.map
|
package/dist/db/meta.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DatabaseSyncLike } from './sqlite.js';
|
|
2
|
+
export declare const META_TABLE_DDL = "\n CREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n );\n ";
|
|
2
3
|
export declare function ensureMetaTable(db: DatabaseSyncLike): void;
|
|
3
4
|
export declare function getSchemaVersion(db: DatabaseSyncLike): number;
|
|
4
5
|
export declare function setSchemaVersion(db: DatabaseSyncLike, version: number): void;
|
package/dist/db/meta.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { tableExists } from './tables.js';
|
|
2
|
-
export
|
|
3
|
-
db.exec(`
|
|
2
|
+
export const META_TABLE_DDL = `
|
|
4
3
|
CREATE TABLE IF NOT EXISTS meta (
|
|
5
4
|
key TEXT PRIMARY KEY,
|
|
6
5
|
value TEXT NOT NULL
|
|
7
6
|
);
|
|
8
|
-
|
|
7
|
+
`;
|
|
8
|
+
export function ensureMetaTable(db) {
|
|
9
|
+
db.exec(META_TABLE_DDL);
|
|
9
10
|
}
|
|
10
11
|
export function getSchemaVersion(db) {
|
|
11
12
|
if (!tableExists(db, 'meta'))
|
package/dist/db/migrate.d.ts
CHANGED
|
@@ -5,5 +5,14 @@ export declare class IncompatibleBinaryError extends Error {
|
|
|
5
5
|
}
|
|
6
6
|
/** Refuse a store stamped for a newer binary. Fails closed: runMigrations creates meta first, so a failed read is a real error. */
|
|
7
7
|
export declare function assertBinaryCompatible(db: DatabaseSyncLike): void;
|
|
8
|
+
/** The fast path's single probe: versions, the binary minimum, the FTS flag, and counts of the meta defaults and required objects. */
|
|
9
|
+
export declare const SCHEMA_PROBE_SQL: string;
|
|
8
10
|
export declare function runMigrations(db: DatabaseSyncLike, hippoRoot?: string, busyWaitMs?: number): void;
|
|
11
|
+
/** Row counts of `memories` and its full-text index, which differ once the index drifts; null when either table is missing. */
|
|
12
|
+
export declare function ftsRowCounts(db: DatabaseSyncLike): {
|
|
13
|
+
memories: number;
|
|
14
|
+
fts: number;
|
|
15
|
+
} | null;
|
|
16
|
+
/** Re-syncs `memories_fts` with `memories` when their counts differ and returns whether it did; sleep runs it because a fast open does not. */
|
|
17
|
+
export declare function repairFtsDrift(db: DatabaseSyncLike): boolean;
|
|
9
18
|
//# sourceMappingURL=migrate.d.ts.map
|
package/dist/db/migrate.js
CHANGED
|
@@ -4,7 +4,7 @@ import { importLegacyEmbeddingIndex } from '../vector-store.js';
|
|
|
4
4
|
import { execWithBusyRetry } from './busy.js';
|
|
5
5
|
import { tableExists } from './tables.js';
|
|
6
6
|
import { ensureMetaTable, getMeta, getSchemaVersion, setMeta, setSchemaVersion } from './meta.js';
|
|
7
|
-
import { ensureContinuityIndexes, ensureContinuityTables } from './continuity.js';
|
|
7
|
+
import { MEMORIES_FTS_DDL, REQUIRED_SCHEMA_OBJECTS, ensureContinuityIndexes, ensureContinuityTables } from './continuity.js';
|
|
8
8
|
import { CURRENT_SCHEMA_VERSION, MIGRATIONS } from './migrations/index.js';
|
|
9
9
|
export function getCurrentSchemaVersion() {
|
|
10
10
|
return CURRENT_SCHEMA_VERSION;
|
|
@@ -12,15 +12,64 @@ export function getCurrentSchemaVersion() {
|
|
|
12
12
|
/** Thrown by {@link assertBinaryCompatible}; doctor uses it to pick the upgrade fix over a generic permissions fix. */
|
|
13
13
|
export class IncompatibleBinaryError extends Error {
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
export function assertBinaryCompatible(db) {
|
|
17
|
-
const minRequired = getMeta(db, 'min_compatible_binary');
|
|
15
|
+
function assertMinBinary(minRequired) {
|
|
18
16
|
if (minRequired && compareSemver(minRequired, PACKAGE_VERSION) > 0) {
|
|
19
17
|
throw new IncompatibleBinaryError(`hippo-memory: this database requires hippo-memory >= ${minRequired}, but the running binary is ${PACKAGE_VERSION}. ` +
|
|
20
18
|
`Upgrade hippo-memory to open it; an older binary does not know this schema and could expose private rows or damage the store.`);
|
|
21
19
|
}
|
|
22
20
|
}
|
|
21
|
+
/** Refuse a store stamped for a newer binary. Fails closed: runMigrations creates meta first, so a failed read is a real error. */
|
|
22
|
+
export function assertBinaryCompatible(db) {
|
|
23
|
+
assertMinBinary(getMeta(db, 'min_compatible_binary'));
|
|
24
|
+
}
|
|
25
|
+
const META_DEFAULTS = [
|
|
26
|
+
['schema_version', String(CURRENT_SCHEMA_VERSION)],
|
|
27
|
+
['last_retrieval_ids', '[]'],
|
|
28
|
+
['last_trace_id', ''],
|
|
29
|
+
['total_remembered', '0'],
|
|
30
|
+
['total_recalled', '0'],
|
|
31
|
+
['total_forgotten', '0'],
|
|
32
|
+
['fts5_available', '0'],
|
|
33
|
+
];
|
|
34
|
+
// Names come from our own constants and match \w+, so inlining them as literals is safe.
|
|
35
|
+
const sqlList = (names) => names.map((n) => `'${n}'`).join(', ');
|
|
36
|
+
/** The fast path's single probe: versions, the binary minimum, the FTS flag, and counts of the meta defaults and required objects. */
|
|
37
|
+
export const SCHEMA_PROBE_SQL = `SELECT
|
|
38
|
+
(SELECT user_version FROM pragma_user_version) AS user_version,
|
|
39
|
+
(SELECT value FROM meta WHERE key = 'schema_version') AS schema_version,
|
|
40
|
+
(SELECT value FROM meta WHERE key = 'min_compatible_binary') AS min_binary,
|
|
41
|
+
(SELECT value FROM meta WHERE key = 'fts5_available') AS fts5,
|
|
42
|
+
(SELECT COUNT(*) FROM meta WHERE key IN (${sqlList(META_DEFAULTS.map(([key]) => key))})) AS meta_keys,
|
|
43
|
+
(SELECT COUNT(*) FROM sqlite_master WHERE type IN ('table', 'index') AND name IN (${sqlList(REQUIRED_SCHEMA_OBJECTS)})) AS objects`;
|
|
44
|
+
/** True when the store is current and whole, so the open can skip every DDL statement. Runs the binary check either way. */
|
|
45
|
+
function schemaIsCurrent(db) {
|
|
46
|
+
let probe;
|
|
47
|
+
try {
|
|
48
|
+
// SAFETY: the SELECT names exactly these six columns and always returns one row.
|
|
49
|
+
probe = db.prepare(SCHEMA_PROBE_SQL).get();
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
// A store with no meta table yet is the slow path's job; anything else is a real error.
|
|
53
|
+
if (err instanceof Error && /no such table: meta/.test(err.message))
|
|
54
|
+
return false;
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
if (!probe)
|
|
58
|
+
return false;
|
|
59
|
+
assertMinBinary(probe.min_binary);
|
|
60
|
+
return Number(probe.user_version) === CURRENT_SCHEMA_VERSION
|
|
61
|
+
&& Number(probe.schema_version) === CURRENT_SCHEMA_VERSION
|
|
62
|
+
&& Number(probe.meta_keys) === META_DEFAULTS.length
|
|
63
|
+
&& Number(probe.objects) === REQUIRED_SCHEMA_OBJECTS.length
|
|
64
|
+
&& probe.fts5 === '1';
|
|
65
|
+
}
|
|
23
66
|
export function runMigrations(db, hippoRoot, busyWaitMs) {
|
|
67
|
+
if (!schemaIsCurrent(db))
|
|
68
|
+
migrateAndHeal(db, hippoRoot, busyWaitMs);
|
|
69
|
+
if (hippoRoot)
|
|
70
|
+
importLegacyVectors(db, hippoRoot);
|
|
71
|
+
}
|
|
72
|
+
function migrateAndHeal(db, hippoRoot, busyWaitMs) {
|
|
24
73
|
ensureMetaTable(db);
|
|
25
74
|
// Before anything writes, so a stale binary never repairs or migrates a store it does not understand.
|
|
26
75
|
assertBinaryCompatible(db);
|
|
@@ -30,36 +79,46 @@ export function runMigrations(db, hippoRoot, busyWaitMs) {
|
|
|
30
79
|
for (const migration of MIGRATIONS) {
|
|
31
80
|
if (migration.version <= currentVersion)
|
|
32
81
|
continue;
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
// A newer binary may have migrated and raised the minimum while we waited for the lock.
|
|
36
|
-
assertBinaryCompatible(db);
|
|
37
|
-
// Re-read under the write lock: another process may have applied this
|
|
38
|
-
// migration while we waited, and re-running one is not idempotent.
|
|
39
|
-
const applied = getSchemaVersion(db);
|
|
40
|
-
if (applied >= migration.version) {
|
|
41
|
-
db.exec('COMMIT');
|
|
42
|
-
currentVersion = applied;
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
migration.up(db, { hippoRoot });
|
|
46
|
-
setSchemaVersion(db, migration.version);
|
|
47
|
-
db.exec('COMMIT');
|
|
48
|
-
currentVersion = migration.version;
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
try {
|
|
52
|
-
db.exec('ROLLBACK');
|
|
53
|
-
}
|
|
54
|
-
catch { /* already rolled back; keep the original error */ }
|
|
55
|
-
throw error;
|
|
56
|
-
}
|
|
82
|
+
currentVersion = applyMigration(db, migration, hippoRoot, busyWaitMs);
|
|
57
83
|
}
|
|
84
|
+
healUserVersion(db, currentVersion, busyWaitMs);
|
|
58
85
|
ensureContinuityIndexes(db);
|
|
59
86
|
ensureMetaDefaults(db);
|
|
60
87
|
ensureOptionalFts(db);
|
|
61
|
-
|
|
62
|
-
|
|
88
|
+
}
|
|
89
|
+
/** Applies one migration under the write lock and returns the version the store is now at. */
|
|
90
|
+
function applyMigration(db, migration, hippoRoot, busyWaitMs) {
|
|
91
|
+
execWithBusyRetry(db, 'BEGIN IMMEDIATE', busyWaitMs);
|
|
92
|
+
try {
|
|
93
|
+
// A newer binary may have migrated and raised the minimum while we waited for the lock.
|
|
94
|
+
assertBinaryCompatible(db);
|
|
95
|
+
// Re-read under the write lock: another process may have applied this
|
|
96
|
+
// migration while we waited, and re-running one is not idempotent.
|
|
97
|
+
const applied = getSchemaVersion(db);
|
|
98
|
+
if (applied >= migration.version) {
|
|
99
|
+
db.exec('COMMIT');
|
|
100
|
+
return applied;
|
|
101
|
+
}
|
|
102
|
+
migration.up(db, { hippoRoot });
|
|
103
|
+
setSchemaVersion(db, migration.version);
|
|
104
|
+
db.exec('COMMIT');
|
|
105
|
+
return migration.version;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
try {
|
|
109
|
+
db.exec('ROLLBACK');
|
|
110
|
+
}
|
|
111
|
+
catch { /* already rolled back; keep the original error */ }
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// The fast path requires user_version as well, and a store stamped only in meta would otherwise never reach it.
|
|
116
|
+
function healUserVersion(db, version, busyWaitMs) {
|
|
117
|
+
// SAFETY: PRAGMA user_version returns one row with one integer column.
|
|
118
|
+
const row = db.prepare('PRAGMA user_version').get();
|
|
119
|
+
const target = Math.max(0, Math.trunc(version));
|
|
120
|
+
if (Number(row?.user_version ?? 0) < target)
|
|
121
|
+
execWithBusyRetry(db, `PRAGMA user_version = ${target}`, busyWaitMs);
|
|
63
122
|
}
|
|
64
123
|
// A failed import leaves embeddings.json in place for the next open to retry; the store still opens.
|
|
65
124
|
function importLegacyVectors(db, hippoRoot) {
|
|
@@ -71,27 +130,18 @@ function importLegacyVectors(db, hippoRoot) {
|
|
|
71
130
|
}
|
|
72
131
|
}
|
|
73
132
|
function ensureMetaDefaults(db) {
|
|
74
|
-
const defaults = [
|
|
75
|
-
['schema_version', String(CURRENT_SCHEMA_VERSION)],
|
|
76
|
-
['last_retrieval_ids', '[]'],
|
|
77
|
-
['last_trace_id', ''],
|
|
78
|
-
['total_remembered', '0'],
|
|
79
|
-
['total_recalled', '0'],
|
|
80
|
-
['total_forgotten', '0'],
|
|
81
|
-
['fts5_available', '0'],
|
|
82
|
-
];
|
|
83
133
|
// Read-first: an already-current store has all 7 keys, so this is one
|
|
84
134
|
// SELECT and zero writes instead of 7 no-op INSERT OR IGNOREs, each of
|
|
85
135
|
// which takes a RESERVED lock even when nothing changes.
|
|
86
|
-
const keys =
|
|
136
|
+
const keys = META_DEFAULTS.map(([key]) => key);
|
|
87
137
|
const present = new Set(db
|
|
88
138
|
.prepare(`SELECT key FROM meta WHERE key IN (${keys.map(() => '?').join(',')})`)
|
|
89
139
|
.all(...keys)
|
|
90
140
|
.map((r) => r.key));
|
|
91
|
-
if (present.size ===
|
|
141
|
+
if (present.size === META_DEFAULTS.length)
|
|
92
142
|
return;
|
|
93
143
|
const stmt = db.prepare(`INSERT OR IGNORE INTO meta(key, value) VALUES (?, ?)`);
|
|
94
|
-
for (const [key, value] of
|
|
144
|
+
for (const [key, value] of META_DEFAULTS) {
|
|
95
145
|
if (!present.has(key))
|
|
96
146
|
stmt.run(key, value);
|
|
97
147
|
}
|
|
@@ -99,8 +149,8 @@ function ensureMetaDefaults(db) {
|
|
|
99
149
|
function ensureOptionalFts(db) {
|
|
100
150
|
let available = false;
|
|
101
151
|
try {
|
|
102
|
-
db.exec(
|
|
103
|
-
|
|
152
|
+
db.exec(MEMORIES_FTS_DDL);
|
|
153
|
+
repairFtsDrift(db);
|
|
104
154
|
available = true;
|
|
105
155
|
}
|
|
106
156
|
catch (err) {
|
|
@@ -112,16 +162,23 @@ function ensureOptionalFts(db) {
|
|
|
112
162
|
if (getMeta(db, 'fts5_available') !== flag)
|
|
113
163
|
setMeta(db, 'fts5_available', flag);
|
|
114
164
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// SAFETY: this get() result's shape matches the single aliased `c` column named in the SELECT
|
|
165
|
+
/** Row counts of `memories` and its full-text index, which differ once the index drifts; null when either table is missing. */
|
|
166
|
+
export function ftsRowCounts(db) {
|
|
167
|
+
if (!tableExists(db, 'memories_fts') || !tableExists(db, 'memories'))
|
|
168
|
+
return null;
|
|
169
|
+
// SAFETY: this get() result's shape matches the single aliased `c` column named in the SELECT.
|
|
170
|
+
const memories = db.prepare(`SELECT COUNT(*) AS c FROM memories`).get()?.c ?? 0;
|
|
120
171
|
// memories_fts_docsize is FTS5's cheaper one-row-per-document shadow table; fall back for a foreign-built index.
|
|
121
172
|
const ftsTable = tableExists(db, 'memories_fts_docsize') ? 'memories_fts_docsize' : 'memories_fts';
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
173
|
+
// SAFETY: this get() result's shape matches the single aliased `c` column named in the SELECT.
|
|
174
|
+
const fts = db.prepare(`SELECT COUNT(*) AS c FROM ${ftsTable}`).get()?.c ?? 0;
|
|
175
|
+
return { memories: Number(memories), fts: Number(fts) };
|
|
176
|
+
}
|
|
177
|
+
/** Re-syncs `memories_fts` with `memories` when their counts differ and returns whether it did; sleep runs it because a fast open does not. */
|
|
178
|
+
export function repairFtsDrift(db) {
|
|
179
|
+
const counts = ftsRowCounts(db);
|
|
180
|
+
if (counts === null || counts.memories === counts.fts)
|
|
181
|
+
return false;
|
|
125
182
|
db.exec(`
|
|
126
183
|
INSERT INTO memories_fts(id, content, tags)
|
|
127
184
|
SELECT m.id, m.content, m.tags_json
|
|
@@ -134,5 +191,6 @@ function backfillFtsIndex(db) {
|
|
|
134
191
|
DELETE FROM memories_fts
|
|
135
192
|
WHERE id NOT IN (SELECT id FROM memories)
|
|
136
193
|
`);
|
|
194
|
+
return true;
|
|
137
195
|
}
|
|
138
196
|
//# sourceMappingURL=migrate.js.map
|
|
@@ -2,7 +2,7 @@ import { tableHasColumn } from '../tables.js';
|
|
|
2
2
|
export const v14 = {
|
|
3
3
|
version: 14,
|
|
4
4
|
up: (db) => {
|
|
5
|
-
//
|
|
5
|
+
// Provenance envelope: kind, scope, owner, artifact_ref.
|
|
6
6
|
// SQLite ALTER TABLE ADD COLUMN cannot add CHECK; CHECK enforcement lives
|
|
7
7
|
// in INSERT/UPDATE triggers added later in this migration.
|
|
8
8
|
if (!tableHasColumn(db, 'memories', 'kind')) {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
export const v15 = {
|
|
2
2
|
version: 15,
|
|
3
3
|
up: (db) => {
|
|
4
|
-
//
|
|
5
|
-
// dedup safety. Both findings landed in /review on commits 41b1f4d..6456e7d.
|
|
4
|
+
// Close the NULL-kind bypass and add raw_archive dedup safety.
|
|
6
5
|
//
|
|
7
6
|
// (1) Original v14 triggers used `WHEN NEW.kind IS NOT NULL AND NEW.kind NOT IN (...)`.
|
|
8
7
|
// A direct INSERT/UPDATE setting kind=NULL bypassed the CHECK substitute. Replace
|
|
@@ -2,9 +2,8 @@ import { tableHasColumn } from '../tables.js';
|
|
|
2
2
|
export const v16 = {
|
|
3
3
|
version: 16,
|
|
4
4
|
up: (db) => {
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// so future B-track tables don't have to backfill.
|
|
5
|
+
// Stub auth: add tenant_id to all data tables. Single-tenant per deployment for now;
|
|
6
|
+
// the columns land early so later tables don't have to backfill.
|
|
8
7
|
if (!tableHasColumn(db, 'memories', 'tenant_id')) {
|
|
9
8
|
db.exec(`ALTER TABLE memories ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
|
|
10
9
|
}
|
|
@@ -26,7 +25,7 @@ export const v16 = {
|
|
|
26
25
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_working_memory_tenant ON working_memory(tenant_id, importance DESC, created_at DESC)`);
|
|
27
26
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_consolidation_runs_tenant_ts ON consolidation_runs(tenant_id, timestamp DESC)`);
|
|
28
27
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_task_snapshots_tenant_status ON task_snapshots(tenant_id, status, updated_at DESC)`);
|
|
29
|
-
//
|
|
28
|
+
// Stub auth: api_keys (scrypt-hashed; plaintext returned to caller exactly once)
|
|
30
29
|
// and audit_log (append-only mutation trail). Both carry tenant_id from day 1 so
|
|
31
30
|
// future multi-tenant enforcement is a config flip, not a re-migration.
|
|
32
31
|
db.exec(`
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
export const v17 = {
|
|
2
2
|
version: 17,
|
|
3
3
|
up: (db) => {
|
|
4
|
-
//
|
|
5
|
-
// See docs/plans/2026-04-29-e1.3-slack-ingestion.md.
|
|
4
|
+
// Slack ingestion: idempotency log, per-channel backfill cursors, DLQ.
|
|
6
5
|
db.exec(`
|
|
7
6
|
CREATE TABLE IF NOT EXISTS slack_event_log (
|
|
8
7
|
event_id TEXT PRIMARY KEY,
|
|
@@ -31,7 +30,7 @@ export const v17 = {
|
|
|
31
30
|
)
|
|
32
31
|
`);
|
|
33
32
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_slack_dlq_tenant_received ON slack_dlq(tenant_id, received_at)`);
|
|
34
|
-
// Multi-tenant routing seam
|
|
33
|
+
// Multi-tenant routing seam. Empty by default: single-
|
|
35
34
|
// tenant deployments resolve via HIPPO_TENANT fallback. Multi-workspace
|
|
36
35
|
// deployments populate this table to map team_id → tenant_id.
|
|
37
36
|
db.exec(`
|
|
@@ -2,7 +2,7 @@ import { tableHasColumn } from '../tables.js';
|
|
|
2
2
|
export const v19 = {
|
|
3
3
|
version: 19,
|
|
4
4
|
up: (db) => {
|
|
5
|
-
//
|
|
5
|
+
// Slack hardening: widen slack_dlq with bucketing,
|
|
6
6
|
// retry tracking, and the signature/timestamp pair that lets `hippo
|
|
7
7
|
// slack dlq replay` re-verify before re-running ingest. ALTER ADD
|
|
8
8
|
// COLUMN with DEFAULT is non-destructive — legacy rows take the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const v20 = {
|
|
2
2
|
version: 20,
|
|
3
3
|
up: (db) => {
|
|
4
|
-
//
|
|
4
|
+
// GDPR Path A backfill: redact every existing
|
|
5
5
|
// raw_archive.payload_json so historical archives match the new
|
|
6
6
|
// metadata-only contract from src/raw-archive.ts. Read each row, parse
|
|
7
7
|
// the existing JSON to extract tenant_id and kind (best effort), then
|