hippo-memory 1.63.2 → 1.64.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/dist/agent-memories/claude-code.js +3 -3
- package/dist/ambient-store.d.ts +2 -0
- package/dist/ambient-store.js +4 -3
- package/dist/api/assemble.d.ts +2 -0
- package/dist/api/assemble.js +11 -7
- package/dist/api/auth.d.ts +15 -11
- package/dist/api/auth.js +116 -22
- package/dist/api/context-select.d.ts +5 -3
- package/dist/api/context-select.js +6 -11
- package/dist/api/context-types.d.ts +2 -1
- package/dist/api/context.d.ts +1 -1
- package/dist/api/context.js +79 -55
- package/dist/api/dormant.d.ts +5 -5
- package/dist/api/dormant.js +13 -9
- package/dist/api/drill-down.d.ts +2 -0
- package/dist/api/drill-down.js +12 -7
- package/dist/api/forget.d.ts +1 -1
- package/dist/api/forget.js +8 -4
- package/dist/api/learn.js +5 -3
- package/dist/api/outcome.d.ts +1 -1
- package/dist/api/outcome.js +2 -1
- package/dist/api/promote.js +8 -5
- package/dist/api/recall-types.d.ts +4 -2
- package/dist/api/recall.js +51 -36
- package/dist/api/remember.d.ts +7 -0
- package/dist/api/remember.js +26 -2
- package/dist/api/sleep-run.d.ts +28 -0
- package/dist/api/sleep-run.js +267 -0
- package/dist/api/sleep.d.ts +5 -45
- package/dist/api/sleep.js +5 -252
- package/dist/api/types.d.ts +5 -0
- package/dist/api/types.js +4 -0
- package/dist/audit-prune.js +1 -1
- package/dist/audit.d.ts +1 -3
- package/dist/audit.js +7 -103
- package/dist/auth.d.ts +48 -4
- package/dist/auth.js +111 -42
- package/dist/autolearn.js +2 -2
- package/dist/capture/caller-session.d.ts +16 -0
- package/dist/capture/caller-session.js +58 -0
- package/dist/capture/command.d.ts +19 -1
- package/dist/capture/command.js +28 -21
- package/dist/capture/compact-resume-caller.d.ts +11 -0
- package/dist/capture/compact-resume-caller.js +35 -0
- package/dist/capture/compact.d.ts +0 -14
- package/dist/capture/compact.js +7 -160
- package/dist/capture/compaction-items-caller.d.ts +16 -0
- package/dist/capture/compaction-items-caller.js +32 -0
- package/dist/capture/extract.d.ts +2 -14
- package/dist/capture/extract.js +135 -346
- package/dist/capture/failure-caller.d.ts +21 -0
- package/dist/capture/failure-caller.js +74 -0
- package/dist/capture/failure-reading.d.ts +44 -0
- package/dist/capture/failure-reading.js +78 -0
- package/dist/capture/pre-compact-caller.d.ts +16 -0
- package/dist/capture/pre-compact-caller.js +39 -0
- package/dist/capture/session-end-caller.d.ts +21 -0
- package/dist/capture/session-end-caller.js +30 -0
- package/dist/capture/session-texts.d.ts +14 -0
- package/dist/capture/session-texts.js +42 -0
- package/dist/capture/transcript.d.ts +6 -1
- package/dist/capture/transcript.js +20 -16
- package/dist/capture/working-state.d.ts +21 -0
- package/dist/capture/working-state.js +154 -0
- package/dist/capture-error.d.ts +8 -22
- package/dist/capture-error.js +9 -61
- package/dist/cli/audit.js +25 -0
- package/dist/cli/auth.js +4 -3
- package/dist/cli/context.js +13 -158
- package/dist/cli/continuity.js +2 -1
- package/dist/cli/init.js +5 -3
- package/dist/cli/last-sleep.d.ts +6 -0
- package/dist/cli/last-sleep.js +68 -0
- package/dist/cli/maintenance.d.ts +2 -1
- package/dist/cli/maintenance.js +17 -11
- package/dist/cli/quality-repair-once.d.ts +3 -0
- package/dist/cli/quality-repair-once.js +36 -0
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.d.ts +2 -0
- package/dist/cli/remember.js +4 -1
- package/dist/cli/session-hooks.d.ts +0 -3
- package/dist/cli/session-hooks.js +12 -49
- package/dist/cli/setup.js +10 -2
- package/dist/cli/shared.d.ts +7 -14
- package/dist/cli/shared.js +19 -78
- package/dist/cli/sleep.js +6 -3
- package/dist/cli/usage.js +18 -9
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -2
- package/dist/client.js +1 -1
- package/dist/compaction-record.d.ts +19 -1
- package/dist/compaction-record.js +61 -132
- package/dist/compaction-spool.d.ts +45 -0
- package/dist/compaction-spool.js +454 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +49 -0
- package/dist/connectors/github/deletion.d.ts +1 -1
- package/dist/connectors/github/deletion.js +5 -17
- package/dist/consolidate/conflicts.js +4 -0
- package/dist/consolidate/flush-units.d.ts +9 -0
- package/dist/consolidate/flush-units.js +63 -0
- package/dist/consolidate/llm-passes.js +5 -4
- package/dist/consolidate/merge.js +5 -1
- package/dist/consolidate/run.d.ts +2 -1
- package/dist/consolidate/sleep.d.ts +2 -0
- package/dist/consolidate/sleep.js +82 -15
- package/dist/context-render.d.ts +14 -0
- package/dist/context-render.js +28 -0
- package/dist/dag.js +6 -0
- package/dist/dashboard-actions.js +3 -2
- package/dist/dashboard.js +4 -3
- package/dist/db/backup.d.ts +6 -0
- package/dist/db/backup.js +29 -0
- package/dist/db/busy.d.ts +9 -0
- package/dist/db/busy.js +35 -0
- package/dist/db/continuity.d.ts +1 -0
- package/dist/db/continuity.js +31 -4
- package/dist/db/meta.d.ts +2 -0
- package/dist/db/meta.js +7 -0
- package/dist/db/migrate.js +2 -3
- package/dist/db/migrations/index.d.ts +1 -1
- package/dist/db/migrations/index.js +5 -1
- package/dist/db/migrations/v39.js +2 -7
- package/dist/db/migrations/v53.d.ts +3 -0
- package/dist/db/migrations/v53.js +16 -0
- package/dist/db/migrations/v54.d.ts +3 -0
- package/dist/db/migrations/v54.js +25 -0
- package/dist/db/open.d.ts +11 -0
- package/dist/db/open.js +27 -0
- package/dist/db/tables.d.ts +2 -0
- package/dist/db/tables.js +6 -2
- package/dist/db.d.ts +2 -2
- package/dist/db.js +2 -2
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +31 -15
- package/dist/dormant.d.ts +16 -20
- package/dist/dormant.js +24 -30
- package/dist/embedding-provider.js +4 -3
- package/dist/entry/json-hooks.d.ts +3 -0
- package/dist/entry/json-hooks.js +4 -0
- package/dist/entry/project-identity.d.ts +2 -0
- package/dist/entry/project-identity.js +3 -0
- package/dist/entry/session-text.d.ts +7 -0
- package/dist/entry/session-text.js +8 -0
- package/dist/extract.js +6 -0
- package/dist/failure-log.d.ts +10 -2
- package/dist/failure-log.js +19 -2
- package/dist/gated-write.js +1 -1
- package/dist/graph/delta.d.ts +44 -0
- package/dist/graph/delta.js +109 -0
- package/dist/graph/types.d.ts +5 -0
- package/dist/graph/write.d.ts +20 -20
- package/dist/graph/write.js +103 -36
- package/dist/graph-extract.d.ts +43 -9
- package/dist/graph-extract.js +140 -157
- package/dist/graph-recall.d.ts +1 -0
- package/dist/graph-recall.js +2 -2
- package/dist/graph-view.d.ts +1 -0
- package/dist/graph-view.js +25 -2
- package/dist/half-life-migration.js +5 -1
- package/dist/handoff-evidence.d.ts +5 -0
- package/dist/handoff-evidence.js +30 -0
- package/dist/home-path.d.ts +5 -0
- package/dist/home-path.js +24 -0
- package/dist/hooks/json-hooks.d.ts +13 -2
- package/dist/hooks/json-hooks.js +246 -117
- package/dist/hooks/opencode.js +1 -1
- package/dist/hooks/shared.d.ts +2 -0
- package/dist/hooks/shared.js +6 -1
- package/dist/http-util.d.ts +16 -7
- package/dist/http-util.js +65 -20
- package/dist/importers/vault.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/json.d.ts +2 -0
- package/dist/json.js +7 -0
- package/dist/ledger-db.d.ts +10 -0
- package/dist/ledger-db.js +42 -0
- package/dist/local-embedding.d.ts +2 -0
- package/dist/local-embedding.js +20 -4
- package/dist/mcp/admin-tools.d.ts +2 -2
- package/dist/mcp/admin-tools.js +47 -5
- package/dist/mcp/memory-tools.js +7 -2
- package/dist/mcp/protocol.d.ts +6 -0
- package/dist/mcp/protocol.js +4 -2
- package/dist/mcp/recall-tools.js +19 -6
- package/dist/mcp/request.js +4 -0
- package/dist/mcp/session-state.d.ts +4 -0
- package/dist/mcp/session-state.js +15 -6
- package/dist/mcp/shared-gate.d.ts +4 -0
- package/dist/mcp/shared-gate.js +22 -0
- package/dist/mcp/tools.js +1 -0
- package/dist/memory-quality.d.ts +35 -0
- package/dist/memory-quality.js +191 -0
- package/dist/memory.d.ts +4 -4
- package/dist/physics-state.js +1 -1
- package/dist/pilot-arm.d.ts +14 -3
- package/dist/pilot-arm.js +32 -10
- package/dist/postinstall.js +2 -2
- package/dist/project-briefs.js +4 -4
- package/dist/project-identity.d.ts +14 -10
- package/dist/project-identity.js +29 -11
- package/dist/project-merge.d.ts +0 -2
- package/dist/project-merge.js +9 -13
- package/dist/prompt-hook.d.ts +65 -0
- package/dist/prompt-hook.js +213 -0
- package/dist/prompt-recall.js +1 -1
- package/dist/quality-repair.d.ts +27 -0
- package/dist/quality-repair.js +220 -0
- package/dist/rate-limit.d.ts +1 -0
- package/dist/rate-limit.js +5 -3
- package/dist/raw-archive.d.ts +1 -1
- package/dist/raw-archive.js +6 -16
- package/dist/recall-scope.d.ts +28 -5
- package/dist/recall-scope.js +52 -7
- package/dist/recall-trace.js +1 -1
- package/dist/reject-flow.d.ts +2 -0
- package/dist/reject-flow.js +20 -6
- package/dist/secret-detect.js +7 -8
- package/dist/server/auth.d.ts +8 -6
- package/dist/server/auth.js +85 -38
- package/dist/server/client-ip.d.ts +2 -0
- package/dist/server/client-ip.js +16 -1
- package/dist/server/mcp-http.d.ts +9 -3
- package/dist/server/mcp-http.js +56 -13
- package/dist/server/request.d.ts +1 -1
- package/dist/server/request.js +5 -5
- package/dist/server/routes/admin.js +12 -6
- package/dist/server/routes/memories.js +9 -1
- package/dist/server/routes/recall.js +14 -2
- package/dist/server/types.d.ts +42 -5
- package/dist/server/validation.d.ts +5 -2
- package/dist/server/validation.js +19 -2
- package/dist/server.d.ts +21 -4
- package/dist/server.js +207 -49
- package/dist/session-digest.d.ts +0 -1
- package/dist/session-digest.js +3 -12
- package/dist/session-owners.d.ts +7 -0
- package/dist/session-owners.js +54 -0
- package/dist/share-scrub.d.ts +3 -0
- package/dist/share-scrub.js +8 -0
- package/dist/shared.d.ts +1 -0
- package/dist/shared.js +26 -18
- package/dist/store/candidates.d.ts +14 -3
- package/dist/store/candidates.js +48 -26
- package/dist/store/conflicts.d.ts +15 -0
- package/dist/store/conflicts.js +68 -44
- package/dist/store/delete-and-batch.d.ts +25 -1
- package/dist/store/delete-and-batch.js +121 -69
- package/dist/store/entry-reads.d.ts +5 -6
- package/dist/store/entry-reads.js +24 -12
- package/dist/store/entry-row.d.ts +7 -10
- package/dist/store/entry-row.js +40 -18
- package/dist/store/entry-writes.d.ts +3 -3
- package/dist/store/entry-writes.js +9 -23
- package/dist/store/handoffs.d.ts +9 -6
- package/dist/store/handoffs.js +72 -59
- package/dist/store/index-and-stats.js +2 -2
- package/dist/store/open.d.ts +3 -1
- package/dist/store/open.js +3 -3
- package/dist/store/search-rows.d.ts +8 -5
- package/dist/store/search-rows.js +15 -18
- package/dist/store/sessions.d.ts +25 -6
- package/dist/store/sessions.js +61 -19
- package/dist/store/summaries.d.ts +1 -1
- package/dist/store/summaries.js +3 -18
- package/dist/store/tenant-lookup.d.ts +7 -3
- package/dist/store/tenant-lookup.js +9 -9
- package/dist/store-port.d.ts +13 -0
- package/dist/store-port.js +20 -0
- package/dist/version.d.ts +5 -1
- package/dist/version.js +5 -1
- package/dist/working-memory.js +1 -1
- package/dist/write-budget.d.ts +15 -0
- package/dist/write-budget.js +15 -0
- 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 +8 -4
package/dist/shared.js
CHANGED
|
@@ -15,18 +15,20 @@ import { loadAllEntries, readEntry } from './store/entry-reads.js';
|
|
|
15
15
|
import { loadSearchEntries, loadRecallSearchEntries, recallScopeFilter } from './store/search-rows.js';
|
|
16
16
|
import { tallySources } from './store/candidates.js';
|
|
17
17
|
import { loadIndex } from './store/index-and-stats.js';
|
|
18
|
-
import { passesScopeFilterForRecall, passesCliRecallScopeFilter } from './recall-scope.js';
|
|
18
|
+
import { isPersonalScope, passesScopeFilterForRecall, passesCliRecallScopeFilter } from './recall-scope.js';
|
|
19
19
|
import { search } from './search/bm25-search.js';
|
|
20
20
|
import { hybridSearch } from './search/hybrid.js';
|
|
21
21
|
import { fitBudget } from './search/finalize.js';
|
|
22
22
|
import { DEFAULT_LOCAL_BUMP, DEFAULT_RECALL_BUDGET } from './search/types.js';
|
|
23
23
|
import { evalNow } from './ablation.js';
|
|
24
|
-
import {
|
|
24
|
+
import { fallbackOrigin, classifyOriginProject, resolveGlobalRootDir, resolveProjectIdentity } from './project-identity.js';
|
|
25
|
+
import { isSharedStore } from './config.js';
|
|
25
26
|
import { detectSecret } from './secret-detect.js';
|
|
26
27
|
import { isQuarantineScope } from './quarantine.js';
|
|
27
28
|
import { RejectedValueError } from './rejection.js';
|
|
28
29
|
import { embedMemory, embedAll } from './embeddings.js';
|
|
29
30
|
import { duplicateKey, storedTextKeys } from './same-text.js';
|
|
31
|
+
import { isReusable } from './memory-quality.js';
|
|
30
32
|
import { log } from './log.js';
|
|
31
33
|
// The rows are already copied; a failed background embed only delays vectors, so it warns instead of throwing.
|
|
32
34
|
function logEmbedAllFailure(caller, err) {
|
|
@@ -67,6 +69,9 @@ export function promoteToGlobal(localRoot, id, opts) {
|
|
|
67
69
|
if (isQuarantineScope(entry.scope)) {
|
|
68
70
|
throw new BadRequestError(`Refusing to promote ${id}: it is quarantined pending review. Approve it first via 'hippo quarantine approve ${id}'.`);
|
|
69
71
|
}
|
|
72
|
+
if (isPersonalScope(entry.scope ?? null)) {
|
|
73
|
+
throw new BadRequestError(`Refusing to promote ${id}: it is a personal memory and stays with its owner on this server.`);
|
|
74
|
+
}
|
|
70
75
|
// Secret producer veto: promote is a producer path to the global store
|
|
71
76
|
// exactly like shareMemory - same hard rule.
|
|
72
77
|
const promoteSecret = detectSecret(entry);
|
|
@@ -76,14 +81,12 @@ export function promoteToGlobal(localRoot, id, opts) {
|
|
|
76
81
|
}
|
|
77
82
|
initGlobal();
|
|
78
83
|
const globalRoot = getGlobalRoot();
|
|
79
|
-
//
|
|
80
|
-
// local entry's write-time stamp via the spread; back-stop it for pre-v39
|
|
81
|
-
// local rows so a promoted copy never lands NULL in the global store.
|
|
84
|
+
// A project store's NULL row gets its folder back; a shared store's folder is no caller's project, so NULL stays and the label names no path.
|
|
82
85
|
const globalEntry = {
|
|
83
86
|
...entry,
|
|
84
87
|
id: generateId('g'),
|
|
85
|
-
source: `promoted:${localRoot}`,
|
|
86
|
-
origin_project: entry.origin_project ??
|
|
88
|
+
source: isSharedStore(localRoot) ? `shared::${new Date().toISOString()}` : `promoted:${localRoot}`,
|
|
89
|
+
origin_project: entry.origin_project ?? fallbackOrigin(localRoot),
|
|
87
90
|
};
|
|
88
91
|
writeEntry(globalRoot, globalEntry, { actor: opts?.actor, afterWrite: opts?.afterWrite });
|
|
89
92
|
// Fire-and-forget: embedMemory gates on availability and never rejects.
|
|
@@ -161,19 +164,19 @@ export async function searchBothHybrid(query, localRoot, globalRoot, options = {
|
|
|
161
164
|
if (!fs.existsSync(root))
|
|
162
165
|
return [];
|
|
163
166
|
return recallScope
|
|
164
|
-
? loadRecallSearchEntries(root, query, searchWindow, tenantId, recallScope.requested, recallScope.additive ? 'additive' : 'exact', Boolean(includeSuperseded) || Boolean(asOf))
|
|
167
|
+
? loadRecallSearchEntries(root, query, searchWindow, tenantId, recallScope.requested, recallScope.additive ? 'additive' : 'exact', Boolean(includeSuperseded) || Boolean(asOf), undefined, recallScope.ownScope)
|
|
165
168
|
: loadSearchEntries(root, query, searchWindow, tenantId);
|
|
166
169
|
};
|
|
167
170
|
const passesScope = (e) => !recallScope || (recallScope.additive
|
|
168
|
-
? passesCliRecallScopeFilter(e.scope ?? null, recallScope.requested)
|
|
169
|
-
: passesScopeFilterForRecall(e.scope ?? null, recallScope.requested));
|
|
171
|
+
? passesCliRecallScopeFilter(e.scope ?? null, recallScope.requested) || passesScopeFilterForRecall(e.scope ?? null, undefined, recallScope.ownScope)
|
|
172
|
+
: passesScopeFilterForRecall(e.scope ?? null, recallScope.requested, recallScope.ownScope));
|
|
170
173
|
const admit = (e) => passesScope(e) && (!entryFilter || entryFilter(e));
|
|
171
174
|
const localEntries = loadEntries(localRoot).filter(admit);
|
|
172
175
|
const globalEntries = loadEntries(globalRoot).filter(admit);
|
|
173
176
|
// The vector arm loads under the same SQL rules as loadEntries, then the same JS admission.
|
|
174
177
|
const vectorCandidates = {
|
|
175
178
|
tenantId,
|
|
176
|
-
scope: recallScope ? recallScopeFilter(recallScope.requested, recallScope.additive ? 'additive' : 'exact') : undefined,
|
|
179
|
+
scope: recallScope ? recallScopeFilter(recallScope.requested, recallScope.additive ? 'additive' : 'exact', recallScope.ownScope) : undefined,
|
|
177
180
|
includeSuperseded: !recallScope || Boolean(includeSuperseded) || Boolean(asOf),
|
|
178
181
|
admit,
|
|
179
182
|
};
|
|
@@ -292,20 +295,22 @@ export function shareMemory(localRoot, id, options = {}) {
|
|
|
292
295
|
if (isQuarantineScope(entry.scope)) {
|
|
293
296
|
throw new BadRequestError(`Refusing to share ${id}: it is quarantined pending review. Approve it first via 'hippo quarantine approve ${id}'.`);
|
|
294
297
|
}
|
|
298
|
+
if (isPersonalScope(entry.scope ?? null)) {
|
|
299
|
+
throw new BadRequestError(`Refusing to share ${id}: it is a personal memory and stays with its owner on this server.`);
|
|
300
|
+
}
|
|
295
301
|
const score = transferScore(entry);
|
|
296
302
|
if (score < 0.3 && !options.force)
|
|
297
303
|
return null;
|
|
298
304
|
initGlobal();
|
|
299
305
|
const globalRoot = getGlobalRoot();
|
|
300
|
-
//
|
|
301
|
-
// store-location-derived); the localRoot parent basename is only a
|
|
302
|
-
// fallback for pre-v39 rows and keeps the legacy source format intact.
|
|
306
|
+
// The label keeps the folder name for a user-global row; a NULL row gets `shared::`, which originFromSource reads as no project.
|
|
303
307
|
const fallbackName = path.basename(path.resolve(localRoot, '..'));
|
|
304
|
-
const originName = entry.origin_project ??
|
|
308
|
+
const originName = entry.origin_project ?? fallbackOrigin(localRoot);
|
|
309
|
+
const label = originName === '' ? fallbackName : (originName ?? '');
|
|
305
310
|
const globalEntry = {
|
|
306
311
|
...entry,
|
|
307
312
|
id: generateId('g'),
|
|
308
|
-
source: `shared:${
|
|
313
|
+
source: `shared:${label}:${new Date().toISOString()}`,
|
|
309
314
|
origin_project: originName,
|
|
310
315
|
};
|
|
311
316
|
writeEntry(globalRoot, globalEntry);
|
|
@@ -365,8 +370,8 @@ export function listPeers(globalRoot, tenantId) {
|
|
|
365
370
|
.sort((a, b) => b.count - a.count);
|
|
366
371
|
}
|
|
367
372
|
function isAutoShareCandidate(entry, globalContentSet, minScore, stats) {
|
|
368
|
-
// shareMemory refuses quarantined rows; filtering here keeps sleep from aborting on one.
|
|
369
|
-
if (isQuarantineScope(entry.scope ?? null))
|
|
373
|
+
// shareMemory refuses quarantined and personal rows; filtering here keeps sleep from aborting on one.
|
|
374
|
+
if (isQuarantineScope(entry.scope ?? null) || isPersonalScope(entry.scope ?? null) || !isReusable(entry))
|
|
370
375
|
return false;
|
|
371
376
|
// Before the score: these rows describe one project only, and a git seed's 'error' tag clears the bar.
|
|
372
377
|
if (entry.tags.some((t) => NEVER_AUTO_SHARE_TAGS.has(t)) || entry.source.startsWith(AGENT_MEMORY_SOURCE_PREFIX)) {
|
|
@@ -475,6 +480,9 @@ export function autoShare(localRoot, options = {}) {
|
|
|
475
480
|
* Returns the count of newly copied entries.
|
|
476
481
|
*/
|
|
477
482
|
export function syncGlobalToLocal(localRoot, globalRoot, opts = {}) {
|
|
483
|
+
if (isSharedStore(localRoot)) {
|
|
484
|
+
throw new BadRequestError(`Refusing to sync into ${localRoot}: a shared store takes no copies of a personal global store, whose rows would reach every member.`);
|
|
485
|
+
}
|
|
478
486
|
if (!fs.existsSync(globalRoot))
|
|
479
487
|
return 0;
|
|
480
488
|
// Host-wide read. syncGlobalToLocal copies the global union into a
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { MemoryEntry, StrengthInputs } from '../memory.js';
|
|
2
|
+
import { type SqlFragment } from '../recall-scope.js';
|
|
2
3
|
export interface AmbientRecallRequest {
|
|
3
4
|
terms: string[];
|
|
4
5
|
limit: number;
|
|
6
|
+
ownScope?: string;
|
|
5
7
|
}
|
|
6
8
|
export interface AmbientLoadResult {
|
|
7
9
|
entries: MemoryEntry[];
|
|
@@ -11,11 +13,18 @@ export interface AmbientLoadResult {
|
|
|
11
13
|
export declare const AMBIENT_PINNED_WHERE = "pinned = 1 AND superseded_by IS NULL AND tenant_id = ? ORDER BY created ASC, id ASC";
|
|
12
14
|
/** Exported so the plan test runs the exact SQL; idx_memories_created_drift (db.ts v51) serves it. */
|
|
13
15
|
export declare const AMBIENT_DRIFT_SQL = "SELECT 1 FROM memories WHERE superseded_by IS NULL AND tenant_id = ? AND (length(created) <> 24 OR created NOT LIKE '%Z') LIMIT 1";
|
|
14
|
-
|
|
16
|
+
/** The origins a caller's recent rows may carry: its project names, and user-global ('') rows when `userGlobal`. */
|
|
17
|
+
export interface RecentOrigins {
|
|
18
|
+
readonly names: readonly string[];
|
|
19
|
+
readonly userGlobal: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function loadAmbientCandidates(hippoRoot: string, tenantId: string, recentNeeded: number, admit: (e: MemoryEntry) => boolean, recall?: AmbientRecallRequest, origins?: RecentOrigins): AmbientLoadResult;
|
|
15
22
|
/** The rows a non-pinned context read may admit; each predicate is one getContext's admission applies again in JS. */
|
|
16
23
|
export interface ContextCandidateFilter {
|
|
17
24
|
/** Envelope scope asked for by name; absent applies recall's default deny. */
|
|
18
25
|
exactScope?: string;
|
|
26
|
+
/** The caller's personal scope, which the default deny admits. */
|
|
27
|
+
ownScope?: string;
|
|
19
28
|
/** Rows carrying one of these project names, and user-global rows, pass; absent admits every origin. */
|
|
20
29
|
project?: readonly string[];
|
|
21
30
|
/** Most rows returned; past it, the rows decay has worn least win. */
|
|
@@ -28,8 +37,10 @@ export declare function loadContextCandidates(hippoRoot: string, tenantId: strin
|
|
|
28
37
|
export type StrengthRow = StrengthInputs & Pick<MemoryEntry, 'tags'>;
|
|
29
38
|
/** Every tenant row as a StrengthRow, for whole-store health numbers; defaults match rowToEntry's. */
|
|
30
39
|
export declare function loadStrengthRows(hippoRoot: string, tenantId: string): StrengthRow[];
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
export type HeldText = Pick<MemoryEntry, 'content' | 'source' | 'origin_project'>;
|
|
41
|
+
/** Text, source and origin of tenant rows holding any of `words` that `admit` passes, by default the team-visible ones; a row equal to a text apart from spacing holds its every word.
|
|
42
|
+
* With `project`, only rows carrying one of those names and user-global rows, as loadContextCandidates' filter. */
|
|
43
|
+
export declare function loadTextsHoldingWords(hippoRoot: string, tenantId: string, words: readonly string[], project?: readonly string[], admit?: SqlFragment): HeldText[];
|
|
33
44
|
export interface SourceTally {
|
|
34
45
|
source: string;
|
|
35
46
|
count: number;
|
package/dist/store/candidates.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { closeHippoDb } from '../db.js';
|
|
2
|
-
import {
|
|
2
|
+
import { scopeAdmitSql } from '../recall-scope.js';
|
|
3
3
|
import { MEMORY_SELECT_COLUMNS, rowToEntry, parseJsonArray } from './rows.js';
|
|
4
4
|
import { openStore } from './open.js';
|
|
5
5
|
import { originInSql } from '../project-identity.js';
|
|
@@ -9,9 +9,39 @@ const AMBIENT_SCOPED = 'superseded_by IS NULL AND tenant_id = ?';
|
|
|
9
9
|
export const AMBIENT_PINNED_WHERE = `pinned = 1 AND ${AMBIENT_SCOPED} ORDER BY created ASC, id ASC`;
|
|
10
10
|
/** Exported so the plan test runs the exact SQL; idx_memories_created_drift (db.ts v51) serves it. */
|
|
11
11
|
export const AMBIENT_DRIFT_SQL = `SELECT 1 FROM memories WHERE ${AMBIENT_SCOPED} AND (length(created) <> 24 OR created NOT LIKE '%Z') LIMIT 1`;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
function inOrigins(e, origins) {
|
|
13
|
+
const origin = e.origin_project ?? null;
|
|
14
|
+
return origin === '' ? origins.userGlobal : origin !== null && origins.names.includes(origin);
|
|
15
|
+
}
|
|
16
|
+
// The newest rows `admit` keeps, newest first. The first window stays unfiltered so admit still sees, and the
|
|
17
|
+
// delivery ledger still counts, the other-project rows it refuses; past it, the reads narrow to the caller's origins.
|
|
18
|
+
function loadRecentRows(run, drifted, tenantId, needed, admit, origins) {
|
|
19
|
+
const keep = origins ? (e) => admit(e) && inOrigins(e, origins) : admit;
|
|
20
|
+
// `id DESC` mirrors getContext's comparator, not loadFreshRawMemories'
|
|
21
|
+
// cross-ingest-stable order: that would change what the hook injects.
|
|
22
|
+
const newest = 'ORDER BY created DESC, id DESC';
|
|
23
|
+
const window = Math.max(needed * 4, 32);
|
|
24
|
+
const originSql = origins && (origins.userGlobal ? `(origin_project = '' OR ${originInSql(origins.names)})` : originInSql(origins.names));
|
|
25
|
+
const ownWhere = originSql ? `${AMBIENT_SCOPED} AND ${originSql}` : AMBIENT_SCOPED;
|
|
26
|
+
const ownParams = [tenantId, ...(origins?.names ?? [])];
|
|
27
|
+
if (!drifted) {
|
|
28
|
+
const windowed = run(`${AMBIENT_SCOPED} ${newest} LIMIT ?`, [tenantId, window]);
|
|
29
|
+
const kept = windowed.filter(keep);
|
|
30
|
+
if (kept.length >= needed || windowed.length < window)
|
|
31
|
+
return kept;
|
|
32
|
+
if (origins) {
|
|
33
|
+
const ownWindow = run(`${ownWhere} ${newest} LIMIT ?`, [...ownParams, window]);
|
|
34
|
+
const ownKept = ownWindow.filter(keep);
|
|
35
|
+
if (ownKept.length >= needed || ownWindow.length < window)
|
|
36
|
+
return ownKept;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Text order is chronological only for standard UTC ISO (memory.ts), and a window of junk can hide older rows.
|
|
40
|
+
return run(`${ownWhere} ${newest}`, ownParams).filter(keep);
|
|
41
|
+
}
|
|
42
|
+
// The pins plus the `recentNeeded` newest rows that pass `admit`, for ambient injection. One connection;
|
|
43
|
+
// `recall` piggybacks the prompt-recall FTS query on it too. `origins` narrows the recent rows past the first window.
|
|
44
|
+
export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit, recall, origins) {
|
|
15
45
|
// A SQL LIMIT takes an integer; the Array.slice this replaced truncated one,
|
|
16
46
|
// and include_recent is any non-negative finite number at the HTTP edge.
|
|
17
47
|
const needed = Math.trunc(recentNeeded);
|
|
@@ -20,25 +50,13 @@ export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit,
|
|
|
20
50
|
// SAFETY: every `where` below starts from MEMORY_SELECT_COLUMNS' table.
|
|
21
51
|
const run = (where, params) => db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE ${where}`).all(...params).map(rowToEntry);
|
|
22
52
|
const byId = new Map();
|
|
23
|
-
const scoped = AMBIENT_SCOPED;
|
|
24
53
|
for (const e of run(AMBIENT_PINNED_WHERE, [tenantId])) {
|
|
25
54
|
if (admit(e))
|
|
26
55
|
byId.set(e.id, e);
|
|
27
56
|
}
|
|
28
57
|
if (needed > 0) {
|
|
29
|
-
// Text order is chronological only for canonical UTC ISO (memory.ts).
|
|
30
58
|
const drifted = db.prepare(AMBIENT_DRIFT_SQL).get(tenantId) !== undefined;
|
|
31
|
-
|
|
32
|
-
// cross-ingest-stable order: that would change what the hook injects.
|
|
33
|
-
const window = Math.max(needed * 4, 32);
|
|
34
|
-
const windowed = drifted
|
|
35
|
-
? []
|
|
36
|
-
: run(`${scoped} ORDER BY created DESC, id DESC LIMIT ?`, [tenantId, window]);
|
|
37
|
-
let kept = windowed.filter(admit);
|
|
38
|
-
if (drifted || (kept.length < needed && windowed.length === window)) {
|
|
39
|
-
kept = run(`${scoped} ORDER BY created DESC, id DESC`, [tenantId]).filter(admit);
|
|
40
|
-
}
|
|
41
|
-
for (const e of kept)
|
|
59
|
+
for (const e of loadRecentRows(run, drifted, tenantId, needed, admit, origins))
|
|
42
60
|
byId.set(e.id, e);
|
|
43
61
|
}
|
|
44
62
|
// loadAllEntries' order: rankedPinned's comparator can tie and Array.sort
|
|
@@ -51,7 +69,7 @@ export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit,
|
|
|
51
69
|
return { entries };
|
|
52
70
|
const ftsQuery = pickRarestFtsQuery(db, recall.terms);
|
|
53
71
|
const recallEntries = ftsQuery
|
|
54
|
-
? loadRecallSearchEntriesFromDb(db, ftsQuery, recall.limit, tenantId, undefined, 'exact', false)
|
|
72
|
+
? loadRecallSearchEntriesFromDb(db, ftsQuery, recall.limit, tenantId, undefined, 'exact', false, undefined, recall.ownScope)
|
|
55
73
|
: [];
|
|
56
74
|
return { entries, recall: recallEntries };
|
|
57
75
|
}
|
|
@@ -74,9 +92,9 @@ export function loadContextCandidates(hippoRoot, tenantId, filter) {
|
|
|
74
92
|
params.push(filter.exactScope);
|
|
75
93
|
}
|
|
76
94
|
else {
|
|
77
|
-
|
|
78
|
-
where.push(
|
|
79
|
-
params.push(...
|
|
95
|
+
const admit = scopeAdmitSql('', filter.ownScope);
|
|
96
|
+
where.push(admit.sql);
|
|
97
|
+
params.push(...admit.params);
|
|
80
98
|
}
|
|
81
99
|
if (filter.project !== undefined) {
|
|
82
100
|
where.push(`(origin_project = '' OR ${originInSql(filter.project)})`);
|
|
@@ -117,19 +135,23 @@ export function loadStrengthRows(hippoRoot, tenantId) {
|
|
|
117
135
|
closeHippoDb(db);
|
|
118
136
|
}
|
|
119
137
|
}
|
|
120
|
-
|
|
121
|
-
|
|
138
|
+
// No owner: for session capture, a personal or connector-private row must never stop a team copy being written.
|
|
139
|
+
const TEAM_VISIBLE = scopeAdmitSql('');
|
|
140
|
+
/** Text, source and origin of tenant rows holding any of `words` that `admit` passes, by default the team-visible ones; a row equal to a text apart from spacing holds its every word.
|
|
141
|
+
* With `project`, only rows carrying one of those names and user-global rows, as loadContextCandidates' filter. */
|
|
142
|
+
export function loadTextsHoldingWords(hippoRoot, tenantId, words, project, admit = TEAM_VISIBLE) {
|
|
122
143
|
const unique = [...new Set(words)];
|
|
123
144
|
const out = [];
|
|
145
|
+
const originWhere = project === undefined ? '' : ` AND (origin_project = '' OR ${originInSql(project)})`;
|
|
124
146
|
const db = openStore(hippoRoot);
|
|
125
147
|
try {
|
|
126
148
|
// Chunked so one statement stays far under SQLite's bound-parameter limit.
|
|
127
149
|
for (let i = 0; i < unique.length; i += 200) {
|
|
128
150
|
const chunk = unique.slice(i, i + 200);
|
|
129
|
-
// SAFETY: rows' shape matches the
|
|
130
|
-
const rows = db.prepare(`SELECT content, source FROM memories WHERE tenant_id =
|
|
151
|
+
// SAFETY: rows' shape matches the three columns named in the SELECT below.
|
|
152
|
+
const rows = db.prepare(`SELECT content, source, origin_project FROM memories WHERE tenant_id = ?${originWhere} AND ${admit.sql} AND (${chunk.map(() => 'instr(content, ?) > 0').join(' OR ')})`).all(tenantId, ...(project ?? []), ...admit.params, ...chunk);
|
|
131
153
|
for (const row of rows)
|
|
132
|
-
out.push({ content: row.content, source: row.source ?? 'cli' });
|
|
154
|
+
out.push({ content: row.content, source: row.source ?? 'cli', origin_project: row.origin_project });
|
|
133
155
|
}
|
|
134
156
|
return out;
|
|
135
157
|
}
|
|
@@ -1,12 +1,27 @@
|
|
|
1
|
+
import { type DatabaseSyncLike } from '../db.js';
|
|
1
2
|
import { type MemoryConflict } from './rows.js';
|
|
2
3
|
export declare function listMemoryConflicts(hippoRoot: string, status?: string, tenantId?: string): MemoryConflict[];
|
|
4
|
+
/** Conflicts whose two rows `actor` may both touch; someone else's personal row hides its whole pair. */
|
|
5
|
+
export declare function listTouchableConflicts(hippoRoot: string, status: string, tenantId: string, actor: {
|
|
6
|
+
owner?: string;
|
|
7
|
+
}): MemoryConflict[];
|
|
3
8
|
type DetectedConflict = {
|
|
4
9
|
memory_a_id: string;
|
|
5
10
|
memory_b_id: string;
|
|
6
11
|
reason: string;
|
|
7
12
|
score: number;
|
|
8
13
|
};
|
|
14
|
+
type SameTenant = (a: string, b: string) => boolean;
|
|
9
15
|
export declare function replaceDetectedConflicts(hippoRoot: string, detected: Array<DetectedConflict>, detectedAt?: string): void;
|
|
16
|
+
/** Every memory's tenant, and each stored conflicts_with_json other than '[]', read before the refresh takes the write lock. */
|
|
17
|
+
export interface ConflictRefreshReads {
|
|
18
|
+
sameTenant: SameTenant;
|
|
19
|
+
storedRefs: ReadonlyMap<string, string | null>;
|
|
20
|
+
}
|
|
21
|
+
/** The refresh's read of the whole memories table, kept out of the write lock because it grows with the store. */
|
|
22
|
+
export declare function readConflictRefresh(db: DatabaseSyncLike): ConflictRefreshReads;
|
|
23
|
+
/** Under the write lock: the memory_conflicts rows, then each memory whose refs change; returns the ids it rewrote. */
|
|
24
|
+
export declare function writeConflictRefresh(db: DatabaseSyncLike, reads: ConflictRefreshReads, detected: readonly DetectedConflict[], detectedAt: string): string[];
|
|
10
25
|
/**
|
|
11
26
|
* AT1 (plan §5): additive-optional opts for resolveConflict.
|
|
12
27
|
* `rejectLoserValue` implies removal of the loser regardless of
|
package/dist/store/conflicts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { closeHippoDb } from '../db.js';
|
|
1
|
+
import { closeHippoDb, openHippoDb } from '../db.js';
|
|
2
2
|
import { rejectionDigest, insertRejectedValue, normalizeValueForRejection } from '../rejection.js';
|
|
3
3
|
import { archiveRawMemory } from '../raw-archive.js';
|
|
4
4
|
import { rowToMemoryConflict } from './rows.js';
|
|
@@ -7,6 +7,9 @@ import { syncChangedMirrors, purgeMirrorBestEffort } from './mirrors.js';
|
|
|
7
7
|
import { selectEntriesByIds } from './entry-reads.js';
|
|
8
8
|
import { openStore } from './open.js';
|
|
9
9
|
import { deleteEntryCore } from './delete-and-batch.js';
|
|
10
|
+
import { BadRequestError } from '../api-errors.js';
|
|
11
|
+
import { canTouchScope, isPersonalScope } from '../recall-scope.js';
|
|
12
|
+
import { selectMemoryReach } from './tenant-lookup.js';
|
|
10
13
|
function canonicalConflictPair(aId, bId) {
|
|
11
14
|
return aId < bId
|
|
12
15
|
? { memory_a_id: aId, memory_b_id: bId }
|
|
@@ -70,50 +73,66 @@ export function listMemoryConflicts(hippoRoot, status = 'open', tenantId) {
|
|
|
70
73
|
closeHippoDb(db);
|
|
71
74
|
}
|
|
72
75
|
}
|
|
76
|
+
/** Conflicts whose two rows `actor` may both touch; someone else's personal row hides its whole pair. */
|
|
77
|
+
export function listTouchableConflicts(hippoRoot, status, tenantId, actor) {
|
|
78
|
+
const conflicts = listMemoryConflicts(hippoRoot, status, tenantId);
|
|
79
|
+
const db = openHippoDb(hippoRoot);
|
|
80
|
+
try {
|
|
81
|
+
return conflicts.filter((c) => [c.memory_a_id, c.memory_b_id].every((id) => canTouchScope(actor, selectMemoryReach(db, id)?.scope ?? null)));
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
closeHippoDb(db);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
73
87
|
export function replaceDetectedConflicts(hippoRoot, detected, detectedAt = new Date().toISOString()) {
|
|
74
88
|
const db = openStore(hippoRoot);
|
|
75
89
|
try {
|
|
76
|
-
db
|
|
77
|
-
const sameTenant = loadSameTenantCheck(db);
|
|
78
|
-
const canonicalDetected = detected.map((conflict) => ({
|
|
79
|
-
...canonicalConflictPair(conflict.memory_a_id, conflict.memory_b_id),
|
|
80
|
-
reason: conflict.reason,
|
|
81
|
-
score: conflict.score,
|
|
82
|
-
}));
|
|
83
|
-
resolveStaleOpenConflicts(db, canonicalDetected, sameTenant, detectedAt);
|
|
84
|
-
upsertDetectedConflicts(db, canonicalDetected, sameTenant, detectedAt);
|
|
85
|
-
const changedIds = rebuildConflictsWithJson(db, sameTenant);
|
|
86
|
-
db.exec('COMMIT');
|
|
90
|
+
const changedIds = writeConflictRefresh(db, readConflictRefresh(db), detected, detectedAt);
|
|
87
91
|
syncChangedMirrors(hippoRoot, db, [...selectEntriesByIds(db, changedIds).values()]);
|
|
88
92
|
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
try {
|
|
91
|
-
db.exec('ROLLBACK');
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
// Ignore nested rollback failures.
|
|
95
|
-
}
|
|
96
|
-
throw error;
|
|
97
|
-
}
|
|
98
93
|
finally {
|
|
99
94
|
closeHippoDb(db);
|
|
100
95
|
}
|
|
101
96
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
// cross-tenant row can neither persist nor leak a foreign id.
|
|
97
|
+
/** The refresh's read of the whole memories table, kept out of the write lock because it grows with the store. */
|
|
98
|
+
export function readConflictRefresh(db) {
|
|
99
|
+
// Tenant guard (E2): a conflict is meaningful only within one tenant, so cross-tenant pairs are
|
|
100
|
+
// skipped on insert and on rebuild, and a stale cross-tenant row can neither persist nor leak a foreign id.
|
|
107
101
|
const tenantById = new Map();
|
|
108
|
-
|
|
109
|
-
|
|
102
|
+
const storedRefs = new Map();
|
|
103
|
+
// SAFETY: rows' shape matches the three columns named in the SELECT.
|
|
104
|
+
for (const r of db.prepare(`SELECT id, tenant_id, conflicts_with_json FROM memories`).all()) {
|
|
110
105
|
tenantById.set(r.id, r.tenant_id);
|
|
106
|
+
if (r.conflicts_with_json !== '[]')
|
|
107
|
+
storedRefs.set(r.id, r.conflicts_with_json);
|
|
111
108
|
}
|
|
112
|
-
|
|
109
|
+
const sameTenant = (a, b) => {
|
|
113
110
|
const ta = tenantById.get(a);
|
|
114
111
|
const tb = tenantById.get(b);
|
|
115
112
|
return ta !== undefined && tb !== undefined && ta === tb;
|
|
116
113
|
};
|
|
114
|
+
return { sameTenant, storedRefs };
|
|
115
|
+
}
|
|
116
|
+
/** Under the write lock: the memory_conflicts rows, then each memory whose refs change; returns the ids it rewrote. */
|
|
117
|
+
export function writeConflictRefresh(db, reads, detected, detectedAt) {
|
|
118
|
+
const canonicalDetected = detected.map((conflict) => ({
|
|
119
|
+
...canonicalConflictPair(conflict.memory_a_id, conflict.memory_b_id),
|
|
120
|
+
reason: conflict.reason,
|
|
121
|
+
score: conflict.score,
|
|
122
|
+
}));
|
|
123
|
+
db.exec('BEGIN IMMEDIATE');
|
|
124
|
+
try {
|
|
125
|
+
resolveStaleOpenConflicts(db, canonicalDetected, reads.sameTenant, detectedAt);
|
|
126
|
+
upsertDetectedConflicts(db, canonicalDetected, reads.sameTenant, detectedAt);
|
|
127
|
+
const changedIds = rebuildConflictsWithJson(db, reads);
|
|
128
|
+
db.exec('COMMIT');
|
|
129
|
+
return changedIds;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (db.isTransaction !== false)
|
|
133
|
+
db.exec('ROLLBACK');
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
117
136
|
}
|
|
118
137
|
function resolveStaleOpenConflicts(db, canonicalDetected, sameTenant, detectedAt) {
|
|
119
138
|
const detectedKeys = new Set(canonicalDetected.map((conflict) => `${conflict.memory_a_id}::${conflict.memory_b_id}`));
|
|
@@ -156,7 +175,7 @@ function upsertDetectedConflicts(db, canonicalDetected, sameTenant, detectedAt)
|
|
|
156
175
|
}
|
|
157
176
|
}
|
|
158
177
|
/** Rewrites only the rows whose conflicts_with_json changes, and returns their ids. */
|
|
159
|
-
function rebuildConflictsWithJson(db, sameTenant) {
|
|
178
|
+
function rebuildConflictsWithJson(db, { sameTenant, storedRefs }) {
|
|
160
179
|
// SAFETY: openConflicts' shape matches the two columns named above.
|
|
161
180
|
const openConflicts = db.prepare(`
|
|
162
181
|
SELECT memory_a_id, memory_b_id
|
|
@@ -176,16 +195,16 @@ function rebuildConflictsWithJson(db, sameTenant) {
|
|
|
176
195
|
refMap.get(row.memory_a_id).add(row.memory_b_id);
|
|
177
196
|
refMap.get(row.memory_b_id).add(row.memory_a_id);
|
|
178
197
|
}
|
|
179
|
-
//
|
|
180
|
-
const
|
|
181
|
-
const update = db.prepare(`UPDATE memories SET conflicts_with_json = ?, updated_at = datetime('now') WHERE id = ?`);
|
|
198
|
+
// Only a row holding refs, or due some, can change. Compare-and-set, so a writer since the read keeps its value and the next sleep redoes the row.
|
|
199
|
+
const update = db.prepare(`UPDATE memories SET conflicts_with_json = ?, updated_at = datetime('now') WHERE id = ? AND conflicts_with_json IS ?`);
|
|
182
200
|
const changedIds = [];
|
|
183
|
-
for (const
|
|
184
|
-
const
|
|
185
|
-
|
|
201
|
+
for (const id of new Set([...storedRefs.keys(), ...refMap.keys()])) {
|
|
202
|
+
const stored = storedRefs.has(id) ? storedRefs.get(id) ?? null : '[]';
|
|
203
|
+
const refsJson = JSON.stringify(Array.from(refMap.get(id) ?? []).sort());
|
|
204
|
+
if (stored === refsJson)
|
|
186
205
|
continue;
|
|
187
|
-
update.run(refsJson,
|
|
188
|
-
|
|
206
|
+
if (Number(update.run(refsJson, id, stored).changes ?? 0) > 0)
|
|
207
|
+
changedIds.push(id);
|
|
189
208
|
}
|
|
190
209
|
return changedIds;
|
|
191
210
|
}
|
|
@@ -228,7 +247,7 @@ export function resolveConflict(hippoRoot, conflictId, keepId, forgetLoser = fal
|
|
|
228
247
|
: null;
|
|
229
248
|
if (!loserId)
|
|
230
249
|
return null;
|
|
231
|
-
db.exec('BEGIN');
|
|
250
|
+
db.exec('BEGIN IMMEDIATE');
|
|
232
251
|
// Mark conflict as resolved
|
|
233
252
|
db.prepare(`UPDATE memory_conflicts SET status = 'resolved', updated_at = datetime('now') WHERE id = ?`)
|
|
234
253
|
.run(conflictId);
|
|
@@ -297,10 +316,10 @@ function removeConflictLoser(db, t) {
|
|
|
297
316
|
extraRemovedRawIds: [],
|
|
298
317
|
};
|
|
299
318
|
const { loserId, opts } = t;
|
|
300
|
-
// SAFETY: loserRow's shape matches the
|
|
319
|
+
// SAFETY: loserRow's shape matches the four columns named in the
|
|
301
320
|
// SELECT above.
|
|
302
321
|
const loserRow = db
|
|
303
|
-
.prepare(`SELECT kind, content, tenant_id FROM memories WHERE id = ?${t.scope.memScope}`)
|
|
322
|
+
.prepare(`SELECT kind, content, tenant_id, scope FROM memories WHERE id = ?${t.scope.memScope}`)
|
|
304
323
|
.get(loserId, ...t.scope.memArgs);
|
|
305
324
|
if (loserRow) {
|
|
306
325
|
const actor = opts?.rejectedBy ?? 'cli';
|
|
@@ -323,6 +342,9 @@ function removeConflictLoser(db, t) {
|
|
|
323
342
|
}
|
|
324
343
|
/** Tombstones the loser's digest and removes its same-tenant duplicates into `removal`; returns the digest. */
|
|
325
344
|
function tombstoneLoserValue(db, t, loserRow, who, removal) {
|
|
345
|
+
if (isPersonalScope(loserRow.scope)) {
|
|
346
|
+
throw new BadRequestError(`cannot reject the value of personal memory ${t.loserId}: a rejection reaches the whole tenant, so its reason would show to everyone; resolve with forget instead`);
|
|
347
|
+
}
|
|
326
348
|
const { actor, reason } = who;
|
|
327
349
|
const rejectedDigest = rejectionDigest(loserRow.content);
|
|
328
350
|
insertRejectedValue(db, {
|
|
@@ -347,14 +369,16 @@ function tombstoneLoserValue(db, t, loserRow, who, removal) {
|
|
|
347
369
|
// to keep it in this same resolution, and this branch must not
|
|
348
370
|
// undo that choice in the same transaction.
|
|
349
371
|
const loserTenantId = loserRow.tenant_id ?? 'default';
|
|
350
|
-
// SAFETY: dupRows' shape matches the
|
|
351
|
-
// SELECT above.
|
|
372
|
+
// SAFETY: dupRows' shape matches the four columns named in the SELECT above.
|
|
352
373
|
const dupRows = db
|
|
353
|
-
.prepare(`SELECT id, kind, content FROM memories WHERE tenant_id = ? AND id != ? AND id != ?`)
|
|
374
|
+
.prepare(`SELECT id, kind, content, scope FROM memories WHERE tenant_id = ? AND id != ? AND id != ?`)
|
|
354
375
|
.all(loserTenantId, t.loserId, t.keepId);
|
|
355
376
|
for (const dup of dupRows) {
|
|
356
377
|
if (rejectionDigest(dup.content) !== rejectedDigest)
|
|
357
378
|
continue;
|
|
379
|
+
// Another person's personal row is outside this resolver's reach, as it is outside their recall.
|
|
380
|
+
if (isPersonalScope(dup.scope) && dup.scope !== loserRow.scope)
|
|
381
|
+
continue;
|
|
358
382
|
if (dup.kind === 'raw') {
|
|
359
383
|
archiveRawMemory(db, dup.id, { reason, who: actor });
|
|
360
384
|
removal.extraRemovedRawIds.push(dup.id);
|
|
@@ -5,6 +5,8 @@ import { type DormantMove } from '../dormant.js';
|
|
|
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
|
+
/** memoriesBackingObjects on the caller's handle. */
|
|
9
|
+
export declare function memoriesBackingObjectsOn(db: DatabaseSyncLike): Set<string>;
|
|
8
10
|
/**
|
|
9
11
|
* db-scoped delete core, so a delete can compose inside a caller's transaction.
|
|
10
12
|
* NO filesystem I/O: the caller's transaction may still roll back, and mirrors are written post-commit.
|
|
@@ -46,7 +48,7 @@ export declare function deleteEntryOn(db: DatabaseSyncLike, hippoRoot: string, i
|
|
|
46
48
|
reason?: string;
|
|
47
49
|
automatic?: boolean;
|
|
48
50
|
}): boolean;
|
|
49
|
-
/**
|
|
51
|
+
/** Writes, deletes and dormant moves in one transaction. With `snapshot` (rows as the caller loaded them), a write keeps only
|
|
50
52
|
* the fields the caller changed, takes the rest from the live row, and never resurrects a row that is gone.
|
|
51
53
|
*
|
|
52
54
|
* `dormant` (src/dormant.ts): each move's snapshot is inserted into `dormant_memories` and its `memories` row
|
|
@@ -57,4 +59,26 @@ export declare function batchWriteAndDelete(hippoRoot: string, toWrite: MemoryEn
|
|
|
57
59
|
snapshot?: ReadonlyMap<string, MemoryEntry>;
|
|
58
60
|
dormant?: DormantMove[];
|
|
59
61
|
}): string[];
|
|
62
|
+
/** Ops that must commit in one transaction, so a flush split across several never lands part of one. */
|
|
63
|
+
export interface FlushComponent {
|
|
64
|
+
writes: readonly MemoryEntry[];
|
|
65
|
+
deletes: readonly string[];
|
|
66
|
+
dormant: readonly DormantMove[];
|
|
67
|
+
}
|
|
68
|
+
/** Tags a flush error with the ids of the component it stopped at, for the partial sleep audit row; the first tag wins. */
|
|
69
|
+
export declare function noteFailedUnit(err: Error, component: FlushComponent | undefined): void;
|
|
70
|
+
/** The ids noteFailedUnit tagged `err` with, if a flush threw it. */
|
|
71
|
+
export declare function failedUnitOf(err: Error | null): string[] | undefined;
|
|
72
|
+
/** batchWriteAndDelete on the caller's open store from component `from`, each component whole, closing the transaction at the
|
|
73
|
+
* first component boundary after `holdMs`. Returns the next component's index and the ids that left `memories`. */
|
|
74
|
+
export declare function batchWriteAndDeleteOn(db: DatabaseSyncLike, hippoRoot: string, components: readonly FlushComponent[], from: number, opts: {
|
|
75
|
+
snapshot?: ReadonlyMap<string, MemoryEntry>;
|
|
76
|
+
holdMs: number;
|
|
77
|
+
clock?: () => number;
|
|
78
|
+
}): FlushChunk;
|
|
79
|
+
/** Where the next transaction starts, and the ids this one removed from `memories`. */
|
|
80
|
+
export interface FlushChunk {
|
|
81
|
+
next: number;
|
|
82
|
+
removedIds: string[];
|
|
83
|
+
}
|
|
60
84
|
//# sourceMappingURL=delete-and-batch.d.ts.map
|