hippo-memory 1.62.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 +9 -0
- 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.js +9 -6
- 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 +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 +6 -9
- package/dist/dag.d.ts +10 -21
- package/dist/dag.js +23 -73
- 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 +6 -6
- package/dist/dedupe.js +86 -61
- package/dist/delivery-recorder.js +154 -135
- package/dist/doctor.js +119 -104
- 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 +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 +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 +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 +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/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -38,20 +38,11 @@ export function memoriesBackingObjects(hippoRoot) {
|
|
|
38
38
|
return ids;
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* never compose inside a caller's transaction (unlike writeEntry/
|
|
44
|
-
* writeEntryDbOnly, which already split this way). Split identically: row-
|
|
45
|
-
* meta SELECT, `DELETE FROM memories`, FTS delete, `forget` audit, DAG
|
|
46
|
-
* dirty-mark. NO filesystem I/O — the caller's own transaction may still be
|
|
47
|
-
* rolled back, and mirror writes must only happen post-commit.
|
|
41
|
+
* db-scoped delete core, so a delete can compose inside a caller's transaction.
|
|
42
|
+
* NO filesystem I/O: the caller's transaction may still roll back, and mirrors are written post-commit.
|
|
48
43
|
*
|
|
49
|
-
* `opts.suppressForgetAudit` (default false
|
|
50
|
-
* so a removed
|
|
51
|
-
* already writes its own aggregate audit trail — `src/reject-flow.ts`'s
|
|
52
|
-
* `rejectValue` (single `reject_value` row covering every same-digest row
|
|
53
|
-
* removed) and `resolveConflict` (`conflict_resolve` row per resolution).
|
|
54
|
-
* Default keeps `deleteEntry` byte-identical to its pre-split behavior.
|
|
44
|
+
* `opts.suppressForgetAudit` (default false): `rejectValue` and `resolveConflict` set it because each
|
|
45
|
+
* writes its own aggregate audit row, so a removed row must not ALSO emit a `forget` row.
|
|
55
46
|
*
|
|
56
47
|
* Returns `{tenantId, dagParentId}` for the removed row, or `null` if no row with `id`
|
|
57
48
|
* existed or `automatic` refused it (pinned, raw, kept for good or backing an object at DELETE time, so a late pin wins).
|
|
@@ -70,11 +61,8 @@ export function deleteEntryCore(db, id, opts) {
|
|
|
70
61
|
if (!opts?.suppressForgetAudit) {
|
|
71
62
|
audit(db, 'forget', id, opts?.reason ? { reason: opts.reason } : undefined, opts?.actor ?? 'cli', row.tenant_id);
|
|
72
63
|
}
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// wrapper here, same as pre-split deleteEntry); markSummaryDirtyInTx is
|
|
76
|
-
// idempotent so any future child mutation re-marks parent if this fails.
|
|
77
|
-
// Acceptable degradation, mirrors the pre-split audit best-effort posture.
|
|
64
|
+
// Forgetting a child of a summary marks the parent dirty. Not atomic with the DELETE, but
|
|
65
|
+
// markSummaryDirtyInTx is idempotent, so the next child mutation re-marks the parent if this fails.
|
|
78
66
|
if (row.dag_parent_id) {
|
|
79
67
|
markSummaryDirtyInTx(db, row.dag_parent_id, row.tenant_id ?? 'default', opts?.actor ?? 'cli');
|
|
80
68
|
}
|
|
@@ -94,26 +82,30 @@ export function deleteEntryCore(db, id, opts) {
|
|
|
94
82
|
export function deleteEntry(hippoRoot, id, opts) {
|
|
95
83
|
const db = openStore(hippoRoot);
|
|
96
84
|
try {
|
|
97
|
-
db
|
|
98
|
-
let result;
|
|
99
|
-
try {
|
|
100
|
-
result = deleteEntryCore(db, id, opts);
|
|
101
|
-
db.exec('COMMIT');
|
|
102
|
-
}
|
|
103
|
-
catch (err) {
|
|
104
|
-
if (db.isTransaction !== false)
|
|
105
|
-
db.exec('ROLLBACK');
|
|
106
|
-
throw err;
|
|
107
|
-
}
|
|
108
|
-
if (!result)
|
|
109
|
-
return false;
|
|
110
|
-
purgeMirrorBestEffort(hippoRoot, id, false, 'deleteEntry');
|
|
111
|
-
return true;
|
|
85
|
+
return deleteEntryOn(db, hippoRoot, id, opts);
|
|
112
86
|
}
|
|
113
87
|
finally {
|
|
114
88
|
closeHippoDb(db);
|
|
115
89
|
}
|
|
116
90
|
}
|
|
91
|
+
/** deleteEntry on the caller's open store, so a loop of deletes opens the store once; each delete still commits alone. */
|
|
92
|
+
export function deleteEntryOn(db, hippoRoot, id, opts) {
|
|
93
|
+
db.exec('BEGIN IMMEDIATE');
|
|
94
|
+
let result;
|
|
95
|
+
try {
|
|
96
|
+
result = deleteEntryCore(db, id, opts);
|
|
97
|
+
db.exec('COMMIT');
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
if (db.isTransaction !== false)
|
|
101
|
+
db.exec('ROLLBACK');
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
if (!result)
|
|
105
|
+
return false;
|
|
106
|
+
purgeMirrorBestEffort(hippoRoot, id, false, 'deleteEntry');
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
117
109
|
// The child fields a level-2/3 summary is built from (loadChildrenOfSummary, generateDagSummary).
|
|
118
110
|
const SUMMARY_INPUTS = ['content', 'created', 'dag_parent_id', 'kind'];
|
|
119
111
|
function mergeOwnChanges(base, ours, live) {
|
|
@@ -138,26 +130,16 @@ export function batchWriteAndDelete(hippoRoot, toWrite, toDeleteIds, opts) {
|
|
|
138
130
|
return [];
|
|
139
131
|
const db = openStore(hippoRoot);
|
|
140
132
|
try {
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
// WAL snapshot; a concurrent writer (e.g. `hippo reject`) committing
|
|
144
|
-
// between probe and first upsert would make the later write-lock upgrade
|
|
145
|
-
// fail with SQLITE_BUSY and roll back the ENTIRE batch — the exact race
|
|
146
|
-
// the probe exists to contain. Taking the write lock up front serializes
|
|
147
|
-
// the probe and the writes on one consistent snapshot.
|
|
133
|
+
// IMMEDIATE: the tombstone probes below read before the first write, and under a deferred BEGIN a
|
|
134
|
+
// concurrent `hippo reject` would make the lock upgrade fail with SQLITE_BUSY and roll back the batch.
|
|
148
135
|
db.exec('BEGIN IMMEDIATE');
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
// inside the same BEGIN so the SELECT sees pre-delete state.
|
|
152
|
-
// independent-review-critic R1 HIGH: consolidate.ts/sleep flushes through
|
|
153
|
-
// this path every cycle; without these hooks parents NEVER get marked
|
|
154
|
-
// dirty for the dominant mutation source (decay, merge, garbage-collect).
|
|
136
|
+
// Snapshot every doomed row's dag_parent_id before the deletes: consolidation flushes through here
|
|
137
|
+
// every cycle, so without it parents would never be marked dirty for decay, merge or garbage-collect.
|
|
155
138
|
const dirty = { parents: new Set(), tenantById: new Map() };
|
|
156
139
|
const deletableIds = [];
|
|
157
140
|
if (toDeleteIds.length > 0) {
|
|
158
141
|
// A row pinned after the caller decided to delete it survives.
|
|
159
|
-
const
|
|
160
|
-
for (const row of selectAutoDeletableRows(db, placeholders, toDeleteIds, dirty))
|
|
142
|
+
for (const row of selectAutoDeletableRows(db, toDeleteIds, dirty))
|
|
161
143
|
deletableIds.push(row.id);
|
|
162
144
|
}
|
|
163
145
|
// v39: batch writers bypass writeEntry, so stamp store-derived origins here too (a NULL origin hides new
|
|
@@ -202,8 +184,7 @@ function moveDormantAndDelete(db, dormantMoves, deletableIds, dirty) {
|
|
|
202
184
|
const movable = [];
|
|
203
185
|
if (dormantMoves.length > 0) {
|
|
204
186
|
const byId = new Map(dormantMoves.map((m) => [m.entry.id, m]));
|
|
205
|
-
const
|
|
206
|
-
for (const row of selectAutoDeletableRows(db, placeholders, [...byId.keys()], dirty))
|
|
187
|
+
for (const row of selectAutoDeletableRows(db, [...byId.keys()], dirty))
|
|
207
188
|
movable.push(byId.get(row.id));
|
|
208
189
|
}
|
|
209
190
|
for (const move of movable) {
|
|
@@ -216,10 +197,10 @@ function moveDormantAndDelete(db, dormantMoves, deletableIds, dirty) {
|
|
|
216
197
|
}
|
|
217
198
|
return removedIds;
|
|
218
199
|
}
|
|
219
|
-
/** The still auto-deletable rows among `ids`, recording each one's DAG parent as dirty. */
|
|
220
|
-
function selectAutoDeletableRows(db,
|
|
200
|
+
/** The still auto-deletable rows among `ids`, recording each one's DAG parent as dirty. Placeholders come from `ids` itself. */
|
|
201
|
+
function selectAutoDeletableRows(db, ids, dirty) {
|
|
221
202
|
// SAFETY: rows' shape matches the three columns named in the SELECT.
|
|
222
|
-
const rows = db.prepare(`SELECT id, dag_parent_id, tenant_id FROM memories WHERE id IN (${
|
|
203
|
+
const rows = db.prepare(`SELECT id, dag_parent_id, tenant_id FROM memories WHERE id IN (${ids.map(() => '?').join(',')}) AND ${AUTOMATIC_DELETE_SQL}`).all(...ids);
|
|
223
204
|
for (const row of rows) {
|
|
224
205
|
if (row.dag_parent_id) {
|
|
225
206
|
dirty.parents.add(row.dag_parent_id);
|
|
@@ -229,23 +210,8 @@ function selectAutoDeletableRows(db, placeholders, ids, dirty) {
|
|
|
229
210
|
return rows;
|
|
230
211
|
}
|
|
231
212
|
function applyBatchWrites(db, stampedWrites, snapshot, dirty) {
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
// on a different connection. A `hippo reject X` that commits in that
|
|
235
|
-
// window is invisible to it — a queued same-id write of X already
|
|
236
|
-
// sitting in `toWrite` (decay/replay re-persist, or a merge built before
|
|
237
|
-
// the reject) would silently re-INSERT the just-rejected row via the
|
|
238
|
-
// blind bypass. Fix: one indexed point probe per batch entry, on THIS
|
|
239
|
-
// connection, INSIDE this transaction — closes the race regardless of
|
|
240
|
-
// which write class hits it. N is small per sleep, so the extra query
|
|
241
|
-
// per entry is cheap.
|
|
242
|
-
//
|
|
243
|
-
// Skip, don't throw: the batch must still complete for every OTHER
|
|
244
|
-
// entry. Skipping is correct for every write class here — a merge
|
|
245
|
-
// summary skip just means that rollup is absent this cycle (its source
|
|
246
|
-
// facts stay merely demoted, recoverable next sleep); a skipped
|
|
247
|
-
// demotion/replay re-persist of a rejected-removed row means it stays
|
|
248
|
-
// gone, which is the entire point of the tombstone.
|
|
213
|
+
// Probe tombstones per entry on THIS connection inside the transaction: the producer's check ran earlier
|
|
214
|
+
// on another connection, so a reject committed in between would be re-inserted. Skip, never throw, so the rest lands.
|
|
249
215
|
let batchRejectedSkips = 0;
|
|
250
216
|
const written = [];
|
|
251
217
|
const readLiveRow = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id = ?`);
|
|
@@ -262,18 +228,8 @@ function applyBatchWrites(db, stampedWrites, snapshot, dirty) {
|
|
|
262
228
|
if (base && !live)
|
|
263
229
|
continue;
|
|
264
230
|
written.push(row);
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
// consolidate.ts:736-751) of already-guarded leaf facts, not an LLM
|
|
268
|
-
// paraphrase — refusing mid-batch would abort the whole consolidation
|
|
269
|
-
// transaction. The bypass is safe because consolidate.ts's merge pass
|
|
270
|
-
// now checks the merged content's rejection digest against the
|
|
271
|
-
// tenant's tombstones BEFORE ever pushing a merge into pendingWrites,
|
|
272
|
-
// skipping that merge entirely on a hit, AND because the point-probe
|
|
273
|
-
// immediately above closes the race window between that producer
|
|
274
|
-
// check and this COMMIT. The guard itself still belongs on leaf
|
|
275
|
-
// inserts, which write through writeEntry / writeEntryDbOnly and stay
|
|
276
|
-
// guarded (bypassRejectionGuard defaults false).
|
|
231
|
+
// Bypass the guard: merges concatenate already-guarded facts, the merge pass checks merged content, and
|
|
232
|
+
// the probe above closes the race; a mid-batch refusal would abort the whole consolidation.
|
|
277
233
|
upsertEntryRow(db, row, true);
|
|
278
234
|
// Hook for writes: a new child, or a change to what its summary reads, marks the parent dirty; decay alone does not.
|
|
279
235
|
if (row.dag_parent_id && (!live || SUMMARY_INPUTS.some((k) => row[k] !== live[k]))) {
|
|
@@ -286,15 +242,8 @@ function applyBatchWrites(db, stampedWrites, snapshot, dirty) {
|
|
|
286
242
|
/** True, after auditing the refusal, when the write would introduce a rejected value. */
|
|
287
243
|
function isRejectedBatchWrite(db, row) {
|
|
288
244
|
const entryTenantId = row.tenantId ?? 'default';
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
// classification must apply here too. A tombstone can legitimately
|
|
292
|
-
// coexist with a live same-content row (resolveConflict deliberately
|
|
293
|
-
// excludes keepId from its sweep; unreject-then-re-reject windows), and
|
|
294
|
-
// an unconditional skip would starve that row of decay/replay metadata
|
|
295
|
-
// updates forever. The guard throws only when the write is new-row or
|
|
296
|
-
// changes content TO the rejected value; unchanged same-id re-persists
|
|
297
|
-
// pass through, exactly as on the writeEntry path.
|
|
245
|
+
// checkRejectionGuard, not a bare tombstone probe: a tombstone can coexist with a live same-content row,
|
|
246
|
+
// and skipping every re-persist would starve it of decay/replay updates; only new or changed content is refused.
|
|
298
247
|
try {
|
|
299
248
|
checkRejectionGuard(db, entryTenantId, row.id, row.content);
|
|
300
249
|
}
|
|
@@ -8,11 +8,18 @@ import { type DatabaseSyncLike } from '../db.js';
|
|
|
8
8
|
* legacy single-tenant callers and the writeEntry/readEntry round-trip.
|
|
9
9
|
*/
|
|
10
10
|
export declare function readEntry(hippoRoot: string, id: string, tenantId?: string): MemoryEntry | null;
|
|
11
|
+
/** Ids per `IN (...)` list: far under SQLite's bound-parameter limit, with room for the tenant filter. */
|
|
12
|
+
export declare const ID_CHUNK = 500;
|
|
13
|
+
/** `items` in consecutive slices of at most `size`. */
|
|
14
|
+
export declare function chunked<T>(items: readonly T[], size?: number): T[][];
|
|
15
|
+
/** Rows by id on the caller's handle, one query per chunk; an id missing or in another tenant is absent from the map. */
|
|
16
|
+
export declare function selectEntriesByIds(db: DatabaseSyncLike, ids: readonly string[], tenantId?: string): Map<string, MemoryEntry>;
|
|
17
|
+
/** Direct children of each parent, one query per chunk; each list is in `created ASC, id ASC` order. */
|
|
18
|
+
export declare function selectChildrenByParent(db: DatabaseSyncLike, parentIds: readonly string[], tenantId?: string): Map<string, MemoryEntry[]>;
|
|
11
19
|
/**
|
|
12
20
|
* Batched lookup. Caps at 500 ids per call to keep the IN(?,?,...) clause
|
|
13
21
|
* within SQLite limits. Tenant filter is enforced when `tenantId` is passed.
|
|
14
|
-
* Used by DAG-aware recall
|
|
15
|
-
* to fetch parent summaries for a set of overflowed leaves.
|
|
22
|
+
* Used by DAG-aware recall to fetch parent summaries for a set of overflowed leaves.
|
|
16
23
|
*/
|
|
17
24
|
export declare function loadEntriesByIds(hippoRoot: string, ids: readonly string[], tenantId?: string): MemoryEntry[];
|
|
18
25
|
/**
|
|
@@ -20,10 +27,8 @@ export declare function loadEntriesByIds(hippoRoot: string, ids: readonly string
|
|
|
20
27
|
* oldest-first. Used by `api.assemble` to walk a session's chronological
|
|
21
28
|
* context. Excludes superseded rows.
|
|
22
29
|
*
|
|
23
|
-
* Cap semantics
|
|
24
|
-
*
|
|
25
|
-
* reversed to oldest-first client-side. Pre-v1.6.2 ordered ASC + LIMIT,
|
|
26
|
-
* which silently dropped the newest rows and broke fresh-tail in assemble.
|
|
30
|
+
* Cap semantics: when `cap` is provided, the NEWEST `cap` rows are loaded (DESC LIMIT server-side,
|
|
31
|
+
* reversed client-side); ASC + LIMIT would drop the newest rows and break fresh-tail in assemble.
|
|
27
32
|
*
|
|
28
33
|
* Returns `[]` for an empty sessionId. Final order: `created ASC, id ASC`.
|
|
29
34
|
*/
|
|
@@ -33,11 +38,8 @@ export declare function loadSessionRawMemories(hippoRoot: string, sessionId: str
|
|
|
33
38
|
* the full session size even when `rowCap` truncates the loaded window,
|
|
34
39
|
* WITHOUT leaking rows the caller wouldn't have been allowed to load.
|
|
35
40
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* private rows by comparing `totalRaw` against `items.length`. This
|
|
39
|
-
* version SQL-encodes the same default-deny rule `passesScopeFilterForRecall`
|
|
40
|
-
* applies in TS:
|
|
41
|
+
* An unscoped COUNT would let a no-scope caller infer private rows by comparing `totalRaw`
|
|
42
|
+
* against `items.length`, so this SQL-encodes the default-deny rule `passesScopeFilterForRecall` applies in TS:
|
|
41
43
|
* - explicit scope passed: exact-match
|
|
42
44
|
* - no scope: rows where scope IS NULL, or scope is NOT a `<source>:private:*`
|
|
43
45
|
* pattern AND not the `unknown:legacy` quarantine bucket.
|
|
@@ -51,29 +53,19 @@ export declare function countSessionRawMemories(hippoRoot: string, sessionId: st
|
|
|
51
53
|
* `sessionId` is supplied, also constrains to a specific session — that
|
|
52
54
|
* is the correct shape for "what did I just see in THIS session."
|
|
53
55
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* rows from other sessions and stamped them `isFreshTail=true`. Callers
|
|
57
|
-
* that want session-scoped fresh-tail now pass `sessionId`. The
|
|
58
|
-
* tenant-wide form (no sessionId) still exists for "anything new across
|
|
59
|
-
* the whole tenant" — pass undefined to opt in.
|
|
56
|
+
* Without `sessionId`, concurrent sessions in a tenant surface each other's rows as fresh tail;
|
|
57
|
+
* pass undefined only for "anything new across the whole tenant".
|
|
60
58
|
*
|
|
61
59
|
* Bounded count cap at 200 — beyond that the caller should filter via
|
|
62
60
|
* tags/scope rather than time-windowed recall.
|
|
63
61
|
*
|
|
64
|
-
*
|
|
65
|
-
* `
|
|
66
|
-
* conversation". `api.recall` enforces session scoping when
|
|
67
|
-
* `HIPPO_REQUIRE_SESSION_SCOPED_FRESH_TAIL=1` is set, throwing
|
|
68
|
-
* `RecallContractError` instead. Tenant-wide remains the back-compat default
|
|
69
|
-
* but is discouraged for new callers. Passing `sessionId` is fully supported
|
|
70
|
-
* and recommended; this function is NOT deprecated as a whole.
|
|
62
|
+
* The tenant-wide shape is the back-compat default but discouraged; `api.recall` throws
|
|
63
|
+
* `RecallContractError` for it when `HIPPO_REQUIRE_SESSION_SCOPED_FRESH_TAIL=1` is set.
|
|
71
64
|
*/
|
|
72
65
|
export declare function loadFreshRawMemories(hippoRoot: string, count: number, tenantId?: string, sessionId?: string): MemoryEntry[];
|
|
73
66
|
/**
|
|
74
67
|
* Direct DAG children of a parent summary. Tenant scoped. Returns only rows
|
|
75
68
|
* whose `dag_parent_id` matches `parentId`; does NOT walk recursively.
|
|
76
|
-
* Used by `drillDown` (Task 3).
|
|
77
69
|
*/
|
|
78
70
|
export declare function loadChildrenOf(hippoRoot: string, parentId: string, tenantId?: string): MemoryEntry[];
|
|
79
71
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { closeHippoDb } from '../db.js';
|
|
2
2
|
import { MEMORY_SELECT_COLUMNS, rowToEntry, parseJsonArray } from './rows.js';
|
|
3
3
|
import { openStore } from './open.js';
|
|
4
|
+
import { escapeLike } from '../escape.js';
|
|
4
5
|
/**
|
|
5
6
|
* Read a memory entry by ID.
|
|
6
7
|
*
|
|
@@ -22,11 +23,54 @@ export function readEntry(hippoRoot, id, tenantId) {
|
|
|
22
23
|
closeHippoDb(db);
|
|
23
24
|
}
|
|
24
25
|
}
|
|
26
|
+
/** Ids per `IN (...)` list: far under SQLite's bound-parameter limit, with room for the tenant filter. */
|
|
27
|
+
export const ID_CHUNK = 500;
|
|
28
|
+
/** `items` in consecutive slices of at most `size`. */
|
|
29
|
+
export function chunked(items, size = ID_CHUNK) {
|
|
30
|
+
const out = [];
|
|
31
|
+
for (let i = 0; i < items.length; i += size)
|
|
32
|
+
out.push(items.slice(i, i + size));
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/** Rows by id on the caller's handle, one query per chunk; an id missing or in another tenant is absent from the map. */
|
|
36
|
+
export function selectEntriesByIds(db, ids, tenantId) {
|
|
37
|
+
const byId = new Map();
|
|
38
|
+
const tenantClause = tenantId !== undefined ? ' AND tenant_id = ?' : '';
|
|
39
|
+
const tenantArgs = tenantId !== undefined ? [tenantId] : [];
|
|
40
|
+
for (const chunk of chunked([...new Set(ids)])) {
|
|
41
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
42
|
+
// SAFETY: selects exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
|
|
43
|
+
const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${placeholders})${tenantClause}`).all(...chunk, ...tenantArgs);
|
|
44
|
+
for (const row of rows)
|
|
45
|
+
byId.set(row.id, rowToEntry(row));
|
|
46
|
+
}
|
|
47
|
+
return byId;
|
|
48
|
+
}
|
|
49
|
+
/** Direct children of each parent, one query per chunk; each list is in `created ASC, id ASC` order. */
|
|
50
|
+
export function selectChildrenByParent(db, parentIds, tenantId) {
|
|
51
|
+
const byParent = new Map();
|
|
52
|
+
const tenantClause = tenantId !== undefined ? ' AND tenant_id = ?' : '';
|
|
53
|
+
const tenantArgs = tenantId !== undefined ? [tenantId] : [];
|
|
54
|
+
for (const chunk of chunked([...new Set(parentIds)])) {
|
|
55
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
56
|
+
// SAFETY: selects exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
|
|
57
|
+
const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE dag_parent_id IN (${placeholders})${tenantClause} ORDER BY created ASC, id ASC`).all(...chunk, ...tenantArgs);
|
|
58
|
+
for (const row of rows) {
|
|
59
|
+
const entry = rowToEntry(row);
|
|
60
|
+
const parentId = entry.dag_parent_id ?? '';
|
|
61
|
+
const bucket = byParent.get(parentId);
|
|
62
|
+
if (bucket)
|
|
63
|
+
bucket.push(entry);
|
|
64
|
+
else
|
|
65
|
+
byParent.set(parentId, [entry]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return byParent;
|
|
69
|
+
}
|
|
25
70
|
/**
|
|
26
71
|
* Batched lookup. Caps at 500 ids per call to keep the IN(?,?,...) clause
|
|
27
72
|
* within SQLite limits. Tenant filter is enforced when `tenantId` is passed.
|
|
28
|
-
* Used by DAG-aware recall
|
|
29
|
-
* to fetch parent summaries for a set of overflowed leaves.
|
|
73
|
+
* Used by DAG-aware recall to fetch parent summaries for a set of overflowed leaves.
|
|
30
74
|
*/
|
|
31
75
|
export function loadEntriesByIds(hippoRoot, ids, tenantId) {
|
|
32
76
|
if (ids.length === 0)
|
|
@@ -35,9 +79,7 @@ export function loadEntriesByIds(hippoRoot, ids, tenantId) {
|
|
|
35
79
|
const db = openStore(hippoRoot);
|
|
36
80
|
try {
|
|
37
81
|
const placeholders = capped.map(() => '?').join(',');
|
|
38
|
-
//
|
|
39
|
-
// (undefined w.r.t. the caller's `ids` order). created ASC, id ASC
|
|
40
|
-
// makes it deterministic.
|
|
82
|
+
// Without ORDER BY, rows follow SQLite's IN(...) scan order, which is undefined w.r.t. `ids`.
|
|
41
83
|
// SAFETY: both branches select exactly MEMORY_SELECT_COLUMNS, matching
|
|
42
84
|
// MemoryRow's field set.
|
|
43
85
|
const rows = tenantId !== undefined
|
|
@@ -54,10 +96,8 @@ export function loadEntriesByIds(hippoRoot, ids, tenantId) {
|
|
|
54
96
|
* oldest-first. Used by `api.assemble` to walk a session's chronological
|
|
55
97
|
* context. Excludes superseded rows.
|
|
56
98
|
*
|
|
57
|
-
* Cap semantics
|
|
58
|
-
*
|
|
59
|
-
* reversed to oldest-first client-side. Pre-v1.6.2 ordered ASC + LIMIT,
|
|
60
|
-
* which silently dropped the newest rows and broke fresh-tail in assemble.
|
|
99
|
+
* Cap semantics: when `cap` is provided, the NEWEST `cap` rows are loaded (DESC LIMIT server-side,
|
|
100
|
+
* reversed client-side); ASC + LIMIT would drop the newest rows and break fresh-tail in assemble.
|
|
61
101
|
*
|
|
62
102
|
* Returns `[]` for an empty sessionId. Final order: `created ASC, id ASC`.
|
|
63
103
|
*/
|
|
@@ -94,11 +134,8 @@ export function loadSessionRawMemories(hippoRoot, sessionId, tenantId, cap) {
|
|
|
94
134
|
* the full session size even when `rowCap` truncates the loaded window,
|
|
95
135
|
* WITHOUT leaking rows the caller wouldn't have been allowed to load.
|
|
96
136
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* private rows by comparing `totalRaw` against `items.length`. This
|
|
100
|
-
* version SQL-encodes the same default-deny rule `passesScopeFilterForRecall`
|
|
101
|
-
* applies in TS:
|
|
137
|
+
* An unscoped COUNT would let a no-scope caller infer private rows by comparing `totalRaw`
|
|
138
|
+
* against `items.length`, so this SQL-encodes the default-deny rule `passesScopeFilterForRecall` applies in TS:
|
|
102
139
|
* - explicit scope passed: exact-match
|
|
103
140
|
* - no scope: rows where scope IS NULL, or scope is NOT a `<source>:private:*`
|
|
104
141
|
* pattern AND not the `unknown:legacy` quarantine bucket.
|
|
@@ -140,23 +177,14 @@ export function countSessionRawMemories(hippoRoot, sessionId, tenantId, scope) {
|
|
|
140
177
|
* `sessionId` is supplied, also constrains to a specific session — that
|
|
141
178
|
* is the correct shape for "what did I just see in THIS session."
|
|
142
179
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* rows from other sessions and stamped them `isFreshTail=true`. Callers
|
|
146
|
-
* that want session-scoped fresh-tail now pass `sessionId`. The
|
|
147
|
-
* tenant-wide form (no sessionId) still exists for "anything new across
|
|
148
|
-
* the whole tenant" — pass undefined to opt in.
|
|
180
|
+
* Without `sessionId`, concurrent sessions in a tenant surface each other's rows as fresh tail;
|
|
181
|
+
* pass undefined only for "anything new across the whole tenant".
|
|
149
182
|
*
|
|
150
183
|
* Bounded count cap at 200 — beyond that the caller should filter via
|
|
151
184
|
* tags/scope rather than time-windowed recall.
|
|
152
185
|
*
|
|
153
|
-
*
|
|
154
|
-
* `
|
|
155
|
-
* conversation". `api.recall` enforces session scoping when
|
|
156
|
-
* `HIPPO_REQUIRE_SESSION_SCOPED_FRESH_TAIL=1` is set, throwing
|
|
157
|
-
* `RecallContractError` instead. Tenant-wide remains the back-compat default
|
|
158
|
-
* but is discouraged for new callers. Passing `sessionId` is fully supported
|
|
159
|
-
* and recommended; this function is NOT deprecated as a whole.
|
|
186
|
+
* The tenant-wide shape is the back-compat default but discouraged; `api.recall` throws
|
|
187
|
+
* `RecallContractError` for it when `HIPPO_REQUIRE_SESSION_SCOPED_FRESH_TAIL=1` is set.
|
|
160
188
|
*/
|
|
161
189
|
export function loadFreshRawMemories(hippoRoot, count, tenantId, sessionId) {
|
|
162
190
|
if (count <= 0)
|
|
@@ -174,11 +202,8 @@ export function loadFreshRawMemories(hippoRoot, count, tenantId, sessionId) {
|
|
|
174
202
|
sql += ' AND source_session_id = ?';
|
|
175
203
|
params.push(sessionId);
|
|
176
204
|
}
|
|
177
|
-
//
|
|
178
|
-
// same
|
|
179
|
-
// review): ids are random UUIDs, so an id-only tail would pick WHICH
|
|
180
|
-
// same-created rows make the window per-instance; content is
|
|
181
|
-
// cross-ingest-stable.
|
|
205
|
+
// Tie tail makes same-`created` rows deterministic; `content` before `id` because ids are random
|
|
206
|
+
// UUIDs, so an id-only tail would pick which same-created rows make the window per instance.
|
|
182
207
|
sql += ' ORDER BY created DESC, content ASC, id ASC LIMIT ?';
|
|
183
208
|
params.push(capped);
|
|
184
209
|
// SAFETY: sql starts from MEMORY_SELECT_COLUMNS, matching MemoryRow.
|
|
@@ -192,17 +217,11 @@ export function loadFreshRawMemories(hippoRoot, count, tenantId, sessionId) {
|
|
|
192
217
|
/**
|
|
193
218
|
* Direct DAG children of a parent summary. Tenant scoped. Returns only rows
|
|
194
219
|
* whose `dag_parent_id` matches `parentId`; does NOT walk recursively.
|
|
195
|
-
* Used by `drillDown` (Task 3).
|
|
196
220
|
*/
|
|
197
221
|
export function loadChildrenOf(hippoRoot, parentId, tenantId) {
|
|
198
222
|
const db = openStore(hippoRoot);
|
|
199
223
|
try {
|
|
200
|
-
|
|
201
|
-
// MemoryRow's field set.
|
|
202
|
-
const rows = tenantId !== undefined
|
|
203
|
-
? db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE dag_parent_id = ? AND tenant_id = ? ORDER BY created ASC, id ASC`).all(parentId, tenantId)
|
|
204
|
-
: db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE dag_parent_id = ? ORDER BY created ASC, id ASC`).all(parentId);
|
|
205
|
-
return rows.map(rowToEntry);
|
|
224
|
+
return selectChildrenByParent(db, [parentId], tenantId).get(parentId) ?? [];
|
|
206
225
|
}
|
|
207
226
|
finally {
|
|
208
227
|
closeHippoDb(db);
|
|
@@ -236,7 +255,7 @@ export function selectAllEntries(db, tenantId) {
|
|
|
236
255
|
/** Live rows whose source starts with `prefix`, on the caller's handle; LIKE folds case, so the prefix is checked again exactly. */
|
|
237
256
|
export function selectLiveEntriesBySourcePrefix(db, tenantId, prefix) {
|
|
238
257
|
// SAFETY: selects exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
|
|
239
|
-
const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE tenant_id = ? AND superseded_by IS NULL AND source LIKE ? ESCAPE '\\'`).all(tenantId, `${prefix
|
|
258
|
+
const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE tenant_id = ? AND superseded_by IS NULL AND source LIKE ? ESCAPE '\\'`).all(tenantId, `${escapeLike(prefix)}%`);
|
|
240
259
|
return rows.map(rowToEntry).filter((entry) => entry.source.startsWith(prefix));
|
|
241
260
|
}
|
|
242
261
|
// Content of every tenant row tagged `tag`, without reading the rest of the store.
|
|
@@ -1,26 +1,8 @@
|
|
|
1
1
|
import type { MemoryEntry } from '../memory.js';
|
|
2
2
|
import { openHippoDb } from '../db.js';
|
|
3
3
|
/**
|
|
4
|
-
* `bypassRejectionGuard
|
|
5
|
-
*
|
|
6
|
-
* (mergeContents, consolidate.ts:736-751), not LLM paraphrase — the bypass
|
|
7
|
-
* is safe because the producer (consolidate.ts's merge pass) now checks the
|
|
8
|
-
* merged content's rejection digest against the tenant's tombstones BEFORE
|
|
9
|
-
* ever assembling a batch to write, and skips the merge entirely on a hit.
|
|
10
|
-
* Every other caller (writeEntryDbOnly, bootstrapLegacyStore, rebuildIndex)
|
|
11
|
-
* leaves this false and the guard runs live.
|
|
12
|
-
*
|
|
13
|
-
* AT1 P1 fix (codex, batch-transaction rejection race): the producer check
|
|
14
|
-
* above runs on a DIFFERENT connection BEFORE this transaction opens — a
|
|
15
|
-
* `hippo reject X` that commits in that window is invisible to it. This
|
|
16
|
-
* parameter's contract is UNCHANGED (still the sole bypass, still trusted
|
|
17
|
-
* by the producer-side check for the common case); what changed is that
|
|
18
|
-
* `batchWriteAndDelete` no longer trusts it BLINDLY. It now runs its own
|
|
19
|
-
* in-transaction point-probe (same connection, same digest lookup this
|
|
20
|
-
* function's guard would have done) immediately before each upsert and
|
|
21
|
-
* skips — rather than writes — any entry whose content matches a tombstone
|
|
22
|
-
* that landed after the producer's check. See batchWriteAndDelete for the
|
|
23
|
-
* skip logic.
|
|
4
|
+
* `bypassRejectionGuard`: ONLY `batchWriteAndDelete` passes `true`; its merges concatenate
|
|
5
|
+
* already-guarded facts, and it re-probes tombstones in-transaction before each upsert.
|
|
24
6
|
*/
|
|
25
7
|
export declare function upsertEntryRow(db: ReturnType<typeof openHippoDb>, entry: MemoryEntry, bypassRejectionGuard?: boolean): void;
|
|
26
8
|
export declare function syncFtsRow(db: ReturnType<typeof openHippoDb>, entry: MemoryEntry, isNewRow?: boolean): void;
|
|
@@ -29,14 +11,14 @@ export declare function deleteFtsRow(db: ReturnType<typeof openHippoDb>, id: str
|
|
|
29
11
|
* Write a memory entry to SQLite and refresh compatibility mirrors.
|
|
30
12
|
*
|
|
31
13
|
* `opts.actor` defaults to 'cli' so unauthenticated direct-CLI callers still
|
|
32
|
-
* get the right audit attribution. The HTTP server
|
|
14
|
+
* get the right audit attribution. The HTTP server and api.* layer pass
|
|
33
15
|
* the resolved actor (`api_key:<key_id>` / `localhost:cli`) so audit events
|
|
34
16
|
* land with one row per write, no double-emit.
|
|
35
17
|
*
|
|
36
18
|
* `opts.afterWrite` is invoked inside the same SAVEPOINT as the memories
|
|
37
19
|
* INSERT (mirrors archiveRawMemory's shape in raw-archive.ts). On callback
|
|
38
20
|
* throw, the SAVEPOINT rolls back — the memory row never lands, and the
|
|
39
|
-
* filesystem mirrors / audit emit never run. Used by
|
|
21
|
+
* filesystem mirrors / audit emit never run. Used by connectors to
|
|
40
22
|
* stamp idempotency rows atomically with the memory write.
|
|
41
23
|
*/
|
|
42
24
|
/**
|
|
@@ -51,7 +33,7 @@ export declare function deleteFtsRow(db: ReturnType<typeof openHippoDb>, id: str
|
|
|
51
33
|
* v39 migration found no evidence for" and is deny-by-default in ambient
|
|
52
34
|
* context. A writeback (e.g. markRetrieved on a crossProject-included row)
|
|
53
35
|
* must not launder it into an injectable origin - the migration is the only
|
|
54
|
-
* evidence-based NULL converter
|
|
36
|
+
* evidence-based NULL converter.
|
|
55
37
|
*/
|
|
56
38
|
export declare function stampOriginProject(hippoRoot: string, entry: MemoryEntry): MemoryEntry;
|
|
57
39
|
/**
|
|
@@ -61,7 +43,7 @@ export declare function stampOriginProject(hippoRoot: string, entry: MemoryEntry
|
|
|
61
43
|
* backfill. Same evidence order as the migration: the provenance source
|
|
62
44
|
* (`shared:<project>:` / `promoted:<localRoot>`) wins over the destination
|
|
63
45
|
* store's location, so a shared row imported into the global store keeps its
|
|
64
|
-
* owning project instead of becoming user-global
|
|
46
|
+
* owning project instead of becoming user-global.
|
|
65
47
|
*/
|
|
66
48
|
export declare function stampOriginProjectForImport(hippoRoot: string, entry: MemoryEntry): MemoryEntry;
|
|
67
49
|
//# sourceMappingURL=entry-row.d.ts.map
|
package/dist/store/entry-row.js
CHANGED
|
@@ -4,26 +4,8 @@ import { deriveOriginProject, originFromSource } from '../project-identity.js';
|
|
|
4
4
|
import { checkRejectionGuard } from '../rejection.js';
|
|
5
5
|
import { log } from '../log.js';
|
|
6
6
|
/**
|
|
7
|
-
* `bypassRejectionGuard
|
|
8
|
-
*
|
|
9
|
-
* (mergeContents, consolidate.ts:736-751), not LLM paraphrase — the bypass
|
|
10
|
-
* is safe because the producer (consolidate.ts's merge pass) now checks the
|
|
11
|
-
* merged content's rejection digest against the tenant's tombstones BEFORE
|
|
12
|
-
* ever assembling a batch to write, and skips the merge entirely on a hit.
|
|
13
|
-
* Every other caller (writeEntryDbOnly, bootstrapLegacyStore, rebuildIndex)
|
|
14
|
-
* leaves this false and the guard runs live.
|
|
15
|
-
*
|
|
16
|
-
* AT1 P1 fix (codex, batch-transaction rejection race): the producer check
|
|
17
|
-
* above runs on a DIFFERENT connection BEFORE this transaction opens — a
|
|
18
|
-
* `hippo reject X` that commits in that window is invisible to it. This
|
|
19
|
-
* parameter's contract is UNCHANGED (still the sole bypass, still trusted
|
|
20
|
-
* by the producer-side check for the common case); what changed is that
|
|
21
|
-
* `batchWriteAndDelete` no longer trusts it BLINDLY. It now runs its own
|
|
22
|
-
* in-transaction point-probe (same connection, same digest lookup this
|
|
23
|
-
* function's guard would have done) immediately before each upsert and
|
|
24
|
-
* skips — rather than writes — any entry whose content matches a tombstone
|
|
25
|
-
* that landed after the producer's check. See batchWriteAndDelete for the
|
|
26
|
-
* skip logic.
|
|
7
|
+
* `bypassRejectionGuard`: ONLY `batchWriteAndDelete` passes `true`; its merges concatenate
|
|
8
|
+
* already-guarded facts, and it re-probes tombstones in-transaction before each upsert.
|
|
27
9
|
*/
|
|
28
10
|
export function upsertEntryRow(db, entry, bypassRejectionGuard = false) {
|
|
29
11
|
if (!bypassRejectionGuard) {
|
|
@@ -158,14 +140,14 @@ export function deleteFtsRow(db, id) {
|
|
|
158
140
|
* Write a memory entry to SQLite and refresh compatibility mirrors.
|
|
159
141
|
*
|
|
160
142
|
* `opts.actor` defaults to 'cli' so unauthenticated direct-CLI callers still
|
|
161
|
-
* get the right audit attribution. The HTTP server
|
|
143
|
+
* get the right audit attribution. The HTTP server and api.* layer pass
|
|
162
144
|
* the resolved actor (`api_key:<key_id>` / `localhost:cli`) so audit events
|
|
163
145
|
* land with one row per write, no double-emit.
|
|
164
146
|
*
|
|
165
147
|
* `opts.afterWrite` is invoked inside the same SAVEPOINT as the memories
|
|
166
148
|
* INSERT (mirrors archiveRawMemory's shape in raw-archive.ts). On callback
|
|
167
149
|
* throw, the SAVEPOINT rolls back — the memory row never lands, and the
|
|
168
|
-
* filesystem mirrors / audit emit never run. Used by
|
|
150
|
+
* filesystem mirrors / audit emit never run. Used by connectors to
|
|
169
151
|
* stamp idempotency rows atomically with the memory write.
|
|
170
152
|
*/
|
|
171
153
|
/**
|
|
@@ -180,7 +162,7 @@ export function deleteFtsRow(db, id) {
|
|
|
180
162
|
* v39 migration found no evidence for" and is deny-by-default in ambient
|
|
181
163
|
* context. A writeback (e.g. markRetrieved on a crossProject-included row)
|
|
182
164
|
* must not launder it into an injectable origin - the migration is the only
|
|
183
|
-
* evidence-based NULL converter
|
|
165
|
+
* evidence-based NULL converter.
|
|
184
166
|
*/
|
|
185
167
|
export function stampOriginProject(hippoRoot, entry) {
|
|
186
168
|
if (entry.origin_project !== undefined)
|
|
@@ -194,7 +176,7 @@ export function stampOriginProject(hippoRoot, entry) {
|
|
|
194
176
|
* backfill. Same evidence order as the migration: the provenance source
|
|
195
177
|
* (`shared:<project>:` / `promoted:<localRoot>`) wins over the destination
|
|
196
178
|
* store's location, so a shared row imported into the global store keeps its
|
|
197
|
-
* owning project instead of becoming user-global
|
|
179
|
+
* owning project instead of becoming user-global.
|
|
198
180
|
*/
|
|
199
181
|
export function stampOriginProjectForImport(hippoRoot, entry) {
|
|
200
182
|
if (entry.origin_project !== undefined)
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { type MemoryEntry } from '../memory.js';
|
|
2
2
|
import { type DatabaseSyncLike } from '../db.js';
|
|
3
|
-
export
|
|
3
|
+
export interface WriteEntryOptions {
|
|
4
4
|
actor?: string;
|
|
5
5
|
afterWrite?: (db: DatabaseSyncLike, memoryId: string) => void;
|
|
6
|
-
/** Runs
|
|
7
|
-
* BEFORE the markdown mirrors are written. Lets a caller perform a post-commit
|
|
8
|
-
* side effect (e.g. mark the graph dirty) that must still happen even if a
|
|
9
|
-
* mirror write then throws. Keep it best-effort — it runs on a committed,
|
|
10
|
-
* idle connection, so opening another handle inside it is safe. */
|
|
6
|
+
/** Runs after the row commits and before the mirrors, on an idle connection; keep it best-effort. */
|
|
11
7
|
afterCommit?: () => void;
|
|
12
|
-
}
|
|
8
|
+
}
|
|
9
|
+
export declare function writeEntry(hippoRoot: string, entry: MemoryEntry, opts?: WriteEntryOptions): void;
|
|
10
|
+
/** writeEntry on the caller's open store, so a loop of writes opens the store once; each row still commits alone. */
|
|
11
|
+
export declare function writeEntryOn(db: DatabaseSyncLike, hippoRoot: string, entry: MemoryEntry, opts?: WriteEntryOptions): void;
|
|
13
12
|
/**
|
|
14
13
|
* DB-only write path. Caller owns the open `db` handle. Runs SAVEPOINT +
|
|
15
14
|
* upsert + afterWrite hook + audit row inside the SAVEPOINT scope. Caller
|