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/store/conflicts.js
CHANGED
|
@@ -3,7 +3,8 @@ import { rejectionDigest, insertRejectedValue, normalizeValueForRejection } from
|
|
|
3
3
|
import { archiveRawMemory } from '../raw-archive.js';
|
|
4
4
|
import { rowToMemoryConflict } from './rows.js';
|
|
5
5
|
import { audit } from './audit-event.js';
|
|
6
|
-
import {
|
|
6
|
+
import { syncChangedMirrors, purgeMirrorBestEffort } from './mirrors.js';
|
|
7
|
+
import { selectEntriesByIds } from './entry-reads.js';
|
|
7
8
|
import { openStore } from './open.js';
|
|
8
9
|
import { deleteEntryCore } from './delete-and-batch.js';
|
|
9
10
|
function canonicalConflictPair(aId, bId) {
|
|
@@ -81,9 +82,9 @@ export function replaceDetectedConflicts(hippoRoot, detected, detectedAt = new D
|
|
|
81
82
|
}));
|
|
82
83
|
resolveStaleOpenConflicts(db, canonicalDetected, sameTenant, detectedAt);
|
|
83
84
|
upsertDetectedConflicts(db, canonicalDetected, sameTenant, detectedAt);
|
|
84
|
-
rebuildConflictsWithJson(db, sameTenant);
|
|
85
|
+
const changedIds = rebuildConflictsWithJson(db, sameTenant);
|
|
85
86
|
db.exec('COMMIT');
|
|
86
|
-
|
|
87
|
+
syncChangedMirrors(hippoRoot, db, [...selectEntriesByIds(db, changedIds).values()]);
|
|
87
88
|
}
|
|
88
89
|
catch (error) {
|
|
89
90
|
try {
|
|
@@ -123,36 +124,38 @@ function resolveStaleOpenConflicts(db, canonicalDetected, sameTenant, detectedAt
|
|
|
123
124
|
FROM memory_conflicts
|
|
124
125
|
WHERE status = 'open'
|
|
125
126
|
`).all();
|
|
127
|
+
const resolve = db.prepare(`UPDATE memory_conflicts SET status = 'resolved', updated_at = ? WHERE id = ?`);
|
|
126
128
|
for (const row of openRows) {
|
|
127
129
|
const key = `${row.memory_a_id}::${row.memory_b_id}`;
|
|
128
130
|
const stale = !detectedKeys.has(key);
|
|
129
131
|
// v1.11.0 residue: auto-resolve any open cross-tenant row. The insert
|
|
130
|
-
// loop
|
|
132
|
+
// loop in upsertDetectedConflicts and the refMap rebuild skip
|
|
131
133
|
// cross-tenant pairs, but the resolve-stale loop previously left
|
|
132
134
|
// re-detected cross-tenant rows lingering status='open'. The
|
|
133
135
|
// sameTenant() helper is already built one block up; no extra query.
|
|
134
136
|
const crossTenant = !sameTenant(row.memory_a_id, row.memory_b_id);
|
|
135
|
-
if (stale || crossTenant)
|
|
136
|
-
|
|
137
|
-
}
|
|
137
|
+
if (stale || crossTenant)
|
|
138
|
+
resolve.run(detectedAt, row.id);
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
function upsertDetectedConflicts(db, canonicalDetected, sameTenant, detectedAt) {
|
|
142
|
+
const upsert = db.prepare(`
|
|
143
|
+
INSERT INTO memory_conflicts(memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at)
|
|
144
|
+
VALUES (?, ?, ?, ?, 'open', ?, ?)
|
|
145
|
+
ON CONFLICT(memory_a_id, memory_b_id) DO UPDATE SET
|
|
146
|
+
reason = excluded.reason,
|
|
147
|
+
score = excluded.score,
|
|
148
|
+
status = 'open',
|
|
149
|
+
updated_at = excluded.updated_at
|
|
150
|
+
`);
|
|
141
151
|
for (const conflict of canonicalDetected) {
|
|
142
152
|
// Skip cross-tenant pairs — never persist a conflict spanning tenants.
|
|
143
153
|
if (!sameTenant(conflict.memory_a_id, conflict.memory_b_id))
|
|
144
154
|
continue;
|
|
145
|
-
|
|
146
|
-
INSERT INTO memory_conflicts(memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at)
|
|
147
|
-
VALUES (?, ?, ?, ?, 'open', ?, ?)
|
|
148
|
-
ON CONFLICT(memory_a_id, memory_b_id) DO UPDATE SET
|
|
149
|
-
reason = excluded.reason,
|
|
150
|
-
score = excluded.score,
|
|
151
|
-
status = 'open',
|
|
152
|
-
updated_at = excluded.updated_at
|
|
153
|
-
`).run(conflict.memory_a_id, conflict.memory_b_id, conflict.reason, conflict.score, detectedAt, detectedAt);
|
|
155
|
+
upsert.run(conflict.memory_a_id, conflict.memory_b_id, conflict.reason, conflict.score, detectedAt, detectedAt);
|
|
154
156
|
}
|
|
155
157
|
}
|
|
158
|
+
/** Rewrites only the rows whose conflicts_with_json changes, and returns their ids. */
|
|
156
159
|
function rebuildConflictsWithJson(db, sameTenant) {
|
|
157
160
|
// SAFETY: openConflicts' shape matches the two columns named above.
|
|
158
161
|
const openConflicts = db.prepare(`
|
|
@@ -173,13 +176,18 @@ function rebuildConflictsWithJson(db, sameTenant) {
|
|
|
173
176
|
refMap.get(row.memory_a_id).add(row.memory_b_id);
|
|
174
177
|
refMap.get(row.memory_b_id).add(row.memory_a_id);
|
|
175
178
|
}
|
|
176
|
-
// SAFETY: memoryRows' shape matches the
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
+
// SAFETY: memoryRows' shape matches the two columns selected below.
|
|
180
|
+
const memoryRows = db.prepare(`SELECT id, conflicts_with_json FROM memories`).all();
|
|
181
|
+
const update = db.prepare(`UPDATE memories SET conflicts_with_json = ?, updated_at = datetime('now') WHERE id = ?`);
|
|
182
|
+
const changedIds = [];
|
|
179
183
|
for (const memory of memoryRows) {
|
|
180
|
-
const
|
|
181
|
-
|
|
184
|
+
const refsJson = JSON.stringify(Array.from(refMap.get(memory.id) ?? []).sort());
|
|
185
|
+
if (memory.conflicts_with_json === refsJson)
|
|
186
|
+
continue;
|
|
187
|
+
update.run(refsJson, memory.id);
|
|
188
|
+
changedIds.push(memory.id);
|
|
182
189
|
}
|
|
190
|
+
return changedIds;
|
|
183
191
|
}
|
|
184
192
|
/**
|
|
185
193
|
* Resolve a conflict by keeping one memory and weakening the other.
|
|
@@ -230,7 +238,7 @@ export function resolveConflict(hippoRoot, conflictId, keepId, forgetLoser = fal
|
|
|
230
238
|
stripConflictRefs(db, target, removal.loserRemoved);
|
|
231
239
|
auditConflictResolve(db, target, removal, tenantId);
|
|
232
240
|
db.exec('COMMIT');
|
|
233
|
-
|
|
241
|
+
syncChangedMirrors(hippoRoot, db, [...selectEntriesByIds(db, [keepId, loserId]).values()]);
|
|
234
242
|
if (removal.loserRemoved)
|
|
235
243
|
purgeRemovedLoserMirrors(hippoRoot, db, loserId, removal);
|
|
236
244
|
return { conflict: { ...conflict, status: 'resolved' }, loserId };
|
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
import { type MemoryEntry } from '../memory.js';
|
|
2
|
-
import { openHippoDb } from '../db.js';
|
|
2
|
+
import { openHippoDb, type DatabaseSyncLike } from '../db.js';
|
|
3
3
|
import { type DormantMove } from '../dormant.js';
|
|
4
4
|
/** Tables whose rows keep a first-class object's backing memory in `memory_id` (ON DELETE SET NULL); tests/dormant-memories.test.ts pins it to the schema. */
|
|
5
5
|
export declare const MEMORY_BACKED_TABLES: readonly ["predictions", "decisions", "incidents", "processes", "policies", "skills", "project_briefs", "customer_notes"];
|
|
6
6
|
/** Ids of memories that back a first-class object, for passes that plan deletes before making them. A table missing from an older schema is skipped. */
|
|
7
7
|
export declare function memoriesBackingObjects(hippoRoot: string): Set<string>;
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* never compose inside a caller's transaction (unlike writeEntry/
|
|
12
|
-
* writeEntryDbOnly, which already split this way). Split identically: row-
|
|
13
|
-
* meta SELECT, `DELETE FROM memories`, FTS delete, `forget` audit, DAG
|
|
14
|
-
* dirty-mark. NO filesystem I/O — the caller's own transaction may still be
|
|
15
|
-
* rolled back, and mirror writes must only happen post-commit.
|
|
9
|
+
* db-scoped delete core, so a delete can compose inside a caller's transaction.
|
|
10
|
+
* NO filesystem I/O: the caller's transaction may still roll back, and mirrors are written post-commit.
|
|
16
11
|
*
|
|
17
|
-
* `opts.suppressForgetAudit` (default false
|
|
18
|
-
* so a removed
|
|
19
|
-
* already writes its own aggregate audit trail — `src/reject-flow.ts`'s
|
|
20
|
-
* `rejectValue` (single `reject_value` row covering every same-digest row
|
|
21
|
-
* removed) and `resolveConflict` (`conflict_resolve` row per resolution).
|
|
22
|
-
* Default keeps `deleteEntry` byte-identical to its pre-split behavior.
|
|
12
|
+
* `opts.suppressForgetAudit` (default false): `rejectValue` and `resolveConflict` set it because each
|
|
13
|
+
* writes its own aggregate audit row, so a removed row must not ALSO emit a `forget` row.
|
|
23
14
|
*
|
|
24
15
|
* Returns `{tenantId, dagParentId}` for the removed row, or `null` if no row with `id`
|
|
25
16
|
* existed or `automatic` refused it (pinned, raw, kept for good or backing an object at DELETE time, so a late pin wins).
|
|
@@ -49,6 +40,12 @@ export declare function deleteEntry(hippoRoot: string, id: string, opts?: {
|
|
|
49
40
|
reason?: string;
|
|
50
41
|
automatic?: boolean;
|
|
51
42
|
}): boolean;
|
|
43
|
+
/** deleteEntry on the caller's open store, so a loop of deletes opens the store once; each delete still commits alone. */
|
|
44
|
+
export declare function deleteEntryOn(db: DatabaseSyncLike, hippoRoot: string, id: string, opts?: {
|
|
45
|
+
actor?: string;
|
|
46
|
+
reason?: string;
|
|
47
|
+
automatic?: boolean;
|
|
48
|
+
}): boolean;
|
|
52
49
|
/** Consolidation's flush, one transaction. With `snapshot` (rows as the caller loaded them), a write keeps only
|
|
53
50
|
* the fields the caller changed, takes the rest from the live row, and never resurrects a row that is gone.
|
|
54
51
|
*
|
|
@@ -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.
|