hippo-memory 1.62.0 → 1.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/agent-memories/apply.d.ts +2 -0
- package/dist/agent-memories/apply.js +23 -1
- package/dist/agent-memories/claude-code.d.ts +2 -1
- package/dist/agent-memories/claude-code.js +4 -4
- package/dist/agent-memories/report.d.ts +1 -0
- package/dist/agent-memories/report.js +2 -2
- package/dist/agent-memories/sync.js +37 -23
- package/dist/ambient-store.d.ts +4 -4
- package/dist/ambient-store.js +4 -3
- package/dist/api/assemble.d.ts +7 -10
- package/dist/api/assemble.js +7 -12
- package/dist/api/audit.d.ts +2 -2
- package/dist/api/audit.js +2 -2
- package/dist/api/auth.d.ts +6 -6
- package/dist/api/auth.js +5 -7
- package/dist/api/context-select.d.ts +4 -3
- package/dist/api/context-select.js +9 -11
- package/dist/api/context-types.d.ts +12 -17
- package/dist/api/context.d.ts +3 -2
- package/dist/api/context.js +20 -20
- package/dist/api/dormant.js +1 -1
- package/dist/api/drill-down.d.ts +4 -4
- package/dist/api/drill-down.js +22 -10
- package/dist/api/outcome.d.ts +8 -13
- package/dist/api/outcome.js +13 -17
- package/dist/api/promote.d.ts +5 -8
- package/dist/api/quarantine.js +3 -2
- package/dist/api/recall-types.d.ts +43 -55
- package/dist/api/recall.d.ts +3 -3
- package/dist/api/recall.js +7 -18
- package/dist/api/remember.d.ts +2 -2
- package/dist/api/sleep.d.ts +7 -35
- package/dist/api/sleep.js +3 -2
- package/dist/api/tokens.d.ts +2 -2
- package/dist/api/tokens.js +2 -2
- package/dist/api/types.d.ts +6 -10
- package/dist/api/types.js +2 -4
- package/dist/audit-prune.d.ts +4 -6
- package/dist/audit-prune.js +3 -5
- package/dist/audit.js +11 -33
- package/dist/auth.d.ts +8 -9
- package/dist/auth.js +5 -7
- package/dist/autolearn.d.ts +1 -1
- package/dist/autolearn.js +1 -1
- package/dist/availability.js +3 -5
- package/dist/capture/command.d.ts +5 -4
- package/dist/capture/command.js +8 -22
- package/dist/capture/compact.d.ts +1 -1
- package/dist/capture/compact.js +9 -13
- package/dist/capture/extract.js +37 -115
- package/dist/capture-error.d.ts +1 -1
- package/dist/capture-error.js +1 -1
- package/dist/churn-git.d.ts +1 -1
- package/dist/churn-git.js +1 -1
- package/dist/cli/audit.js +3 -4
- package/dist/cli/auth.js +3 -6
- package/dist/cli/curate.d.ts +1 -1
- package/dist/cli/curate.js +8 -24
- package/dist/cli/dag.js +5 -9
- package/dist/cli/decisions.js +10 -21
- package/dist/cli/explain.js +2 -1
- package/dist/cli/goals.js +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/playbooks.js +4 -9
- package/dist/cli/projects.js +3 -1
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.js +6 -18
- package/dist/cli/session-hooks.js +21 -40
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.js +2 -6
- package/dist/cli/shared.js +3 -3
- package/dist/cli/slack.js +1 -1
- package/dist/cli/sleep.js +27 -1
- package/dist/cli/status.d.ts +4 -4
- package/dist/cli/status.js +7 -12
- package/dist/cli/transfer.js +8 -13
- package/dist/cli/usage.js +9 -6
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -9
- package/dist/compaction-record.d.ts +2 -0
- package/dist/compaction-record.js +89 -68
- package/dist/compare.d.ts +11 -16
- package/dist/compare.js +11 -16
- package/dist/config.d.ts +19 -18
- package/dist/config.js +90 -67
- package/dist/connectors/github/backfill.d.ts +2 -2
- package/dist/connectors/github/backfill.js +8 -15
- package/dist/connectors/github/cli-impl.js +3 -8
- package/dist/connectors/github/deletion.d.ts +5 -12
- package/dist/connectors/github/deletion.js +5 -12
- package/dist/connectors/github/dlq.d.ts +6 -9
- package/dist/connectors/github/dlq.js +2 -3
- package/dist/connectors/github/ingest.d.ts +5 -7
- package/dist/connectors/github/ingest.js +8 -12
- package/dist/connectors/github/octokit-client.d.ts +3 -5
- package/dist/connectors/github/octokit-client.js +5 -6
- package/dist/connectors/github/signature.d.ts +9 -39
- package/dist/connectors/github/signature.js +9 -39
- package/dist/connectors/github/tenant-routing.d.ts +1 -1
- package/dist/connectors/github/tenant-routing.js +1 -1
- package/dist/connectors/github/transform.js +2 -2
- package/dist/connectors/github/types.d.ts +2 -10
- package/dist/connectors/github/types.js +1 -3
- package/dist/connectors/slack/deletion.d.ts +3 -8
- package/dist/connectors/slack/deletion.js +3 -8
- package/dist/connectors/slack/dlq.d.ts +1 -1
- package/dist/connectors/slack/ingest.d.ts +1 -1
- package/dist/connectors/slack/ingest.js +7 -16
- package/dist/connectors/slack/signature.d.ts +1 -1
- package/dist/connectors/slack/tenant-routing.d.ts +3 -5
- package/dist/connectors/slack/tenant-routing.js +3 -5
- package/dist/connectors/slack/transform.d.ts +5 -6
- package/dist/connectors/slack/transform.js +5 -6
- package/dist/connectors/slack/types.d.ts +2 -6
- package/dist/connectors/slack/types.js +1 -3
- package/dist/connectors/slack/web-client.js +10 -3
- package/dist/connectors/slack/workspaces.d.ts +3 -5
- package/dist/connectors/slack/workspaces.js +3 -5
- package/dist/consolidate/conflicts.js +3 -14
- package/dist/consolidate/decay.js +9 -29
- package/dist/consolidate/llm-passes.js +4 -5
- package/dist/consolidate/merge.js +8 -23
- package/dist/consolidate/run.d.ts +1 -8
- package/dist/consolidate/run.js +3 -25
- package/dist/consolidate/sleep.js +5 -17
- package/dist/consolidate/traces.js +9 -21
- package/dist/customer-notes.d.ts +5 -7
- package/dist/customer-notes.js +6 -9
- package/dist/dag.d.ts +10 -21
- package/dist/dag.js +23 -73
- package/dist/db/continuity.js +2 -2
- package/dist/db/migrations/v14.js +1 -1
- package/dist/db/migrations/v15.js +1 -2
- package/dist/db/migrations/v16.js +3 -4
- package/dist/db/migrations/v17.js +2 -3
- package/dist/db/migrations/v19.js +1 -1
- package/dist/db/migrations/v20.js +1 -1
- package/dist/db/migrations/v21.js +2 -6
- package/dist/db/migrations/v22.js +2 -4
- package/dist/db/migrations/v23.js +1 -1
- package/dist/db/migrations/v24.js +4 -6
- package/dist/db/migrations/v25.js +2 -3
- package/dist/db/migrations/v26.js +3 -3
- package/dist/db/migrations/v27.js +2 -10
- package/dist/db/migrations/v28.js +5 -8
- package/dist/db/migrations/v29.js +3 -4
- package/dist/db/migrations/v30.js +2 -2
- package/dist/db/migrations/v31.js +1 -1
- package/dist/db/migrations/v32.js +1 -1
- package/dist/db/migrations/v33.js +3 -3
- package/dist/db/migrations/v34.js +1 -1
- package/dist/db/migrations/v35.js +3 -4
- package/dist/db/migrations/v36.js +3 -4
- package/dist/db/migrations/v37.js +5 -5
- package/dist/db/migrations/v38.js +7 -8
- package/dist/db/migrations/v39.js +1 -1
- package/dist/db/migrations/v40.js +4 -16
- package/dist/db/migrations/v41.js +3 -4
- package/dist/db/migrations/v42.js +3 -4
- package/dist/db/migrations/v45.js +1 -1
- package/dist/db/migrations/v46.js +1 -1
- package/dist/db/migrations/v47.js +1 -1
- package/dist/db/migrations/v48.js +1 -1
- package/dist/decisions.d.ts +2 -2
- package/dist/decisions.js +6 -6
- package/dist/dedupe.js +86 -61
- package/dist/delivery-recorder.js +154 -135
- package/dist/doctor.js +119 -104
- package/dist/dormant.js +1 -4
- package/dist/embedding-provider.d.ts +4 -8
- package/dist/embedding-provider.js +4 -8
- package/dist/embeddings.js +55 -47
- package/dist/env.d.ts +1 -1
- package/dist/env.js +12 -12
- package/dist/escape.d.ts +5 -0
- package/dist/escape.js +10 -0
- package/dist/eval-stats.d.ts +1 -2
- package/dist/eval-stats.js +1 -2
- package/dist/eval-suite.js +27 -21
- package/dist/extract.js +4 -9
- package/dist/failure-log.d.ts +3 -3
- package/dist/failure-log.js +1 -1
- package/dist/forward-claim-detector.d.ts +2 -4
- package/dist/forward-claim-detector.js +6 -11
- package/dist/goals.d.ts +3 -3
- package/dist/goals.js +5 -6
- package/dist/graph/read.d.ts +2 -2
- package/dist/graph/read.js +5 -6
- package/dist/graph/types.d.ts +8 -8
- package/dist/graph/write.d.ts +7 -14
- package/dist/graph/write.js +16 -23
- package/dist/graph-extract.d.ts +7 -8
- package/dist/graph-extract.js +62 -72
- package/dist/graph-recall.d.ts +2 -2
- package/dist/graph-recall.js +55 -49
- package/dist/graph-stream.d.ts +5 -6
- package/dist/graph-stream.js +66 -57
- package/dist/graph-view.d.ts +2 -2
- package/dist/graph-view.js +7 -7
- package/dist/half-life-migration.d.ts +1 -2
- package/dist/half-life-migration.js +2 -3
- package/dist/hooks/codex-session.js +1 -1
- package/dist/hooks/codex-wrapper.d.ts +1 -1
- package/dist/hooks/codex-wrapper.js +3 -2
- package/dist/hooks/json-hooks.d.ts +2 -2
- package/dist/hooks/json-hooks.js +5 -4
- package/dist/hooks/opencode.d.ts +1 -1
- package/dist/hooks/opencode.js +5 -4
- package/dist/hooks/shared.d.ts +3 -7
- package/dist/hooks/shared.js +1 -8
- package/dist/http-util.d.ts +2 -3
- package/dist/http-util.js +3 -0
- package/dist/importers/core.d.ts +2 -9
- package/dist/importers/core.js +15 -30
- package/dist/importers/sources.js +2 -1
- package/dist/importers/vault.js +2 -20
- package/dist/incidents.d.ts +1 -1
- package/dist/incidents.js +1 -1
- package/dist/instruction-detect.d.ts +1 -1
- package/dist/instruction-detect.js +1 -1
- package/dist/invalidation.d.ts +3 -0
- package/dist/invalidation.js +160 -114
- package/dist/json.d.ts +5 -0
- package/dist/json.js +4 -0
- package/dist/judgment.js +1 -2
- package/dist/local-embedding.js +1 -1
- package/dist/mcp/admin-tools.js +7 -17
- package/dist/mcp/format.js +1 -1
- package/dist/mcp/framing.js +3 -6
- package/dist/mcp/protocol.d.ts +2 -5
- package/dist/mcp/protocol.js +1 -3
- package/dist/mcp/recall-tools.js +12 -15
- package/dist/mcp/request.js +4 -3
- package/dist/mcp/session-state.js +2 -3
- package/dist/mcp/stdio.js +2 -1
- package/dist/mcp/tools.js +9 -6
- package/dist/memory-value-weights.d.ts +5 -8
- package/dist/memory-value-weights.js +5 -8
- package/dist/memory-value.d.ts +13 -13
- package/dist/memory-value.js +26 -37
- package/dist/memory.d.ts +20 -22
- package/dist/memory.js +24 -48
- package/dist/multihop.d.ts +1 -1
- package/dist/multihop.js +3 -2
- package/dist/owner-validation.d.ts +4 -5
- package/dist/owner-validation.js +4 -5
- package/dist/physics.d.ts +4 -4
- package/dist/physics.js +7 -9
- package/dist/policies.d.ts +9 -10
- package/dist/policies.js +12 -14
- package/dist/postinstall.js +3 -6
- package/dist/predictions/planning-fallacy.d.ts +9 -14
- package/dist/predictions/planning-fallacy.js +10 -16
- package/dist/predictions/store.d.ts +15 -23
- package/dist/predictions/store.js +36 -33
- package/dist/processes.d.ts +2 -7
- package/dist/processes.js +3 -3
- package/dist/project-briefs.d.ts +2 -3
- package/dist/project-briefs.js +9 -13
- package/dist/project-identity.d.ts +22 -9
- package/dist/project-identity.js +47 -12
- package/dist/project-merge.d.ts +17 -2
- package/dist/project-merge.js +109 -27
- package/dist/project-remote.d.ts +12 -0
- package/dist/project-remote.js +138 -0
- package/dist/prompt-recall.js +1 -2
- package/dist/rate-limit.d.ts +1 -1
- package/dist/rate-limit.js +1 -1
- package/dist/raw-archive.d.ts +9 -0
- package/dist/raw-archive.js +70 -53
- package/dist/recall-history.d.ts +19 -20
- package/dist/recall-history.js +24 -42
- package/dist/recall-pipeline.js +4 -28
- package/dist/recall-scope.d.ts +7 -8
- package/dist/recall-scope.js +7 -8
- package/dist/recall-trace.d.ts +5 -9
- package/dist/recall-trace.js +6 -10
- package/dist/refine-llm.d.ts +1 -1
- package/dist/refine-llm.js +2 -2
- package/dist/reject-flow.d.ts +3 -4
- package/dist/reject-flow.js +122 -117
- package/dist/rejection.d.ts +5 -6
- package/dist/rejection.js +7 -15
- package/dist/rerankers/clef.d.ts +1 -1
- package/dist/rerankers/jev.d.ts +1 -2
- package/dist/rerankers/jev.js +4 -5
- package/dist/rerankers/llm.d.ts +1 -2
- package/dist/rerankers/llm.js +1 -2
- package/dist/rerankers/types.d.ts +1 -2
- package/dist/rrf.d.ts +2 -2
- package/dist/rrf.js +2 -2
- package/dist/search/bm25-search.d.ts +1 -1
- package/dist/search/bm25-search.js +2 -1
- package/dist/search/boosts.js +2 -1
- package/dist/search/hybrid.d.ts +1 -1
- package/dist/search/hybrid.js +2 -1
- package/dist/search/physics-search.d.ts +1 -1
- package/dist/search/physics-search.js +2 -1
- package/dist/search/types.d.ts +2 -0
- package/dist/search/types.js +3 -1
- package/dist/secret-detect.d.ts +4 -5
- package/dist/secret-detect.js +6 -10
- package/dist/server/auth.js +5 -5
- package/dist/server/client-ip.js +1 -1
- package/dist/server/cursor.js +2 -1
- package/dist/server/mcp-http.js +4 -4
- package/dist/server/request.d.ts +3 -6
- package/dist/server/request.js +6 -7
- package/dist/server/routes/admin.js +5 -4
- package/dist/server/routes/customer-notes.js +6 -5
- package/dist/server/routes/decisions.js +4 -3
- package/dist/server/routes/incidents.js +7 -5
- package/dist/server/routes/memories.js +7 -7
- package/dist/server/routes/policies.js +3 -2
- package/dist/server/routes/predictions.js +12 -15
- package/dist/server/routes/processes.js +3 -2
- package/dist/server/routes/project-briefs.js +8 -7
- package/dist/server/routes/recall.js +95 -93
- package/dist/server/routes/skills.js +6 -5
- package/dist/server/types.d.ts +1 -1
- package/dist/server/validation.d.ts +1 -2
- package/dist/server/validation.js +7 -14
- package/dist/server-detect.js +72 -58
- package/dist/server.d.ts +2 -2
- package/dist/server.js +131 -117
- package/dist/shared.d.ts +17 -17
- package/dist/shared.js +93 -97
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +8 -8
- package/dist/store/audit-event.d.ts +2 -2
- package/dist/store/audit-event.js +1 -1
- package/dist/store/candidates.d.ts +2 -2
- package/dist/store/candidates.js +4 -3
- package/dist/store/conflicts.js +30 -22
- package/dist/store/delete-and-batch.d.ts +11 -14
- package/dist/store/delete-and-batch.js +40 -91
- package/dist/store/entry-reads.d.ts +17 -25
- package/dist/store/entry-reads.js +59 -40
- package/dist/store/entry-row.d.ts +6 -24
- package/dist/store/entry-row.js +6 -24
- package/dist/store/entry-writes.d.ts +6 -7
- package/dist/store/entry-writes.js +13 -11
- package/dist/store/handoffs.d.ts +1 -1
- package/dist/store/handoffs.js +7 -10
- package/dist/store/index-and-stats.d.ts +2 -6
- package/dist/store/index-and-stats.js +4 -10
- package/dist/store/mirrors.d.ts +6 -19
- package/dist/store/mirrors.js +14 -39
- package/dist/store/open.js +9 -31
- package/dist/store/rows.d.ts +5 -11
- package/dist/store/rows.js +6 -11
- package/dist/store/search-rows.d.ts +17 -34
- package/dist/store/search-rows.js +31 -59
- package/dist/store/sessions.d.ts +4 -5
- package/dist/store/sessions.js +5 -6
- package/dist/store/summaries.d.ts +13 -17
- package/dist/store/summaries.js +26 -70
- package/dist/support-bundle.js +4 -8
- package/dist/tenant.d.ts +1 -5
- package/dist/token-ledger.d.ts +1 -1
- package/dist/token-ledger.js +3 -5
- package/dist/trace.js +1 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -8,14 +8,15 @@ import { rankBothStores } from '../shared.js';
|
|
|
8
8
|
import { evalNow } from '../ablation.js';
|
|
9
9
|
import { hybridSearch } from '../search/hybrid.js';
|
|
10
10
|
import { physicsSearch } from '../search/physics-search.js';
|
|
11
|
+
import { DEFAULT_LOCAL_BUMP } from '../search/types.js';
|
|
11
12
|
import { compareScoredResults } from '../compare.js';
|
|
12
13
|
import { scopeMatch } from '../scope.js';
|
|
13
|
-
import { loadConfig } from '../config.js';
|
|
14
14
|
import { promptTokens, contentTokens, gatePromptRecall, } from '../prompt-recall.js';
|
|
15
|
+
const GLOBAL_DISCOUNT = 1 / DEFAULT_LOCAL_BUMP;
|
|
15
16
|
// Share and promote copy a memory to the global store under a new id, so equal content is the only link.
|
|
16
17
|
// A pinned copy wins, then the stronger one after the ranking's own global discount; a tie keeps the local copy.
|
|
17
18
|
export function oneCopyPerMemory(local, global, now) {
|
|
18
|
-
const score = (e, isGlobal) => calculateStrength(e, now) * (isGlobal ?
|
|
19
|
+
const score = (e, isGlobal) => calculateStrength(e, now) * (isGlobal ? GLOBAL_DISCOUNT : 1);
|
|
19
20
|
const best = new Map();
|
|
20
21
|
const offer = (entry, isGlobal) => {
|
|
21
22
|
const held = best.get(entry.content);
|
|
@@ -34,10 +35,8 @@ export function oneCopyPerMemory(local, global, now) {
|
|
|
34
35
|
}
|
|
35
36
|
export const finiteOr = (v, dflt, min) => Number.isFinite(v) && v >= min ? v : dflt;
|
|
36
37
|
/** Pins plus the prompt-recall or recent-N backfill; null means the block is empty. */
|
|
37
|
-
export function selectPinned(
|
|
38
|
-
const { obs, primaryIsGlobal } = plan;
|
|
39
|
-
// loadConfig is safe even when local isn't initialised — returns defaults.
|
|
40
|
-
const pinnedCfg = loadConfig(ctx.hippoRoot);
|
|
38
|
+
export function selectPinned(opts, plan, left, pools, admission) {
|
|
39
|
+
const { obs, primaryIsGlobal, config: pinnedCfg } = plan;
|
|
41
40
|
if (!pinnedCfg.pinnedInject.enabled) {
|
|
42
41
|
return null;
|
|
43
42
|
}
|
|
@@ -85,7 +84,7 @@ function rankPinned(plan, pinnedLocal, pinnedGlobal, nowP) {
|
|
|
85
84
|
const sBst = scopeSig === 1 ? 1.5 : scopeSig === -1 ? 0.5 : 1.0;
|
|
86
85
|
return {
|
|
87
86
|
entry,
|
|
88
|
-
score: calculateStrength(entry, nowP) * (isGlobal ?
|
|
87
|
+
score: calculateStrength(entry, nowP) * (isGlobal ? GLOBAL_DISCOUNT : 1) * sBst,
|
|
89
88
|
tokens: plan.price(entry, isGlobal),
|
|
90
89
|
isGlobal,
|
|
91
90
|
};
|
|
@@ -204,7 +203,7 @@ function backfillRecent(plan, localPool, globalPool, picked, recentBudget, nowP)
|
|
|
204
203
|
.slice(0, plan.includeRecent)
|
|
205
204
|
.map(({ entry, isGlobal }) => ({
|
|
206
205
|
entry,
|
|
207
|
-
score: calculateStrength(entry, nowP) * (isGlobal ?
|
|
206
|
+
score: calculateStrength(entry, nowP) * (isGlobal ? GLOBAL_DISCOUNT : 1),
|
|
208
207
|
tokens: plan.price(entry, isGlobal),
|
|
209
208
|
isGlobal,
|
|
210
209
|
}));
|
|
@@ -225,7 +224,7 @@ export function selectStrongest(plan, left, pools) {
|
|
|
225
224
|
const globalRanked = globalPool
|
|
226
225
|
.map((e) => ({
|
|
227
226
|
entry: e,
|
|
228
|
-
score: calculateStrength(e, now) *
|
|
227
|
+
score: calculateStrength(e, now) * GLOBAL_DISCOUNT,
|
|
229
228
|
tokens: plan.price(e, true),
|
|
230
229
|
isGlobal: true,
|
|
231
230
|
}))
|
|
@@ -275,8 +274,7 @@ function contextVectorSpec(ctx, plan, admit) {
|
|
|
275
274
|
return { tenantId: ctx.tenantId, scope: recallScopeFilter(plan.exactScope, 'exact'), includeSuperseded: false, admit };
|
|
276
275
|
}
|
|
277
276
|
async function searchLocalRows(ctx, plan, left, minResults, localEntries, admit) {
|
|
278
|
-
const { cost, price, primaryIsGlobal, query } = plan;
|
|
279
|
-
const ctxConfig = loadConfig(ctx.hippoRoot);
|
|
277
|
+
const { cost, price, primaryIsGlobal, query, config: ctxConfig } = plan;
|
|
280
278
|
const usePhysicsCtx = ctxConfig.physics?.enabled !== false;
|
|
281
279
|
const localCost = cost && ((r) => price(r.entry, primaryIsGlobal));
|
|
282
280
|
const vectorCandidates = contextVectorSpec(ctx, plan, admit);
|
|
@@ -3,22 +3,17 @@ import type { SessionHandoff } from '../handoff.js';
|
|
|
3
3
|
import type { MemoryEntry } from '../memory.js';
|
|
4
4
|
import type { DeliveryObserver } from '../delivery-recorder.js';
|
|
5
5
|
import type { AmbientState } from '../ambient.js';
|
|
6
|
+
import type { ProjectRef } from '../project-identity.js';
|
|
6
7
|
/**
|
|
7
8
|
* Options for `getContext` — assemble a budget-bounded context bundle
|
|
8
9
|
* (recalled memories + active task snapshot + handoff + recent events).
|
|
9
|
-
* Extracted from `cmdContext` in `cli.ts` in Episode A of the api.ts refactor.
|
|
10
10
|
*
|
|
11
11
|
* Named `getContext` (not `context`) to avoid collision with the `Context`
|
|
12
12
|
* interface above and the ubiquitous `ctx: Context` convention. Follows the
|
|
13
13
|
* existing `getEntry` naming pattern in store.ts.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* helpers (`printContextMarkdown`, `printActiveTaskSnapshot`, `printHandoff`,
|
|
18
|
-
* `printSessionEvents`) are shared with `cmdRecall` / `cmdSnapshot` /
|
|
19
|
-
* `cmdHandoffShow` — moving them into api.ts would expand T5 to also rewire
|
|
20
|
-
* those commands. CLI handles rendering + auto-resolution. Episode B can add
|
|
21
|
-
* `api.renderContext` once a shared rendering need actually materializes.
|
|
15
|
+
* Rendering opts (`format`, `framing`, `rendered`) and host-side opts (`auto`) stay in the CLI,
|
|
16
|
+
* because its print helpers are shared with `cmdRecall` / `cmdSnapshot` / `cmdHandoffShow`.
|
|
22
17
|
*/
|
|
23
18
|
export interface ContextOpts {
|
|
24
19
|
q?: string;
|
|
@@ -30,7 +25,7 @@ export interface ContextOpts {
|
|
|
30
25
|
/** Envelope scope to match exactly, as in `recall`: admits that scope even when private, after the actor's scope check. */
|
|
31
26
|
exactScope?: string;
|
|
32
27
|
/** With `pinnedOnly`, also inject the N most recent writes that pass the
|
|
33
|
-
* quality floor (`isContentWorthStoring
|
|
28
|
+
* quality floor (`isContentWorthStoring`). Filtering happens BEFORE
|
|
34
29
|
* the take-N, so a caller asking for 5 gets 5 qualifying entries rather
|
|
35
30
|
* than 5-minus-junk; pinned entries bypass the floor. Entries are only
|
|
36
31
|
* skipped for this read, never mutated or deleted. Ignored when
|
|
@@ -40,12 +35,12 @@ export interface ContextOpts {
|
|
|
40
35
|
* origin partition excludes by default. They come back tagged
|
|
41
36
|
* `category: 'cross-project'` so renderers can demarcate them. */
|
|
42
37
|
crossProject?: boolean;
|
|
43
|
-
/** The active project
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* should pass it
|
|
47
|
-
currentProject?:
|
|
48
|
-
/**
|
|
38
|
+
/** The active project for the origin partition ('' = not in a project): a
|
|
39
|
+
* name, or an identity whose rows may also carry its legacy folder name.
|
|
40
|
+
* Defaults to `resolveProjectIdentity(process.cwd())`; surfaces whose
|
|
41
|
+
* process cwd is not the caller's project (HTTP server) should pass it. */
|
|
42
|
+
currentProject?: ProjectRef;
|
|
43
|
+
/** The calling
|
|
49
44
|
* session's id. Stamped on this call's recall trace, and the owner-match input to
|
|
50
45
|
* `loadFreshActiveTaskSnapshot` — when it strictly equals the active
|
|
51
46
|
* snapshot's `session_id`, the read is unbounded (same-session
|
|
@@ -54,7 +49,7 @@ export interface ContextOpts {
|
|
|
54
49
|
* it just means every snapshot goes through the age check. Host-resolved
|
|
55
50
|
* (stdin payload, HIPPO_SESSION_ID, else the host's session var) so this stays host-agnostic. */
|
|
56
51
|
currentSessionId?: string | null;
|
|
57
|
-
/**
|
|
52
|
+
/** Raw hook-payload prompt; only the pinned-only branch reads it, gated on `pinnedInject.promptRecall`. */
|
|
58
53
|
prompt?: string;
|
|
59
54
|
/** What the budget pays for, from the caller that renders the block. Absent = the memory text alone. */
|
|
60
55
|
cost?: ContextCost;
|
|
@@ -83,7 +78,7 @@ export interface ContextResultEntry {
|
|
|
83
78
|
tokens: number;
|
|
84
79
|
isGlobal?: boolean;
|
|
85
80
|
isFreshTail?: boolean;
|
|
86
|
-
/**
|
|
81
|
+
/** Admitted by the prompt-recall gate, not the recent-N backfill or a pin. */
|
|
87
82
|
promptRecall?: boolean;
|
|
88
83
|
/** v39: the entry's owning project ('' = user-global, null = legacy row). */
|
|
89
84
|
origin?: string | null;
|
package/dist/api/context.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { type MemoryEntry } from '../memory.js';
|
|
2
|
+
import { type ProjectRef } from '../project-identity.js';
|
|
2
3
|
import type { ContextOpts, ContextResult } from './context-types.js';
|
|
3
4
|
import type { Context } from './types.js';
|
|
4
5
|
export { oneCopyPerMemory } from './context-select.js';
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
+
* The secret half of the ambient policy on its own, for callers
|
|
7
8
|
* that apply their own scope rule. A flagged row is only admitted inside its owning project;
|
|
8
9
|
* flagged rows with no project origin never ambient-inject.
|
|
9
10
|
*/
|
|
10
|
-
export declare function ambientSecretAdmit(e: MemoryEntry,
|
|
11
|
+
export declare function ambientSecretAdmit(e: MemoryEntry, currentProject: ProjectRef): boolean;
|
|
11
12
|
/** Most rows per store a no-query context reads; past it, ranking and ambientState see the strongest by decay. */
|
|
12
13
|
export declare const CONTEXT_CANDIDATE_CAP = 2000;
|
|
13
14
|
/**
|
package/dist/api/context.js
CHANGED
|
@@ -15,7 +15,7 @@ import { writeRecallTraceAtRoot } from '../recall-trace.js';
|
|
|
15
15
|
import { evalNow, isRecallBoostAblated } from '../ablation.js';
|
|
16
16
|
import { dropHeldCopies } from '../same-text.js';
|
|
17
17
|
import { loadConfig } from '../config.js';
|
|
18
|
-
import { resolveProjectIdentity, classifyOriginProject, isGlobalStoreRoot } from '../project-identity.js';
|
|
18
|
+
import { resolveProjectIdentity, classifyOriginProject, isGlobalStoreRoot, projectId, projectNames } from '../project-identity.js';
|
|
19
19
|
import { promptTokens } from '../prompt-recall.js';
|
|
20
20
|
import { detectSecret } from '../secret-detect.js';
|
|
21
21
|
import { isSessionDigestRow } from '../session-digest.js';
|
|
@@ -28,36 +28,36 @@ export { oneCopyPerMemory } from './context-select.js';
|
|
|
28
28
|
* v39: the single ambient-injection admission policy, shared by getContext
|
|
29
29
|
* and the CLI-side ambient-state summary so the two cannot drift.
|
|
30
30
|
*
|
|
31
|
-
* -
|
|
31
|
+
* - Secret veto is UNCONDITIONAL: neither crossProject nor
|
|
32
32
|
* contextProjectIsolation:false re-includes secrets. A flagged row only
|
|
33
33
|
* injects inside its owning project; flagged rows with no project origin
|
|
34
34
|
* (''/null) never ambient-inject at all. Explicit recall is unaffected -
|
|
35
35
|
* recalling a secret is a deliberate act.
|
|
36
|
-
* -
|
|
37
|
-
* -
|
|
36
|
+
* - Envelope parity: private/quarantine scopes never inject unless `exactScope` names one.
|
|
37
|
+
* - Origin partition: other-project rows are excluded unless
|
|
38
38
|
* `includeCrossProject`.
|
|
39
39
|
*/
|
|
40
|
-
function ambientAdmitEntry(e,
|
|
41
|
-
if (!ambientSecretAdmit(e,
|
|
40
|
+
function ambientAdmitEntry(e, currentProject, includeCrossProject, exactScope) {
|
|
41
|
+
if (!ambientSecretAdmit(e, currentProject))
|
|
42
42
|
return false;
|
|
43
43
|
if (!passesScopeFilterForRecall(e.scope ?? null, exactScope))
|
|
44
44
|
return false;
|
|
45
45
|
if (includeCrossProject)
|
|
46
46
|
return true;
|
|
47
|
-
return classifyOriginProject(e.origin_project,
|
|
47
|
+
return classifyOriginProject(e.origin_project, currentProject) !== 'cross-project';
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
50
|
+
* The secret half of the ambient policy on its own, for callers
|
|
51
51
|
* that apply their own scope rule. A flagged row is only admitted inside its owning project;
|
|
52
52
|
* flagged rows with no project origin never ambient-inject.
|
|
53
53
|
*/
|
|
54
|
-
export function ambientSecretAdmit(e,
|
|
54
|
+
export function ambientSecretAdmit(e, currentProject) {
|
|
55
55
|
if (!detectSecret(e).flagged)
|
|
56
56
|
return true;
|
|
57
57
|
const origin = e.origin_project;
|
|
58
58
|
if (origin === undefined || origin === null || origin === '')
|
|
59
59
|
return false;
|
|
60
|
-
return origin
|
|
60
|
+
return projectNames(currentProject).includes(origin);
|
|
61
61
|
}
|
|
62
62
|
/** Most rows per store a no-query context reads; past it, ranking and ambientState see the strongest by decay. */
|
|
63
63
|
export const CONTEXT_CANDIDATE_CAP = 2000;
|
|
@@ -69,7 +69,7 @@ function loadAmbientEntries(hippoRoot, tenantId, pinnedOnly, includeRecent, admi
|
|
|
69
69
|
: loadContextCandidates(hippoRoot, tenantId, window);
|
|
70
70
|
return { entries: rows.filter(admit) };
|
|
71
71
|
}
|
|
72
|
-
//
|
|
72
|
+
// The quality floor runs on the recent-N slice AFTER this load, so the load
|
|
73
73
|
// counts by it too, or it stops short of a store whose newest rows are junk.
|
|
74
74
|
const admitAmbient = (e) => {
|
|
75
75
|
if (!admit(e))
|
|
@@ -113,7 +113,7 @@ export async function getContext(ctx, opts = {}) {
|
|
|
113
113
|
return { entries: [], tokens: 0 };
|
|
114
114
|
}
|
|
115
115
|
const picked = plan.pinnedOnly
|
|
116
|
-
? selectPinned(
|
|
116
|
+
? selectPinned(opts, plan, sections.left, pools, admission)
|
|
117
117
|
: plan.query === '*'
|
|
118
118
|
? selectStrongest(plan, sections.left, pools)
|
|
119
119
|
: await selectBySearch(ctx, plan, sections.left, pools, admission);
|
|
@@ -153,13 +153,13 @@ function planContext(ctx, opts) {
|
|
|
153
153
|
// excluded unless the caller asks for them (crossProject) or isolation is disabled.
|
|
154
154
|
const config = loadConfig(ctx.hippoRoot);
|
|
155
155
|
const isolationEnabled = config.contextProjectIsolation !== false;
|
|
156
|
-
const
|
|
156
|
+
const currentProject = opts.currentProject ?? resolveProjectIdentity(process.cwd());
|
|
157
157
|
const includeCrossProject = opts.crossProject === true || !isolationEnabled;
|
|
158
158
|
// Decided before the ambient loads so the pinned-only FTS candidate query can share their connection.
|
|
159
159
|
const promptRecallPending = pinnedOnly && Boolean(opts.prompt?.trim()) && config.pinnedInject.promptRecall === true;
|
|
160
160
|
const cost = opts.cost;
|
|
161
161
|
const price = (entry, isGlobal, promptRecall) => cost
|
|
162
|
-
? cost.entry({ entry, isGlobal, promptRecall, origin: entry.origin_project ?? null, category: classifyOriginProject(entry.origin_project,
|
|
162
|
+
? cost.entry({ entry, isGlobal, promptRecall, origin: entry.origin_project ?? null, category: classifyOriginProject(entry.origin_project, currentProject) })
|
|
163
163
|
: estimateTokens(entry.content);
|
|
164
164
|
return {
|
|
165
165
|
pinnedOnly,
|
|
@@ -174,9 +174,9 @@ function planContext(ctx, opts) {
|
|
|
174
174
|
primaryIsGlobal,
|
|
175
175
|
hasLocalTaskState: hasLocal && !primaryIsGlobal,
|
|
176
176
|
config,
|
|
177
|
-
|
|
177
|
+
currentProject,
|
|
178
178
|
includeCrossProject,
|
|
179
|
-
originProject: includeCrossProject ||
|
|
179
|
+
originProject: includeCrossProject || projectId(currentProject) === '' ? undefined : projectNames(currentProject),
|
|
180
180
|
promptRecallPending,
|
|
181
181
|
cost,
|
|
182
182
|
price,
|
|
@@ -197,7 +197,7 @@ function promptRecallRequest(opts, plan) {
|
|
|
197
197
|
function openBlockBudget(plan, opts, budget) {
|
|
198
198
|
const { config, cost, obs, pinnedOnly, promptRecallPending } = plan;
|
|
199
199
|
const blockBudget = pinnedOnly && opts.budget === undefined ? config.pinnedInject.budget : budget;
|
|
200
|
-
obs?.facts({ projectName: plan.
|
|
200
|
+
obs?.facts({ projectName: projectId(plan.currentProject), budgetTokens: blockBudget, promptRecall: promptRecallPending });
|
|
201
201
|
if (pinnedOnly && !config.pinnedInject.enabled)
|
|
202
202
|
obs?.disabled();
|
|
203
203
|
return cost
|
|
@@ -261,7 +261,7 @@ function ambientAdmission(opts, plan, shownHandoff) {
|
|
|
261
261
|
digestHiddenForHandoff = true;
|
|
262
262
|
return false;
|
|
263
263
|
}
|
|
264
|
-
return ambientAdmitEntry(e, plan.
|
|
264
|
+
return ambientAdmitEntry(e, plan.currentProject, plan.includeCrossProject, plan.exactScope);
|
|
265
265
|
};
|
|
266
266
|
const ownSessionId = opts.currentSessionId || '';
|
|
267
267
|
// Inside admit, not after the load, so the loader's window widens past a session's own items.
|
|
@@ -316,7 +316,7 @@ function finalizeSelection(picked, plan) {
|
|
|
316
316
|
selected = selected.map((r) => ({
|
|
317
317
|
...r,
|
|
318
318
|
origin: r.entry.origin_project ?? null,
|
|
319
|
-
category: classifyOriginProject(r.entry.origin_project, plan.
|
|
319
|
+
category: classifyOriginProject(r.entry.origin_project, plan.currentProject),
|
|
320
320
|
}));
|
|
321
321
|
obs?.selected(selected);
|
|
322
322
|
return { items: selected, tokens };
|
|
@@ -369,7 +369,7 @@ function recordRetrieval(ctx, opts, plan, selected, activeSnapshot) {
|
|
|
369
369
|
return { items, ambientState: plan.config.ambient.enabled ? readAmbientState(ctx, plan) : undefined };
|
|
370
370
|
}
|
|
371
371
|
function readAmbientState(ctx, plan) {
|
|
372
|
-
const filter = { exactScope: plan.exactScope, project: plan.originProject, currentProject: plan.
|
|
372
|
+
const filter = { exactScope: plan.exactScope, project: plan.originProject, currentProject: projectNames(plan.currentProject), now: evalNow() };
|
|
373
373
|
const roots = [...(plan.hasLocal ? [ctx.hippoRoot] : []), ...(plan.hasGlobal && !plan.primaryIsGlobal ? [plan.globalRoot] : [])];
|
|
374
374
|
const tallies = roots.map((root) => loadAmbientTallies(root, ctx.tenantId, filter));
|
|
375
375
|
const total = tallies.length > 0 ? tallies.reduce(addAmbientTallies) : undefined;
|
package/dist/api/dormant.js
CHANGED
|
@@ -63,7 +63,7 @@ export function restoreDormant(ctx, id) {
|
|
|
63
63
|
writeEntryDbOnly(db, restored, { actor: ctx.actor.subject });
|
|
64
64
|
deleteDormantRow(db, ctx.tenantId, id);
|
|
65
65
|
// A restore is a labelled "forgot it, then needed it" event: the
|
|
66
|
-
// signal a learned lifecycle
|
|
66
|
+
// signal a learned lifecycle trains on. Same transaction
|
|
67
67
|
// as the restore, so the label exists exactly when the restore does.
|
|
68
68
|
appendAuditEvent(db, {
|
|
69
69
|
tenantId: ctx.tenantId,
|
package/dist/api/drill-down.d.ts
CHANGED
|
@@ -73,11 +73,11 @@ export type DrillDownOutcome = DrillDownResult | DrillDownFailure;
|
|
|
73
73
|
* if the underlying DAG accidentally linked across scopes.
|
|
74
74
|
*
|
|
75
75
|
* Returns a discriminated `DrillDownOutcome`: `DrillDownResult` on success,
|
|
76
|
-
* or `{failure: '...'}` for `not_found` (covers genuinely-missing
|
|
77
|
-
* tenant, intentionally indistinguishable)
|
|
78
|
-
*
|
|
76
|
+
* or `{failure: '...'}` for `not_found` (covers genuinely-missing, wrong-
|
|
77
|
+
* tenant and scope-blocked, intentionally indistinguishable) or
|
|
78
|
+
* `not_drillable` (id is a leaf row).
|
|
79
79
|
*
|
|
80
|
-
* Pre-v1.6.4 returned null for all
|
|
80
|
+
* Pre-v1.6.4 returned null for all three cases. JS callers migrate via
|
|
81
81
|
* `'failure' in result` checks; HTTP route maps `not_drillable` to 422.
|
|
82
82
|
*/
|
|
83
83
|
export declare function drillDown(ctx: Context, summaryId: string, opts?: DrillDownOpts): DrillDownOutcome;
|
package/dist/api/drill-down.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// DAG drill-down from a summary to its children.
|
|
2
|
-
import {
|
|
2
|
+
import { closeHippoDb } from '../db.js';
|
|
3
|
+
import { openStore } from '../store/open.js';
|
|
4
|
+
import { selectEntriesByIds, selectChildrenByParent } from '../store/entry-reads.js';
|
|
3
5
|
import { estimateTokens } from '../token-ledger.js';
|
|
4
6
|
import { passesScopeFilterForRecall } from '../recall-scope.js';
|
|
5
7
|
/**
|
|
@@ -14,11 +16,11 @@ import { passesScopeFilterForRecall } from '../recall-scope.js';
|
|
|
14
16
|
* if the underlying DAG accidentally linked across scopes.
|
|
15
17
|
*
|
|
16
18
|
* Returns a discriminated `DrillDownOutcome`: `DrillDownResult` on success,
|
|
17
|
-
* or `{failure: '...'}` for `not_found` (covers genuinely-missing
|
|
18
|
-
* tenant, intentionally indistinguishable)
|
|
19
|
-
*
|
|
19
|
+
* or `{failure: '...'}` for `not_found` (covers genuinely-missing, wrong-
|
|
20
|
+
* tenant and scope-blocked, intentionally indistinguishable) or
|
|
21
|
+
* `not_drillable` (id is a leaf row).
|
|
20
22
|
*
|
|
21
|
-
* Pre-v1.6.4 returned null for all
|
|
23
|
+
* Pre-v1.6.4 returned null for all three cases. JS callers migrate via
|
|
22
24
|
* `'failure' in result` checks; HTTP route maps `not_drillable` to 422.
|
|
23
25
|
*/
|
|
24
26
|
export function drillDown(ctx, summaryId, opts = {}) {
|
|
@@ -26,8 +28,17 @@ export function drillDown(ctx, summaryId, opts = {}) {
|
|
|
26
28
|
// v0.30 / E5: depth defaults 1 (backward compat); hard cap 10 levels
|
|
27
29
|
// prevents pathological deep trees. CLI/HTTP/MCP reject invalid values.
|
|
28
30
|
const depth = Math.max(1, Math.min(Math.trunc(opts.depth ?? 1), 10));
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
+
const db = openStore(ctx.hippoRoot);
|
|
32
|
+
try {
|
|
33
|
+
return drillDownOn(db, ctx, summaryId, depth, opts, limit);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
closeHippoDb(db);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function drillDownOn(db, ctx, summaryId, depth, opts, limit) {
|
|
40
|
+
const summary = selectEntriesByIds(db, [summaryId], ctx.tenantId).get(summaryId) ?? null;
|
|
41
|
+
// No unscoped cross-tenant probe here: the tenant-scoped read's miss covers
|
|
31
42
|
// both "doesn't exist" and "exists in another tenant" by design.
|
|
32
43
|
// Distinguishing them via an unscoped lookup would leak existence to
|
|
33
44
|
// unauthorised tenants. The two cases collapse into not_found.
|
|
@@ -42,7 +53,7 @@ export function drillDown(ctx, summaryId, opts = {}) {
|
|
|
42
53
|
// already preventing. Match the HTTP behaviour at the API level.
|
|
43
54
|
return { failure: 'not_found' };
|
|
44
55
|
}
|
|
45
|
-
const { collected, level0DirectCount } = collectDescendants(ctx, summaryId, depth);
|
|
56
|
+
const { collected, level0DirectCount } = collectDescendants(db, ctx.tenantId, summaryId, depth);
|
|
46
57
|
const summaryOut = {
|
|
47
58
|
id: summary.id,
|
|
48
59
|
content: summary.content,
|
|
@@ -72,15 +83,16 @@ export function drillDown(ctx, summaryId, opts = {}) {
|
|
|
72
83
|
}
|
|
73
84
|
// BFS with a visited set: dag_parent_id is not unique, so a misconfigured tree could emit a child twice past depth 1.
|
|
74
85
|
// The level-0 count is kept apart so a legacy summary's descendantCount fallback counts direct children only.
|
|
75
|
-
function collectDescendants(
|
|
86
|
+
function collectDescendants(db, tenantId, summaryId, depth) {
|
|
76
87
|
const collected = [];
|
|
77
88
|
const visited = new Set([summaryId]);
|
|
78
89
|
let frontier = [summaryId];
|
|
79
90
|
let level0DirectCount = 0;
|
|
80
91
|
for (let level = 0; level < depth; level++) {
|
|
81
92
|
const nextFrontier = [];
|
|
93
|
+
const kidsByParent = selectChildrenByParent(db, frontier, tenantId);
|
|
82
94
|
for (const parentId of frontier) {
|
|
83
|
-
const kids =
|
|
95
|
+
const kids = kidsByParent.get(parentId) ?? [];
|
|
84
96
|
const eligibleKids = kids.filter((c) => passesScopeFilterForRecall(c.scope ?? null, undefined));
|
|
85
97
|
for (const k of eligibleKids) {
|
|
86
98
|
if (visited.has(k.id))
|
package/dist/api/outcome.d.ts
CHANGED
|
@@ -8,14 +8,13 @@ import type { Context } from './types.js';
|
|
|
8
8
|
* op='outcome' tagged with ctx.actor.subject.
|
|
9
9
|
*
|
|
10
10
|
* Returns `{applied, appliedIds}`. `appliedIds` is the tenant-filtered subset
|
|
11
|
-
* of input ids that actually had `applyOutcome` run on them (i.e. ids
|
|
12
|
-
*
|
|
11
|
+
* of input ids that actually had `applyOutcome` run on them (i.e. ids found
|
|
12
|
+
* in ctx.tenantId). Callers that surface the id list
|
|
13
13
|
* over a multi-tenant boundary (HTTP /v1/outcome last-recall path, Python SDK)
|
|
14
14
|
* MUST return `appliedIds` instead of the raw input list — otherwise the
|
|
15
|
-
* non-applied (cross-tenant) ids leak to the caller.
|
|
16
|
-
* close that disclosure path on POST /v1/outcome.
|
|
15
|
+
* non-applied (cross-tenant) ids leak to the caller.
|
|
17
16
|
*
|
|
18
|
-
* `opts.traceId
|
|
17
|
+
* `opts.traceId`:
|
|
19
18
|
* OPTIONAL additive opt so a programmatic caller can link this outcome to
|
|
20
19
|
* the recall_traces row it judges. NOT applied unconditionally — an SDK
|
|
21
20
|
* caller passing explicit ids with no preceding CLI/context recall would
|
|
@@ -36,18 +35,14 @@ export declare function outcome(ctx: Context, ids: ReadonlyArray<string>, good:
|
|
|
36
35
|
*
|
|
37
36
|
* Reads `loadIndex(ctx.hippoRoot).last_retrieval_ids` (per-hippoRoot local
|
|
38
37
|
* state; not tenant-scoped at the index layer) and forwards to `outcome()`,
|
|
39
|
-
* which DOES tenant-filter
|
|
38
|
+
* which DOES tenant-filter its read by `ctx.tenantId`. Cross-tenant
|
|
40
39
|
* ids in `last_retrieval_ids` are silently skipped, matching the MCP
|
|
41
40
|
* `hippo_outcome` semantics.
|
|
42
41
|
*
|
|
43
|
-
* **Tenant-safe response shape
|
|
42
|
+
* **Tenant-safe response shape:** the returned `ids`
|
|
44
43
|
* field contains ONLY the tenant-filtered subset that actually had outcomes
|
|
45
|
-
* applied (i.e. `appliedIds` from the inner `outcome()` call).
|
|
46
|
-
*
|
|
47
|
-
* leaked cross-tenant memory IDs to the caller via POST /v1/outcome's
|
|
48
|
-
* no-body last-recall response. The fix is at this helper so all callers
|
|
49
|
-
* (CLI cmdOutcome, HTTP /v1/outcome, MCP `hippo_outcome` if added later)
|
|
50
|
-
* inherit the tenant-safe contract.
|
|
44
|
+
* applied (i.e. `appliedIds` from the inner `outcome()` call). It lives in this
|
|
45
|
+
* helper so every caller (CLI, HTTP /v1/outcome, MCP) inherits the contract.
|
|
51
46
|
*
|
|
52
47
|
* Do NOT tighten `loadIndex` with `tenantId` inside this helper — doing so
|
|
53
48
|
* would break the (correct) cross-tenant-silent-skip behavior covered by
|
package/dist/api/outcome.js
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
// Outcome feedback on recalled memories.
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { closeHippoDb } from '../db.js';
|
|
3
|
+
import { openStore } from '../store/open.js';
|
|
4
|
+
import { writeEntryOn } from '../store/entry-writes.js';
|
|
5
|
+
import { selectEntriesByIds } from '../store/entry-reads.js';
|
|
5
6
|
import { loadIndex } from '../store/index-and-stats.js';
|
|
6
7
|
import { applyOutcome, CHURN_STALE_TAG } from '../memory.js';
|
|
7
8
|
import { appendAuditEvent } from '../audit.js';
|
|
8
9
|
import { recordTraceOutcome } from '../recall-trace.js';
|
|
9
10
|
export function outcome(ctx, ids, good, opts) {
|
|
10
11
|
const appliedIds = [];
|
|
11
|
-
const db =
|
|
12
|
+
const db = openStore(ctx.hippoRoot);
|
|
12
13
|
try {
|
|
14
|
+
const live = selectEntriesByIds(db, ids, ctx.tenantId);
|
|
13
15
|
for (const id of ids) {
|
|
14
|
-
const entry =
|
|
16
|
+
const entry = live.get(id);
|
|
15
17
|
if (!entry)
|
|
16
18
|
continue;
|
|
17
19
|
let updated = applyOutcome(entry, good);
|
|
18
|
-
if (good && updated.tags.includes(CHURN_STALE_TAG)) { //
|
|
20
|
+
if (good && updated.tags.includes(CHURN_STALE_TAG)) { // a good outcome reconfirms the entry
|
|
19
21
|
updated = { ...updated, tags: updated.tags.filter((t) => t !== CHURN_STALE_TAG) };
|
|
20
22
|
}
|
|
21
|
-
|
|
23
|
+
writeEntryOn(db, ctx.hippoRoot, updated, { actor: ctx.actor.subject });
|
|
24
|
+
live.set(id, updated); // a repeated id builds on its first outcome, as a fresh read would
|
|
22
25
|
appendAuditEvent(db, {
|
|
23
26
|
tenantId: ctx.tenantId,
|
|
24
27
|
actor: ctx.actor.subject,
|
|
@@ -28,7 +31,7 @@ export function outcome(ctx, ids, good, opts) {
|
|
|
28
31
|
});
|
|
29
32
|
appliedIds.push(id);
|
|
30
33
|
}
|
|
31
|
-
//
|
|
34
|
+
// Link the outcome to its trace, recording only the ids actually
|
|
32
35
|
// credited (post tenant-filtering, matches appliedIds). Lives in its own
|
|
33
36
|
// append-only table so audit_log pruning can never erase training data.
|
|
34
37
|
if (opts?.traceId !== undefined && appliedIds.length > 0) {
|
|
@@ -50,15 +53,8 @@ export function outcomeForLastRecall(ctx, good) {
|
|
|
50
53
|
const ids = idx.last_retrieval_ids;
|
|
51
54
|
if (ids.length === 0)
|
|
52
55
|
return { applied: 0, ids: [] };
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// (idx.last_trace_id) — a single-snapshot read, not a second DB round
|
|
56
|
-
// trip via a now-deleted readLastTraceId helper. The value is already
|
|
57
|
-
// strict-parsed by buildIndexFromDb's parseLastTraceId (store.ts): every
|
|
58
|
-
// consumer gets a clean positive-integer string or null, never a garbage
|
|
59
|
-
// value that could reach outcome() and INSERT trace_id=0/NaN. null on a
|
|
60
|
-
// fresh store / pre-v40 flow / api.recall-only usage — outcome() skips
|
|
61
|
-
// linkage silently when traceId is undefined.
|
|
56
|
+
// Same `loadIndex` snapshot as the ids; buildIndexFromDb already strict-parses it to a
|
|
57
|
+
// positive-integer string or null, so no trace_id=0/NaN reaches outcome().
|
|
62
58
|
const traceId = idx.last_trace_id !== null ? Number(idx.last_trace_id) : null;
|
|
63
59
|
const { applied, appliedIds } = outcome(ctx, ids, good, traceId !== null ? { traceId } : undefined);
|
|
64
60
|
return { applied, ids: appliedIds };
|
package/dist/api/promote.d.ts
CHANGED
|
@@ -8,8 +8,7 @@ import type { Context } from './types.js';
|
|
|
8
8
|
*
|
|
9
9
|
* Note: `promoteToGlobal` does not currently take a tenantId override — it
|
|
10
10
|
* reads the entry from the local root via `readEntry` (no tenant filter) and
|
|
11
|
-
* preserves the entry's existing tenantId on the global side.
|
|
12
|
-
* tighten this once writeEntry/readEntry thread tenant context.
|
|
11
|
+
* preserves the entry's existing tenantId on the global side.
|
|
13
12
|
*/
|
|
14
13
|
export interface PromoteResult {
|
|
15
14
|
ok: true;
|
|
@@ -19,9 +18,8 @@ export interface PromoteResult {
|
|
|
19
18
|
export declare function promote(ctx: Context, id: string): PromoteResult;
|
|
20
19
|
/**
|
|
21
20
|
* Replace an old memory with new content, chaining old.superseded_by = new.id.
|
|
22
|
-
* Mirrors `cmdSupersede` in cli.ts
|
|
23
|
-
*
|
|
24
|
-
* flags and pass the resolved values once Task 4 lands).
|
|
21
|
+
* Mirrors `cmdSupersede` in cli.ts minus the flag-driven layer/tag/pin
|
|
22
|
+
* overrides: the CLI handler resolves those so the API stays minimal.
|
|
25
23
|
*/
|
|
26
24
|
export interface SupersedeResult {
|
|
27
25
|
ok: true;
|
|
@@ -34,13 +32,12 @@ export declare function supersede(ctx: Context, oldId: string, newContent: strin
|
|
|
34
32
|
*
|
|
35
33
|
* `archiveRawMemory` audits the operation internally (op='archive_raw') using the
|
|
36
34
|
* row's own tenant_id. We DO NOT emit a second audit event here to avoid double-
|
|
37
|
-
* emitting the archive_raw op
|
|
38
|
-
* helpers hardcode actor='cli'). Instead we pass `ctx.actor.subject` through as `who`,
|
|
35
|
+
* emitting the archive_raw op. Instead we pass `ctx.actor.subject` through as `who`,
|
|
39
36
|
* and raw-archive.ts uses that for the audit row.
|
|
40
37
|
*/
|
|
41
38
|
export interface ArchiveRawOpts {
|
|
42
39
|
/**
|
|
43
|
-
* Connector idempotency hook
|
|
40
|
+
* Connector idempotency hook. Runs inside the same
|
|
44
41
|
* SAVEPOINT as the archive — throwing rolls the archive back. Used by the
|
|
45
42
|
* Slack deletion connector to mark the deletion event seen atomically.
|
|
46
43
|
*/
|
package/dist/api/quarantine.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { openHippoDb, closeHippoDb } from '../db.js';
|
|
3
3
|
import { ConflictError, ForbiddenError, NotFoundError } from '../api-errors.js';
|
|
4
4
|
import { writeEntryMirrors } from '../store/entry-writes.js';
|
|
5
|
-
import { readEntry } from '../store/entry-reads.js';
|
|
5
|
+
import { readEntry, selectEntriesByIds } from '../store/entry-reads.js';
|
|
6
6
|
import { quarantineScopeFor, getQuarantineRow, listQuarantineRows, approveQuarantineRow, rejectQuarantineRow, } from '../quarantine.js';
|
|
7
7
|
import { log } from '../log.js';
|
|
8
8
|
import { appendAuditEvent } from '../audit.js';
|
|
@@ -12,8 +12,9 @@ export function quarantineList(ctx, opts = {}) {
|
|
|
12
12
|
const db = openHippoDb(ctx.hippoRoot);
|
|
13
13
|
try {
|
|
14
14
|
const rows = listQuarantineRows(db, ctx.tenantId, opts.status ?? 'pending', opts.limit, opts.after);
|
|
15
|
+
const entries = selectEntriesByIds(db, rows.map((row) => row.memoryId), ctx.tenantId);
|
|
15
16
|
return rows.map((row) => {
|
|
16
|
-
const entry =
|
|
17
|
+
const entry = entries.get(row.memoryId);
|
|
17
18
|
return {
|
|
18
19
|
id: row.memoryId,
|
|
19
20
|
originalScope: row.originalScope,
|