hippo-memory 1.61.0 → 1.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) hide show
  1. package/README.md +37 -53
  2. package/dist/agent-memories/apply.d.ts +3 -1
  3. package/dist/agent-memories/apply.js +23 -1
  4. package/dist/agent-memories/claude-code.d.ts +4 -1
  5. package/dist/agent-memories/claude-code.js +53 -9
  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.d.ts +3 -3
  9. package/dist/agent-memories/sync.js +46 -24
  10. package/dist/agent-memories/types.d.ts +0 -2
  11. package/dist/ambient-store.d.ts +4 -4
  12. package/dist/ambient-store.js +4 -3
  13. package/dist/api/assemble.d.ts +7 -10
  14. package/dist/api/assemble.js +62 -66
  15. package/dist/api/audit.d.ts +2 -2
  16. package/dist/api/audit.js +2 -2
  17. package/dist/api/auth.d.ts +6 -6
  18. package/dist/api/auth.js +5 -7
  19. package/dist/api/context-select.d.ts +50 -0
  20. package/dist/api/context-select.js +342 -0
  21. package/dist/api/context-types.d.ts +12 -17
  22. package/dist/api/context.d.ts +5 -4
  23. package/dist/api/context.js +208 -535
  24. package/dist/api/dormant.js +1 -1
  25. package/dist/api/drill-down.d.ts +4 -4
  26. package/dist/api/drill-down.js +56 -41
  27. package/dist/api/outcome.d.ts +8 -13
  28. package/dist/api/outcome.js +13 -17
  29. package/dist/api/promote.d.ts +5 -8
  30. package/dist/api/promote.js +55 -66
  31. package/dist/api/quarantine.js +3 -2
  32. package/dist/api/recall-types.d.ts +43 -55
  33. package/dist/api/recall.d.ts +3 -3
  34. package/dist/api/recall.js +310 -456
  35. package/dist/api/remember.d.ts +2 -2
  36. package/dist/api/sleep.d.ts +7 -35
  37. package/dist/api/sleep.js +206 -220
  38. package/dist/api/tokens.d.ts +2 -2
  39. package/dist/api/tokens.js +2 -2
  40. package/dist/api/types.d.ts +6 -10
  41. package/dist/api/types.js +2 -4
  42. package/dist/audit-prune.d.ts +4 -6
  43. package/dist/audit-prune.js +3 -5
  44. package/dist/audit.js +11 -33
  45. package/dist/auth.d.ts +8 -9
  46. package/dist/auth.js +5 -7
  47. package/dist/autolearn.d.ts +1 -1
  48. package/dist/autolearn.js +1 -1
  49. package/dist/availability.js +3 -5
  50. package/dist/capture/command.d.ts +5 -4
  51. package/dist/capture/command.js +8 -22
  52. package/dist/capture/compact.d.ts +2 -2
  53. package/dist/capture/compact.js +11 -15
  54. package/dist/capture/extract.js +37 -115
  55. package/dist/capture-error.d.ts +1 -1
  56. package/dist/capture-error.js +1 -1
  57. package/dist/churn-git.d.ts +1 -1
  58. package/dist/churn-git.js +1 -1
  59. package/dist/cli/audit.js +3 -4
  60. package/dist/cli/auth.js +3 -6
  61. package/dist/cli/briefs.js +324 -306
  62. package/dist/cli/context.js +44 -34
  63. package/dist/cli/continuity.js +283 -271
  64. package/dist/cli/curate.d.ts +1 -1
  65. package/dist/cli/curate.js +43 -58
  66. package/dist/cli/dag.js +5 -9
  67. package/dist/cli/decisions.js +334 -345
  68. package/dist/cli/explain.js +68 -61
  69. package/dist/cli/goals.js +1 -1
  70. package/dist/cli/init.js +1 -1
  71. package/dist/cli/maintenance.js +62 -51
  72. package/dist/cli/playbooks.js +391 -379
  73. package/dist/cli/projects.js +11 -6
  74. package/dist/cli/recall.js +30 -44
  75. package/dist/cli/remember.js +118 -87
  76. package/dist/cli/session-hooks.js +106 -115
  77. package/dist/cli/setup.d.ts +1 -1
  78. package/dist/cli/setup.js +267 -250
  79. package/dist/cli/shared.js +3 -3
  80. package/dist/cli/slack.js +1 -1
  81. package/dist/cli/sleep.js +27 -1
  82. package/dist/cli/status.d.ts +4 -4
  83. package/dist/cli/status.js +80 -76
  84. package/dist/cli/transfer.js +88 -107
  85. package/dist/cli/usage.js +9 -6
  86. package/dist/cli.d.ts +1 -1
  87. package/dist/cli.js +4 -9
  88. package/dist/compaction-record.d.ts +2 -2
  89. package/dist/compaction-record.js +89 -68
  90. package/dist/compare.d.ts +11 -16
  91. package/dist/compare.js +11 -16
  92. package/dist/config.d.ts +19 -18
  93. package/dist/config.js +90 -67
  94. package/dist/connectors/github/backfill.d.ts +2 -2
  95. package/dist/connectors/github/backfill.js +8 -15
  96. package/dist/connectors/github/cli-impl.js +3 -8
  97. package/dist/connectors/github/deletion.d.ts +5 -12
  98. package/dist/connectors/github/deletion.js +5 -12
  99. package/dist/connectors/github/dlq.d.ts +6 -9
  100. package/dist/connectors/github/dlq.js +2 -3
  101. package/dist/connectors/github/ingest.d.ts +5 -7
  102. package/dist/connectors/github/ingest.js +8 -12
  103. package/dist/connectors/github/octokit-client.d.ts +3 -5
  104. package/dist/connectors/github/octokit-client.js +5 -6
  105. package/dist/connectors/github/signature.d.ts +9 -39
  106. package/dist/connectors/github/signature.js +9 -39
  107. package/dist/connectors/github/tenant-routing.d.ts +1 -1
  108. package/dist/connectors/github/tenant-routing.js +1 -1
  109. package/dist/connectors/github/transform.js +2 -2
  110. package/dist/connectors/github/types.d.ts +2 -10
  111. package/dist/connectors/github/types.js +1 -3
  112. package/dist/connectors/slack/deletion.d.ts +3 -8
  113. package/dist/connectors/slack/deletion.js +3 -8
  114. package/dist/connectors/slack/dlq.d.ts +1 -1
  115. package/dist/connectors/slack/ingest.d.ts +1 -1
  116. package/dist/connectors/slack/ingest.js +7 -16
  117. package/dist/connectors/slack/signature.d.ts +1 -1
  118. package/dist/connectors/slack/tenant-routing.d.ts +3 -5
  119. package/dist/connectors/slack/tenant-routing.js +3 -5
  120. package/dist/connectors/slack/transform.d.ts +5 -6
  121. package/dist/connectors/slack/transform.js +5 -6
  122. package/dist/connectors/slack/types.d.ts +2 -6
  123. package/dist/connectors/slack/types.js +1 -3
  124. package/dist/connectors/slack/web-client.js +10 -3
  125. package/dist/connectors/slack/workspaces.d.ts +3 -5
  126. package/dist/connectors/slack/workspaces.js +3 -5
  127. package/dist/consolidate/conflicts.js +3 -14
  128. package/dist/consolidate/decay.js +9 -29
  129. package/dist/consolidate/llm-passes.js +4 -5
  130. package/dist/consolidate/merge.js +8 -23
  131. package/dist/consolidate/run.d.ts +1 -8
  132. package/dist/consolidate/run.js +3 -25
  133. package/dist/consolidate/sleep.js +5 -17
  134. package/dist/consolidate/traces.js +9 -21
  135. package/dist/customer-notes.d.ts +5 -7
  136. package/dist/customer-notes.js +82 -76
  137. package/dist/dag.d.ts +10 -21
  138. package/dist/dag.js +189 -203
  139. package/dist/db/continuity.js +2 -2
  140. package/dist/db/migrations/v14.js +1 -1
  141. package/dist/db/migrations/v15.js +1 -2
  142. package/dist/db/migrations/v16.js +3 -4
  143. package/dist/db/migrations/v17.js +2 -3
  144. package/dist/db/migrations/v19.js +1 -1
  145. package/dist/db/migrations/v20.js +1 -1
  146. package/dist/db/migrations/v21.js +2 -6
  147. package/dist/db/migrations/v22.js +2 -4
  148. package/dist/db/migrations/v23.js +1 -1
  149. package/dist/db/migrations/v24.js +4 -6
  150. package/dist/db/migrations/v25.js +2 -3
  151. package/dist/db/migrations/v26.js +3 -3
  152. package/dist/db/migrations/v27.js +2 -10
  153. package/dist/db/migrations/v28.js +5 -8
  154. package/dist/db/migrations/v29.js +3 -4
  155. package/dist/db/migrations/v30.js +2 -2
  156. package/dist/db/migrations/v31.js +1 -1
  157. package/dist/db/migrations/v32.js +1 -1
  158. package/dist/db/migrations/v33.js +3 -3
  159. package/dist/db/migrations/v34.js +1 -1
  160. package/dist/db/migrations/v35.js +3 -4
  161. package/dist/db/migrations/v36.js +3 -4
  162. package/dist/db/migrations/v37.js +5 -5
  163. package/dist/db/migrations/v38.js +7 -8
  164. package/dist/db/migrations/v39.js +1 -1
  165. package/dist/db/migrations/v40.js +4 -16
  166. package/dist/db/migrations/v41.js +3 -4
  167. package/dist/db/migrations/v42.js +3 -4
  168. package/dist/db/migrations/v45.js +1 -1
  169. package/dist/db/migrations/v46.js +1 -1
  170. package/dist/db/migrations/v47.js +1 -1
  171. package/dist/db/migrations/v48.js +1 -1
  172. package/dist/decisions.d.ts +2 -2
  173. package/dist/decisions.js +97 -80
  174. package/dist/dedupe.js +86 -61
  175. package/dist/delivery-recorder.js +154 -135
  176. package/dist/doctor.js +129 -110
  177. package/dist/dormant.js +1 -4
  178. package/dist/embedding-provider.d.ts +4 -8
  179. package/dist/embedding-provider.js +4 -8
  180. package/dist/embeddings.js +55 -47
  181. package/dist/env.d.ts +1 -1
  182. package/dist/env.js +12 -12
  183. package/dist/escape.d.ts +5 -0
  184. package/dist/escape.js +10 -0
  185. package/dist/eval-stats.d.ts +1 -2
  186. package/dist/eval-stats.js +1 -2
  187. package/dist/eval-suite.js +27 -21
  188. package/dist/extract.js +4 -9
  189. package/dist/failure-log.d.ts +3 -3
  190. package/dist/failure-log.js +1 -1
  191. package/dist/forward-claim-detector.d.ts +2 -4
  192. package/dist/forward-claim-detector.js +6 -11
  193. package/dist/goals.d.ts +3 -3
  194. package/dist/goals.js +103 -91
  195. package/dist/graph/read.d.ts +2 -2
  196. package/dist/graph/read.js +5 -6
  197. package/dist/graph/types.d.ts +8 -8
  198. package/dist/graph/write.d.ts +7 -14
  199. package/dist/graph/write.js +16 -23
  200. package/dist/graph-extract.d.ts +7 -8
  201. package/dist/graph-extract.js +62 -72
  202. package/dist/graph-recall.d.ts +2 -2
  203. package/dist/graph-recall.js +55 -49
  204. package/dist/graph-stream.d.ts +5 -6
  205. package/dist/graph-stream.js +66 -57
  206. package/dist/graph-view.d.ts +2 -2
  207. package/dist/graph-view.js +7 -7
  208. package/dist/half-life-migration.d.ts +1 -2
  209. package/dist/half-life-migration.js +2 -3
  210. package/dist/hooks/codex-session.js +1 -1
  211. package/dist/hooks/codex-wrapper.d.ts +1 -1
  212. package/dist/hooks/codex-wrapper.js +3 -2
  213. package/dist/hooks/json-hooks.d.ts +2 -2
  214. package/dist/hooks/json-hooks.js +5 -4
  215. package/dist/hooks/opencode.d.ts +1 -1
  216. package/dist/hooks/opencode.js +5 -4
  217. package/dist/hooks/shared.d.ts +3 -7
  218. package/dist/hooks/shared.js +1 -8
  219. package/dist/http-util.d.ts +2 -3
  220. package/dist/http-util.js +3 -0
  221. package/dist/importers/core.d.ts +2 -9
  222. package/dist/importers/core.js +15 -30
  223. package/dist/importers/sources.js +2 -1
  224. package/dist/importers/vault.js +2 -20
  225. package/dist/incidents.d.ts +1 -1
  226. package/dist/incidents.js +46 -39
  227. package/dist/instruction-detect.d.ts +1 -1
  228. package/dist/instruction-detect.js +1 -1
  229. package/dist/invalidation.d.ts +3 -0
  230. package/dist/invalidation.js +160 -114
  231. package/dist/json.d.ts +5 -0
  232. package/dist/json.js +4 -0
  233. package/dist/judgment.js +1 -2
  234. package/dist/local-embedding.js +1 -1
  235. package/dist/mcp/admin-tools.js +7 -17
  236. package/dist/mcp/format.js +1 -1
  237. package/dist/mcp/framing.js +3 -6
  238. package/dist/mcp/protocol.d.ts +2 -5
  239. package/dist/mcp/protocol.js +1 -3
  240. package/dist/mcp/recall-tools.js +12 -15
  241. package/dist/mcp/request.js +4 -3
  242. package/dist/mcp/session-state.js +2 -3
  243. package/dist/mcp/stdio.js +2 -1
  244. package/dist/mcp/tools.js +9 -6
  245. package/dist/memory-value-weights.d.ts +5 -8
  246. package/dist/memory-value-weights.js +5 -8
  247. package/dist/memory-value.d.ts +13 -13
  248. package/dist/memory-value.js +26 -37
  249. package/dist/memory.d.ts +20 -22
  250. package/dist/memory.js +24 -48
  251. package/dist/multihop.d.ts +1 -1
  252. package/dist/multihop.js +3 -2
  253. package/dist/owner-validation.d.ts +4 -5
  254. package/dist/owner-validation.js +4 -5
  255. package/dist/physics.d.ts +4 -4
  256. package/dist/physics.js +7 -9
  257. package/dist/policies.d.ts +9 -10
  258. package/dist/policies.js +96 -81
  259. package/dist/postinstall.js +3 -6
  260. package/dist/predictions/planning-fallacy.d.ts +9 -14
  261. package/dist/predictions/planning-fallacy.js +10 -16
  262. package/dist/predictions/store.d.ts +15 -23
  263. package/dist/predictions/store.js +36 -33
  264. package/dist/processes.d.ts +2 -7
  265. package/dist/processes.js +88 -72
  266. package/dist/project-briefs.d.ts +2 -3
  267. package/dist/project-briefs.js +141 -118
  268. package/dist/project-identity.d.ts +22 -9
  269. package/dist/project-identity.js +47 -12
  270. package/dist/project-merge.d.ts +28 -5
  271. package/dist/project-merge.js +213 -46
  272. package/dist/project-remote.d.ts +12 -0
  273. package/dist/project-remote.js +138 -0
  274. package/dist/prompt-recall.js +1 -2
  275. package/dist/rate-limit.d.ts +1 -1
  276. package/dist/rate-limit.js +1 -1
  277. package/dist/raw-archive.d.ts +9 -0
  278. package/dist/raw-archive.js +70 -53
  279. package/dist/recall-history.d.ts +19 -20
  280. package/dist/recall-history.js +24 -42
  281. package/dist/recall-pipeline.js +4 -28
  282. package/dist/recall-scope.d.ts +7 -8
  283. package/dist/recall-scope.js +7 -8
  284. package/dist/recall-trace.d.ts +5 -9
  285. package/dist/recall-trace.js +6 -10
  286. package/dist/refine-llm.d.ts +1 -1
  287. package/dist/refine-llm.js +2 -2
  288. package/dist/reject-flow.d.ts +3 -4
  289. package/dist/reject-flow.js +122 -117
  290. package/dist/rejection.d.ts +5 -6
  291. package/dist/rejection.js +7 -15
  292. package/dist/rerankers/clef.d.ts +1 -1
  293. package/dist/rerankers/jev.d.ts +1 -2
  294. package/dist/rerankers/jev.js +4 -5
  295. package/dist/rerankers/llm.d.ts +1 -2
  296. package/dist/rerankers/llm.js +1 -2
  297. package/dist/rerankers/types.d.ts +1 -2
  298. package/dist/rrf.d.ts +2 -2
  299. package/dist/rrf.js +2 -2
  300. package/dist/search/bm25-search.d.ts +1 -1
  301. package/dist/search/bm25-search.js +2 -1
  302. package/dist/search/boosts.js +2 -1
  303. package/dist/search/hybrid.d.ts +1 -1
  304. package/dist/search/hybrid.js +2 -1
  305. package/dist/search/physics-search.d.ts +1 -1
  306. package/dist/search/physics-search.js +2 -1
  307. package/dist/search/types.d.ts +2 -0
  308. package/dist/search/types.js +3 -1
  309. package/dist/secret-detect.d.ts +4 -5
  310. package/dist/secret-detect.js +6 -10
  311. package/dist/server/auth.js +5 -5
  312. package/dist/server/client-ip.js +1 -1
  313. package/dist/server/cursor.js +2 -1
  314. package/dist/server/mcp-http.js +4 -4
  315. package/dist/server/request.d.ts +3 -6
  316. package/dist/server/request.js +6 -7
  317. package/dist/server/routes/admin.js +5 -4
  318. package/dist/server/routes/customer-notes.js +6 -5
  319. package/dist/server/routes/decisions.js +4 -3
  320. package/dist/server/routes/incidents.js +7 -5
  321. package/dist/server/routes/memories.js +7 -7
  322. package/dist/server/routes/policies.js +3 -2
  323. package/dist/server/routes/predictions.js +12 -15
  324. package/dist/server/routes/processes.js +3 -2
  325. package/dist/server/routes/project-briefs.js +8 -7
  326. package/dist/server/routes/recall.js +95 -93
  327. package/dist/server/routes/skills.js +6 -5
  328. package/dist/server/types.d.ts +1 -1
  329. package/dist/server/validation.d.ts +1 -2
  330. package/dist/server/validation.js +7 -14
  331. package/dist/server-detect.js +72 -58
  332. package/dist/server.d.ts +2 -2
  333. package/dist/server.js +131 -117
  334. package/dist/shared.d.ts +26 -17
  335. package/dist/shared.js +102 -104
  336. package/dist/skills.d.ts +3 -3
  337. package/dist/skills.js +88 -72
  338. package/dist/store/audit-event.d.ts +2 -2
  339. package/dist/store/audit-event.js +1 -1
  340. package/dist/store/candidates.d.ts +2 -2
  341. package/dist/store/candidates.js +4 -3
  342. package/dist/store/conflicts.js +30 -22
  343. package/dist/store/delete-and-batch.d.ts +11 -14
  344. package/dist/store/delete-and-batch.js +40 -91
  345. package/dist/store/entry-reads.d.ts +17 -25
  346. package/dist/store/entry-reads.js +59 -40
  347. package/dist/store/entry-row.d.ts +6 -24
  348. package/dist/store/entry-row.js +6 -24
  349. package/dist/store/entry-writes.d.ts +6 -7
  350. package/dist/store/entry-writes.js +13 -11
  351. package/dist/store/handoffs.d.ts +1 -1
  352. package/dist/store/handoffs.js +7 -10
  353. package/dist/store/index-and-stats.d.ts +2 -6
  354. package/dist/store/index-and-stats.js +4 -10
  355. package/dist/store/mirrors.d.ts +6 -19
  356. package/dist/store/mirrors.js +14 -39
  357. package/dist/store/open.js +9 -31
  358. package/dist/store/rows.d.ts +5 -11
  359. package/dist/store/rows.js +6 -11
  360. package/dist/store/search-rows.d.ts +17 -34
  361. package/dist/store/search-rows.js +34 -56
  362. package/dist/store/sessions.d.ts +4 -5
  363. package/dist/store/sessions.js +5 -6
  364. package/dist/store/summaries.d.ts +13 -17
  365. package/dist/store/summaries.js +26 -70
  366. package/dist/support-bundle.js +4 -8
  367. package/dist/tenant.d.ts +1 -5
  368. package/dist/token-ledger.d.ts +1 -1
  369. package/dist/token-ledger.js +3 -5
  370. package/dist/trace.js +1 -3
  371. package/dist/version.d.ts +1 -1
  372. package/dist/version.js +1 -1
  373. package/dist/working-memory.d.ts +1 -1
  374. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  375. package/extensions/openclaw-plugin/package.json +1 -1
  376. package/openclaw.plugin.json +1 -1
  377. package/package.json +1 -1
