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
package/dist/shared.js CHANGED
@@ -19,8 +19,9 @@ import { passesScopeFilterForRecall, passesCliRecallScopeFilter } from './recall
19
19
  import { search } from './search/bm25-search.js';
20
20
  import { hybridSearch } from './search/hybrid.js';
21
21
  import { fitBudget } from './search/finalize.js';
22
+ import { DEFAULT_LOCAL_BUMP, DEFAULT_RECALL_BUDGET } from './search/types.js';
22
23
  import { evalNow } from './ablation.js';
23
- import { deriveOriginProject, classifyOriginProject, resolveGlobalRootDir } from './project-identity.js';
24
+ import { deriveOriginProject, classifyOriginProject, resolveGlobalRootDir, resolveProjectIdentity } from './project-identity.js';
24
25
  import { detectSecret } from './secret-detect.js';
25
26
  import { isQuarantineScope } from './quarantine.js';
26
27
  import { RejectedValueError } from './rejection.js';
@@ -62,12 +63,12 @@ export function promoteToGlobal(localRoot, id, opts) {
62
63
  const entry = readEntry(localRoot, id, opts?.tenantId);
63
64
  if (!entry)
64
65
  throw new NotFoundError(`Memory not found: ${id}`);
65
- // CD5: same veto as shareMemory; a promoted copy would have no quarantine record to review.
66
+ // Same quarantine veto as shareMemory; a promoted copy would have no quarantine record to review.
66
67
  if (isQuarantineScope(entry.scope)) {
67
68
  throw new BadRequestError(`Refusing to promote ${id}: it is quarantined pending review. Approve it first via 'hippo quarantine approve ${id}'.`);
68
69
  }
69
- // v39 S4 producer veto: promote is a producer path to the global store
70
- // exactly like shareMemory - same hard rule (codex gating review P2).
70
+ // Secret producer veto: promote is a producer path to the global store
71
+ // exactly like shareMemory - same hard rule.
71
72
  const promoteSecret = detectSecret(entry);
72
73
  if (promoteSecret.flagged) {
73
74
  throw new BadRequestError(`Refusing to promote ${id} to the global store: content matches secret material (${promoteSecret.reason}). ` +
@@ -95,7 +96,7 @@ export function promoteToGlobal(localRoot, id, opts) {
95
96
  * Returns results sorted by adjusted score, within combined token budget.
96
97
  */
97
98
  export function searchBoth(query, localRoot, globalRoot, options = {}) {
98
- const { budget = 4000, now = evalNow(), minResults, tenantId } = options;
99
+ const { budget = DEFAULT_RECALL_BUDGET, now = evalNow(), minResults, tenantId } = options;
99
100
  const effectiveMin = minResults ?? 1;
100
101
  const localEntries = fs.existsSync(localRoot) ? loadSearchEntries(localRoot, query, undefined, tenantId) : [];
101
102
  const globalEntries = fs.existsSync(globalRoot) ? loadSearchEntries(globalRoot, query, undefined, tenantId) : [];
@@ -105,14 +106,13 @@ export function searchBoth(query, localRoot, globalRoot, options = {}) {
105
106
  const localResults = search(query, localEntries, { budget, now, minResults });
106
107
  const globalResults = search(query, globalEntries, { budget, now, minResults });
107
108
  // Tag global results. Local memories get a configurable priority bump.
108
- const syncLocalBump = 1.2;
109
109
  const tagged = [
110
110
  ...localResults.map((r) => ({
111
111
  ...r,
112
112
  isGlobal: false,
113
- score: r.score * syncLocalBump,
113
+ score: r.score * DEFAULT_LOCAL_BUMP,
114
114
  breakdown: r.breakdown
115
- ? { ...r.breakdown, sourceBump: syncLocalBump, final: r.breakdown.final * syncLocalBump }
115
+ ? { ...r.breakdown, sourceBump: DEFAULT_LOCAL_BUMP, final: r.breakdown.final * DEFAULT_LOCAL_BUMP }
116
116
  : undefined,
117
117
  })),
118
118
  ...globalResults.map((r) => ({ ...r, isGlobal: true })),
@@ -126,11 +126,8 @@ export function searchBoth(query, localRoot, globalRoot, options = {}) {
126
126
  seen.add(key);
127
127
  return true;
128
128
  });
129
- // T2 note: PLAIN stable score sort on purpose -- local/global inputs are
130
- // each deterministically ordered (content tail applied in the underlying
131
- // search), stability inherits that, and an exact post-bump tie keeps the
132
- // LOCAL result ahead of the global one (the concat order), preserving the
133
- // pre-T2 semantics.
129
+ // PLAIN stable score sort on purpose: both inputs are deterministically ordered,
130
+ // and an exact tie keeps the LOCAL result ahead of the global one (concat order).
134
131
  deduped.sort((a, b) => b.score - a.score);
135
132
  // Apply combined token budget (guarantee at least minResults items)
136
133
  const results = [];
@@ -151,13 +148,10 @@ export async function searchBothHybrid(query, localRoot, globalRoot, options = {
151
148
  const { includeSuperseded, asOf, tenantId, entryFilter, recallScope } = options;
152
149
  // When an admission filter is active, lift the per-store candidate cap
153
150
  // (default 200): excluded rows matching the query could otherwise fill the
154
- // window before any admitted row is even loaded (codex gating round 6).
155
- // 5000 = 25x the default 200-row window: large enough that exclusion
156
- // crowding is a non-issue on real stores, bounded so a common query term
157
- // on a 100k-row store cannot stall an interactive call by ranking every
158
- // match (post-merge adversarial review, 2026-07-02).
151
+ // window before any admitted row is even loaded. 5000 is bounded so a common
152
+ // term on a large store cannot stall an interactive call by ranking every match.
159
153
  const searchWindow = entryFilter ? 5000 : undefined;
160
- // v1.25.0 recall mode: push the scope predicate into SQL exactly like
154
+ // Recall mode: push the scope predicate into SQL exactly like
161
155
  // api.recall (loadRecallSearchEntries), so quarantine/private rows never
162
156
  // enter the candidate set, never shadow admitted duplicates in the dedupe
163
157
  // pass, and never consume budget. The JS post-filter below is the
@@ -187,7 +181,7 @@ export async function searchBothHybrid(query, localRoot, globalRoot, options = {
187
181
  }
188
182
  /** Hybrid ranking of rows already loaded from each store: the local bump, one copy per text, then the shared budget. */
189
183
  export async function rankBothStores(query, roots, entries, vectorCandidates, options = {}) {
190
- const { budget = 4000, now = evalNow(), embeddingWeight, explain, mmr, mmrLambda, localBump = 1.2, minResults, cost, scope, includeSuperseded, asOf, summaryDeboost, summaryFreshness } = options;
184
+ const { budget = DEFAULT_RECALL_BUDGET, now = evalNow(), embeddingWeight, explain, mmr, mmrLambda, localBump = DEFAULT_LOCAL_BUMP, minResults, cost, scope, includeSuperseded, asOf, summaryDeboost, summaryFreshness } = options;
191
185
  if (entries.local.length === 0 && entries.global.length === 0)
192
186
  return [];
193
187
  const shared = { budget, now, embeddingWeight, explain, mmr, mmrLambda, minResults, cost, scope, includeSuperseded, asOf, summaryDeboost, summaryFreshness, vectorCandidates };
@@ -214,7 +208,7 @@ export async function rankBothStores(query, roots, entries, vectorCandidates, op
214
208
  seen.add(key);
215
209
  return true;
216
210
  });
217
- // T2 note: PLAIN stable score sort on purpose -- see searchBoth above;
211
+ // PLAIN stable score sort on purpose -- see searchBoth above;
218
212
  // same rationale (deterministic inputs + stability; local-first on ties).
219
213
  deduped.sort((a, b) => b.score - a.score);
220
214
  return fitBudget(deduped, budget, minResults ?? 1, cost);
@@ -286,7 +280,7 @@ export function shareMemory(localRoot, id, options = {}) {
286
280
  const entry = readEntry(localRoot, id, options.tenantId);
287
281
  if (!entry)
288
282
  throw new NotFoundError(`Memory not found: ${id}`);
289
- // v39 S4 producer veto: secrets never go to the global store, not even
283
+ // Secret producer veto: secrets never go to the global store, not even
290
284
  // with --force. Explicit and loud - a silent null would read as "low
291
285
  // transfer score" and invite retries.
292
286
  const secret = detectSecret(entry);
@@ -294,7 +288,7 @@ export function shareMemory(localRoot, id, options = {}) {
294
288
  throw new BadRequestError(`Refusing to share ${id} to the global store: content matches secret material (${secret.reason}). ` +
295
289
  `Secrets stay in their owning project's store.`);
296
290
  }
297
- // CD5: a quarantined row is unreviewed input, not a lesson; sharing it would spread poison globally.
291
+ // A quarantined row is unreviewed input, not a lesson; sharing it would spread poison globally.
298
292
  if (isQuarantineScope(entry.scope)) {
299
293
  throw new BadRequestError(`Refusing to share ${id}: it is quarantined pending review. Approve it first via 'hippo quarantine approve ${id}'.`);
300
294
  }
@@ -328,7 +322,7 @@ export function shareMemory(localRoot, id, options = {}) {
328
322
  * List all projects that have contributed memories to the global store.
329
323
  * Parses the source field for 'shared:<project>:' or 'promoted:<path>' patterns.
330
324
  *
331
- * D4 v1.12.10: `tenantId` is now optional. When provided, the global entries
325
+ * `tenantId` is optional. When provided, the global entries
332
326
  * are filtered to that tenant before aggregation — matches every other
333
327
  * read path's default-safe behaviour. When undefined, host-wide (back-compat
334
328
  * for legacy callers like CLI standalone + dashboard internal use). Operators
@@ -339,7 +333,7 @@ export function listPeers(globalRoot, tenantId) {
339
333
  const root = globalRoot ?? getGlobalRoot();
340
334
  if (!fs.existsSync(root))
341
335
  return [];
342
- // D4: tenant-scoped by default when tenantId provided. Host-wide when
336
+ // Tenant-scoped by default when tenantId provided. Host-wide when
343
337
  // undefined (preserves back-compat).
344
338
  const tallies = tallySources(root, tenantId).sort((a, b) => (a.first < b.first ? -1 : a.first > b.first ? 1 : 0));
345
339
  const peerMap = new Map();
@@ -370,25 +364,85 @@ export function listPeers(globalRoot, tenantId) {
370
364
  .map(([project, data]) => ({ project, ...data }))
371
365
  .sort((a, b) => b.count - a.count);
372
366
  }
367
+ function isAutoShareCandidate(entry, globalContentSet, minScore, stats) {
368
+ // shareMemory refuses quarantined rows; filtering here keeps sleep from aborting on one.
369
+ if (isQuarantineScope(entry.scope ?? null))
370
+ return false;
371
+ // Before the score: these rows describe one project only, and a git seed's 'error' tag clears the bar.
372
+ if (entry.tags.some((t) => NEVER_AUTO_SHARE_TAGS.has(t)) || entry.source.startsWith(AGENT_MEMORY_SOURCE_PREFIX)) {
373
+ if (stats)
374
+ stats.neverAutoShareSkipped = (stats.neverAutoShareSkipped ?? 0) + 1;
375
+ return false;
376
+ }
377
+ const score = transferScore(entry);
378
+ if (score < minScore)
379
+ return false;
380
+ // Skip if already shared (same text apart from spacing)
381
+ if (globalContentSet.has(duplicateKey(entry.content)))
382
+ return false;
383
+ // Secret producer veto: secret rows never auto-share, regardless of
384
+ // transfer score. (shareMemory would throw; filtering here keeps the
385
+ // sleep pipeline fail-safe.) Checked LAST so the stats counter
386
+ // only counts rows the veto actually withheld — a row failing the score
387
+ // or dedupe gates was never going to share, secret or not.
388
+ if (detectSecret(entry).flagged) {
389
+ if (stats)
390
+ stats.secretSkipped++;
391
+ return false;
392
+ }
393
+ return true;
394
+ }
395
+ // Rejection containment (sync/promote/share copy paths must not let ONE rejected
396
+ // candidate kill the batch): shareMemory -> writeEntry hits the LIVE guard
397
+ // against the GLOBAL store's tombstones. A matching candidate throws
398
+ // RejectedValueError, which (uncaught) would abort this whole loop and,
399
+ // via api.ts's sleep pipeline, the entire autoShare sleep phase. Mirrors
400
+ // syncGlobalToLocal's per-item catch just above in this file. writeEntry's
401
+ // own catch already writes the reject_refusal audit before rethrowing,
402
+ // so do not double-audit here, just count and continue.
403
+ function shareCandidates(localRoot, candidates, stats) {
404
+ const shared = [];
405
+ let rejectedSkipped = 0;
406
+ for (const entry of candidates) {
407
+ try {
408
+ // skipEmbed: batching invariant, this is a batch producer, so it embeds
409
+ // once via embedAll() below rather than once per row inside shareMemory.
410
+ const result = shareMemory(localRoot, entry.id, { force: true, skipEmbed: true });
411
+ if (result)
412
+ shared.push(result);
413
+ }
414
+ catch (err) {
415
+ if (err instanceof RejectedValueError) {
416
+ rejectedSkipped++;
417
+ if (stats)
418
+ stats.rejectedSkipped = (stats.rejectedSkipped ?? 0) + 1;
419
+ continue;
420
+ }
421
+ throw err;
422
+ }
423
+ }
424
+ if (rejectedSkipped > 0) {
425
+ log.warn(`autoShare: skipped ${rejectedSkipped} candidate(s) refused by the global store's rejection tombstone`);
426
+ }
427
+ return shared;
428
+ }
373
429
  /**
374
430
  * Auto-share: local memories with high transfer scores, not already global, no NEVER_AUTO_SHARE_TAGS tag.
375
431
  * Returns the list of shared entries.
376
432
  *
377
- * L9: `options.tenantId` is opt-in. When provided, the LOCAL-entries read is
433
+ * `options.tenantId` is opt-in. When provided, the LOCAL-entries read is
378
434
  * scoped to that tenant. When undefined, the local read is host-wide (current
379
435
  * behaviour). The GLOBAL-entries read is always unioned — the global root IS
380
- * the cross-tenant aggregate by design. The only intentional unscoped
381
- * internal caller as of v1.12.1 is `api.sleep` (`src/api.ts:2041`), which
382
- * passes options without tenantId because `sleep` is host-wide by intent;
383
- * see `src/api.ts:2073-2077` for the cross-tenant dedup rationale.
436
+ * the cross-tenant aggregate by design. `api.sleep` passes no tenantId
437
+ * because `sleep` is host-wide by intent.
384
438
  *
385
- * v1.25.0: `options.stats` is an opt-in out-param. When provided,
439
+ * `options.stats` is an opt-in out-param. When provided,
386
440
  * `stats.secretSkipped` is incremented once per row that passed every OTHER
387
441
  * admission gate (transfer score, not-already-global) and was withheld SOLELY
388
442
  * by the secret veto — i.e. it counts shares actually prevented, not secret
389
443
  * rows merely present. Filled identically under `dryRun`.
390
444
  *
391
- * AT1: `stats.rejectedSkipped` (optional) is incremented once per candidate
445
+ * `stats.rejectedSkipped` (optional) is incremented once per candidate
392
446
  * refused by the GLOBAL store's rejection tombstone (RejectedValueError from
393
447
  * shareMemory -> writeEntry). Unlike secretSkipped, this can only be
394
448
  * detected by attempting the write — `dryRun` returns candidates before the
@@ -401,73 +455,15 @@ export function autoShare(localRoot, options = {}) {
401
455
  const localEntries = loadAllEntries(localRoot, options.tenantId);
402
456
  initGlobal();
403
457
  const globalRoot = getGlobalRoot();
404
- // L9: host-wide read. The global store IS the union across all tenants;
458
+ // Host-wide read. The global store IS the union across all tenants;
405
459
  // per-tenant filtering on the global root would defeat the purpose.
406
460
  const globalEntries = loadAllEntries(globalRoot);
407
461
  // Build set of global content hashes to avoid duplicates
408
462
  const globalContentSet = storedTextKeys(globalEntries);
409
- const candidates = localEntries.filter((entry) => {
410
- // CD5: shareMemory refuses quarantined rows; filtering here keeps sleep from aborting on one.
411
- if (isQuarantineScope(entry.scope ?? null))
412
- return false;
413
- // Before the score: these rows describe one project only, and a git seed's 'error' tag clears the bar.
414
- if (entry.tags.some((t) => NEVER_AUTO_SHARE_TAGS.has(t)) || entry.source.startsWith(AGENT_MEMORY_SOURCE_PREFIX)) {
415
- if (options.stats)
416
- options.stats.neverAutoShareSkipped = (options.stats.neverAutoShareSkipped ?? 0) + 1;
417
- return false;
418
- }
419
- const score = transferScore(entry);
420
- if (score < minScore)
421
- return false;
422
- // Skip if already shared (same text apart from spacing)
423
- if (globalContentSet.has(duplicateKey(entry.content)))
424
- return false;
425
- // v39 S4 producer veto: secret rows never auto-share, regardless of
426
- // transfer score. (shareMemory would throw; filtering here keeps the
427
- // sleep pipeline fail-safe.) Checked LAST (v1.25.0) so the stats counter
428
- // only counts rows the veto actually withheld — a row failing the score
429
- // or dedupe gates was never going to share, secret or not.
430
- if (detectSecret(entry).flagged) {
431
- if (options.stats)
432
- options.stats.secretSkipped++;
433
- return false;
434
- }
435
- return true;
436
- });
463
+ const candidates = localEntries.filter((entry) => isAutoShareCandidate(entry, globalContentSet, minScore, options.stats));
437
464
  if (dryRun)
438
465
  return candidates;
439
- const shared = [];
440
- // AT1 containment (docs/plans/2026-08-15-at1-rejected-value-tombstone.md
441
- // plan §3 — sync/promote/share copy paths must not let ONE rejected
442
- // candidate kill the batch): shareMemory -> writeEntry hits the LIVE guard
443
- // against the GLOBAL store's tombstones. A matching candidate throws
444
- // RejectedValueError, which (uncaught) would abort this whole loop and,
445
- // via api.ts's sleep pipeline, the entire autoShare sleep phase. Mirrors
446
- // syncGlobalToLocal's per-item catch just above in this file. writeEntry's
447
- // own catch already writes the reject_refusal audit before rethrowing
448
- // (plan §3) — do not double-audit here, just count and continue.
449
- let rejectedSkipped = 0;
450
- for (const entry of candidates) {
451
- try {
452
- // skipEmbed: batching invariant, this is a batch producer, so it embeds
453
- // once via embedAll() below rather than once per row inside shareMemory.
454
- const result = shareMemory(localRoot, entry.id, { force: true, skipEmbed: true });
455
- if (result)
456
- shared.push(result);
457
- }
458
- catch (err) {
459
- if (err instanceof RejectedValueError) {
460
- rejectedSkipped++;
461
- if (options.stats)
462
- options.stats.rejectedSkipped = (options.stats.rejectedSkipped ?? 0) + 1;
463
- continue;
464
- }
465
- throw err;
466
- }
467
- }
468
- if (rejectedSkipped > 0) {
469
- log.warn(`autoShare: skipped ${rejectedSkipped} candidate(s) refused by the global store's rejection tombstone`);
470
- }
466
+ const shared = shareCandidates(localRoot, candidates, options.stats);
471
467
  if (shared.length > 0) {
472
468
  void embedAll(globalRoot).catch((err) => logEmbedAllFailure('autoShare', err));
473
469
  }
@@ -481,18 +477,18 @@ export function autoShare(localRoot, options = {}) {
481
477
  export function syncGlobalToLocal(localRoot, globalRoot, opts = {}) {
482
478
  if (!fs.existsSync(globalRoot))
483
479
  return 0;
484
- // L9: host-wide read. syncGlobalToLocal copies the global union into a
480
+ // Host-wide read. syncGlobalToLocal copies the global union into a
485
481
  // tenant-scoped local store; writeEntry on each row carries the tenant if
486
482
  // the local-root context provides one.
487
483
  const globalEntries = loadAllEntries(globalRoot);
488
484
  const localIndex = loadIndex(localRoot);
489
485
  const textKey = (e) => `${e.tenantId}\n${e.content}`;
490
486
  const localText = new Set(loadAllEntries(localRoot).map(textKey));
491
- // v39 (codex P1-4): syncing down must not re-import what ambient context
487
+ // Syncing down must not re-import what ambient context
492
488
  // excludes - other-project rows are skipped by default and secret rows
493
489
  // are never copied. origin_project is preserved on the copy (writeEntry
494
490
  // only stamps when the field is missing).
495
- const currentName = deriveOriginProject(path.dirname(path.resolve(localRoot)));
491
+ const currentProject = resolveProjectIdentity(path.dirname(path.resolve(localRoot)));
496
492
  let count = 0;
497
493
  // A locally rejected value must not come back through sync down: caught per item, printed as one line.
498
494
  let rejected = 0;
@@ -508,7 +504,7 @@ export function syncGlobalToLocal(localRoot, globalRoot, opts = {}) {
508
504
  if (detectSecret(entry).flagged)
509
505
  continue;
510
506
  if (!opts.includeCrossProject &&
511
- classifyOriginProject(entry.origin_project, currentName) === 'cross-project')
507
+ classifyOriginProject(entry.origin_project, currentProject) === 'cross-project')
512
508
  continue;
513
509
  try {
514
510
  writeEntry(localRoot, entry);
package/dist/skills.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * E2 skill first-class object (docs/plans/2026-05-30-e2-skill-object.md).
2
+ * Skills as a first-class, versioned object.
3
3
  *
4
4
  * A `skill` is a reusable, agent-followable capability: an `instructions` body
5
5
  * plus an optional `trigger` ("when to apply"), evolving via the supersede delta
@@ -29,8 +29,8 @@ export declare const VALID_SKILL_STATES: ReadonlySet<SkillStatus>;
29
29
  export declare const MAX_SKILL_NAME_LEN = 256;
30
30
  export declare const MAX_SKILL_INSTRUCTIONS_LEN = 8192;
31
31
  export declare const MAX_SKILL_TRIGGER_LEN = 1024;
32
- /** Aggregate bound on a single export render (plan-eng-critic: cap the unbounded
33
- * export body). Realistic active-skill counts are tens; 1000 is a generous bound. */
32
+ /** Aggregate bound on a single export render so the export body is never unbounded.
33
+ * Realistic active-skill counts are tens; 1000 is a generous bound. */
34
34
  export declare const MAX_EXPORT_SKILLS = 1000;
35
35
  export interface Skill {
36
36
  id: number;
package/dist/skills.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * E2 skill first-class object (docs/plans/2026-05-30-e2-skill-object.md).
2
+ * Skills as a first-class, versioned object.
3
3
  *
4
4
  * A `skill` is a reusable, agent-followable capability: an `instructions` body
5
5
  * plus an optional `trigger` ("when to apply"), evolving via the supersede delta
@@ -39,13 +39,13 @@ export const VALID_SKILL_STATES = new Set([
39
39
  export const MAX_SKILL_NAME_LEN = 256;
40
40
  export const MAX_SKILL_INSTRUCTIONS_LEN = 8192;
41
41
  export const MAX_SKILL_TRIGGER_LEN = 1024;
42
- /** Aggregate bound on a single export render (plan-eng-critic: cap the unbounded
43
- * export body). Realistic active-skill counts are tens; 1000 is a generous bound. */
42
+ /** Aggregate bound on a single export render so the export body is never unbounded.
43
+ * Realistic active-skill counts are tens; 1000 is a generous bound. */
44
44
  export const MAX_EXPORT_SKILLS = 1000;
45
45
  /**
46
46
  * Validate + normalise skill fields. skill_name is trimmed and MUST be a single
47
- * line (no newlines) so it cannot break the H2 header in the export render
48
- * (plan-eng-critic). instructions are kept verbatim (operator content) but capped.
47
+ * line (no newlines) so it cannot break the H2 header in the export render.
48
+ * instructions are kept verbatim (operator content) but capped.
49
49
  * Returns the normalised name + trigger (null when absent/empty).
50
50
  */
51
51
  function validateSkillFields(skillName, instructions, trigger) {
@@ -69,8 +69,8 @@ function validateSkillFields(skillName, instructions, trigger) {
69
69
  throw new BadRequestError(`saveSkill: trigger exceeds the ${MAX_SKILL_TRIGGER_LEN}-char cap`);
70
70
  }
71
71
  // Single-line, like skill_name: a trigger is a short "when to apply" phrase,
72
- // and a newline would let it forge a heading inside the export **When:** line
73
- // (independent-review 2026-05-30). Reject rather than emit a multi-line trigger.
72
+ // and a newline would let it forge a heading inside the export **When:** line.
73
+ // Reject rather than emit a multi-line trigger.
74
74
  if (/[\r\n]/.test(trigger)) {
75
75
  throw new BadRequestError('saveSkill: trigger must be a single line (no newlines)');
76
76
  }
@@ -114,7 +114,7 @@ function buildSkillContent(skillName, instructions, trigger) {
114
114
  // Preflight the supersede target BEFORE inserting the new row (so the new
115
115
  // autoincrement id can never be its own supersede target); read the
116
116
  // predecessor version in the same SELECT for server-derived versioning.
117
- // Mirrors saveProcess / savePolicy (codex P1 2026-05-28).
117
+ // Mirrors saveProcess / savePolicy.
118
118
  function preflightSkillSupersede(db, tenantId, supersedesId) {
119
119
  // SAFETY: row shape matches the `status, version` columns named in the SELECT below.
120
120
  const pred = db.prepare(`SELECT status, version FROM skills WHERE id = ? AND tenant_id = ?`).get(supersedesId, tenantId);
@@ -1,7 +1,7 @@
1
1
  import { openHippoDb } from '../db.js';
2
2
  import { type AuditOp } from '../audit.js';
3
3
  import { RejectedValueError } from '../rejection.js';
4
- import type { JsonValue } from './rows.js';
4
+ import type { JsonValue } from '../json.js';
5
5
  /**
6
6
  * Emit an audit event for a mutation against `db`. Wrapped so a broken audit
7
7
  * log can never crash the surrounding mutation — the SQLite store is still the
@@ -9,7 +9,7 @@ import type { JsonValue } from './rows.js';
9
9
  */
10
10
  export declare function audit(db: ReturnType<typeof openHippoDb>, op: AuditOp, targetId?: string, metadata?: Record<string, JsonValue>, actor?: string, tenantId?: string): void;
11
11
  /**
12
- * Refusal audit for the AT1 rejection guard (plan §3). Written by the
12
+ * Refusal audit for the rejected-value guard. Written by the
13
13
  * transaction OWNER post-rollback — writeEntry's catch (no outer tx exists
14
14
  * there, so this lands in a fresh implicit transaction) and api.supersede's
15
15
  * catch (after its own ROLLBACK) — never inside a scope the caller's own
@@ -21,7 +21,7 @@ export function audit(db, op, targetId, metadata, actor = 'cli', tenantId) {
21
21
  }
22
22
  }
23
23
  /**
24
- * Refusal audit for the AT1 rejection guard (plan §3). Written by the
24
+ * Refusal audit for the rejected-value guard. Written by the
25
25
  * transaction OWNER post-rollback — writeEntry's catch (no outer tx exists
26
26
  * there, so this lands in a fresh implicit transaction) and api.supersede's
27
27
  * catch (after its own ROLLBACK) — never inside a scope the caller's own
@@ -16,8 +16,8 @@ export declare function loadAmbientCandidates(hippoRoot: string, tenantId: strin
16
16
  export interface ContextCandidateFilter {
17
17
  /** Envelope scope asked for by name; absent applies recall's default deny. */
18
18
  exactScope?: string;
19
- /** Rows of this project and user-global rows pass; absent admits every origin. */
20
- project?: string;
19
+ /** Rows carrying one of these project names, and user-global rows, pass; absent admits every origin. */
20
+ project?: readonly string[];
21
21
  /** Most rows returned; past it, the rows decay has worn least win. */
22
22
  cap: number;
23
23
  now: Date;
@@ -2,6 +2,7 @@ import { closeHippoDb } from '../db.js';
2
2
  import { RECALL_DEFAULT_DENY_SCOPES } from '../recall-scope.js';
3
3
  import { MEMORY_SELECT_COLUMNS, rowToEntry, parseJsonArray } from './rows.js';
4
4
  import { openStore } from './open.js';
5
+ import { originInSql } from '../project-identity.js';
5
6
  import { pickRarestFtsQuery, loadRecallSearchEntriesFromDb } from './search-rows.js';
6
7
  const AMBIENT_SCOPED = 'superseded_by IS NULL AND tenant_id = ?';
7
8
  /** Exported so the plan test runs the exact SQL; idx_memories_pinned (db.ts v51) serves it. */
@@ -9,7 +10,7 @@ export const AMBIENT_PINNED_WHERE = `pinned = 1 AND ${AMBIENT_SCOPED} ORDER BY c
9
10
  /** Exported so the plan test runs the exact SQL; idx_memories_created_drift (db.ts v51) serves it. */
10
11
  export const AMBIENT_DRIFT_SQL = `SELECT 1 FROM memories WHERE ${AMBIENT_SCOPED} AND (length(created) <> 24 OR created NOT LIKE '%Z') LIMIT 1`;
11
12
  // The pins plus the `recentNeeded` newest rows that pass `admit`, for ambient
12
- // injection. One connection; `recall` piggybacks the Z1 FTS query on it too.
13
+ // injection. One connection; `recall` piggybacks the prompt-recall FTS query on it too.
13
14
  export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit, recall) {
14
15
  // A SQL LIMIT takes an integer; the Array.slice this replaced truncated one,
15
16
  // and include_recent is any non-negative finite number at the HTTP edge.
@@ -78,8 +79,8 @@ export function loadContextCandidates(hippoRoot, tenantId, filter) {
78
79
  params.push(...RECALL_DEFAULT_DENY_SCOPES);
79
80
  }
80
81
  if (filter.project !== undefined) {
81
- where.push(`(origin_project = '' OR origin_project = ?)`);
82
- params.push(filter.project);
82
+ where.push(`(origin_project = '' OR ${originInSql(filter.project)})`);
83
+ params.push(...filter.project);
83
84
  }
84
85
  const db = openStore(hippoRoot);
85
86
  try {
@@ -3,7 +3,8 @@ import { rejectionDigest, insertRejectedValue, normalizeValueForRejection } from
3
3
  import { archiveRawMemory } from '../raw-archive.js';
4
4
  import { rowToMemoryConflict } from './rows.js';
5
5
  import { audit } from './audit-event.js';
6
- import { syncMirrorFiles, purgeMirrorBestEffort } from './mirrors.js';
6
+ import { syncChangedMirrors, purgeMirrorBestEffort } from './mirrors.js';
7
+ import { selectEntriesByIds } from './entry-reads.js';
7
8
  import { openStore } from './open.js';
8
9
  import { deleteEntryCore } from './delete-and-batch.js';
9
10
  function canonicalConflictPair(aId, bId) {
@@ -81,9 +82,9 @@ export function replaceDetectedConflicts(hippoRoot, detected, detectedAt = new D
81
82
  }));
82
83
  resolveStaleOpenConflicts(db, canonicalDetected, sameTenant, detectedAt);
83
84
  upsertDetectedConflicts(db, canonicalDetected, sameTenant, detectedAt);
84
- rebuildConflictsWithJson(db, sameTenant);
85
+ const changedIds = rebuildConflictsWithJson(db, sameTenant);
85
86
  db.exec('COMMIT');
86
- syncMirrorFiles(hippoRoot, db);
87
+ syncChangedMirrors(hippoRoot, db, [...selectEntriesByIds(db, changedIds).values()]);
87
88
  }
88
89
  catch (error) {
89
90
  try {
@@ -123,36 +124,38 @@ function resolveStaleOpenConflicts(db, canonicalDetected, sameTenant, detectedAt
123
124
  FROM memory_conflicts
124
125
  WHERE status = 'open'
125
126
  `).all();
127
+ const resolve = db.prepare(`UPDATE memory_conflicts SET status = 'resolved', updated_at = ? WHERE id = ?`);
126
128
  for (const row of openRows) {
127
129
  const key = `${row.memory_a_id}::${row.memory_b_id}`;
128
130
  const stale = !detectedKeys.has(key);
129
131
  // v1.11.0 residue: auto-resolve any open cross-tenant row. The insert
130
- // loop below (line 2089) and the refMap rebuild (line 2117) skip
132
+ // loop in upsertDetectedConflicts and the refMap rebuild skip
131
133
  // cross-tenant pairs, but the resolve-stale loop previously left
132
134
  // re-detected cross-tenant rows lingering status='open'. The
133
135
  // sameTenant() helper is already built one block up; no extra query.
134
136
  const crossTenant = !sameTenant(row.memory_a_id, row.memory_b_id);
135
- if (stale || crossTenant) {
136
- db.prepare(`UPDATE memory_conflicts SET status = 'resolved', updated_at = ? WHERE id = ?`).run(detectedAt, row.id);
137
- }
137
+ if (stale || crossTenant)
138
+ resolve.run(detectedAt, row.id);
138
139
  }
139
140
  }
140
141
  function upsertDetectedConflicts(db, canonicalDetected, sameTenant, detectedAt) {
142
+ const upsert = db.prepare(`
143
+ INSERT INTO memory_conflicts(memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at)
144
+ VALUES (?, ?, ?, ?, 'open', ?, ?)
145
+ ON CONFLICT(memory_a_id, memory_b_id) DO UPDATE SET
146
+ reason = excluded.reason,
147
+ score = excluded.score,
148
+ status = 'open',
149
+ updated_at = excluded.updated_at
150
+ `);
141
151
  for (const conflict of canonicalDetected) {
142
152
  // Skip cross-tenant pairs — never persist a conflict spanning tenants.
143
153
  if (!sameTenant(conflict.memory_a_id, conflict.memory_b_id))
144
154
  continue;
145
- db.prepare(`
146
- INSERT INTO memory_conflicts(memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at)
147
- VALUES (?, ?, ?, ?, 'open', ?, ?)
148
- ON CONFLICT(memory_a_id, memory_b_id) DO UPDATE SET
149
- reason = excluded.reason,
150
- score = excluded.score,
151
- status = 'open',
152
- updated_at = excluded.updated_at
153
- `).run(conflict.memory_a_id, conflict.memory_b_id, conflict.reason, conflict.score, detectedAt, detectedAt);
155
+ upsert.run(conflict.memory_a_id, conflict.memory_b_id, conflict.reason, conflict.score, detectedAt, detectedAt);
154
156
  }
155
157
  }
158
+ /** Rewrites only the rows whose conflicts_with_json changes, and returns their ids. */
156
159
  function rebuildConflictsWithJson(db, sameTenant) {
157
160
  // SAFETY: openConflicts' shape matches the two columns named above.
158
161
  const openConflicts = db.prepare(`
@@ -173,13 +176,18 @@ function rebuildConflictsWithJson(db, sameTenant) {
173
176
  refMap.get(row.memory_a_id).add(row.memory_b_id);
174
177
  refMap.get(row.memory_b_id).add(row.memory_a_id);
175
178
  }
176
- // SAFETY: memoryRows' shape matches the single `id` column selected
177
- // above.
178
- const memoryRows = db.prepare(`SELECT id FROM memories`).all();
179
+ // SAFETY: memoryRows' shape matches the two columns selected below.
180
+ const memoryRows = db.prepare(`SELECT id, conflicts_with_json FROM memories`).all();
181
+ const update = db.prepare(`UPDATE memories SET conflicts_with_json = ?, updated_at = datetime('now') WHERE id = ?`);
182
+ const changedIds = [];
179
183
  for (const memory of memoryRows) {
180
- const refs = Array.from(refMap.get(memory.id) ?? []).sort();
181
- db.prepare(`UPDATE memories SET conflicts_with_json = ?, updated_at = datetime('now') WHERE id = ?`).run(JSON.stringify(refs), memory.id);
184
+ const refsJson = JSON.stringify(Array.from(refMap.get(memory.id) ?? []).sort());
185
+ if (memory.conflicts_with_json === refsJson)
186
+ continue;
187
+ update.run(refsJson, memory.id);
188
+ changedIds.push(memory.id);
182
189
  }
190
+ return changedIds;
183
191
  }
184
192
  /**
185
193
  * Resolve a conflict by keeping one memory and weakening the other.
@@ -230,7 +238,7 @@ export function resolveConflict(hippoRoot, conflictId, keepId, forgetLoser = fal
230
238
  stripConflictRefs(db, target, removal.loserRemoved);
231
239
  auditConflictResolve(db, target, removal, tenantId);
232
240
  db.exec('COMMIT');
233
- syncMirrorFiles(hippoRoot, db);
241
+ syncChangedMirrors(hippoRoot, db, [...selectEntriesByIds(db, [keepId, loserId]).values()]);
234
242
  if (removal.loserRemoved)
235
243
  purgeRemovedLoserMirrors(hippoRoot, db, loserId, removal);
236
244
  return { conflict: { ...conflict, status: 'resolved' }, loserId };
@@ -1,25 +1,16 @@
1
1
  import { type MemoryEntry } from '../memory.js';
2
- import { openHippoDb } from '../db.js';
2
+ import { openHippoDb, type DatabaseSyncLike } from '../db.js';
3
3
  import { type DormantMove } from '../dormant.js';
4
4
  /** Tables whose rows keep a first-class object's backing memory in `memory_id` (ON DELETE SET NULL); tests/dormant-memories.test.ts pins it to the schema. */
5
5
  export declare const MEMORY_BACKED_TABLES: readonly ["predictions", "decisions", "incidents", "processes", "policies", "skills", "project_briefs", "customer_notes"];
6
6
  /** Ids of memories that back a first-class object, for passes that plan deletes before making them. A table missing from an older schema is skipped. */
7
7
  export declare function memoriesBackingObjects(hippoRoot: string): Set<string>;
8
8
  /**
9
- * AT1 (plan §4, round-2 fix, designed from source): db-scoped delete core.
10
- * `deleteEntry` used to open+close its OWN connection, which meant it could
11
- * never compose inside a caller's transaction (unlike writeEntry/
12
- * writeEntryDbOnly, which already split this way). Split identically: row-
13
- * meta SELECT, `DELETE FROM memories`, FTS delete, `forget` audit, DAG
14
- * dirty-mark. NO filesystem I/O — the caller's own transaction may still be
15
- * rolled back, and mirror writes must only happen post-commit.
9
+ * db-scoped delete core, so a delete can compose inside a caller's transaction.
10
+ * NO filesystem I/O: the caller's transaction may still roll back, and mirrors are written post-commit.
16
11
  *
17
- * `opts.suppressForgetAudit` (default false, off): two AT1 callers set this
18
- * so a removed non-raw row does NOT ALSO emit a `forget` row, because each
19
- * already writes its own aggregate audit trail — `src/reject-flow.ts`'s
20
- * `rejectValue` (single `reject_value` row covering every same-digest row
21
- * removed) and `resolveConflict` (`conflict_resolve` row per resolution).
22
- * Default keeps `deleteEntry` byte-identical to its pre-split behavior.
12
+ * `opts.suppressForgetAudit` (default false): `rejectValue` and `resolveConflict` set it because each
13
+ * writes its own aggregate audit row, so a removed row must not ALSO emit a `forget` row.
23
14
  *
24
15
  * Returns `{tenantId, dagParentId}` for the removed row, or `null` if no row with `id`
25
16
  * existed or `automatic` refused it (pinned, raw, kept for good or backing an object at DELETE time, so a late pin wins).
@@ -49,6 +40,12 @@ export declare function deleteEntry(hippoRoot: string, id: string, opts?: {
49
40
  reason?: string;
50
41
  automatic?: boolean;
51
42
  }): boolean;
43
+ /** deleteEntry on the caller's open store, so a loop of deletes opens the store once; each delete still commits alone. */
44
+ export declare function deleteEntryOn(db: DatabaseSyncLike, hippoRoot: string, id: string, opts?: {
45
+ actor?: string;
46
+ reason?: string;
47
+ automatic?: boolean;
48
+ }): boolean;
52
49
  /** Consolidation's flush, one transaction. With `snapshot` (rows as the caller loaded them), a write keeps only
53
50
  * the fields the caller changed, takes the rest from the live row, and never resurrects a row that is gone.
54
51
  *