hippo-memory 1.61.0 → 1.63.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 +37 -53
- package/dist/agent-memories/apply.d.ts +3 -1
- package/dist/agent-memories/apply.js +23 -1
- package/dist/agent-memories/claude-code.d.ts +4 -1
- package/dist/agent-memories/claude-code.js +53 -9
- package/dist/agent-memories/report.d.ts +1 -0
- package/dist/agent-memories/report.js +2 -2
- package/dist/agent-memories/sync.d.ts +3 -3
- package/dist/agent-memories/sync.js +46 -24
- package/dist/agent-memories/types.d.ts +0 -2
- 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 +62 -66
- 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 +50 -0
- package/dist/api/context-select.js +342 -0
- package/dist/api/context-types.d.ts +12 -17
- package/dist/api/context.d.ts +5 -4
- package/dist/api/context.js +208 -535
- package/dist/api/dormant.js +1 -1
- package/dist/api/drill-down.d.ts +4 -4
- package/dist/api/drill-down.js +56 -41
- 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/promote.js +55 -66
- 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 +310 -456
- package/dist/api/remember.d.ts +2 -2
- package/dist/api/sleep.d.ts +7 -35
- package/dist/api/sleep.js +206 -220
- 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 +2 -2
- package/dist/capture/compact.js +11 -15
- 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/briefs.js +324 -306
- package/dist/cli/context.js +44 -34
- package/dist/cli/continuity.js +283 -271
- package/dist/cli/curate.d.ts +1 -1
- package/dist/cli/curate.js +43 -58
- package/dist/cli/dag.js +5 -9
- package/dist/cli/decisions.js +334 -345
- package/dist/cli/explain.js +68 -61
- package/dist/cli/goals.js +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/maintenance.js +62 -51
- package/dist/cli/playbooks.js +391 -379
- package/dist/cli/projects.js +11 -6
- package/dist/cli/recall.js +30 -44
- package/dist/cli/remember.js +118 -87
- package/dist/cli/session-hooks.js +106 -115
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.js +267 -250
- 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 +80 -76
- package/dist/cli/transfer.js +88 -107
- package/dist/cli/usage.js +9 -6
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -9
- package/dist/compaction-record.d.ts +2 -2
- 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 +1 -8
- package/dist/consolidate/run.js +3 -25
- package/dist/consolidate/sleep.js +5 -17
- package/dist/consolidate/traces.js +9 -21
- package/dist/customer-notes.d.ts +5 -7
- package/dist/customer-notes.js +82 -76
- package/dist/dag.d.ts +10 -21
- package/dist/dag.js +189 -203
- package/dist/db/continuity.js +2 -2
- 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/decisions.d.ts +2 -2
- package/dist/decisions.js +97 -80
- package/dist/dedupe.js +86 -61
- package/dist/delivery-recorder.js +154 -135
- package/dist/doctor.js +129 -110
- 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 +103 -91
- 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 +46 -39
- 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 +96 -81
- 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 +88 -72
- package/dist/project-briefs.d.ts +2 -3
- package/dist/project-briefs.js +141 -118
- package/dist/project-identity.d.ts +22 -9
- package/dist/project-identity.js +47 -12
- package/dist/project-merge.d.ts +28 -5
- package/dist/project-merge.js +213 -46
- 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 +70 -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 +26 -17
- package/dist/shared.js +102 -104
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +88 -72
- 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 +9 -31
- 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 +34 -56
- 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/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/dag.js
CHANGED
|
@@ -87,17 +87,10 @@ export async function generateDagSummary(label, factContents, opts) {
|
|
|
87
87
|
return null;
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// Hardening follow-up (mirrors consolidate.ts's mergeCandidatesByTenant,
|
|
95
|
-
// T1): partition unparented facts by tenantId BEFORE clustering so a
|
|
96
|
-
// cluster can never mix facts from different tenants into one
|
|
97
|
-
// LLM-synthesized summary. Map preserves insertion order, so
|
|
98
|
-
// single-tenant stores (every row 'default') get exactly one partition
|
|
99
|
-
// and iterate in the same order as before this fix — byte-identical
|
|
100
|
-
// 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.
|
|
93
|
+
function partitionFactsByTenant(unparented) {
|
|
101
94
|
const unparentedByTenant = new Map();
|
|
102
95
|
for (const fact of unparented) {
|
|
103
96
|
const key = derivationPartitionKey(fact.tenantId, fact.scope, fact.origin_project);
|
|
@@ -107,95 +100,144 @@ export async function buildDag(hippoRoot, facts, opts) {
|
|
|
107
100
|
else
|
|
108
101
|
unparentedByTenant.set(key, [fact]);
|
|
109
102
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
103
|
+
return unparentedByTenant;
|
|
104
|
+
}
|
|
105
|
+
/** The L2 summary entry for one cluster, landing in the facts' own tenant and scope. */
|
|
106
|
+
function createClusterSummaryEntry(summary, cluster, home) {
|
|
107
|
+
const memberCreatedAts = cluster.members.map((m) => m.created).sort();
|
|
108
|
+
// Every member of `cluster` shares home.tenantId by construction (the
|
|
109
|
+
// tenant partition above), so the summary lands in the same tenant
|
|
110
|
+
// as the facts it summarizes instead of always 'default'
|
|
111
|
+
// (memory.ts:535 defaults tenantId when the option is omitted).
|
|
112
|
+
const summaryEntry = createMemory(summary, {
|
|
113
|
+
layer: Layer.Semantic,
|
|
114
|
+
tags: [...cluster.entityTags, ...neverAutoShareTags(cluster.members), 'dag-summary'],
|
|
115
|
+
confidence: 'inferred',
|
|
116
|
+
dag_level: 2,
|
|
117
|
+
tenantId: home.tenantId,
|
|
118
|
+
scope: home.scope,
|
|
119
|
+
baseHalfLifeDays: home.baseHalfLifeDays,
|
|
120
|
+
});
|
|
121
|
+
summaryEntry.origin_project = home.originProject;
|
|
122
|
+
// Cache descendant_count + earliest/latest_at on the summary row so
|
|
123
|
+
// DAG-aware recall can reason about scope without walking the children.
|
|
124
|
+
summaryEntry.descendant_count = cluster.members.length;
|
|
125
|
+
summaryEntry.earliest_at = memberCreatedAts[0];
|
|
126
|
+
summaryEntry.latest_at = memberCreatedAts[memberCreatedAts.length - 1];
|
|
127
|
+
return summaryEntry;
|
|
128
|
+
}
|
|
129
|
+
/** Summarize one eligible cluster, write the summary, then re-parent its members under it. */
|
|
130
|
+
async function summarizeCluster(hippoRoot, cluster, home, opts, result) {
|
|
131
|
+
const summary = await generateDagSummary(cluster.label, cluster.members.map((m) => m.content), opts);
|
|
132
|
+
if (!summary)
|
|
133
|
+
return;
|
|
134
|
+
const summaryEntry = createClusterSummaryEntry(summary, cluster, home);
|
|
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.
|
|
137
|
+
try {
|
|
138
|
+
writeEntry(hippoRoot, summaryEntry);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
if (err instanceof RejectedValueError) {
|
|
142
|
+
result.rejected++;
|
|
143
|
+
log.warn(`buildDag: cluster "${cluster.label}" skipped: summary matches a rejected value`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
result.summariesCreated++;
|
|
149
|
+
for (const member of cluster.members) {
|
|
150
|
+
const updated = { ...member, dag_parent_id: summaryEntry.id };
|
|
151
|
+
writeEntry(hippoRoot, updated);
|
|
152
|
+
result.factsLinked++;
|
|
153
|
+
}
|
|
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.
|
|
156
|
+
clearSummaryDirtyAfterBuild(hippoRoot, summaryEntry.id, summaryEntry.tenantId, 'buildDag');
|
|
157
|
+
}
|
|
158
|
+
export async function buildDag(hippoRoot, facts, opts) {
|
|
159
|
+
const result = { candidateClusters: 0, summariesCreated: 0, factsLinked: 0, rejected: 0 };
|
|
160
|
+
const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
|
|
161
|
+
const unparented = facts.filter((f) => f.dag_level === 1 && !f.dag_parent_id && f.tags.includes('extracted'));
|
|
162
|
+
for (const [, tenantFacts] of partitionFactsByTenant(unparented)) {
|
|
163
|
+
const home = {
|
|
164
|
+
tenantId: tenantFacts[0].tenantId,
|
|
165
|
+
scope: derivationScope(tenantFacts[0].scope),
|
|
166
|
+
originProject: tenantFacts[0].origin_project,
|
|
167
|
+
baseHalfLifeDays,
|
|
168
|
+
};
|
|
113
169
|
const clusters = clusterFacts(tenantFacts);
|
|
114
170
|
const eligibleClusters = clusters.filter((c) => c.members.length >= 3);
|
|
115
171
|
result.candidateClusters += eligibleClusters.length;
|
|
116
172
|
for (const cluster of eligibleClusters) {
|
|
117
|
-
|
|
118
|
-
if (!summary)
|
|
119
|
-
continue;
|
|
120
|
-
const memberCreatedAts = cluster.members.map((m) => m.created).sort();
|
|
121
|
-
// Every member of `cluster` shares factTenant by construction (the
|
|
122
|
-
// tenant partition above), so the summary lands in the same tenant
|
|
123
|
-
// as the facts it summarizes instead of always 'default'
|
|
124
|
-
// (memory.ts:535 defaults tenantId when the option is omitted).
|
|
125
|
-
const summaryEntry = createMemory(summary, {
|
|
126
|
-
layer: Layer.Semantic,
|
|
127
|
-
tags: [...cluster.entityTags, ...neverAutoShareTags(cluster.members), 'dag-summary'],
|
|
128
|
-
confidence: 'inferred',
|
|
129
|
-
dag_level: 2,
|
|
130
|
-
tenantId: factTenant,
|
|
131
|
-
scope: factScope,
|
|
132
|
-
baseHalfLifeDays,
|
|
133
|
-
});
|
|
134
|
-
summaryEntry.origin_project = tenantFacts[0].origin_project;
|
|
135
|
-
// Schema v25: cache descendant_count + earliest/latest_at on the summary
|
|
136
|
-
// row so DAG-aware recall (docs/plans/2026-05-05-dag-recall.md Task 2)
|
|
137
|
-
// can reason about scope without walking the children.
|
|
138
|
-
summaryEntry.descendant_count = cluster.members.length;
|
|
139
|
-
summaryEntry.earliest_at = memberCreatedAts[0];
|
|
140
|
-
summaryEntry.latest_at = memberCreatedAts[memberCreatedAts.length - 1];
|
|
141
|
-
// AT1 (plan §3 containment): a refused LLM-synthesized summary skips
|
|
142
|
-
// ONLY this cluster — the sleep cycle continues to the next one. The
|
|
143
|
-
// member re-parenting writes below never run for a skipped cluster
|
|
144
|
-
// (there is no summary id to parent them under).
|
|
145
|
-
//
|
|
146
|
-
// The tombstone check itself is tenant-scoped for free: writeEntry ->
|
|
147
|
-
// writeEntryDbOnly -> upsertEntryRow calls
|
|
148
|
-
// checkRejectionGuard(db, entry.tenantId ?? 'default', ...)
|
|
149
|
-
// (store/entry-writes.ts), reading tenantId off the entry being written. Now
|
|
150
|
-
// that summaryEntry carries factTenant instead of the implicit
|
|
151
|
-
// 'default', the guard consults that tenant's tombstones — no
|
|
152
|
-
// separate check needed here (unlike consolidate.ts's merge pass,
|
|
153
|
-
// which pre-checks via findRejectedValue because it writes through
|
|
154
|
-
// batchWriteAndDelete's bypassRejectionGuard path instead of
|
|
155
|
-
// writeEntry).
|
|
156
|
-
try {
|
|
157
|
-
writeEntry(hippoRoot, summaryEntry);
|
|
158
|
-
}
|
|
159
|
-
catch (err) {
|
|
160
|
-
if (err instanceof RejectedValueError) {
|
|
161
|
-
result.rejected++;
|
|
162
|
-
log.warn(`buildDag: cluster "${cluster.label}" skipped: summary matches a rejected value`);
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
throw err;
|
|
166
|
-
}
|
|
167
|
-
result.summariesCreated++;
|
|
168
|
-
for (const member of cluster.members) {
|
|
169
|
-
const updated = { ...member, dag_parent_id: summaryEntry.id };
|
|
170
|
-
writeEntry(hippoRoot, updated);
|
|
171
|
-
result.factsLinked++;
|
|
172
|
-
}
|
|
173
|
-
// v0.30 / E3 — cancel the cascade of dirty-marks fired by member
|
|
174
|
-
// writeEntry calls (E2 hook on writeEntryDbOnly in store/entry-writes.ts).
|
|
175
|
-
// The summary we just built IS fresh, no rebuild needed. Without this,
|
|
176
|
-
// E3 in the SAME sleep cycle would re-rebuild every new summary
|
|
177
|
-
// (2x LLM cost). plan-eng-r1 HIGH must-fix.
|
|
178
|
-
clearSummaryDirtyAfterBuild(hippoRoot, summaryEntry.id, summaryEntry.tenantId, 'buildDag');
|
|
173
|
+
await summarizeCluster(hippoRoot, cluster, home, opts, result);
|
|
179
174
|
}
|
|
180
175
|
}
|
|
181
176
|
return result;
|
|
182
177
|
}
|
|
178
|
+
/** Regenerate one dirty summary from its children and tally the outcome; throws reach the caller's isolation. */
|
|
179
|
+
async function rebuildOneSummary(hippoRoot, summary, opts, result) {
|
|
180
|
+
const children = loadChildrenOfSummary(hippoRoot, summary.id, summary.tenantId);
|
|
181
|
+
if (children.length === 0) {
|
|
182
|
+
// Zero-child case: clear dirty + zero counts, no LLM call, no rebuild_count bump.
|
|
183
|
+
const { changed } = applyRebuildResult(hippoRoot, summary, {
|
|
184
|
+
content: summary.content,
|
|
185
|
+
descendant_count: 0,
|
|
186
|
+
earliest_at: null,
|
|
187
|
+
latest_at: null,
|
|
188
|
+
bumpRebuildCount: false,
|
|
189
|
+
zeroChildren: true,
|
|
190
|
+
actor: 'sleep',
|
|
191
|
+
});
|
|
192
|
+
if (changed)
|
|
193
|
+
result.zeroChildSkipped++;
|
|
194
|
+
// changed=false → race lost / row vanished; silently skip. `refused`
|
|
195
|
+
// is always false here — applyRebuildResult only checks the
|
|
196
|
+
// tombstone when bumpRebuildCount is true (store/summaries.ts).
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// Derive label from summary's existing entity tags (mirrors clusterFacts)
|
|
200
|
+
const entityTags = summary.tags.filter((t) => t.startsWith('speaker:') || t.startsWith('topic:'));
|
|
201
|
+
const label = entityTags.length > 0
|
|
202
|
+
? entityTags.map((t) => t.split(':')[1]).join(': ')
|
|
203
|
+
: summary.content.slice(0, 40);
|
|
204
|
+
const newContent = await generateDagSummary(label, children.map((c) => c.content), opts);
|
|
205
|
+
if (!newContent) {
|
|
206
|
+
// LLM null / fetch error → leave dirty for next cycle
|
|
207
|
+
result.failed++;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const childCreatedAts = children.map((c) => c.created).sort();
|
|
211
|
+
const { changed, refused } = applyRebuildResult(hippoRoot, summary, {
|
|
212
|
+
content: newContent,
|
|
213
|
+
descendant_count: children.length,
|
|
214
|
+
earliest_at: childCreatedAts[0],
|
|
215
|
+
latest_at: childCreatedAts[childCreatedAts.length - 1],
|
|
216
|
+
bumpRebuildCount: true,
|
|
217
|
+
zeroChildren: false,
|
|
218
|
+
actor: 'sleep',
|
|
219
|
+
});
|
|
220
|
+
if (refused) {
|
|
221
|
+
result.refused++;
|
|
222
|
+
}
|
|
223
|
+
else if (changed) {
|
|
224
|
+
result.rebuilt++;
|
|
225
|
+
}
|
|
226
|
+
// changed=false (refused also false) → race lost; not failure, not
|
|
227
|
+
// success, silently skip
|
|
228
|
+
}
|
|
183
229
|
/**
|
|
184
|
-
*
|
|
230
|
+
* Sleep-cycle phase that drains the dirty L2 summary queue.
|
|
185
231
|
* Thin orchestrator; the heavy lifting lives in store.ts (load + apply)
|
|
186
232
|
* and dag.ts:generateDagSummary (LLM call).
|
|
187
233
|
*
|
|
188
|
-
* Per-summary try/catch
|
|
189
|
-
* 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.
|
|
190
235
|
*
|
|
191
236
|
* Race-loser handling: applyRebuildResult's UPDATE WHERE includes
|
|
192
237
|
* AND summary_dirty=1, so concurrent sleep's second writer returns
|
|
193
238
|
* changed=false. Silent skip (neither rebuilt++ nor refused++ nor failed++).
|
|
194
239
|
*
|
|
195
|
-
*
|
|
196
|
-
* (rebuild content matches a previously-rejected value) increments
|
|
197
|
-
* `refused`, not `rebuilt`. Dirty still clears either way; only the stat
|
|
198
|
-
* 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.
|
|
199
241
|
*/
|
|
200
242
|
export async function rebuildDirtySummaries(hippoRoot, opts) {
|
|
201
243
|
const cap = opts.cap ?? 20;
|
|
@@ -221,76 +263,74 @@ export async function rebuildDirtySummaries(hippoRoot, opts) {
|
|
|
221
263
|
await new Promise((resolve) => setImmediate(resolve));
|
|
222
264
|
}
|
|
223
265
|
try {
|
|
224
|
-
|
|
225
|
-
if (children.length === 0) {
|
|
226
|
-
// Zero-child case: clear dirty + zero counts, no LLM call, no rebuild_count bump.
|
|
227
|
-
const { changed } = applyRebuildResult(hippoRoot, summary, {
|
|
228
|
-
content: summary.content,
|
|
229
|
-
descendant_count: 0,
|
|
230
|
-
earliest_at: null,
|
|
231
|
-
latest_at: null,
|
|
232
|
-
bumpRebuildCount: false,
|
|
233
|
-
zeroChildren: true,
|
|
234
|
-
actor: 'sleep',
|
|
235
|
-
});
|
|
236
|
-
if (changed)
|
|
237
|
-
result.zeroChildSkipped++;
|
|
238
|
-
// changed=false → race lost / row vanished; silently skip. `refused`
|
|
239
|
-
// is always false here — applyRebuildResult only checks the
|
|
240
|
-
// tombstone when bumpRebuildCount is true (store/summaries.ts).
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
// Derive label from summary's existing entity tags (mirrors clusterFacts)
|
|
244
|
-
const entityTags = summary.tags.filter((t) => t.startsWith('speaker:') || t.startsWith('topic:'));
|
|
245
|
-
const label = entityTags.length > 0
|
|
246
|
-
? entityTags.map((t) => t.split(':')[1]).join(': ')
|
|
247
|
-
: summary.content.slice(0, 40);
|
|
248
|
-
const newContent = await generateDagSummary(label, children.map((c) => c.content), opts);
|
|
249
|
-
if (!newContent) {
|
|
250
|
-
// LLM null / fetch error → leave dirty for next cycle
|
|
251
|
-
result.failed++;
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
const childCreatedAts = children.map((c) => c.created).sort();
|
|
255
|
-
const { changed, refused } = applyRebuildResult(hippoRoot, summary, {
|
|
256
|
-
content: newContent,
|
|
257
|
-
descendant_count: children.length,
|
|
258
|
-
earliest_at: childCreatedAts[0],
|
|
259
|
-
latest_at: childCreatedAts[childCreatedAts.length - 1],
|
|
260
|
-
bumpRebuildCount: true,
|
|
261
|
-
zeroChildren: false,
|
|
262
|
-
actor: 'sleep',
|
|
263
|
-
});
|
|
264
|
-
if (refused) {
|
|
265
|
-
result.refused++;
|
|
266
|
-
}
|
|
267
|
-
else if (changed) {
|
|
268
|
-
result.rebuilt++;
|
|
269
|
-
}
|
|
270
|
-
// changed=false (refused also false) → race lost; not failure, not
|
|
271
|
-
// success, silently skip
|
|
266
|
+
await rebuildOneSummary(hippoRoot, summary, opts, result);
|
|
272
267
|
}
|
|
273
268
|
catch (err) {
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
// (audit() wraps its own writes try/catch per store/audit-event.ts, so a
|
|
277
|
-
// 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.
|
|
278
271
|
result.failed++;
|
|
279
272
|
log.error(`rebuildDirtySummaries: summary ${summary.id} (tenant ${summary.tenantId}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
280
273
|
}
|
|
281
274
|
}
|
|
282
275
|
return result;
|
|
283
276
|
}
|
|
277
|
+
/** The L3 profile entry for one cluster of L2 summaries. */
|
|
278
|
+
function createProfileEntry(summary, cluster, home) {
|
|
279
|
+
const memberCreatedAts = cluster.members.map((m) => m.created).sort();
|
|
280
|
+
const nowIso = new Date().toISOString();
|
|
281
|
+
const profileEntry = createMemory(summary, {
|
|
282
|
+
layer: Layer.Semantic,
|
|
283
|
+
tags: [...cluster.entityTags, ...neverAutoShareTags(cluster.members), 'dag-entity-profile'],
|
|
284
|
+
confidence: 'inferred',
|
|
285
|
+
dag_level: 3,
|
|
286
|
+
tenantId: home.tenantId,
|
|
287
|
+
scope: home.scope,
|
|
288
|
+
baseHalfLifeDays: home.baseHalfLifeDays,
|
|
289
|
+
});
|
|
290
|
+
profileEntry.origin_project = home.originProject;
|
|
291
|
+
profileEntry.descendant_count = cluster.members.length;
|
|
292
|
+
profileEntry.earliest_at = memberCreatedAts[0];
|
|
293
|
+
profileEntry.latest_at = memberCreatedAts[memberCreatedAts.length - 1];
|
|
294
|
+
profileEntry.dag_level_3_built_at = nowIso;
|
|
295
|
+
return profileEntry;
|
|
296
|
+
}
|
|
297
|
+
/** Profile one eligible cluster of L2s, write it, then re-link the L2s under it. */
|
|
298
|
+
async function profileCluster(hippoRoot, cluster, home, opts, result) {
|
|
299
|
+
const summary = await generateDagSummary(cluster.label, cluster.members.map((m) => m.content), opts);
|
|
300
|
+
if (!summary) {
|
|
301
|
+
result.failed++;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const profileEntry = createProfileEntry(summary, cluster, home);
|
|
305
|
+
// A refused profile skips only this cluster; the re-linking below never runs for it.
|
|
306
|
+
try {
|
|
307
|
+
writeEntry(hippoRoot, profileEntry);
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
if (err instanceof RejectedValueError) {
|
|
311
|
+
result.rejected++;
|
|
312
|
+
log.warn(`buildEntityProfiles: cluster "${cluster.label}" skipped: profile matches a rejected value`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
throw err;
|
|
316
|
+
}
|
|
317
|
+
result.profilesCreated++;
|
|
318
|
+
for (const member of cluster.members) {
|
|
319
|
+
const updated = { ...member, dag_parent_id: profileEntry.id };
|
|
320
|
+
writeEntry(hippoRoot, updated);
|
|
321
|
+
result.l2sLinked++;
|
|
322
|
+
}
|
|
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.
|
|
325
|
+
clearSummaryDirtyAfterBuild(hippoRoot, profileEntry.id, home.tenantId, 'buildEntityProfiles', 'buildEntityProfiles-clean');
|
|
326
|
+
}
|
|
284
327
|
/**
|
|
285
|
-
*
|
|
328
|
+
* Build L3 entity profiles by clustering L2 summaries with
|
|
286
329
|
* shared entity tags. Threshold 2+ L2s per entity. Mirrors buildDag L1->L2
|
|
287
330
|
* pattern, one level up.
|
|
288
331
|
*
|
|
289
|
-
* Born-dirty cancellation
|
|
290
|
-
*
|
|
291
|
-
* call clearSummaryDirtyAfterBuild with source='buildEntityProfiles-clean'
|
|
292
|
-
* so E3 sleep-cycle rebuild doesn't re-rebuild the freshly-built L3 this
|
|
293
|
-
* 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.
|
|
294
334
|
*/
|
|
295
335
|
export async function buildEntityProfiles(hippoRoot, l2Summaries, opts) {
|
|
296
336
|
const result = {
|
|
@@ -303,72 +343,18 @@ export async function buildEntityProfiles(hippoRoot, l2Summaries, opts) {
|
|
|
303
343
|
const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
|
|
304
344
|
// Only L2 with no L3 parent yet (avoid re-clustering already-profiled L2s).
|
|
305
345
|
const unparented = l2Summaries.filter((s) => s.dag_level === 2 && !s.dag_parent_id);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
for (const l2 of unparented) {
|
|
314
|
-
const tid = l2.tenantId ?? 'default';
|
|
315
|
-
const key = derivationPartitionKey(tid, l2.scope, l2.origin_project);
|
|
316
|
-
const list = byTenant.get(key) ?? [];
|
|
317
|
-
list.push(l2);
|
|
318
|
-
byTenant.set(key, list);
|
|
319
|
-
}
|
|
320
|
-
for (const [, tenantL2s] of byTenant) {
|
|
321
|
-
const tenantId = tenantL2s[0].tenantId ?? 'default';
|
|
322
|
-
const scope = derivationScope(tenantL2s[0].scope);
|
|
346
|
+
for (const [, tenantL2s] of partitionFactsByTenant(unparented)) {
|
|
347
|
+
const home = {
|
|
348
|
+
tenantId: tenantL2s[0].tenantId,
|
|
349
|
+
scope: derivationScope(tenantL2s[0].scope),
|
|
350
|
+
originProject: tenantL2s[0].origin_project,
|
|
351
|
+
baseHalfLifeDays,
|
|
352
|
+
};
|
|
323
353
|
const clusters = clusterFacts(tenantL2s);
|
|
324
354
|
const eligible = clusters.filter((c) => c.members.length >= 2);
|
|
325
355
|
result.candidateClusters += eligible.length;
|
|
326
356
|
for (const cluster of eligible) {
|
|
327
|
-
|
|
328
|
-
if (!summary) {
|
|
329
|
-
result.failed++;
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
const memberCreatedAts = cluster.members.map((m) => m.created).sort();
|
|
333
|
-
const nowIso = new Date().toISOString();
|
|
334
|
-
const profileEntry = createMemory(summary, {
|
|
335
|
-
layer: Layer.Semantic,
|
|
336
|
-
tags: [...cluster.entityTags, ...neverAutoShareTags(cluster.members), 'dag-entity-profile'],
|
|
337
|
-
confidence: 'inferred',
|
|
338
|
-
dag_level: 3,
|
|
339
|
-
tenantId, // HIGH #1 fold: thread tenant explicitly
|
|
340
|
-
scope,
|
|
341
|
-
baseHalfLifeDays,
|
|
342
|
-
});
|
|
343
|
-
profileEntry.origin_project = tenantL2s[0].origin_project;
|
|
344
|
-
profileEntry.descendant_count = cluster.members.length;
|
|
345
|
-
profileEntry.earliest_at = memberCreatedAts[0];
|
|
346
|
-
profileEntry.latest_at = memberCreatedAts[memberCreatedAts.length - 1];
|
|
347
|
-
profileEntry.dag_level_3_built_at = nowIso;
|
|
348
|
-
// AT1 (plan §3 containment): per-cluster catch — skip this cluster,
|
|
349
|
-
// count, log once. The member re-linking writes below never run for a
|
|
350
|
-
// skipped cluster (mirrors buildDag above).
|
|
351
|
-
try {
|
|
352
|
-
writeEntry(hippoRoot, profileEntry);
|
|
353
|
-
}
|
|
354
|
-
catch (err) {
|
|
355
|
-
if (err instanceof RejectedValueError) {
|
|
356
|
-
result.rejected++;
|
|
357
|
-
log.warn(`buildEntityProfiles: cluster "${cluster.label}" skipped: profile matches a rejected value`);
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
throw err;
|
|
361
|
-
}
|
|
362
|
-
result.profilesCreated++;
|
|
363
|
-
for (const member of cluster.members) {
|
|
364
|
-
const updated = { ...member, dag_parent_id: profileEntry.id };
|
|
365
|
-
writeEntry(hippoRoot, updated);
|
|
366
|
-
result.l2sLinked++;
|
|
367
|
-
}
|
|
368
|
-
// E3 born-dirty cancellation, same dance as buildDag L161-168 but for
|
|
369
|
-
// L3. Pass source='buildEntityProfiles-clean' to distinguish in audit.
|
|
370
|
-
// Args: (root, id, tenantId, actor, source).
|
|
371
|
-
clearSummaryDirtyAfterBuild(hippoRoot, profileEntry.id, tenantId, 'buildEntityProfiles', 'buildEntityProfiles-clean');
|
|
357
|
+
await profileCluster(hippoRoot, cluster, home, opts, result);
|
|
372
358
|
}
|
|
373
359
|
}
|
|
374
360
|
return result;
|
package/dist/db/continuity.js
CHANGED
|
@@ -112,8 +112,8 @@ const CARD_COMMENTS_DDL = `
|
|
|
112
112
|
tenant_id TEXT NOT NULL DEFAULT 'default'
|
|
113
113
|
)
|
|
114
114
|
`;
|
|
115
|
-
// Before the loop on stamped stores: a table lost after its migration stamped
|
|
116
|
-
//
|
|
115
|
+
// Before the loop on stamped stores: a table lost after its migration stamped
|
|
116
|
+
// is never re-migrated, and v4/v16/v22 ALTER or read it. Fresh stores use the chain.
|
|
117
117
|
export function ensureContinuityTables(db) {
|
|
118
118
|
db.exec(TASK_SNAPSHOTS_DDL);
|
|
119
119
|
db.exec(SESSION_EVENTS_DDL);
|
|
@@ -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
|
|
@@ -2,12 +2,8 @@ import { tableHasColumn } from '../tables.js';
|
|
|
2
2
|
export const v21 = {
|
|
3
3
|
version: 21,
|
|
4
4
|
up: (db) => {
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// one-shot and silently swallowed failed unlinks). With this column the
|
|
8
|
-
// reaper processes only rows WHERE mirror_cleaned_at IS NULL, sets the
|
|
9
|
-
// timestamp on success, and leaves it NULL on any unlink failure so the
|
|
10
|
-
// next openHippoDb retries automatically.
|
|
5
|
+
// Per-row mirror cleanup tracking: a failed unlink leaves mirror_cleaned_at NULL so the
|
|
6
|
+
// next openHippoDb retries it, where a one-shot global gate swallowed the failure.
|
|
11
7
|
if (!tableHasColumn(db, 'raw_archive', 'mirror_cleaned_at')) {
|
|
12
8
|
db.exec(`ALTER TABLE raw_archive ADD COLUMN mirror_cleaned_at TEXT`);
|
|
13
9
|
}
|
|
@@ -2,10 +2,8 @@ import { tableHasColumn } from '../tables.js';
|
|
|
2
2
|
export const v22 = {
|
|
3
3
|
version: 22,
|
|
4
4
|
up: (db) => {
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// and were never added to it, so the v0.40.0 provenance gate work
|
|
8
|
-
// exposed a real cross-tenant leak when continuity primitives are used.
|
|
5
|
+
// session_events and session_handoffs predate the v16 tenant migration and were
|
|
6
|
+
// never added to it: a cross-tenant leak whenever continuity primitives are used.
|
|
9
7
|
// Adds tenant_id (NOT NULL DEFAULT 'default') with smart backfill from
|
|
10
8
|
// task_snapshots.session_id when unambiguous, plus an optional scope
|
|
11
9
|
// column so a private-channel-derived handoff can default-deny via the
|
|
@@ -22,7 +22,7 @@ export const v23 = {
|
|
|
22
22
|
if (!tableHasColumn(db, 'task_snapshots', 'scope')) {
|
|
23
23
|
db.exec(`ALTER TABLE task_snapshots ADD COLUMN scope TEXT`);
|
|
24
24
|
}
|
|
25
|
-
// Quarantine policy
|
|
25
|
+
// Quarantine policy: pre-existing continuity rows
|
|
26
26
|
// with NULL scope cannot be safely classified as public after the fact.
|
|
27
27
|
// Mark them 'unknown:legacy' so the api.recall + cmdRecall default-deny
|
|
28
28
|
// filter excludes them for no-scope callers. Fresh rows from new
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const v24 = {
|
|
2
2
|
version: 24,
|
|
3
3
|
up: (db) => {
|
|
4
|
-
//
|
|
4
|
+
// GitHub connector schema.
|
|
5
5
|
// Six tables + a min_compatible_binary meta row for rollback safety.
|
|
6
6
|
db.exec(`
|
|
7
7
|
CREATE TABLE IF NOT EXISTS github_event_log (
|
|
@@ -50,7 +50,7 @@ export const v24 = {
|
|
|
50
50
|
added_at TEXT NOT NULL
|
|
51
51
|
)
|
|
52
52
|
`);
|
|
53
|
-
// PAT-mode multi-tenant routing
|
|
53
|
+
// PAT-mode multi-tenant routing. Maps repo_full_name to
|
|
54
54
|
// tenant when the webhook envelope has no `installation` field. Composite
|
|
55
55
|
// PK so the same repo can intentionally be visible to multiple tenants
|
|
56
56
|
// (e.g., shared tooling accounts) — collision is on (repo, tenant) pair.
|
|
@@ -62,10 +62,8 @@ export const v24 = {
|
|
|
62
62
|
PRIMARY KEY (repo_full_name, tenant_id)
|
|
63
63
|
)
|
|
64
64
|
`);
|
|
65
|
-
// Rollback-safety guard
|
|
66
|
-
//
|
|
67
|
-
// if it opened this DB. The startup guard in v1.2.1+ refuses to open a
|
|
68
|
-
// DB whose min_compatible_binary is newer than its own version.
|
|
65
|
+
// Rollback-safety guard: an older binary lacks the *:private:* default-deny and would leak
|
|
66
|
+
// github:private:* rows; the startup guard refuses a DB whose min_compatible_binary is newer.
|
|
69
67
|
db.prepare(`INSERT INTO meta(key, value) VALUES('min_compatible_binary', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run('1.2.1');
|
|
70
68
|
},
|
|
71
69
|
};
|
|
@@ -2,12 +2,11 @@ import { tableHasColumn } from '../tables.js';
|
|
|
2
2
|
export const v25 = {
|
|
3
3
|
version: 25,
|
|
4
4
|
up: (db) => {
|
|
5
|
-
//
|
|
5
|
+
// DAG-aware recall: cache summary metadata so the assembler can
|
|
6
6
|
// reason about scope without re-walking the DAG. Three additive,
|
|
7
7
|
// optional columns. No min_compatible_binary bump: these columns are
|
|
8
8
|
// pure metadata; older binaries opening this DB will see them as NULL
|
|
9
|
-
// / 0 and behave as before.
|
|
10
|
-
// Task 1.
|
|
9
|
+
// / 0 and behave as before.
|
|
11
10
|
if (!tableHasColumn(db, 'memories', 'descendant_count')) {
|
|
12
11
|
db.exec(`ALTER TABLE memories ADD COLUMN descendant_count INTEGER NOT NULL DEFAULT 0`);
|
|
13
12
|
}
|