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.
Files changed (370) hide show
  1. package/README.md +9 -0
  2. package/dist/agent-memories/apply.d.ts +2 -0
  3. package/dist/agent-memories/apply.js +23 -1
  4. package/dist/agent-memories/claude-code.d.ts +2 -1
  5. package/dist/agent-memories/claude-code.js +4 -4
  6. package/dist/agent-memories/report.d.ts +1 -0
  7. package/dist/agent-memories/report.js +2 -2
  8. package/dist/agent-memories/sync.js +37 -23
  9. package/dist/ambient-store.d.ts +4 -4
  10. package/dist/ambient-store.js +4 -3
  11. package/dist/api/assemble.d.ts +7 -10
  12. package/dist/api/assemble.js +7 -12
  13. package/dist/api/audit.d.ts +2 -2
  14. package/dist/api/audit.js +2 -2
  15. package/dist/api/auth.d.ts +6 -6
  16. package/dist/api/auth.js +5 -7
  17. package/dist/api/context-select.d.ts +4 -3
  18. package/dist/api/context-select.js +9 -11
  19. package/dist/api/context-types.d.ts +12 -17
  20. package/dist/api/context.d.ts +3 -2
  21. package/dist/api/context.js +20 -20
  22. package/dist/api/dormant.js +1 -1
  23. package/dist/api/drill-down.d.ts +4 -4
  24. package/dist/api/drill-down.js +22 -10
  25. package/dist/api/outcome.d.ts +8 -13
  26. package/dist/api/outcome.js +13 -17
  27. package/dist/api/promote.d.ts +5 -8
  28. package/dist/api/quarantine.js +3 -2
  29. package/dist/api/recall-types.d.ts +43 -55
  30. package/dist/api/recall.d.ts +3 -3
  31. package/dist/api/recall.js +7 -18
  32. package/dist/api/remember.d.ts +2 -2
  33. package/dist/api/sleep.d.ts +7 -35
  34. package/dist/api/sleep.js +3 -2
  35. package/dist/api/tokens.d.ts +2 -2
  36. package/dist/api/tokens.js +2 -2
  37. package/dist/api/types.d.ts +6 -10
  38. package/dist/api/types.js +2 -4
  39. package/dist/audit-prune.d.ts +4 -6
  40. package/dist/audit-prune.js +3 -5
  41. package/dist/audit.js +11 -33
  42. package/dist/auth.d.ts +8 -9
  43. package/dist/auth.js +5 -7
  44. package/dist/autolearn.d.ts +1 -1
  45. package/dist/autolearn.js +1 -1
  46. package/dist/availability.js +3 -5
  47. package/dist/capture/command.d.ts +5 -4
  48. package/dist/capture/command.js +8 -22
  49. package/dist/capture/compact.d.ts +1 -1
  50. package/dist/capture/compact.js +9 -13
  51. package/dist/capture/extract.js +37 -115
  52. package/dist/capture-error.d.ts +1 -1
  53. package/dist/capture-error.js +1 -1
  54. package/dist/churn-git.d.ts +1 -1
  55. package/dist/churn-git.js +1 -1
  56. package/dist/cli/audit.js +3 -4
  57. package/dist/cli/auth.js +3 -6
  58. package/dist/cli/curate.d.ts +1 -1
  59. package/dist/cli/curate.js +8 -24
  60. package/dist/cli/dag.js +5 -9
  61. package/dist/cli/decisions.js +10 -21
  62. package/dist/cli/explain.js +2 -1
  63. package/dist/cli/goals.js +1 -1
  64. package/dist/cli/init.js +1 -1
  65. package/dist/cli/playbooks.js +4 -9
  66. package/dist/cli/projects.js +3 -1
  67. package/dist/cli/recall.js +2 -1
  68. package/dist/cli/remember.js +6 -18
  69. package/dist/cli/session-hooks.js +21 -40
  70. package/dist/cli/setup.d.ts +1 -1
  71. package/dist/cli/setup.js +2 -6
  72. package/dist/cli/shared.js +3 -3
  73. package/dist/cli/slack.js +1 -1
  74. package/dist/cli/sleep.js +27 -1
  75. package/dist/cli/status.d.ts +4 -4
  76. package/dist/cli/status.js +7 -12
  77. package/dist/cli/transfer.js +8 -13
  78. package/dist/cli/usage.js +9 -6
  79. package/dist/cli.d.ts +1 -1
  80. package/dist/cli.js +4 -9
  81. package/dist/compaction-record.d.ts +2 -0
  82. package/dist/compaction-record.js +89 -68
  83. package/dist/compare.d.ts +11 -16
  84. package/dist/compare.js +11 -16
  85. package/dist/config.d.ts +19 -18
  86. package/dist/config.js +90 -67
  87. package/dist/connectors/github/backfill.d.ts +2 -2
  88. package/dist/connectors/github/backfill.js +8 -15
  89. package/dist/connectors/github/cli-impl.js +3 -8
  90. package/dist/connectors/github/deletion.d.ts +5 -12
  91. package/dist/connectors/github/deletion.js +5 -12
  92. package/dist/connectors/github/dlq.d.ts +6 -9
  93. package/dist/connectors/github/dlq.js +2 -3
  94. package/dist/connectors/github/ingest.d.ts +5 -7
  95. package/dist/connectors/github/ingest.js +8 -12
  96. package/dist/connectors/github/octokit-client.d.ts +3 -5
  97. package/dist/connectors/github/octokit-client.js +5 -6
  98. package/dist/connectors/github/signature.d.ts +9 -39
  99. package/dist/connectors/github/signature.js +9 -39
  100. package/dist/connectors/github/tenant-routing.d.ts +1 -1
  101. package/dist/connectors/github/tenant-routing.js +1 -1
  102. package/dist/connectors/github/transform.js +2 -2
  103. package/dist/connectors/github/types.d.ts +2 -10
  104. package/dist/connectors/github/types.js +1 -3
  105. package/dist/connectors/slack/deletion.d.ts +3 -8
  106. package/dist/connectors/slack/deletion.js +3 -8
  107. package/dist/connectors/slack/dlq.d.ts +1 -1
  108. package/dist/connectors/slack/ingest.d.ts +1 -1
  109. package/dist/connectors/slack/ingest.js +7 -16
  110. package/dist/connectors/slack/signature.d.ts +1 -1
  111. package/dist/connectors/slack/tenant-routing.d.ts +3 -5
  112. package/dist/connectors/slack/tenant-routing.js +3 -5
  113. package/dist/connectors/slack/transform.d.ts +5 -6
  114. package/dist/connectors/slack/transform.js +5 -6
  115. package/dist/connectors/slack/types.d.ts +2 -6
  116. package/dist/connectors/slack/types.js +1 -3
  117. package/dist/connectors/slack/web-client.js +10 -3
  118. package/dist/connectors/slack/workspaces.d.ts +3 -5
  119. package/dist/connectors/slack/workspaces.js +3 -5
  120. package/dist/consolidate/conflicts.js +3 -14
  121. package/dist/consolidate/decay.js +9 -29
  122. package/dist/consolidate/llm-passes.js +4 -5
  123. package/dist/consolidate/merge.js +8 -23
  124. package/dist/consolidate/run.d.ts +1 -8
  125. package/dist/consolidate/run.js +3 -25
  126. package/dist/consolidate/sleep.js +5 -17
  127. package/dist/consolidate/traces.js +9 -21
  128. package/dist/customer-notes.d.ts +5 -7
  129. package/dist/customer-notes.js +6 -9
  130. package/dist/dag.d.ts +10 -21
  131. package/dist/dag.js +23 -73
  132. package/dist/db/continuity.js +2 -2
  133. package/dist/db/migrations/v14.js +1 -1
  134. package/dist/db/migrations/v15.js +1 -2
  135. package/dist/db/migrations/v16.js +3 -4
  136. package/dist/db/migrations/v17.js +2 -3
  137. package/dist/db/migrations/v19.js +1 -1
  138. package/dist/db/migrations/v20.js +1 -1
  139. package/dist/db/migrations/v21.js +2 -6
  140. package/dist/db/migrations/v22.js +2 -4
  141. package/dist/db/migrations/v23.js +1 -1
  142. package/dist/db/migrations/v24.js +4 -6
  143. package/dist/db/migrations/v25.js +2 -3
  144. package/dist/db/migrations/v26.js +3 -3
  145. package/dist/db/migrations/v27.js +2 -10
  146. package/dist/db/migrations/v28.js +5 -8
  147. package/dist/db/migrations/v29.js +3 -4
  148. package/dist/db/migrations/v30.js +2 -2
  149. package/dist/db/migrations/v31.js +1 -1
  150. package/dist/db/migrations/v32.js +1 -1
  151. package/dist/db/migrations/v33.js +3 -3
  152. package/dist/db/migrations/v34.js +1 -1
  153. package/dist/db/migrations/v35.js +3 -4
  154. package/dist/db/migrations/v36.js +3 -4
  155. package/dist/db/migrations/v37.js +5 -5
  156. package/dist/db/migrations/v38.js +7 -8
  157. package/dist/db/migrations/v39.js +1 -1
  158. package/dist/db/migrations/v40.js +4 -16
  159. package/dist/db/migrations/v41.js +3 -4
  160. package/dist/db/migrations/v42.js +3 -4
  161. package/dist/db/migrations/v45.js +1 -1
  162. package/dist/db/migrations/v46.js +1 -1
  163. package/dist/db/migrations/v47.js +1 -1
  164. package/dist/db/migrations/v48.js +1 -1
  165. package/dist/decisions.d.ts +2 -2
  166. package/dist/decisions.js +6 -6
  167. package/dist/dedupe.js +86 -61
  168. package/dist/delivery-recorder.js +154 -135
  169. package/dist/doctor.js +119 -104
  170. package/dist/dormant.js +1 -4
  171. package/dist/embedding-provider.d.ts +4 -8
  172. package/dist/embedding-provider.js +4 -8
  173. package/dist/embeddings.js +55 -47
  174. package/dist/env.d.ts +1 -1
  175. package/dist/env.js +12 -12
  176. package/dist/escape.d.ts +5 -0
  177. package/dist/escape.js +10 -0
  178. package/dist/eval-stats.d.ts +1 -2
  179. package/dist/eval-stats.js +1 -2
  180. package/dist/eval-suite.js +27 -21
  181. package/dist/extract.js +4 -9
  182. package/dist/failure-log.d.ts +3 -3
  183. package/dist/failure-log.js +1 -1
  184. package/dist/forward-claim-detector.d.ts +2 -4
  185. package/dist/forward-claim-detector.js +6 -11
  186. package/dist/goals.d.ts +3 -3
  187. package/dist/goals.js +5 -6
  188. package/dist/graph/read.d.ts +2 -2
  189. package/dist/graph/read.js +5 -6
  190. package/dist/graph/types.d.ts +8 -8
  191. package/dist/graph/write.d.ts +7 -14
  192. package/dist/graph/write.js +16 -23
  193. package/dist/graph-extract.d.ts +7 -8
  194. package/dist/graph-extract.js +62 -72
  195. package/dist/graph-recall.d.ts +2 -2
  196. package/dist/graph-recall.js +55 -49
  197. package/dist/graph-stream.d.ts +5 -6
  198. package/dist/graph-stream.js +66 -57
  199. package/dist/graph-view.d.ts +2 -2
  200. package/dist/graph-view.js +7 -7
  201. package/dist/half-life-migration.d.ts +1 -2
  202. package/dist/half-life-migration.js +2 -3
  203. package/dist/hooks/codex-session.js +1 -1
  204. package/dist/hooks/codex-wrapper.d.ts +1 -1
  205. package/dist/hooks/codex-wrapper.js +3 -2
  206. package/dist/hooks/json-hooks.d.ts +2 -2
  207. package/dist/hooks/json-hooks.js +5 -4
  208. package/dist/hooks/opencode.d.ts +1 -1
  209. package/dist/hooks/opencode.js +5 -4
  210. package/dist/hooks/shared.d.ts +3 -7
  211. package/dist/hooks/shared.js +1 -8
  212. package/dist/http-util.d.ts +2 -3
  213. package/dist/http-util.js +3 -0
  214. package/dist/importers/core.d.ts +2 -9
  215. package/dist/importers/core.js +15 -30
  216. package/dist/importers/sources.js +2 -1
  217. package/dist/importers/vault.js +2 -20
  218. package/dist/incidents.d.ts +1 -1
  219. package/dist/incidents.js +1 -1
  220. package/dist/instruction-detect.d.ts +1 -1
  221. package/dist/instruction-detect.js +1 -1
  222. package/dist/invalidation.d.ts +3 -0
  223. package/dist/invalidation.js +160 -114
  224. package/dist/json.d.ts +5 -0
  225. package/dist/json.js +4 -0
  226. package/dist/judgment.js +1 -2
  227. package/dist/local-embedding.js +1 -1
  228. package/dist/mcp/admin-tools.js +7 -17
  229. package/dist/mcp/format.js +1 -1
  230. package/dist/mcp/framing.js +3 -6
  231. package/dist/mcp/protocol.d.ts +2 -5
  232. package/dist/mcp/protocol.js +1 -3
  233. package/dist/mcp/recall-tools.js +12 -15
  234. package/dist/mcp/request.js +4 -3
  235. package/dist/mcp/session-state.js +2 -3
  236. package/dist/mcp/stdio.js +2 -1
  237. package/dist/mcp/tools.js +9 -6
  238. package/dist/memory-value-weights.d.ts +5 -8
  239. package/dist/memory-value-weights.js +5 -8
  240. package/dist/memory-value.d.ts +13 -13
  241. package/dist/memory-value.js +26 -37
  242. package/dist/memory.d.ts +20 -22
  243. package/dist/memory.js +24 -48
  244. package/dist/multihop.d.ts +1 -1
  245. package/dist/multihop.js +3 -2
  246. package/dist/owner-validation.d.ts +4 -5
  247. package/dist/owner-validation.js +4 -5
  248. package/dist/physics.d.ts +4 -4
  249. package/dist/physics.js +7 -9
  250. package/dist/policies.d.ts +9 -10
  251. package/dist/policies.js +12 -14
  252. package/dist/postinstall.js +3 -6
  253. package/dist/predictions/planning-fallacy.d.ts +9 -14
  254. package/dist/predictions/planning-fallacy.js +10 -16
  255. package/dist/predictions/store.d.ts +15 -23
  256. package/dist/predictions/store.js +36 -33
  257. package/dist/processes.d.ts +2 -7
  258. package/dist/processes.js +3 -3
  259. package/dist/project-briefs.d.ts +2 -3
  260. package/dist/project-briefs.js +9 -13
  261. package/dist/project-identity.d.ts +22 -9
  262. package/dist/project-identity.js +47 -12
  263. package/dist/project-merge.d.ts +17 -2
  264. package/dist/project-merge.js +109 -27
  265. package/dist/project-remote.d.ts +12 -0
  266. package/dist/project-remote.js +138 -0
  267. package/dist/prompt-recall.js +1 -2
  268. package/dist/rate-limit.d.ts +1 -1
  269. package/dist/rate-limit.js +1 -1
  270. package/dist/raw-archive.d.ts +9 -0
  271. package/dist/raw-archive.js +70 -53
  272. package/dist/recall-history.d.ts +19 -20
  273. package/dist/recall-history.js +24 -42
  274. package/dist/recall-pipeline.js +4 -28
  275. package/dist/recall-scope.d.ts +7 -8
  276. package/dist/recall-scope.js +7 -8
  277. package/dist/recall-trace.d.ts +5 -9
  278. package/dist/recall-trace.js +6 -10
  279. package/dist/refine-llm.d.ts +1 -1
  280. package/dist/refine-llm.js +2 -2
  281. package/dist/reject-flow.d.ts +3 -4
  282. package/dist/reject-flow.js +122 -117
  283. package/dist/rejection.d.ts +5 -6
  284. package/dist/rejection.js +7 -15
  285. package/dist/rerankers/clef.d.ts +1 -1
  286. package/dist/rerankers/jev.d.ts +1 -2
  287. package/dist/rerankers/jev.js +4 -5
  288. package/dist/rerankers/llm.d.ts +1 -2
  289. package/dist/rerankers/llm.js +1 -2
  290. package/dist/rerankers/types.d.ts +1 -2
  291. package/dist/rrf.d.ts +2 -2
  292. package/dist/rrf.js +2 -2
  293. package/dist/search/bm25-search.d.ts +1 -1
  294. package/dist/search/bm25-search.js +2 -1
  295. package/dist/search/boosts.js +2 -1
  296. package/dist/search/hybrid.d.ts +1 -1
  297. package/dist/search/hybrid.js +2 -1
  298. package/dist/search/physics-search.d.ts +1 -1
  299. package/dist/search/physics-search.js +2 -1
  300. package/dist/search/types.d.ts +2 -0
  301. package/dist/search/types.js +3 -1
  302. package/dist/secret-detect.d.ts +4 -5
  303. package/dist/secret-detect.js +6 -10
  304. package/dist/server/auth.js +5 -5
  305. package/dist/server/client-ip.js +1 -1
  306. package/dist/server/cursor.js +2 -1
  307. package/dist/server/mcp-http.js +4 -4
  308. package/dist/server/request.d.ts +3 -6
  309. package/dist/server/request.js +6 -7
  310. package/dist/server/routes/admin.js +5 -4
  311. package/dist/server/routes/customer-notes.js +6 -5
  312. package/dist/server/routes/decisions.js +4 -3
  313. package/dist/server/routes/incidents.js +7 -5
  314. package/dist/server/routes/memories.js +7 -7
  315. package/dist/server/routes/policies.js +3 -2
  316. package/dist/server/routes/predictions.js +12 -15
  317. package/dist/server/routes/processes.js +3 -2
  318. package/dist/server/routes/project-briefs.js +8 -7
  319. package/dist/server/routes/recall.js +95 -93
  320. package/dist/server/routes/skills.js +6 -5
  321. package/dist/server/types.d.ts +1 -1
  322. package/dist/server/validation.d.ts +1 -2
  323. package/dist/server/validation.js +7 -14
  324. package/dist/server-detect.js +72 -58
  325. package/dist/server.d.ts +2 -2
  326. package/dist/server.js +131 -117
  327. package/dist/shared.d.ts +17 -17
  328. package/dist/shared.js +93 -97
  329. package/dist/skills.d.ts +3 -3
  330. package/dist/skills.js +8 -8
  331. package/dist/store/audit-event.d.ts +2 -2
  332. package/dist/store/audit-event.js +1 -1
  333. package/dist/store/candidates.d.ts +2 -2
  334. package/dist/store/candidates.js +4 -3
  335. package/dist/store/conflicts.js +30 -22
  336. package/dist/store/delete-and-batch.d.ts +11 -14
  337. package/dist/store/delete-and-batch.js +40 -91
  338. package/dist/store/entry-reads.d.ts +17 -25
  339. package/dist/store/entry-reads.js +59 -40
  340. package/dist/store/entry-row.d.ts +6 -24
  341. package/dist/store/entry-row.js +6 -24
  342. package/dist/store/entry-writes.d.ts +6 -7
  343. package/dist/store/entry-writes.js +13 -11
  344. package/dist/store/handoffs.d.ts +1 -1
  345. package/dist/store/handoffs.js +7 -10
  346. package/dist/store/index-and-stats.d.ts +2 -6
  347. package/dist/store/index-and-stats.js +4 -10
  348. package/dist/store/mirrors.d.ts +6 -19
  349. package/dist/store/mirrors.js +14 -39
  350. package/dist/store/open.js +9 -31
  351. package/dist/store/rows.d.ts +5 -11
  352. package/dist/store/rows.js +6 -11
  353. package/dist/store/search-rows.d.ts +17 -34
  354. package/dist/store/search-rows.js +31 -59
  355. package/dist/store/sessions.d.ts +4 -5
  356. package/dist/store/sessions.js +5 -6
  357. package/dist/store/summaries.d.ts +13 -17
  358. package/dist/store/summaries.js +26 -70
  359. package/dist/support-bundle.js +4 -8
  360. package/dist/tenant.d.ts +1 -5
  361. package/dist/token-ledger.d.ts +1 -1
  362. package/dist/token-ledger.js +3 -5
  363. package/dist/trace.js +1 -3
  364. package/dist/version.d.ts +1 -1
  365. package/dist/version.js +1 -1
  366. package/dist/working-memory.d.ts +1 -1
  367. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  368. package/extensions/openclaw-plugin/package.json +1 -1
  369. package/openclaw.plugin.json +1 -1
  370. 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 ? 1 / 1.2 : 1);
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(ctx, opts, plan, left, pools, admission) {
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 ? 1 / 1.2 : 1) * sBst,
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 ? 1 / 1.2 : 1),
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) * (1 / 1.2),
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
- * Scope narrow (T5 execute decision): rendering opts (`format`, `framing`,
16
- * `rendered`) and host-side opts (`auto`) are NOT included here. The print
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`, DF3). Filtering happens BEFORE
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 name for the origin partition ('' = not in a
44
- * project). Defaults to `resolveProjectIdentity(process.cwd()).name`;
45
- * surfaces whose process cwd is not the caller's project (HTTP server)
46
- * should pass it explicitly. */
47
- currentProject?: string;
48
- /** DF1 (docs/plans/2026-08-23-df1-snapshot-lifecycle.md, T2): the calling
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
- /** Z1: raw hook-payload prompt; only the pinned-only branch reads it, gated on `pinnedInject.promptRecall`. */
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
- /** Z1: admitted by the prompt-recall gate, not the recent-N backfill or a pin. */
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;
@@ -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
- * v39 S4: the secret half of the ambient policy on its own, for callers
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, currentProjectName: string): boolean;
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
  /**
@@ -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
- * - S4 secret veto is UNCONDITIONAL: neither crossProject nor
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
- * - S2 envelope parity: private/quarantine scopes never inject unless `exactScope` names one.
37
- * - S3 origin partition: other-project rows are excluded unless
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, currentProjectName, includeCrossProject, exactScope) {
41
- if (!ambientSecretAdmit(e, currentProjectName))
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, currentProjectName) !== 'cross-project';
47
+ return classifyOriginProject(e.origin_project, currentProject) !== 'cross-project';
48
48
  }
49
49
  /**
50
- * v39 S4: the secret half of the ambient policy on its own, for callers
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, currentProjectName) {
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 === currentProjectName;
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
- // DF3's quality floor runs on the recent-N slice AFTER this load, so the load
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(ctx, opts, plan, sections.left, pools, admission)
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 currentProjectName = opts.currentProject ?? resolveProjectIdentity(process.cwd()).name;
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, currentProjectName) })
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
- currentProjectName,
177
+ currentProject,
178
178
  includeCrossProject,
179
- originProject: includeCrossProject || currentProjectName === '' ? undefined : currentProjectName,
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.currentProjectName, budgetTokens: blockBudget, promptRecall: promptRecallPending });
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.currentProjectName, plan.includeCrossProject, plan.exactScope);
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.currentProjectName),
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.currentProjectName, now: evalNow() };
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;
@@ -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 (ROADMAP LC3) trains on. Same transaction
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,
@@ -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 AND wrong-
77
- * tenant, intentionally indistinguishable), `not_drillable` (id is a leaf
78
- * row), or `scope_blocked` (caller has no scope grant for the row's scope).
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 four cases. JS callers migrate via
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;
@@ -1,5 +1,7 @@
1
1
  // DAG drill-down from a summary to its children.
2
- import { readEntry, loadChildrenOf } from '../store/entry-reads.js';
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 AND wrong-
18
- * tenant, intentionally indistinguishable), `not_drillable` (id is a leaf
19
- * row), or `scope_blocked` (caller has no scope grant for the row's scope).
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 four cases. JS callers migrate via
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 summary = readEntry(ctx.hippoRoot, summaryId, ctx.tenantId);
30
- // No unscoped cross-tenant probe here — readEntry's null return covers
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(ctx, summaryId, depth) {
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 = loadChildrenOf(ctx.hippoRoot, parentId, ctx.tenantId);
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))
@@ -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 whose
12
- * `readEntry(..., ctx.tenantId)` resolved). Callers that surface the id list
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. Added in v1.11.4 to
16
- * close that disclosure path on POST /v1/outcome.
15
+ * non-applied (cross-tenant) ids leak to the caller.
17
16
  *
18
- * `opts.traceId` (LC1, docs/plans/2026-08-02-lc1-recall-trace-persistence.md):
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 via `readEntry(..., ctx.tenantId)`. Cross-tenant
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 (v1.11.4 security fix):** the returned `ids`
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). Earlier
46
- * versions returned the raw `last_retrieval_ids` regardless of tenant, which
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
@@ -1,24 +1,27 @@
1
1
  // Outcome feedback on recalled memories.
2
- import { openHippoDb, closeHippoDb } from '../db.js';
3
- import { writeEntry } from '../store/entry-writes.js';
4
- import { readEntry } from '../store/entry-reads.js';
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 = openHippoDb(ctx.hippoRoot);
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 = readEntry(ctx.hippoRoot, id, ctx.tenantId);
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)) { // FE2: a good outcome reconfirms the entry
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
- writeEntry(ctx.hippoRoot, updated, { actor: ctx.actor.subject });
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
- // LC1: link the outcome to its trace, recording only the ids actually
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
- // LC1 F1(d) structural fix (docs/plans/2026-08-02-lc1-recall-trace-persistence.md):
54
- // read the trace id from the SAME `loadIndex` snapshot already in hand
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 };
@@ -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. Task 4 may
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 (without flag-driven layer/tag/pin overrides
23
- * — A1 keeps the API minimal; the CLI handler will continue to handle those
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 (unlike Task 1 remember/forget where the underlying
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 (v0.39 commit 3). Runs inside the same
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
  */
@@ -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 = readEntry(ctx.hippoRoot, row.memoryId, ctx.tenantId);
17
+ const entry = entries.get(row.memoryId);
17
18
  return {
18
19
  id: row.memoryId,
19
20
  originalScope: row.originalScope,