hippo-memory 1.63.1 → 1.64.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 +13 -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/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/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/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/api/context.js
CHANGED
|
@@ -9,20 +9,23 @@ import { loadFreshActiveTaskSnapshot, listSessionEvents, SNAPSHOT_AMBIENT_MAX_AG
|
|
|
9
9
|
import { loadLatestHandoff } from '../store/handoffs.js';
|
|
10
10
|
import { estimateTokens } from '../token-ledger.js';
|
|
11
11
|
import { markRetrieved, COMPACTION_MEMORY_TAG } from '../memory.js';
|
|
12
|
-
import {
|
|
12
|
+
import { isWorthSurfacing } from '../memory-quality.js';
|
|
13
13
|
import { getGlobalRoot } from '../shared.js';
|
|
14
14
|
import { writeRecallTraceAtRoot } from '../recall-trace.js';
|
|
15
15
|
import { evalNow, isRecallBoostAblated } from '../ablation.js';
|
|
16
16
|
import { dropHeldCopies } from '../same-text.js';
|
|
17
|
-
import {
|
|
17
|
+
import { BadRequestError } from '../api-errors.js';
|
|
18
|
+
import { isSharedStore, loadConfig } from '../config.js';
|
|
19
|
+
import { log } from '../log.js';
|
|
18
20
|
import { resolveProjectIdentity, classifyOriginProject, isGlobalStoreRoot, projectId, projectNames } from '../project-identity.js';
|
|
19
21
|
import { promptTokens } from '../prompt-recall.js';
|
|
20
22
|
import { detectSecret } from '../secret-detect.js';
|
|
21
23
|
import { isSessionDigestRow } from '../session-digest.js';
|
|
22
24
|
import { addAmbientTallies, ambientStateFromTallies } from '../ambient.js';
|
|
23
25
|
import { loadAmbientTallies } from '../ambient-store.js';
|
|
24
|
-
import { passesScopeFilterForRecall, assertScopeRequestAllowed } from '../recall-scope.js';
|
|
26
|
+
import { passesScopeFilterForRecall, assertScopeRequestAllowed, personalScopeOf } from '../recall-scope.js';
|
|
25
27
|
import { finiteOr, selectBySearch, selectPinned, selectStrongest, } from './context-select.js';
|
|
28
|
+
import { ownerOrSubject } from './types.js';
|
|
26
29
|
export { oneCopyPerMemory } from './context-select.js';
|
|
27
30
|
/**
|
|
28
31
|
* v39: the single ambient-injection admission policy, shared by getContext
|
|
@@ -37,10 +40,10 @@ export { oneCopyPerMemory } from './context-select.js';
|
|
|
37
40
|
* - Origin partition: other-project rows are excluded unless
|
|
38
41
|
* `includeCrossProject`.
|
|
39
42
|
*/
|
|
40
|
-
function ambientAdmitEntry(e, currentProject, includeCrossProject, exactScope) {
|
|
43
|
+
function ambientAdmitEntry(e, currentProject, includeCrossProject, exactScope, own) {
|
|
41
44
|
if (!ambientSecretAdmit(e, currentProject))
|
|
42
45
|
return false;
|
|
43
|
-
if (!passesScopeFilterForRecall(e.scope ?? null, exactScope))
|
|
46
|
+
if (!passesScopeFilterForRecall(e.scope ?? null, exactScope, own))
|
|
44
47
|
return false;
|
|
45
48
|
if (includeCrossProject)
|
|
46
49
|
return true;
|
|
@@ -61,11 +64,11 @@ export function ambientSecretAdmit(e, currentProject) {
|
|
|
61
64
|
}
|
|
62
65
|
/** Most rows per store a no-query context reads; past it, ranking and ambientState see the strongest by decay. */
|
|
63
66
|
export const CONTEXT_CANDIDATE_CAP = 2000;
|
|
64
|
-
// The pinned-only branch needs pins and recent-N candidates, not the corpus; `recall`
|
|
65
|
-
function loadAmbientEntries(hippoRoot, tenantId, pinnedOnly,
|
|
67
|
+
// The pinned-only branch needs pins and recent-N candidates, not the corpus; `recent` and `recall` apply there only.
|
|
68
|
+
function loadAmbientEntries(hippoRoot, tenantId, pinnedOnly, recent, admit, window, recall, onQualityDrop) {
|
|
66
69
|
if (!pinnedOnly) {
|
|
67
70
|
const rows = 'query' in window
|
|
68
|
-
? loadRecallSearchEntries(hippoRoot, window.query, DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId, window.exactScope, 'exact', false, window.project)
|
|
71
|
+
? loadRecallSearchEntries(hippoRoot, window.query, DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId, window.exactScope, 'exact', false, window.project, window.ownScope)
|
|
69
72
|
: loadContextCandidates(hippoRoot, tenantId, window);
|
|
70
73
|
return { entries: rows.filter(admit) };
|
|
71
74
|
}
|
|
@@ -74,12 +77,12 @@ function loadAmbientEntries(hippoRoot, tenantId, pinnedOnly, includeRecent, admi
|
|
|
74
77
|
const admitAmbient = (e) => {
|
|
75
78
|
if (!admit(e))
|
|
76
79
|
return false;
|
|
77
|
-
if (e.pinned ||
|
|
80
|
+
if (e.pinned || isWorthSurfacing(e))
|
|
78
81
|
return true;
|
|
79
82
|
onQualityDrop?.(e);
|
|
80
83
|
return false;
|
|
81
84
|
};
|
|
82
|
-
return loadAmbientCandidates(hippoRoot, tenantId,
|
|
85
|
+
return loadAmbientCandidates(hippoRoot, tenantId, recent.needed, admitAmbient, recall, recent.origins);
|
|
83
86
|
}
|
|
84
87
|
/**
|
|
85
88
|
* Assemble a context bundle: recalled memories (pinned-only / strength-sorted
|
|
@@ -141,20 +144,36 @@ export async function getContext(ctx, opts = {}) {
|
|
|
141
144
|
ambientState: recorded.ambientState,
|
|
142
145
|
};
|
|
143
146
|
}
|
|
147
|
+
const isolationWarned = new Set();
|
|
148
|
+
/** A laptop config copied onto a team store must not open every member's rows to every prompt, so the flag loses there. */
|
|
149
|
+
function warnIsolationIgnored(hippoRoot) {
|
|
150
|
+
if (isolationWarned.has(hippoRoot))
|
|
151
|
+
return;
|
|
152
|
+
isolationWarned.add(hippoRoot);
|
|
153
|
+
log.warn(`${hippoRoot}: "contextProjectIsolation": false is ignored on a shared store; a read opts in with cross_project.`);
|
|
154
|
+
}
|
|
144
155
|
function planContext(ctx, opts) {
|
|
145
156
|
const pinnedOnly = opts.pinnedOnly === true;
|
|
146
157
|
// Global memories do not establish a project boundary for task state.
|
|
147
158
|
const hasLocal = isInitialized(ctx.hippoRoot);
|
|
148
159
|
const query = (opts.q ?? '').trim() || '*';
|
|
149
160
|
const globalRoot = getGlobalRoot();
|
|
150
|
-
|
|
161
|
+
// The store's own flag counts too, so no surface can read a shared store as its owner.
|
|
162
|
+
const sharedStore = opts.sharedStore === true || isSharedStore(ctx.hippoRoot);
|
|
163
|
+
// A store serving many people is not its operator's, so the operator's own global store stays out.
|
|
164
|
+
const hasGlobal = !sharedStore && isInitialized(globalRoot);
|
|
151
165
|
const primaryIsGlobal = isGlobalStoreRoot(ctx.hippoRoot);
|
|
152
166
|
// opts.scope is only the tag boost, opts.exactScope the envelope request; other-project memories are
|
|
153
167
|
// excluded unless the caller asks for them (crossProject) or isolation is disabled.
|
|
154
168
|
const config = loadConfig(ctx.hippoRoot);
|
|
155
169
|
const isolationEnabled = config.contextProjectIsolation !== false;
|
|
156
170
|
const currentProject = opts.currentProject ?? resolveProjectIdentity(process.cwd());
|
|
157
|
-
|
|
171
|
+
// A caller with no project reads every row as its own; on a shared store those rows are other people's.
|
|
172
|
+
if (sharedStore && projectId(currentProject).trim() === '')
|
|
173
|
+
throw new BadRequestError('a shared store needs the caller\'s project');
|
|
174
|
+
if (sharedStore && !isolationEnabled)
|
|
175
|
+
warnIsolationIgnored(ctx.hippoRoot);
|
|
176
|
+
const includeCrossProject = opts.crossProject === true || (!isolationEnabled && !sharedStore);
|
|
158
177
|
// Decided before the ambient loads so the pinned-only FTS candidate query can share their connection.
|
|
159
178
|
const promptRecallPending = pinnedOnly && Boolean(opts.prompt?.trim()) && config.pinnedInject.promptRecall === true;
|
|
160
179
|
const cost = opts.cost;
|
|
@@ -167,12 +186,14 @@ function planContext(ctx, opts) {
|
|
|
167
186
|
includeRecent: opts.includeRecent ?? 0,
|
|
168
187
|
activeScope: opts.scope ?? '',
|
|
169
188
|
exactScope: opts.exactScope || undefined,
|
|
189
|
+
ownScope: personalScopeOf(ctx.actor) ?? undefined,
|
|
170
190
|
query,
|
|
171
191
|
hasLocal,
|
|
172
192
|
hasGlobal,
|
|
173
193
|
globalRoot,
|
|
174
194
|
primaryIsGlobal,
|
|
175
195
|
hasLocalTaskState: hasLocal && !primaryIsGlobal,
|
|
196
|
+
sharedStore,
|
|
176
197
|
config,
|
|
177
198
|
currentProject,
|
|
178
199
|
includeCrossProject,
|
|
@@ -190,7 +211,7 @@ function promptRecallRequest(opts, plan) {
|
|
|
190
211
|
? Array.from(promptTokens(opts.prompt ?? ''))
|
|
191
212
|
: [];
|
|
192
213
|
return promptRecallTerms.length > 0
|
|
193
|
-
? { terms: promptRecallTerms, limit: Math.floor(finiteOr(pinnedInject.promptRecallCandidates, 100, 1)) }
|
|
214
|
+
? { terms: promptRecallTerms, limit: Math.floor(finiteOr(pinnedInject.promptRecallCandidates, 100, 1)), ownScope: plan.ownScope }
|
|
194
215
|
: undefined;
|
|
195
216
|
}
|
|
196
217
|
/** Reports the block's facts to the observer and returns the budget left after the fixed headers. */
|
|
@@ -204,9 +225,33 @@ function openBlockBudget(plan, opts, budget) {
|
|
|
204
225
|
? Math.max(0, blockBudget - cost.fixed(blockBudget, { cross: plan.includeCrossProject, promptRecall: promptRecallPending, ambient: !pinnedOnly && config.ambient.enabled }))
|
|
205
226
|
: blockBudget;
|
|
206
227
|
}
|
|
228
|
+
const NO_TASK_STATE = { snapshot: null, handoff: null, events: [] };
|
|
229
|
+
// Keyed on the RAW snapshot: a scope-hidden active session must not fall through to another session's ambient handoff.
|
|
230
|
+
function loadRawTaskState(ctx, opts, plan) {
|
|
231
|
+
if (!plan.hasLocalTaskState)
|
|
232
|
+
return NO_TASK_STATE;
|
|
233
|
+
// On a shared store task state is one person's, keyed by owner and project so it follows them into their next session.
|
|
234
|
+
const key = plan.sharedStore
|
|
235
|
+
? { owner: ownerOrSubject(ctx.actor), project: projectNames(plan.currentProject) }
|
|
236
|
+
: undefined;
|
|
237
|
+
// Bounded read: an orphaned snapshot ages out of this ambient surface; the owner session's read stays unbounded.
|
|
238
|
+
const snapshot = loadFreshActiveTaskSnapshot(ctx.hippoRoot, ctx.tenantId, { sessionId: opts.currentSessionId }, key);
|
|
239
|
+
const sessionId = snapshot?.session_id;
|
|
240
|
+
const handoff = sessionId
|
|
241
|
+
? loadLatestHandoff(ctx.hippoRoot, ctx.tenantId, sessionId, {}, key)
|
|
242
|
+
: loadLatestHandoff(ctx.hippoRoot, ctx.tenantId, undefined, {
|
|
243
|
+
unfinishedOnly: true,
|
|
244
|
+
maxAgeMs: SNAPSHOT_AMBIENT_MAX_AGE_MS,
|
|
245
|
+
// Scope is admitted in SQL so a newer denied row can't hide an older eligible one before LIMIT 1.
|
|
246
|
+
scopeFilter: 'default-deny',
|
|
247
|
+
}, key);
|
|
248
|
+
// Raw session id here too: each event is admitted on its own scope, same as recall and the CLI.
|
|
249
|
+
const events = sessionId ? listSessionEvents(ctx.hippoRoot, ctx.tenantId, { session_id: sessionId, limit: 5 }) : [];
|
|
250
|
+
return { snapshot, handoff, events };
|
|
251
|
+
}
|
|
207
252
|
// Sections print ahead of the memories, so they are paid first; one that does not fit is dropped, as an oversize entry is.
|
|
208
253
|
function loadTaskSections(ctx, opts, plan, startLeft) {
|
|
209
|
-
const { exactScope,
|
|
254
|
+
const { exactScope, ownScope, cost } = plan;
|
|
210
255
|
let left = startLeft;
|
|
211
256
|
const pays = (tokens) => {
|
|
212
257
|
if (tokens > left)
|
|
@@ -214,38 +259,12 @@ function loadTaskSections(ctx, opts, plan, startLeft) {
|
|
|
214
259
|
left -= tokens;
|
|
215
260
|
return true;
|
|
216
261
|
};
|
|
217
|
-
// Bounded read: an orphaned snapshot ages out of this ambient surface; the owner session's read stays unbounded.
|
|
218
262
|
const rowScope = (r) => r?.scope ?? null;
|
|
219
|
-
const
|
|
220
|
-
? loadFreshActiveTaskSnapshot(ctx.hippoRoot, ctx.tenantId, {
|
|
221
|
-
sessionId: opts.currentSessionId,
|
|
222
|
-
})
|
|
223
|
-
: null;
|
|
263
|
+
const raw = loadRawTaskState(ctx, opts, plan);
|
|
224
264
|
// The same envelope rule ambientAdmitEntry applies to memory rows.
|
|
225
|
-
const activeSnapshot =
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
// Key on the RAW snapshot: a scope-hidden active session must not fall through to another session's ambient handoff.
|
|
229
|
-
const rawSessionHandoff = !hasLocalTaskState
|
|
230
|
-
? null
|
|
231
|
-
: rawActiveSnapshot?.session_id
|
|
232
|
-
? loadLatestHandoff(ctx.hippoRoot, ctx.tenantId, rawActiveSnapshot.session_id)
|
|
233
|
-
: loadLatestHandoff(ctx.hippoRoot, ctx.tenantId, undefined, {
|
|
234
|
-
unfinishedOnly: true,
|
|
235
|
-
maxAgeMs: SNAPSHOT_AMBIENT_MAX_AGE_MS,
|
|
236
|
-
// Scope is admitted in SQL so a newer denied row can't hide an older eligible one before LIMIT 1.
|
|
237
|
-
scopeFilter: 'default-deny',
|
|
238
|
-
});
|
|
239
|
-
const sessionHandoff = rawSessionHandoff && passesScopeFilterForRecall(rowScope(rawSessionHandoff), exactScope)
|
|
240
|
-
? rawSessionHandoff
|
|
241
|
-
: null;
|
|
242
|
-
// Raw session id here too: each event is admitted on its own scope, same as recall and the CLI.
|
|
243
|
-
const recentSessionEvents = hasLocalTaskState && rawActiveSnapshot?.session_id
|
|
244
|
-
? listSessionEvents(ctx.hippoRoot, ctx.tenantId, {
|
|
245
|
-
session_id: rawActiveSnapshot.session_id,
|
|
246
|
-
limit: 5,
|
|
247
|
-
}).filter((e) => passesScopeFilterForRecall(rowScope(e), exactScope))
|
|
248
|
-
: [];
|
|
265
|
+
const activeSnapshot = raw.snapshot && passesScopeFilterForRecall(rowScope(raw.snapshot), exactScope, ownScope) ? raw.snapshot : null;
|
|
266
|
+
const sessionHandoff = raw.handoff && passesScopeFilterForRecall(rowScope(raw.handoff), exactScope, ownScope) ? raw.handoff : null;
|
|
267
|
+
const recentSessionEvents = raw.events.filter((e) => passesScopeFilterForRecall(rowScope(e), exactScope, ownScope));
|
|
249
268
|
const shownSnapshot = activeSnapshot && (!cost || pays(cost.snapshot(activeSnapshot))) ? activeSnapshot : null;
|
|
250
269
|
const shownHandoff = sessionHandoff && (!cost || pays(cost.handoff(sessionHandoff))) ? sessionHandoff : null;
|
|
251
270
|
const shownEvents = recentSessionEvents.length > 0 && (!cost || pays(cost.trail(recentSessionEvents))) ? recentSessionEvents : [];
|
|
@@ -254,14 +273,11 @@ function loadTaskSections(ctx, opts, plan, startLeft) {
|
|
|
254
273
|
}
|
|
255
274
|
function ambientAdmission(opts, plan, shownHandoff) {
|
|
256
275
|
const transcriptHandoffSession = shownHandoff?.evidence?.derivedFrom === 'transcript' ? shownHandoff.sessionId : null;
|
|
257
|
-
let digestHiddenForHandoff = false;
|
|
258
276
|
const ambientAdmit = (e) => {
|
|
259
277
|
// A printed handoff already carries the session's closing message, which its digest would print a second time.
|
|
260
|
-
if (transcriptHandoffSession !== null && e.source_session_id === transcriptHandoffSession && isSessionDigestRow(e))
|
|
261
|
-
digestHiddenForHandoff = true;
|
|
278
|
+
if (transcriptHandoffSession !== null && e.source_session_id === transcriptHandoffSession && isSessionDigestRow(e))
|
|
262
279
|
return false;
|
|
263
|
-
|
|
264
|
-
return ambientAdmitEntry(e, plan.currentProject, plan.includeCrossProject, plan.exactScope);
|
|
280
|
+
return ambientAdmitEntry(e, plan.currentProject, plan.includeCrossProject, plan.exactScope, plan.ownScope);
|
|
265
281
|
};
|
|
266
282
|
const ownSessionId = opts.currentSessionId || '';
|
|
267
283
|
// Inside admit, not after the load, so the loader's window widens past a session's own items.
|
|
@@ -272,10 +288,17 @@ function ambientAdmission(opts, plan, shownHandoff) {
|
|
|
272
288
|
const admit = (e) => !e.superseded_by && !isOwnCompactionItem(e) && ambientAdmit(e);
|
|
273
289
|
// The two-store search has always ranked a session's own compaction items; only the local search drops them.
|
|
274
290
|
const bothStoresAdmit = (e) => !e.superseded_by && ambientAdmit(e);
|
|
275
|
-
return { ambientAdmit, admit, bothStoresAdmit
|
|
291
|
+
return { ambientAdmit, admit, bothStoresAdmit };
|
|
292
|
+
}
|
|
293
|
+
/** Origins the recent backfill may read past its first window; on a shared store, only the caller's own project rows. */
|
|
294
|
+
function recentOrigins(plan) {
|
|
295
|
+
if (plan.sharedStore)
|
|
296
|
+
return { names: projectNames(plan.currentProject).filter((n) => n !== ''), userGlobal: false };
|
|
297
|
+
return plan.originProject && { names: plan.originProject, userGlobal: true };
|
|
276
298
|
}
|
|
277
299
|
function loadPools(ctx, plan, admission, recallRequest) {
|
|
278
|
-
const { obs, pinnedOnly, primaryIsGlobal, hasGlobal, exactScope } = plan;
|
|
300
|
+
const { obs, pinnedOnly, primaryIsGlobal, hasGlobal, exactScope, ownScope } = plan;
|
|
301
|
+
const recent = { needed: plan.includeRecent, origins: recentOrigins(plan) };
|
|
279
302
|
const searches = plan.query !== '*' && !pinnedOnly;
|
|
280
303
|
const searchesBoth = searches && hasGlobal && !primaryIsGlobal;
|
|
281
304
|
const poolAdmit = searchesBoth ? admission.bothStoresAdmit : admission.admit;
|
|
@@ -283,19 +306,20 @@ function loadPools(ctx, plan, admission, recallRequest) {
|
|
|
283
306
|
const qualityDrop = (isGlobal) => obs && !plan.promptRecallPending ? (e) => obs.qualityDropped(e, isGlobal) : undefined;
|
|
284
307
|
// The window's predicates are ones admit applies anyway, so below the cap the admitted rows are unchanged.
|
|
285
308
|
const window = searches
|
|
286
|
-
? { query: plan.query, exactScope, project: plan.originProject }
|
|
309
|
+
? { query: plan.query, exactScope, ownScope, project: plan.originProject }
|
|
287
310
|
: {
|
|
288
311
|
exactScope,
|
|
312
|
+
ownScope,
|
|
289
313
|
project: plan.originProject,
|
|
290
314
|
cap: CONTEXT_CANDIDATE_CAP,
|
|
291
315
|
now: evalNow(),
|
|
292
316
|
};
|
|
293
317
|
// Tenant-scoped loads: never resolveTenantId({}) here.
|
|
294
318
|
const local = plan.hasLocal
|
|
295
|
-
? loadAmbientEntries(ctx.hippoRoot, ctx.tenantId, pinnedOnly,
|
|
319
|
+
? loadAmbientEntries(ctx.hippoRoot, ctx.tenantId, pinnedOnly, recent, loadAdmit, window, recallRequest, qualityDrop(primaryIsGlobal))
|
|
296
320
|
: { entries: [] };
|
|
297
321
|
const global = hasGlobal && !primaryIsGlobal
|
|
298
|
-
? loadAmbientEntries(plan.globalRoot, ctx.tenantId, pinnedOnly,
|
|
322
|
+
? loadAmbientEntries(plan.globalRoot, ctx.tenantId, pinnedOnly, recent, loadAdmit, window, recallRequest, qualityDrop(true))
|
|
299
323
|
: { entries: [] };
|
|
300
324
|
return { local, global };
|
|
301
325
|
}
|
|
@@ -369,7 +393,7 @@ function recordRetrieval(ctx, opts, plan, selected, activeSnapshot) {
|
|
|
369
393
|
return { items, ambientState: plan.config.ambient.enabled ? readAmbientState(ctx, plan) : undefined };
|
|
370
394
|
}
|
|
371
395
|
function readAmbientState(ctx, plan) {
|
|
372
|
-
const filter = { exactScope: plan.exactScope, project: plan.originProject, currentProject: projectNames(plan.currentProject), now: evalNow() };
|
|
396
|
+
const filter = { exactScope: plan.exactScope, ownScope: plan.ownScope, project: plan.originProject, currentProject: projectNames(plan.currentProject), now: evalNow() };
|
|
373
397
|
const roots = [...(plan.hasLocal ? [ctx.hippoRoot] : []), ...(plan.hasGlobal && !plan.primaryIsGlobal ? [plan.globalRoot] : [])];
|
|
374
398
|
const tallies = roots.map((root) => loadAmbientTallies(root, ctx.tenantId, filter));
|
|
375
399
|
const total = tallies.length > 0 ? tallies.reduce(addAmbientTallies) : undefined;
|
package/dist/api/dormant.d.ts
CHANGED
|
@@ -9,12 +9,12 @@ import type { Context } from './types.js';
|
|
|
9
9
|
export declare function listDormant(ctx: Context, opts?: ListDormantOpts): DormantMemory[];
|
|
10
10
|
/**
|
|
11
11
|
* Bring a dormant memory back into active memory. It returns as if just
|
|
12
|
-
* recalled
|
|
13
|
-
*
|
|
14
|
-
* dormant.
|
|
12
|
+
* recalled, with a full half-life. A row audit repair set aside returns
|
|
13
|
+
* `verified`, so no quality check judges it again. Every other field is the
|
|
14
|
+
* snapshot taken when it went dormant.
|
|
15
15
|
*
|
|
16
16
|
* Throws when the tenant has no dormant memory with that id (another
|
|
17
|
-
* tenant's id reads the same way), when a live memory already holds the id,
|
|
17
|
+
* tenant's id, or another person's personal row, reads the same way), when a live memory already holds the id,
|
|
18
18
|
* and RejectedValueError when the value has been rejected since. On any
|
|
19
19
|
* throw the dormant copy stays where it is.
|
|
20
20
|
*/
|
|
@@ -25,6 +25,6 @@ export declare function restoreDormant(ctx: Context, id: string): MemoryEntry;
|
|
|
25
25
|
* dormant memory with that id.
|
|
26
26
|
*/
|
|
27
27
|
export declare function forgetDormant(ctx: Context, id: string): void;
|
|
28
|
-
/** Whether the tenant holds a dormant memory with this id (for "not found" hints). */
|
|
28
|
+
/** Whether the tenant holds a dormant memory with this id that the caller may touch (for "not found" hints). */
|
|
29
29
|
export declare function isDormant(ctx: Context, id: string): boolean;
|
|
30
30
|
//# sourceMappingURL=dormant.d.ts.map
|
package/dist/api/dormant.js
CHANGED
|
@@ -10,6 +10,8 @@ import { listDormantRows, readDormantSnapshot, deleteDormantRow, hasDormantRow,
|
|
|
10
10
|
import { createMemory, calculateStrength } from '../memory.js';
|
|
11
11
|
import { appendAuditEvent, reportAuditWriteFailure } from '../audit.js';
|
|
12
12
|
import { loadConfig } from '../config.js';
|
|
13
|
+
import { canTouchScope, personalScopeOf, touchableScopeSql } from '../recall-scope.js';
|
|
14
|
+
const touchable = (ctx) => touchableScopeSql('', personalScopeOf(ctx.actor));
|
|
13
15
|
/**
|
|
14
16
|
* A tenant's dormant memories (src/dormant.ts): what sleep moved out of
|
|
15
17
|
* active memory instead of deleting, when `dormant.enabled` is on. Newest
|
|
@@ -18,7 +20,7 @@ import { loadConfig } from '../config.js';
|
|
|
18
20
|
export function listDormant(ctx, opts = {}) {
|
|
19
21
|
const db = openHippoDb(ctx.hippoRoot);
|
|
20
22
|
try {
|
|
21
|
-
return listDormantRows(db, ctx.tenantId, opts);
|
|
23
|
+
return listDormantRows(db, ctx.tenantId, opts, touchable(ctx));
|
|
22
24
|
}
|
|
23
25
|
finally {
|
|
24
26
|
closeHippoDb(db);
|
|
@@ -26,12 +28,12 @@ export function listDormant(ctx, opts = {}) {
|
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
30
|
* Bring a dormant memory back into active memory. It returns as if just
|
|
29
|
-
* recalled
|
|
30
|
-
*
|
|
31
|
-
* dormant.
|
|
31
|
+
* recalled, with a full half-life. A row audit repair set aside returns
|
|
32
|
+
* `verified`, so no quality check judges it again. Every other field is the
|
|
33
|
+
* snapshot taken when it went dormant.
|
|
32
34
|
*
|
|
33
35
|
* Throws when the tenant has no dormant memory with that id (another
|
|
34
|
-
* tenant's id reads the same way), when a live memory already holds the id,
|
|
36
|
+
* tenant's id, or another person's personal row, reads the same way), when a live memory already holds the id,
|
|
35
37
|
* and RejectedValueError when the value has been rejected since. On any
|
|
36
38
|
* throw the dormant copy stays where it is.
|
|
37
39
|
*/
|
|
@@ -42,7 +44,7 @@ export function restoreDormant(ctx, id) {
|
|
|
42
44
|
db.exec('BEGIN IMMEDIATE');
|
|
43
45
|
try {
|
|
44
46
|
const dormant = readDormantSnapshot(db, ctx.tenantId, id);
|
|
45
|
-
if (!dormant) {
|
|
47
|
+
if (!dormant || !canTouchScope(ctx.actor, dormant.entry.scope ?? null)) {
|
|
46
48
|
throw new NotFoundError(`dormant memory not found: ${id}`);
|
|
47
49
|
}
|
|
48
50
|
if (db.prepare(`SELECT 1 FROM memories WHERE id = ?`).get(id) !== undefined) {
|
|
@@ -59,6 +61,8 @@ export function restoreDormant(ctx, id) {
|
|
|
59
61
|
...dormant.entry,
|
|
60
62
|
last_retrieved: now.toISOString(),
|
|
61
63
|
};
|
|
64
|
+
if (dormant.reason === 'quality-repair')
|
|
65
|
+
revived.confidence = 'verified';
|
|
62
66
|
restored = stampOriginProject(ctx.hippoRoot, { ...revived, strength: calculateStrength(revived, now) });
|
|
63
67
|
writeEntryDbOnly(db, restored, { actor: ctx.actor.subject });
|
|
64
68
|
deleteDormantRow(db, ctx.tenantId, id);
|
|
@@ -104,7 +108,7 @@ export function restoreDormant(ctx, id) {
|
|
|
104
108
|
export function forgetDormant(ctx, id) {
|
|
105
109
|
const db = openHippoDb(ctx.hippoRoot);
|
|
106
110
|
try {
|
|
107
|
-
if (!deleteDormantRow(db, ctx.tenantId, id)) {
|
|
111
|
+
if (!hasDormantRow(db, ctx.tenantId, id, touchable(ctx)) || !deleteDormantRow(db, ctx.tenantId, id)) {
|
|
108
112
|
throw new NotFoundError(`dormant memory not found: ${id}`);
|
|
109
113
|
}
|
|
110
114
|
try {
|
|
@@ -127,11 +131,11 @@ export function forgetDormant(ctx, id) {
|
|
|
127
131
|
// Counted like every other permanent removal (forget, archiveRaw).
|
|
128
132
|
updateStatsUnlessBusy(ctx.hippoRoot, { forgotten: 1 }, `removed ${id}`);
|
|
129
133
|
}
|
|
130
|
-
/** Whether the tenant holds a dormant memory with this id (for "not found" hints). */
|
|
134
|
+
/** Whether the tenant holds a dormant memory with this id that the caller may touch (for "not found" hints). */
|
|
131
135
|
export function isDormant(ctx, id) {
|
|
132
136
|
const db = openHippoDb(ctx.hippoRoot);
|
|
133
137
|
try {
|
|
134
|
-
return hasDormantRow(db, ctx.tenantId, id);
|
|
138
|
+
return hasDormantRow(db, ctx.tenantId, id, touchable(ctx));
|
|
135
139
|
}
|
|
136
140
|
finally {
|
|
137
141
|
closeHippoDb(db);
|
package/dist/api/drill-down.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CallerProject } from '../prompt-hook.js';
|
|
1
2
|
import type { Context } from './types.js';
|
|
2
3
|
export interface DrillDownOpts {
|
|
3
4
|
/** Cap on number of children returned. Default 50. */
|
|
@@ -19,6 +20,7 @@ export interface DrillDownOpts {
|
|
|
19
20
|
*/
|
|
20
21
|
depth?: number;
|
|
21
22
|
cost?: DrillDownCost;
|
|
23
|
+
project?: CallerProject;
|
|
22
24
|
}
|
|
23
25
|
export interface DrillDownSummary {
|
|
24
26
|
id: string;
|
package/dist/api/drill-down.js
CHANGED
|
@@ -3,7 +3,8 @@ import { closeHippoDb } from '../db.js';
|
|
|
3
3
|
import { openStore } from '../store/open.js';
|
|
4
4
|
import { selectEntriesByIds, selectChildrenByParent } from '../store/entry-reads.js';
|
|
5
5
|
import { estimateTokens } from '../token-ledger.js';
|
|
6
|
-
import { passesScopeFilterForRecall } from '../recall-scope.js';
|
|
6
|
+
import { passesScopeFilterForRecall, personalScopeOf } from '../recall-scope.js';
|
|
7
|
+
import { classifyOriginProject } from '../project-identity.js';
|
|
7
8
|
/**
|
|
8
9
|
* Walk one step down the DAG from a level-2 (or higher) summary to its direct
|
|
9
10
|
* children. Companion to `recall(... summarizeOverflow: true)` — when recall
|
|
@@ -44,16 +45,20 @@ function drillDownOn(db, ctx, summaryId, depth, opts, limit) {
|
|
|
44
45
|
// unauthorised tenants. The two cases collapse into not_found.
|
|
45
46
|
if (!summary)
|
|
46
47
|
return { failure: 'not_found' };
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (!passesScopeFilterForRecall(summary.scope ?? null, undefined)) {
|
|
48
|
+
const own = personalScopeOf(ctx.actor) ?? undefined;
|
|
49
|
+
if (!passesScopeFilterForRecall(summary.scope ?? null, undefined, own)) {
|
|
50
50
|
// codex round 3 P1: collapse to not_found. A distinguishable
|
|
51
51
|
// "scope_blocked" tells a no-scope caller "this row exists, just
|
|
52
52
|
// not for you" — same existence-leak the HTTP 404 collapse was
|
|
53
53
|
// already preventing. Match the HTTP behaviour at the API level.
|
|
54
54
|
return { failure: 'not_found' };
|
|
55
55
|
}
|
|
56
|
-
const
|
|
56
|
+
const shown = (row) => !opts.project || classifyOriginProject(row.origin_project, opts.project) !== 'cross-project';
|
|
57
|
+
if (!shown(summary))
|
|
58
|
+
return { failure: 'not_found' };
|
|
59
|
+
if ((summary.dag_level ?? 0) < 2)
|
|
60
|
+
return { failure: 'not_drillable' };
|
|
61
|
+
const { collected, level0DirectCount } = collectDescendants(db, ctx.tenantId, summaryId, depth, own, shown);
|
|
57
62
|
const summaryOut = {
|
|
58
63
|
id: summary.id,
|
|
59
64
|
content: summary.content,
|
|
@@ -83,7 +88,7 @@ function drillDownOn(db, ctx, summaryId, depth, opts, limit) {
|
|
|
83
88
|
}
|
|
84
89
|
// BFS with a visited set: dag_parent_id is not unique, so a misconfigured tree could emit a child twice past depth 1.
|
|
85
90
|
// The level-0 count is kept apart so a legacy summary's descendantCount fallback counts direct children only.
|
|
86
|
-
function collectDescendants(db, tenantId, summaryId, depth) {
|
|
91
|
+
function collectDescendants(db, tenantId, summaryId, depth, own, shown) {
|
|
87
92
|
const collected = [];
|
|
88
93
|
const visited = new Set([summaryId]);
|
|
89
94
|
let frontier = [summaryId];
|
|
@@ -93,7 +98,7 @@ function collectDescendants(db, tenantId, summaryId, depth) {
|
|
|
93
98
|
const kidsByParent = selectChildrenByParent(db, frontier, tenantId);
|
|
94
99
|
for (const parentId of frontier) {
|
|
95
100
|
const kids = kidsByParent.get(parentId) ?? [];
|
|
96
|
-
const eligibleKids = kids.filter((c) => passesScopeFilterForRecall(c.scope ?? null, undefined));
|
|
101
|
+
const eligibleKids = kids.filter((c) => passesScopeFilterForRecall(c.scope ?? null, undefined, own) && shown(c));
|
|
97
102
|
for (const k of eligibleKids) {
|
|
98
103
|
if (visited.has(k.id))
|
|
99
104
|
continue;
|
package/dist/api/forget.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface RejectResult {
|
|
|
33
33
|
* forms — pass exactly one:
|
|
34
34
|
* - `memoryId`: reject the CURRENT content of an existing memory. Removes
|
|
35
35
|
* that row and every other live row in the tenant whose normalized
|
|
36
|
-
* digest matches (not just the id passed).
|
|
36
|
+
* digest matches (not just the id passed), except another person's personal rows.
|
|
37
37
|
* - `value`: pre-emptive form — tombstone content that may not currently
|
|
38
38
|
* be stored (or is already gone). Zero removals.
|
|
39
39
|
*
|
package/dist/api/forget.js
CHANGED
|
@@ -4,11 +4,13 @@ import { BadRequestError, NotFoundError } from '../api-errors.js';
|
|
|
4
4
|
import { deleteEntry } from '../store/delete-and-batch.js';
|
|
5
5
|
import { updateStatsUnlessBusy } from '../store/index-and-stats.js';
|
|
6
6
|
import { rejectValue, unrejectValue, listRejectionsForTenant } from '../reject-flow.js';
|
|
7
|
-
import {
|
|
7
|
+
import { selectMemoryReach } from '../store/tenant-lookup.js';
|
|
8
|
+
import { canTouchScope, personalScopeOf } from '../recall-scope.js';
|
|
8
9
|
export function forget(ctx, id) {
|
|
9
10
|
const db = openHippoDb(ctx.hippoRoot);
|
|
10
11
|
try {
|
|
11
|
-
|
|
12
|
+
const reach = selectMemoryReach(db, id);
|
|
13
|
+
if (reach?.tenantId !== ctx.tenantId || !canTouchScope(ctx.actor, reach.scope)) {
|
|
12
14
|
throw new NotFoundError(`memory not found: ${id}`);
|
|
13
15
|
}
|
|
14
16
|
}
|
|
@@ -31,7 +33,7 @@ export function forget(ctx, id) {
|
|
|
31
33
|
* forms — pass exactly one:
|
|
32
34
|
* - `memoryId`: reject the CURRENT content of an existing memory. Removes
|
|
33
35
|
* that row and every other live row in the tenant whose normalized
|
|
34
|
-
* digest matches (not just the id passed).
|
|
36
|
+
* digest matches (not just the id passed), except another person's personal rows.
|
|
35
37
|
* - `value`: pre-emptive form — tombstone content that may not currently
|
|
36
38
|
* be stored (or is already gone). Zero removals.
|
|
37
39
|
*
|
|
@@ -46,7 +48,8 @@ export function reject(ctx, opts) {
|
|
|
46
48
|
// keeps the error message consistent with the rest of this module.
|
|
47
49
|
const db = openHippoDb(ctx.hippoRoot);
|
|
48
50
|
try {
|
|
49
|
-
|
|
51
|
+
const reach = selectMemoryReach(db, opts.memoryId);
|
|
52
|
+
if (reach?.tenantId !== ctx.tenantId || !canTouchScope(ctx.actor, reach.scope)) {
|
|
50
53
|
throw new NotFoundError(`memory not found: ${opts.memoryId}`);
|
|
51
54
|
}
|
|
52
55
|
}
|
|
@@ -61,6 +64,7 @@ export function reject(ctx, opts) {
|
|
|
61
64
|
reason: opts.reason,
|
|
62
65
|
memoryId: opts.memoryId,
|
|
63
66
|
value: opts.value,
|
|
67
|
+
ownScope: personalScopeOf(ctx.actor) ?? undefined,
|
|
64
68
|
});
|
|
65
69
|
return { digest: result.digest, removedIds: result.removedIds };
|
|
66
70
|
}
|
package/dist/api/learn.js
CHANGED
|
@@ -6,6 +6,7 @@ import { embedMemory } from '../embeddings.js';
|
|
|
6
6
|
import { extractInvalidationTarget, invalidateMatching } from '../invalidation.js';
|
|
7
7
|
import { computeSchemaFit, createMemory, Layer } from '../memory.js';
|
|
8
8
|
import { extractPathTags } from '../path-context.js';
|
|
9
|
+
import { canReadScope, personalScopeOf, touchableScopeSql } from '../recall-scope.js';
|
|
9
10
|
import { RejectedValueError } from '../rejection.js';
|
|
10
11
|
import { duplicateKey, longestWord, storedTextKeys } from '../same-text.js';
|
|
11
12
|
import { loadTextsHoldingWords } from '../store/candidates.js';
|
|
@@ -37,9 +38,10 @@ export function learn(ctx, opts) {
|
|
|
37
38
|
function writeLessons(ctx, lessons, opts, config) {
|
|
38
39
|
const { hippoRoot, tenantId } = ctx;
|
|
39
40
|
const { profile } = opts;
|
|
40
|
-
// Schema fit needs every row; without it, only rows holding a lesson's longest word can be its copy.
|
|
41
|
-
const existing = profile.full ? loadAllEntries(hippoRoot, tenantId) : undefined;
|
|
42
|
-
const
|
|
41
|
+
// Schema fit needs every row; without it, only rows holding a lesson's longest word can be its copy. Learn is host-admin only, so both read what an admin can.
|
|
42
|
+
const existing = profile.full ? loadAllEntries(hippoRoot, tenantId).filter((e) => e.scope == null || canReadScope(ctx.actor, e.scope)) : undefined;
|
|
43
|
+
const readable = touchableScopeSql('', personalScopeOf(ctx.actor));
|
|
44
|
+
const keys = storedTextKeys(existing ?? loadTextsHoldingWords(hippoRoot, tenantId, lessons.map(longestWord), undefined, readable));
|
|
43
45
|
const pathTags = profile.full ? extractPathTags(opts.repoPath) : [];
|
|
44
46
|
const counts = { added: 0, skipped: 0, rejected: 0, invalidations: [] };
|
|
45
47
|
for (const lesson of lessons) {
|
package/dist/api/outcome.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Context } from './types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Apply a positive/negative outcome to a list of recently-recalled memory ids.
|
|
4
4
|
* Used by the MCP `hippo_outcome` tool and the HTTP `POST /v1/outcome` route.
|
|
5
|
-
* Tenant-scoped: ids
|
|
5
|
+
* Tenant-scoped: ids outside ctx.tenantId, or in someone else's personal scope, are silently skipped
|
|
6
6
|
* (matches the prior MCP semantics — a stale id from another tenant doesn't
|
|
7
7
|
* crash the call). Each successful outcome emits one audit_log row with
|
|
8
8
|
* op='outcome' tagged with ctx.actor.subject.
|
package/dist/api/outcome.js
CHANGED
|
@@ -7,6 +7,7 @@ import { loadIndex } from '../store/index-and-stats.js';
|
|
|
7
7
|
import { applyOutcome, CHURN_STALE_TAG } from '../memory.js';
|
|
8
8
|
import { appendAuditEvent } from '../audit.js';
|
|
9
9
|
import { recordTraceOutcome } from '../recall-trace.js';
|
|
10
|
+
import { canTouchScope } from '../recall-scope.js';
|
|
10
11
|
export function outcome(ctx, ids, good, opts) {
|
|
11
12
|
const appliedIds = [];
|
|
12
13
|
const db = openStore(ctx.hippoRoot);
|
|
@@ -14,7 +15,7 @@ export function outcome(ctx, ids, good, opts) {
|
|
|
14
15
|
const live = selectEntriesByIds(db, ids, ctx.tenantId);
|
|
15
16
|
for (const id of ids) {
|
|
16
17
|
const entry = live.get(id);
|
|
17
|
-
if (!entry)
|
|
18
|
+
if (!entry || !canTouchScope(ctx.actor, entry.scope ?? null))
|
|
18
19
|
continue;
|
|
19
20
|
let updated = applyOutcome(entry, good);
|
|
20
21
|
if (good && updated.tags.includes(CHURN_STALE_TAG)) { // a good outcome reconfirms the entry
|
package/dist/api/promote.js
CHANGED
|
@@ -15,7 +15,8 @@ import { appendAuditEvent } from '../audit.js';
|
|
|
15
15
|
import { promoteToGlobal } from '../shared.js';
|
|
16
16
|
import { archiveRawMemory } from '../raw-archive.js';
|
|
17
17
|
import { loadConfig } from '../config.js';
|
|
18
|
-
import {
|
|
18
|
+
import { selectMemoryReach } from '../store/tenant-lookup.js';
|
|
19
|
+
import { canTouchScope } from '../recall-scope.js';
|
|
19
20
|
export function promote(ctx, id) {
|
|
20
21
|
// Tenant scope: promoteToGlobal reads the entry from the local root via
|
|
21
22
|
// readEntry without a tenant filter, so a Bearer for tenant A could
|
|
@@ -25,7 +26,8 @@ export function promote(ctx, id) {
|
|
|
25
26
|
// another tenant).
|
|
26
27
|
const ownerDb = openHippoDb(ctx.hippoRoot);
|
|
27
28
|
try {
|
|
28
|
-
|
|
29
|
+
const reach = selectMemoryReach(ownerDb, id);
|
|
30
|
+
if (reach?.tenantId !== ctx.tenantId || !canTouchScope(ctx.actor, reach.scope)) {
|
|
29
31
|
throw new NotFoundError(`memory not found: ${id}`);
|
|
30
32
|
}
|
|
31
33
|
}
|
|
@@ -78,10 +80,10 @@ export function supersede(ctx, oldId, newContent) {
|
|
|
78
80
|
}
|
|
79
81
|
return { ok: true, oldId, newId: newEntry.id };
|
|
80
82
|
}
|
|
81
|
-
/** The tenant-scoped row to supersede;
|
|
83
|
+
/** The tenant-scoped row to supersede; another tenant's id, or someone else's personal row, reads as not found. */
|
|
82
84
|
function readSupersedable(ctx, oldId) {
|
|
83
85
|
const old = readEntry(ctx.hippoRoot, oldId, ctx.tenantId);
|
|
84
|
-
if (!old) {
|
|
86
|
+
if (!old || !canTouchScope(ctx.actor, old.scope ?? null)) {
|
|
85
87
|
throw new NotFoundError(`Memory not found: ${oldId}`);
|
|
86
88
|
}
|
|
87
89
|
// The CAS UPDATE closes the race; this check only gives a clearer error in the common single-writer case.
|
|
@@ -140,7 +142,8 @@ export function archiveRaw(ctx, id, reason, opts = {}) {
|
|
|
140
142
|
// pre-check. Deny cross-tenant access with the same not-found message
|
|
141
143
|
// archiveRawMemory itself would throw on a missing row, so we don't
|
|
142
144
|
// leak whether the id exists in another tenant.
|
|
143
|
-
|
|
145
|
+
const reach = selectMemoryReach(db, id);
|
|
146
|
+
if (reach?.tenantId !== ctx.tenantId || !canTouchScope(ctx.actor, reach.scope)) {
|
|
144
147
|
throw new NotFoundError(`memory not found: ${id}`);
|
|
145
148
|
}
|
|
146
149
|
archiveRawMemory(db, id, {
|
|
@@ -5,6 +5,7 @@ import type { RerankStep, SearchResult } from '../search/types.js';
|
|
|
5
5
|
import type { PlanningFallacyHint, PlanningFallacyWatching } from '../predictions/planning-fallacy.js';
|
|
6
6
|
import type { AnchoringHint, RecallHistorySnapshot } from '../recall-history.js';
|
|
7
7
|
import type { AvailabilityHint } from '../availability.js';
|
|
8
|
+
import type { CallerProject } from '../prompt-hook.js';
|
|
8
9
|
export interface RecallOpts {
|
|
9
10
|
query: string;
|
|
10
11
|
limit?: number;
|
|
@@ -76,8 +77,8 @@ export interface RecallOpts {
|
|
|
76
77
|
* session handoff, recent session events) on the result. Default false to keep
|
|
77
78
|
* the hot path cheap; agent boot paths should set this to true.
|
|
78
79
|
*
|
|
79
|
-
* All three lookups are tenant-scoped to ctx.tenantId
|
|
80
|
-
*
|
|
80
|
+
* All three lookups are tenant-scoped to ctx.tenantId. On a shared store they are
|
|
81
|
+
* the caller's own, keyed by owner and `project`, and empty without a project.
|
|
81
82
|
*
|
|
82
83
|
* Note: when no active snapshot exists, sessionHandoff is null and
|
|
83
84
|
* recentSessionEvents is []. We deliberately do NOT fall back to the latest
|
|
@@ -86,6 +87,7 @@ export interface RecallOpts {
|
|
|
86
87
|
* `hippo session resume`.
|
|
87
88
|
*/
|
|
88
89
|
includeContinuity?: boolean;
|
|
90
|
+
project?: CallerProject;
|
|
89
91
|
/**
|
|
90
92
|
* When set AND `(ctx.tenantId, sessionId)` has active goals AND
|
|
91
93
|
* `goalTag` is unset, `api.recall` applies the dlPFC goal-stack boost lifted
|