@@ -0,0 +1,342 @@
1
+ // getContext's selection stages: the pinned-only branch, the strongest-first branch and the search branch.
2
+ import { openHippoDb, closeHippoDb } from '../db.js';
3
+ import { recallScopeFilter } from '../store/search-rows.js';
4
+ import { loadIndex } from '../store/index-and-stats.js';
5
+ import { calculateStrength } from '../memory.js';
6
+ import { appendAuditEvent, auditQueryFields, isContentWorthStoring } from '../audit.js';
7
+ import { rankBothStores } from '../shared.js';
8
+ import { evalNow } from '../ablation.js';
9
+ import { hybridSearch } from '../search/hybrid.js';
10
+ import { physicsSearch } from '../search/physics-search.js';
11
+ import { DEFAULT_LOCAL_BUMP } from '../search/types.js';
12
+ import { compareScoredResults } from '../compare.js';
13
+ import { scopeMatch } from '../scope.js';
14
+ import { promptTokens, contentTokens, gatePromptRecall, } from '../prompt-recall.js';
15
+ const GLOBAL_DISCOUNT = 1 / DEFAULT_LOCAL_BUMP;
16
+ // Share and promote copy a memory to the global store under a new id, so equal content is the only link.
17
+ // A pinned copy wins, then the stronger one after the ranking's own global discount; a tie keeps the local copy.
18
+ export function oneCopyPerMemory(local, global, now) {
19
+ const score = (e, isGlobal) => calculateStrength(e, now) * (isGlobal ? GLOBAL_DISCOUNT : 1);
20
+ const best = new Map();
21
+ const offer = (entry, isGlobal) => {
22
+ const held = best.get(entry.content);
23
+ const wins = !held || (held.entry.pinned !== entry.pinned
24
+ ? entry.pinned
25
+ : score(entry, isGlobal) > score(held.entry, held.isGlobal));
26
+ if (wins)
27
+ best.set(entry.content, { entry, isGlobal });
28
+ };
29
+ for (const e of local)
30
+ offer(e, false);
31
+ for (const e of global)
32
+ offer(e, true);
33
+ const kept = new Set([...best.values()].map((b) => b.entry));
34
+ return [local.filter((e) => kept.has(e)), global.filter((e) => kept.has(e))];
35
+ }
36
+ export const finiteOr = (v, dflt, min) => Number.isFinite(v) && v >= min ? v : dflt;
37
+ /** Pins plus the prompt-recall or recent-N backfill; null means the block is empty. */
38
+ export function selectPinned(opts, plan, left, pools, admission) {
39
+ const { obs, primaryIsGlobal, config: pinnedCfg } = plan;
40
+ if (!pinnedCfg.pinnedInject.enabled) {
41
+ return null;
42
+ }
43
+ // Effective budget: explicit opts.budget wins over config, less what the sections took.
44
+ const effBudget = left;
45
+ const nowP = evalNow(); // honors HIPPO_FAKE_NOW (eval-only; see ablation.ts)
46
+ const localEntries = pools.local.entries;
47
+ const globalEntries = pools.global.entries;
48
+ obs?.offer(localEntries, primaryIsGlobal);
49
+ obs?.offer(globalEntries, true);
50
+ const [localPool, globalPool] = oneCopyPerMemory(localEntries, globalEntries, nowP);
51
+ obs?.dropMissing([...localEntries, ...globalEntries], [...localPool, ...globalPool], 'load', 'duplicate');
52
+ const picked = { items: [], ids: new Set(), used: 0 };
53
+ // Pinned entries are explicit user intent, the recent-N list an automatic
54
+ // backfill. Both loops share ONE budget and the recent loop runs first, so
55
+ // pins are ranked here and reserve their share before it can spend.
56
+ const pinnedLocal = localPool.filter((e) => e.pinned);
57
+ const pinnedGlobal = globalPool.filter((e) => e.pinned);
58
+ const rankedPinned = rankPinned(plan, pinnedLocal, pinnedGlobal, nowP);
59
+ const recentBudget = Math.max(0, effBudget - reservePinned(rankedPinned, effBudget));
60
+ // Prompt recall gates the backfill on the prompt instead of recency.
61
+ if (plan.promptRecallPending) {
62
+ const candidates = () => promptRecallCandidates(plan, pools, admission.admit, rankedPinned, nowP);
63
+ backfillFromPrompt(opts, plan, pinnedCfg, candidates, picked, recentBudget);
64
+ }
65
+ else if (plan.includeRecent > 0) {
66
+ backfillRecent(plan, localPool, globalPool, picked, recentBudget, nowP);
67
+ }
68
+ if (pinnedLocal.length === 0 &&
69
+ pinnedGlobal.length === 0 &&
70
+ picked.items.length === 0 &&
71
+ !admission.digestHidden()) {
72
+ return null;
73
+ }
74
+ admitWithinBudget(rankedPinned, picked, effBudget, obs);
75
+ return picked.items;
76
+ }
77
+ function rankPinned(plan, pinnedLocal, pinnedGlobal, nowP) {
78
+ return [
79
+ ...pinnedLocal.map((e) => ({ entry: e, isGlobal: plan.primaryIsGlobal })),
80
+ ...pinnedGlobal.map((e) => ({ entry: e, isGlobal: true })),
81
+ ]
82
+ .map(({ entry, isGlobal }) => {
83
+ const scopeSig = scopeMatch(entry.tags, plan.activeScope);
84
+ const sBst = scopeSig === 1 ? 1.5 : scopeSig === -1 ? 0.5 : 1.0;
85
+ return {
86
+ entry,
87
+ score: calculateStrength(entry, nowP) * (isGlobal ? GLOBAL_DISCOUNT : 1) * sBst,
88
+ tokens: plan.price(entry, isGlobal),
89
+ isGlobal,
90
+ };
91
+ })
92
+ .sort(compareScoredResults);
93
+ }
94
+ // Mirrors the pin loop's continue-not-break so a big pin cannot block smaller ones from reserving, and dedupes by id
95
+ // because a synced pin sits in both stores. A pin also in the recent slice is counted twice: recents under-fill, safely.
96
+ function reservePinned(rankedPinned, effBudget) {
97
+ let pinnedReserve = 0;
98
+ const reservedIds = new Set();
99
+ for (const r of rankedPinned) {
100
+ if (reservedIds.has(r.entry.id))
101
+ continue;
102
+ if (pinnedReserve + r.tokens <= effBudget) {
103
+ pinnedReserve += r.tokens;
104
+ reservedIds.add(r.entry.id);
105
+ }
106
+ }
107
+ return pinnedReserve;
108
+ }
109
+ /** Skips ids already picked and rows past the budget, so a large row never blocks smaller ones behind it. */
110
+ function admitWithinBudget(rows, picked, budget, obs) {
111
+ for (const r of rows) {
112
+ if (picked.ids.has(r.entry.id))
113
+ continue;
114
+ if (picked.used + r.tokens > budget) {
115
+ obs?.reject(r.entry, 'budget', 'budget', r.score, r.tokens);
116
+ continue;
117
+ }
118
+ picked.items.push(r);
119
+ picked.ids.add(r.entry.id);
120
+ picked.used += r.tokens;
121
+ }
122
+ }
123
+ function backfillFromPrompt(opts, plan, pinnedCfg, candidates, picked, recentBudget) {
124
+ const rawMetric = pinnedCfg.pinnedInject.promptRecallMetric;
125
+ const metric = rawMetric === 'cosine' ? 'cosine' : 'jaccard';
126
+ const gate = {
127
+ metric,
128
+ threshold: finiteOr(pinnedCfg.pinnedInject.promptRecallThreshold, 0.04, 0),
129
+ minShared: finiteOr(pinnedCfg.pinnedInject.promptRecallMinShared, 2, 0),
130
+ maxItems: finiteOr(pinnedCfg.pinnedInject.promptRecallMaxItems, 5, 1),
131
+ };
132
+ const p = promptTokens(opts.prompt ?? '');
133
+ if (p.size === 0)
134
+ return;
135
+ const candidateItems = candidates();
136
+ const gated = gatePromptRecall(p, candidateItems, gate);
137
+ plan.obs?.gated(p, candidateItems, gate, gated);
138
+ for (const g of gated) {
139
+ if (picked.ids.has(g.item.id))
140
+ continue;
141
+ const tokens = plan.price(g.item.entry, g.item.isGlobal, true);
142
+ if (picked.used + tokens > recentBudget) {
143
+ plan.obs?.reject(g.item.entry, 'budget', 'budget', g.score, tokens);
144
+ continue;
145
+ }
146
+ picked.items.push({ entry: g.item.entry, score: g.score, tokens, isGlobal: g.item.isGlobal, promptRecall: true });
147
+ picked.ids.add(g.item.id);
148
+ picked.used += tokens;
149
+ }
150
+ }
151
+ // Candidates came off the ambient load's own connection (the recall request), not a fresh open.
152
+ function promptRecallCandidates(plan, pools, admit, rankedPinned, nowP) {
153
+ const { obs, primaryIsGlobal } = plan;
154
+ // A candidate carrying a pin's text would inject that memory a second time.
155
+ const pinnedText = new Set(rankedPinned.map((r) => r.entry.content));
156
+ const ineligibleReason = (e) => !admit(e) ? 'scope'
157
+ : e.pinned ? 'pinned'
158
+ : !isContentWorthStoring(e.content) ? 'quality'
159
+ : pinnedText.has(e.content) ? 'duplicate'
160
+ : null;
161
+ const eligible = (e) => {
162
+ const why = ineligibleReason(e);
163
+ if (why !== null && why !== 'pinned')
164
+ obs?.reject(e, 'eligible', why);
165
+ return why === null;
166
+ };
167
+ obs?.offer(pools.local.recall ?? [], primaryIsGlobal, 'prompt-recall');
168
+ obs?.offer(pools.global.recall ?? [], true, 'prompt-recall');
169
+ const localEligible = (pools.local.recall ?? []).filter(eligible);
170
+ const globalEligible = (pools.global.recall ?? []).filter(eligible);
171
+ const [localCandidates, globalCandidates] = oneCopyPerMemory(localEligible, globalEligible, nowP);
172
+ obs?.dropMissing([...localEligible, ...globalEligible], [...localCandidates, ...globalCandidates], 'eligible', 'duplicate');
173
+ const seenCandidateIds = new Set();
174
+ const candidateItems = [];
175
+ // Local wins the id collision (a global row synced into the local store).
176
+ for (const e of localCandidates) {
177
+ if (seenCandidateIds.has(e.id))
178
+ continue;
179
+ seenCandidateIds.add(e.id);
180
+ candidateItems.push({ id: e.id, tokens: contentTokens(e.content), entry: e, isGlobal: primaryIsGlobal });
181
+ }
182
+ for (const e of globalCandidates) {
183
+ if (seenCandidateIds.has(e.id))
184
+ continue;
185
+ seenCandidateIds.add(e.id);
186
+ candidateItems.push({ id: e.id, tokens: contentTokens(e.content), entry: e, isGlobal: true });
187
+ }
188
+ return candidateItems;
189
+ }
190
+ function backfillRecent(plan, localPool, globalPool, picked, recentBudget, nowP) {
191
+ const recent = [
192
+ ...localPool.map((entry) => ({ entry, isGlobal: plan.primaryIsGlobal })),
193
+ ...globalPool.map((entry) => ({ entry, isGlobal: true })),
194
+ ]
195
+ // Newest first, then id: stable within one store, but same-millisecond rows fall to random ids across ingests.
196
+ .sort((a, b) => {
197
+ const byCreated = Date.parse(b.entry.created) - Date.parse(a.entry.created);
198
+ return byCreated !== 0 ? byCreated : b.entry.id.localeCompare(a.entry.id);
199
+ })
200
+ // Filter before slice so a junk row is backfilled past, not counted against N. Pins bypass the floor: a dropped
201
+ // pin's share of the shared budget would go to a backfilled row, and the pin loop could not win it back.
202
+ .filter(({ entry }) => entry.pinned || isContentWorthStoring(entry.content))
203
+ .slice(0, plan.includeRecent)
204
+ .map(({ entry, isGlobal }) => ({
205
+ entry,
206
+ score: calculateStrength(entry, nowP) * (isGlobal ? GLOBAL_DISCOUNT : 1),
207
+ tokens: plan.price(entry, isGlobal),
208
+ isGlobal,
209
+ }));
210
+ admitWithinBudget(recent, picked, recentBudget, plan.obs);
211
+ }
212
+ /** No query: the strongest memories by strength, up to budget. */
213
+ export function selectStrongest(plan, left, pools) {
214
+ const now = evalNow(); // honors HIPPO_FAKE_NOW (eval-only; see ablation.ts)
215
+ const [localPool, globalPool] = oneCopyPerMemory(pools.local.entries, pools.global.entries, now);
216
+ const localRanked = localPool
217
+ .map((e) => ({
218
+ entry: e,
219
+ score: calculateStrength(e, now),
220
+ tokens: plan.price(e, plan.primaryIsGlobal),
221
+ isGlobal: plan.primaryIsGlobal,
222
+ }))
223
+ .sort(compareScoredResults);
224
+ const globalRanked = globalPool
225
+ .map((e) => ({
226
+ entry: e,
227
+ score: calculateStrength(e, now) * GLOBAL_DISCOUNT,
228
+ tokens: plan.price(e, true),
229
+ isGlobal: true,
230
+ }))
231
+ .sort(compareScoredResults);
232
+ const combined = [...localRanked, ...globalRanked].sort(compareScoredResults);
233
+ const selected = [];
234
+ let used = 0;
235
+ for (const r of combined) {
236
+ if (used + r.tokens > left)
237
+ continue;
238
+ selected.push(r);
239
+ used += r.tokens;
240
+ }
241
+ return selected;
242
+ }
243
+ /** Real query: hybrid search over both stores, or physics/hybrid over the local rows; emits the 'recall' audit row. */
244
+ export async function selectBySearch(ctx, plan, left, pools, admission) {
245
+ const minResults = plan.cost ? 0 : undefined; // a priced block skips an oversize top hit too, so the budget bounds it
246
+ const results = plan.hasGlobal && !plan.primaryIsGlobal
247
+ ? await searchBothStores(ctx, plan, left, minResults, pools, admission.bothStoresAdmit)
248
+ : await searchLocalRows(ctx, plan, left, minResults, pools.local.entries, admission.admit);
249
+ auditContextRecall(ctx, plan, results.length);
250
+ return results;
251
+ }
252
+ // The pools were admitted at load, before ranking, dedupe and budget: a post-filter would let an excluded row fill the
253
+ // budget or shadow its admitted duplicate.
254
+ async function searchBothStores(ctx, plan, left, minResults, pools, admit) {
255
+ const { cost, price } = plan;
256
+ const localIndex = loadIndex(ctx.hippoRoot);
257
+ const isGlobalHit = (e) => !localIndex.entries[e.id];
258
+ const roots = { local: ctx.hippoRoot, global: plan.globalRoot };
259
+ const merged = await rankBothStores(plan.query, roots, { local: pools.local.entries, global: pools.global.entries }, contextVectorSpec(ctx, plan, admit), {
260
+ budget: left,
261
+ minResults,
262
+ cost: cost && ((r) => price(r.entry, isGlobalHit(r.entry))),
263
+ scope: plan.activeScope,
264
+ });
265
+ return merged.map((r) => ({
266
+ entry: r.entry,
267
+ score: r.score,
268
+ tokens: price(r.entry, isGlobalHit(r.entry)),
269
+ isGlobal: isGlobalHit(r.entry),
270
+ }));
271
+ }
272
+ /** The vector arm under the lexical window's own tenant, scope and current-row rules. */
273
+ function contextVectorSpec(ctx, plan, admit) {
274
+ return { tenantId: ctx.tenantId, scope: recallScopeFilter(plan.exactScope, 'exact'), includeSuperseded: false, admit };
275
+ }
276
+ async function searchLocalRows(ctx, plan, left, minResults, localEntries, admit) {
277
+ const { cost, price, primaryIsGlobal, query, config: ctxConfig } = plan;
278
+ const usePhysicsCtx = ctxConfig.physics?.enabled !== false;
279
+ const localCost = cost && ((r) => price(r.entry, primaryIsGlobal));
280
+ const vectorCandidates = contextVectorSpec(ctx, plan, admit);
281
+ const ctxResults = usePhysicsCtx
282
+ ? await physicsSearch(query, localEntries, {
283
+ budget: left,
284
+ minResults,
285
+ cost: localCost,
286
+ hippoRoot: ctx.hippoRoot,
287
+ physicsConfig: ctxConfig.physics,
288
+ scope: plan.activeScope,
289
+ vectorCandidates,
290
+ })
291
+ : await hybridSearch(query, localEntries, {
292
+ budget: left,
293
+ minResults,
294
+ cost: localCost,
295
+ hippoRoot: ctx.hippoRoot,
296
+ scope: plan.activeScope,
297
+ vectorCandidates,
298
+ });
299
+ return ctxResults.map((r) => ({
300
+ entry: r.entry,
301
+ score: r.score,
302
+ tokens: price(r.entry, primaryIsGlobal),
303
+ isGlobal: primaryIsGlobal,
304
+ }));
305
+ }
306
+ // Same 'recall' op api.recall emits; the pinned-only and no-query branches never search, so they never emit.
307
+ function auditContextRecall(ctx, plan, resultCount) {
308
+ const ctxRecallMetadata = {
309
+ ...auditQueryFields(plan.query),
310
+ results: resultCount,
311
+ mode: 'context',
312
+ };
313
+ if (plan.hasLocal) {
314
+ const localDb = openHippoDb(ctx.hippoRoot);
315
+ try {
316
+ appendAuditEvent(localDb, {
317
+ tenantId: ctx.tenantId,
318
+ actor: ctx.actor.subject,
319
+ op: 'recall',
320
+ metadata: ctxRecallMetadata,
321
+ });
322
+ }
323
+ finally {
324
+ closeHippoDb(localDb);
325
+ }
326
+ }
327
+ if (plan.hasGlobal && !plan.primaryIsGlobal) {
328
+ const globalDb = openHippoDb(plan.globalRoot);
329
+ try {
330
+ appendAuditEvent(globalDb, {
331
+ tenantId: ctx.tenantId,
332
+ actor: ctx.actor.subject,
333
+ op: 'recall',
334
+ metadata: ctxRecallMetadata,
335
+ });
336
+ }
337
+ finally {
338
+ closeHippoDb(globalDb);
339
+ }
340
+ }
341
+ }
342
+ //# sourceMappingURL=context-select.js.map
@@ -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,15 +1,16 @@
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';
5
+ export { oneCopyPerMemory } from './context-select.js';
4
6
  /**
5
- * 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
6
8
  * that apply their own scope rule. A flagged row is only admitted inside its owning project;
7
9
  * flagged rows with no project origin never ambient-inject.
8
10
  */
