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/decisions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Decision first-class object.
|
|
3
3
|
*
|
|
4
4
|
* `hippo decide` used to write only a tagged memory (tags ['decision'], source
|
|
5
5
|
* 'decision') with a 90-day half-life, so an in-force decision decayed out of
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* Tenant scoping: every helper requires tenantId. BEFORE INSERT/UPDATE triggers
|
|
17
17
|
* enforce decisions.tenant_id == the referenced memory's tenant_id, and a
|
|
18
18
|
* superseded_by same-tenant trigger makes cross-tenant supersession
|
|
19
|
-
* unrepresentable. Mirrors the
|
|
19
|
+
* unrepresentable. Mirrors the predictions pattern (src/predictions.ts).
|
|
20
20
|
*
|
|
21
21
|
* Dual-write atomicity: `saveDecision` writes the memory + decisions row (and,
|
|
22
22
|
* when superseding, the old row's UPDATE) inside writeEntry's SAVEPOINT
|
|
@@ -60,6 +60,97 @@ const DECISION_COLS = `
|
|
|
60
60
|
// ---------------------------------------------------------------------------
|
|
61
61
|
// Public API
|
|
62
62
|
// ---------------------------------------------------------------------------
|
|
63
|
+
/** The legacy `hippo decide` memory mirror for a decision. */
|
|
64
|
+
function buildDecisionMemory(hippoRoot, tenantId, opts) {
|
|
65
|
+
const content = opts.context
|
|
66
|
+
? `${opts.decisionText}\n\nContext: ${opts.context}`
|
|
67
|
+
: opts.decisionText;
|
|
68
|
+
const tags = ['decision', ...(opts.extraTags ?? [])];
|
|
69
|
+
return createMemory(content, {
|
|
70
|
+
tags,
|
|
71
|
+
layer: Layer.Semantic,
|
|
72
|
+
confidence: 'verified',
|
|
73
|
+
source: 'decision',
|
|
74
|
+
baseHalfLifeDays: objectHalfLifeDays(hippoRoot),
|
|
75
|
+
tenantId,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// Preflight the supersede target BEFORE inserting the new row. The new
|
|
79
|
+
// row's autoincrement id could otherwise collide with a non-existent
|
|
80
|
+
// supersedesDecisionId (e.g. superseding id 1 on an empty store, where the
|
|
81
|
+
// INSERT below would itself become id 1), making the row supersede itself.
|
|
82
|
+
// Validating first means the new row is never a candidate for its own
|
|
83
|
+
// supersede UPDATE.
|
|
84
|
+
function preflightDecisionSupersede(db, tenantId, supersedesId) {
|
|
85
|
+
// SAFETY: row shape matches the single `status` column named in the SELECT below.
|
|
86
|
+
const pred = db.prepare(`SELECT status FROM decisions WHERE id = ? AND tenant_id = ?`).get(supersedesId, tenantId);
|
|
87
|
+
if (!pred) {
|
|
88
|
+
throw new NotFoundError(`saveDecision: decision ${supersedesId} to supersede not found for tenant ${tenantId}`);
|
|
89
|
+
}
|
|
90
|
+
if (pred.status !== 'active') {
|
|
91
|
+
throw new ConflictError(`saveDecision: decision ${supersedesId} is not active (status='${pred.status}'); only active decisions can be superseded.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function insertDecisionRow(db, memoryId, tenantId, opts, now) {
|
|
95
|
+
const result = db.prepare(`
|
|
96
|
+
INSERT INTO decisions(
|
|
97
|
+
memory_id, tenant_id, decision_text, context,
|
|
98
|
+
status, superseded_by, superseded_at, closed_at, created_at
|
|
99
|
+
) VALUES (?, ?, ?, ?, 'active', NULL, NULL, NULL, ?)
|
|
100
|
+
`).run(memoryId, tenantId, opts.decisionText, opts.context ?? null, now);
|
|
101
|
+
return Number(result.lastInsertRowid ?? 0);
|
|
102
|
+
}
|
|
103
|
+
// Supersede the (preflight-validated) prior active decision in the SAME
|
|
104
|
+
// SAVEPOINT, atomic with the new row. The `id != decisionId` exclusion is
|
|
105
|
+
// defense-in-depth against the self-match described above; combined with
|
|
106
|
+
// the preflight, a 0-change here is unreachable in the single-writer txn.
|
|
107
|
+
function supersedeDecisionRow(db, tenantId, actor, supersedesId, decisionId, now) {
|
|
108
|
+
const sup = db.prepare(`
|
|
109
|
+
UPDATE decisions
|
|
110
|
+
SET status = 'superseded', superseded_by = ?, superseded_at = ?
|
|
111
|
+
WHERE id = ? AND tenant_id = ? AND status = 'active' AND id != ?
|
|
112
|
+
`).run(decisionId, now, supersedesId, tenantId, decisionId);
|
|
113
|
+
if (sup.changes === 0) {
|
|
114
|
+
throw new ConflictError(`saveDecision: decision ${supersedesId} could not be superseded (no longer active or self-reference).`);
|
|
115
|
+
}
|
|
116
|
+
appendAuditEvent(db, {
|
|
117
|
+
tenantId,
|
|
118
|
+
actor,
|
|
119
|
+
op: 'decision_supersede',
|
|
120
|
+
targetId: String(supersedesId),
|
|
121
|
+
metadata: {
|
|
122
|
+
decision_id: supersedesId,
|
|
123
|
+
superseded_by: decisionId,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** The afterWrite body: preflight, INSERT, supersede, reload, create audit, all in one SAVEPOINT. */
|
|
128
|
+
function writeDecisionRow(db, memoryId, tenantId, opts, actor, now) {
|
|
129
|
+
if (opts.supersedesDecisionId !== undefined) {
|
|
130
|
+
preflightDecisionSupersede(db, tenantId, opts.supersedesDecisionId);
|
|
131
|
+
}
|
|
132
|
+
const decisionId = insertDecisionRow(db, memoryId, tenantId, opts, now);
|
|
133
|
+
if (opts.supersedesDecisionId !== undefined) {
|
|
134
|
+
supersedeDecisionRow(db, tenantId, actor, opts.supersedesDecisionId, decisionId, now);
|
|
135
|
+
}
|
|
136
|
+
// SAFETY: row's shape matches the columns named in DECISION_COLS above.
|
|
137
|
+
const row = db.prepare(`SELECT ${DECISION_COLS} FROM decisions WHERE id = ?`)
|
|
138
|
+
.get(decisionId);
|
|
139
|
+
if (!row)
|
|
140
|
+
throw new Error('saveDecision: failed to reload saved decision row');
|
|
141
|
+
// GDPR-light metadata: id + flag only, no decision_text.
|
|
142
|
+
appendAuditEvent(db, {
|
|
143
|
+
tenantId,
|
|
144
|
+
actor,
|
|
145
|
+
op: 'decision_create',
|
|
146
|
+
targetId: String(decisionId),
|
|
147
|
+
metadata: {
|
|
148
|
+
decision_id: decisionId,
|
|
149
|
+
has_context: opts.context !== undefined && opts.context !== null && opts.context !== '',
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
return row;
|
|
153
|
+
}
|
|
63
154
|
/**
|
|
64
155
|
* Create a decision. Writes the memory mirror + the decisions row atomically
|
|
65
156
|
* inside writeEntry's SAVEPOINT 'write_entry'. When supersedesDecisionId is
|
|
@@ -77,92 +168,18 @@ export function saveDecision(hippoRoot, tenantId, opts, actor = 'cli') {
|
|
|
77
168
|
if (!opts.decisionText)
|
|
78
169
|
throw new BadRequestError('saveDecision: decisionText is required');
|
|
79
170
|
const now = new Date().toISOString();
|
|
80
|
-
const
|
|
81
|
-
? `${opts.decisionText}\n\nContext: ${opts.context}`
|
|
82
|
-
: opts.decisionText;
|
|
83
|
-
const tags = ['decision', ...(opts.extraTags ?? [])];
|
|
84
|
-
const mem = createMemory(content, {
|
|
85
|
-
tags,
|
|
86
|
-
layer: Layer.Semantic,
|
|
87
|
-
confidence: 'verified',
|
|
88
|
-
source: 'decision',
|
|
89
|
-
baseHalfLifeDays: objectHalfLifeDays(hippoRoot),
|
|
90
|
-
tenantId,
|
|
91
|
-
});
|
|
171
|
+
const mem = buildDecisionMemory(hippoRoot, tenantId, opts);
|
|
92
172
|
// Populated inside afterWrite so the INSERT, the supersede UPDATE, and the
|
|
93
173
|
// memory write all share one SAVEPOINT.
|
|
94
174
|
let savedRow;
|
|
95
175
|
writeEntry(hippoRoot, mem, {
|
|
96
176
|
actor,
|
|
97
177
|
afterWrite: (db, memoryId) => {
|
|
98
|
-
|
|
99
|
-
// row's autoincrement id could otherwise collide with a non-existent
|
|
100
|
-
// supersedesDecisionId (e.g. superseding id 1 on an empty store, where the
|
|
101
|
-
// INSERT below would itself become id 1), making the row supersede itself.
|
|
102
|
-
// Validating first means the new row is never a candidate for its own
|
|
103
|
-
// supersede UPDATE. codex review 2026-05-28 (P1).
|
|
104
|
-
if (opts.supersedesDecisionId !== undefined) {
|
|
105
|
-
// SAFETY: row shape matches the single `status` column named in the SELECT below.
|
|
106
|
-
const pred = db.prepare(`SELECT status FROM decisions WHERE id = ? AND tenant_id = ?`).get(opts.supersedesDecisionId, tenantId);
|
|
107
|
-
if (!pred) {
|
|
108
|
-
throw new NotFoundError(`saveDecision: decision ${opts.supersedesDecisionId} to supersede not found for tenant ${tenantId}`);
|
|
109
|
-
}
|
|
110
|
-
if (pred.status !== 'active') {
|
|
111
|
-
throw new ConflictError(`saveDecision: decision ${opts.supersedesDecisionId} is not active (status='${pred.status}'); only active decisions can be superseded.`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
const result = db.prepare(`
|
|
115
|
-
INSERT INTO decisions(
|
|
116
|
-
memory_id, tenant_id, decision_text, context,
|
|
117
|
-
status, superseded_by, superseded_at, closed_at, created_at
|
|
118
|
-
) VALUES (?, ?, ?, ?, 'active', NULL, NULL, NULL, ?)
|
|
119
|
-
`).run(memoryId, tenantId, opts.decisionText, opts.context ?? null, now);
|
|
120
|
-
const decisionId = Number(result.lastInsertRowid ?? 0);
|
|
121
|
-
// Supersede the (preflight-validated) prior active decision in the SAME
|
|
122
|
-
// SAVEPOINT, atomic with the new row. The `id != decisionId` exclusion is
|
|
123
|
-
// defense-in-depth against the self-match described above; combined with
|
|
124
|
-
// the preflight, a 0-change here is unreachable in the single-writer txn.
|
|
125
|
-
if (opts.supersedesDecisionId !== undefined) {
|
|
126
|
-
const sup = db.prepare(`
|
|
127
|
-
UPDATE decisions
|
|
128
|
-
SET status = 'superseded', superseded_by = ?, superseded_at = ?
|
|
129
|
-
WHERE id = ? AND tenant_id = ? AND status = 'active' AND id != ?
|
|
130
|
-
`).run(decisionId, now, opts.supersedesDecisionId, tenantId, decisionId);
|
|
131
|
-
if (sup.changes === 0) {
|
|
132
|
-
throw new BadRequestError(`saveDecision: decision ${opts.supersedesDecisionId} could not be superseded (no longer active or self-reference).`);
|
|
133
|
-
}
|
|
134
|
-
appendAuditEvent(db, {
|
|
135
|
-
tenantId,
|
|
136
|
-
actor,
|
|
137
|
-
op: 'decision_supersede',
|
|
138
|
-
targetId: String(opts.supersedesDecisionId),
|
|
139
|
-
metadata: {
|
|
140
|
-
decision_id: opts.supersedesDecisionId,
|
|
141
|
-
superseded_by: decisionId,
|
|
142
|
-
},
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
// SAFETY: row's shape matches the columns named in DECISION_COLS above.
|
|
146
|
-
const row = db.prepare(`SELECT ${DECISION_COLS} FROM decisions WHERE id = ?`)
|
|
147
|
-
.get(decisionId);
|
|
148
|
-
if (!row)
|
|
149
|
-
throw new Error('saveDecision: failed to reload saved decision row');
|
|
150
|
-
savedRow = row;
|
|
151
|
-
// GDPR-light metadata: id + flag only, no decision_text.
|
|
152
|
-
appendAuditEvent(db, {
|
|
153
|
-
tenantId,
|
|
154
|
-
actor,
|
|
155
|
-
op: 'decision_create',
|
|
156
|
-
targetId: String(decisionId),
|
|
157
|
-
metadata: {
|
|
158
|
-
decision_id: decisionId,
|
|
159
|
-
has_context: opts.context !== undefined && opts.context !== null && opts.context !== '',
|
|
160
|
-
},
|
|
161
|
-
});
|
|
178
|
+
savedRow = writeDecisionRow(db, memoryId, tenantId, opts, actor, now);
|
|
162
179
|
},
|
|
163
180
|
// Post-commit hook: mark the tenant's graph dirty AFTER the DB row commits
|
|
164
181
|
// but BEFORE the markdown mirrors are written, so a mirror-write failure can
|
|
165
|
-
// never leave a committed save unflagged
|
|
182
|
+
// never leave a committed save unflagged. markGraphDirty is fail-soft.
|
|
166
183
|
afterCommit: () => markGraphDirty(hippoRoot, tenantId, mem.id),
|
|
167
184
|
});
|
|
168
185
|
if (!savedRow) {
|
|
@@ -215,7 +232,7 @@ export function closeDecision(hippoRoot, tenantId, id, actor = 'cli') {
|
|
|
215
232
|
// Closing removes the object from the graph. Remove its rows DIRECTLY (deterministic),
|
|
216
233
|
// not only via an enqueued rebuild whose queue item is lost if the mirror is later
|
|
217
234
|
// forgotten (the queue row cascade-deletes with the memory), which would leave the closed
|
|
218
|
-
// object stale and could block that forget
|
|
235
|
+
// object stale and could block that forget. Still enqueue when a mirror exists
|
|
219
236
|
// so a concurrent rebuild re-derives consistently (harmless if it also runs).
|
|
220
237
|
removeGraphEntitiesForObject(hippoRoot, tenantId, 'decision', closed.id);
|
|
221
238
|
if (closed.memoryId) {
|
package/dist/dedupe.js
CHANGED
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { textOverlap } from './tokenize.js';
|
|
24
24
|
import { loadAllEntries } from './store/entry-reads.js';
|
|
25
|
-
import {
|
|
25
|
+
import { deleteEntryOn, memoriesBackingObjects } from './store/delete-and-batch.js';
|
|
26
|
+
import { openStore } from './store/open.js';
|
|
27
|
+
import { closeHippoDb } from './db.js';
|
|
26
28
|
import { compareEntryIdentity } from './compare.js';
|
|
27
29
|
import { canAutoDelete } from './memory.js';
|
|
28
30
|
import { derivationPartitionKey } from './recall-scope.js';
|
|
@@ -56,6 +58,71 @@ export function strengthBucket(strength) {
|
|
|
56
58
|
const s = strength ?? 0;
|
|
57
59
|
return Number.isFinite(s) ? Math.round(s / STRENGTH_TIE_EPSILON) : 0;
|
|
58
60
|
}
|
|
61
|
+
// finiteCount mirrors strengthBucket's non-finite hardening on the
|
|
62
|
+
// retrieval leg: a NaN retrieval_count would make the comparator return
|
|
63
|
+
// NaN and break the total order the same way a NaN bucket would.
|
|
64
|
+
// Unreachable via the schema (non-nullable INTEGER column), so this is
|
|
65
|
+
// symmetry, not a live bug.
|
|
66
|
+
const finiteCount = (n) => Number.isFinite(n ?? 0) ? (n ?? 0) : 0;
|
|
67
|
+
/** The v1.26.3 survivor total order: strength bucket desc, then retrieval_count desc, then compareEntryIdentity. */
|
|
68
|
+
function survivorOrder(a, b) {
|
|
69
|
+
const bucketDiff = strengthBucket(b.strength) - strengthBucket(a.strength);
|
|
70
|
+
if (bucketDiff !== 0)
|
|
71
|
+
return bucketDiff;
|
|
72
|
+
const retrievalDiff = finiteCount(b.retrieval_count) - finiteCount(a.retrieval_count);
|
|
73
|
+
if (retrievalDiff !== 0)
|
|
74
|
+
return retrievalDiff;
|
|
75
|
+
return compareEntryIdentity(a, b);
|
|
76
|
+
}
|
|
77
|
+
// Tenant partition (mirrors consolidate.ts mergeCandidatesByTenant and
|
|
78
|
+
// dag.ts unparentedByTenant): group by tenantId BEFORE the sort so a
|
|
79
|
+
// duplicate pair can never form across tenants. Map preserves insertion
|
|
80
|
+
// order, so a single-tenant store (every row 'default') gets exactly one
|
|
81
|
+
// group and the sort plus pair loop below run byte-identical to the
|
|
82
|
+
// pre-fix global pass.
|
|
83
|
+
function entriesByPartition(entries) {
|
|
84
|
+
const entriesByTenant = new Map();
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
// EI2: also split by restricted scope, else a private copy can delete the readable one.
|
|
87
|
+
const key = derivationPartitionKey(entry.tenantId, entry.scope, entry.origin_project);
|
|
88
|
+
const bucket = entriesByTenant.get(key);
|
|
89
|
+
if (bucket)
|
|
90
|
+
bucket.push(entry);
|
|
91
|
+
else
|
|
92
|
+
entriesByTenant.set(key, [entry]);
|
|
93
|
+
}
|
|
94
|
+
return entriesByTenant;
|
|
95
|
+
}
|
|
96
|
+
function dedupPair(kept, dropped) {
|
|
97
|
+
return {
|
|
98
|
+
kept: kept.id,
|
|
99
|
+
keptContent: kept.content,
|
|
100
|
+
keptLayer: kept.layer,
|
|
101
|
+
keptStrength: kept.strength ?? 0,
|
|
102
|
+
removed: dropped.id,
|
|
103
|
+
removedContent: dropped.content,
|
|
104
|
+
removedLayer: dropped.layer,
|
|
105
|
+
removedStrength: dropped.strength ?? 0,
|
|
106
|
+
similarity: textOverlap(kept.content, dropped.content),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** Pairs within one partition, already in survivor order, grouped by survivor; marks each loser in `removed`. */
|
|
110
|
+
function partitionPairs(tenantEntries, removed, backing) {
|
|
111
|
+
const groups = new Map();
|
|
112
|
+
for (const entry of tenantEntries) {
|
|
113
|
+
const key = duplicateKey(entry.content);
|
|
114
|
+
const group = groups.get(key);
|
|
115
|
+
if (!group) {
|
|
116
|
+
groups.set(key, { survivor: entry, pairs: [] });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!canAutoDelete(entry) || backing.has(entry.id))
|
|
120
|
+
continue;
|
|
121
|
+
removed.add(entry.id);
|
|
122
|
+
group.pairs.push(dedupPair(group.survivor, entry));
|
|
123
|
+
}
|
|
124
|
+
return [...groups.values()].flatMap((group) => group.pairs);
|
|
125
|
+
}
|
|
59
126
|
/**
|
|
60
127
|
* Scan the store for duplicates and remove the weaker copy: same text apart
|
|
61
128
|
* from spacing, since a near-duplicate can differ in a value (port, version,
|
|
@@ -70,28 +137,7 @@ export function deduplicateStore(hippoRoot, options = {}) {
|
|
|
70
137
|
// Only current distilled rows compete: raw rows are append-only (the delete
|
|
71
138
|
// trigger would abort sleep mid-loop) and superseded rows are history, as in consolidate.ts.
|
|
72
139
|
const entries = loadAllEntries(hippoRoot).filter((e) => e.kind === 'distilled' && !e.superseded_by);
|
|
73
|
-
|
|
74
|
-
// dag.ts unparentedByTenant): group by tenantId BEFORE the sort so a
|
|
75
|
-
// duplicate pair can never form across tenants. Map preserves insertion
|
|
76
|
-
// order, so a single-tenant store (every row 'default') gets exactly one
|
|
77
|
-
// group and the sort plus pair loop below run byte-identical to the
|
|
78
|
-
// pre-fix global pass.
|
|
79
|
-
const entriesByTenant = new Map();
|
|
80
|
-
for (const entry of entries) {
|
|
81
|
-
// EI2: also split by restricted scope, else a private copy can delete the readable one.
|
|
82
|
-
const key = derivationPartitionKey(entry.tenantId, entry.scope, entry.origin_project);
|
|
83
|
-
const bucket = entriesByTenant.get(key);
|
|
84
|
-
if (bucket)
|
|
85
|
-
bucket.push(entry);
|
|
86
|
-
else
|
|
87
|
-
entriesByTenant.set(key, [entry]);
|
|
88
|
-
}
|
|
89
|
-
// finiteCount mirrors strengthBucket's non-finite hardening on the
|
|
90
|
-
// retrieval leg: a NaN retrieval_count would make the comparator return
|
|
91
|
-
// NaN and break the total order the same way a NaN bucket would.
|
|
92
|
-
// Unreachable via the schema (non-nullable INTEGER column), so this is
|
|
93
|
-
// symmetry, not a live bug.
|
|
94
|
-
const finiteCount = (n) => Number.isFinite(n ?? 0) ? (n ?? 0) : 0;
|
|
140
|
+
const entriesByTenant = entriesByPartition(entries);
|
|
95
141
|
// Shared across tenant groups: safe because memory ids are globally
|
|
96
142
|
// unique (crypto.randomUUID at creation), so an id in `removed` can never
|
|
97
143
|
// collide with another tenant's row, and deleteEntry below deletes by
|
|
@@ -100,45 +146,24 @@ export function deduplicateStore(hippoRoot, options = {}) {
|
|
|
100
146
|
const pairs = [];
|
|
101
147
|
const backing = memoriesBackingObjects(hippoRoot);
|
|
102
148
|
for (const tenantEntries of entriesByTenant.values()) {
|
|
103
|
-
// The
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
tenantEntries.sort((a, b) => {
|
|
108
|
-
const bucketDiff = strengthBucket(b.strength) - strengthBucket(a.strength);
|
|
109
|
-
if (bucketDiff !== 0)
|
|
110
|
-
return bucketDiff;
|
|
111
|
-
const retrievalDiff = finiteCount(b.retrieval_count) - finiteCount(a.retrieval_count);
|
|
112
|
-
if (retrievalDiff !== 0)
|
|
113
|
-
return retrievalDiff;
|
|
114
|
-
return compareEntryIdentity(a, b);
|
|
115
|
-
});
|
|
116
|
-
const texts = tenantEntries.map((e) => duplicateKey(e.content));
|
|
117
|
-
for (let i = 0; i < tenantEntries.length; i++) {
|
|
118
|
-
if (removed.has(tenantEntries[i].id))
|
|
119
|
-
continue;
|
|
120
|
-
for (let j = i + 1; j < tenantEntries.length; j++) {
|
|
121
|
-
if (removed.has(tenantEntries[j].id) || !canAutoDelete(tenantEntries[j]) || backing.has(tenantEntries[j].id))
|
|
122
|
-
continue;
|
|
123
|
-
if (texts[j] !== texts[i])
|
|
124
|
-
continue;
|
|
125
|
-
const similarity = textOverlap(tenantEntries[i].content, tenantEntries[j].content);
|
|
126
|
-
removed.add(tenantEntries[j].id);
|
|
127
|
-
pairs.push({
|
|
128
|
-
kept: tenantEntries[i].id,
|
|
129
|
-
keptContent: tenantEntries[i].content,
|
|
130
|
-
keptLayer: tenantEntries[i].layer,
|
|
131
|
-
keptStrength: tenantEntries[i].strength ?? 0,
|
|
132
|
-
removed: tenantEntries[j].id,
|
|
133
|
-
removedContent: tenantEntries[j].content,
|
|
134
|
-
removedLayer: tenantEntries[j].layer,
|
|
135
|
-
removedStrength: tenantEntries[j].strength ?? 0,
|
|
136
|
-
similarity,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
}
|
|
149
|
+
// The survivor total order (see the file-level docstring), scoped per
|
|
150
|
+
// tenant group: the total order holds within a tenant only, matching the partition above.
|
|
151
|
+
tenantEntries.sort(survivorOrder);
|
|
152
|
+
pairs.push(...partitionPairs(tenantEntries, removed, backing));
|
|
140
153
|
}
|
|
141
|
-
const done = dryRun ? pairs :
|
|
154
|
+
const done = dryRun ? pairs : deletePairs(hippoRoot, pairs, options.actor);
|
|
142
155
|
return { removed: done.length, pairs: done };
|
|
143
156
|
}
|
|
157
|
+
/** The pairs whose loser was deleted; one store handle, one transaction per delete. */
|
|
158
|
+
function deletePairs(hippoRoot, pairs, actor) {
|
|
159
|
+
if (pairs.length === 0)
|
|
160
|
+
return [];
|
|
161
|
+
const db = openStore(hippoRoot);
|
|
162
|
+
try {
|
|
163
|
+
return pairs.filter((p) => deleteEntryOn(db, hippoRoot, p.removed, { actor, reason: `dedup: duplicate of ${p.kept}`, automatic: true }));
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
closeHippoDb(db);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
144
169
|
//# sourceMappingURL=dedupe.js.map
|