9
- export declare function ambientSecretAdmit(e: MemoryEntry, currentProjectName: string): boolean;
11
+ export declare function ambientSecretAdmit(e: MemoryEntry, currentProject: ProjectRef): boolean;
10
12
  /** Most rows per store a no-query context reads; past it, ranking and ambientState see the strongest by decay. */
11
13
  export declare const CONTEXT_CANDIDATE_CAP = 2000;
12
- export declare function oneCopyPerMemory(local: readonly MemoryEntry[], global: readonly MemoryEntry[], now: Date): [MemoryEntry[], MemoryEntry[]];
13
14
  /**
14
15
  * Assemble a context bundle: recalled memories (pinned-only / strength-sorted
15
16
  * fallback / hybrid search) + active task snapshot + session handoff + recent
@@ -23,7 +24,7 @@ export declare function oneCopyPerMemory(local: readonly MemoryEntry[], global:
23
24
  * Tenant scope: all `loadAllEntries` / snapshot / handoff / events reads use
24
25
  * `ctx.tenantId`. Cross-tenant rows are filtered out.
25
26
  *
26
- * Returns an empty result (`entries: []`, snapshot/handoff/events undefined)
27
+ * @returns An empty result (`entries: []`, snapshot/handoff/events undefined)
27
28
  * when there's nothing to surface (no memories AND no snapshot AND no handoff
28
29
  * AND no recent events).
29
30
  */