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
@@ -7,7 +7,7 @@ import { loadConfig } from './config.js';
7
7
  import { closeHippoDb, isSqliteBusy, openHippoDb } from './db.js';
8
8
  import { gatedWrite } from './gated-write.js';
9
9
  import { COMPACTION_MEMORY_TAG, COMPACTION_SOURCE_PREFIX, Layer, createMemory, generateId } from './memory.js';
10
- import { deriveOriginProject, isGlobalStoreRoot } from './project-identity.js';
10
+ import { isGlobalStoreRoot, originInSql, projectId, projectNames, resolveProjectIdentity } from './project-identity.js';
11
11
  import { maskEmails, redactSecretsStrict } from './secret-detect.js';
12
12
  import { strengthenRetrievedOn, writeEntryMirrors } from './store/entry-writes.js';
13
13
  import { isRecallBoostAblated } from './ablation.js';
@@ -30,11 +30,19 @@ const CLAIMED_SUFFIX = '.claimed';
30
30
  /** The marker redactSecretsStrict writes; an item holding it was a secret before it was stored. */
31
31
  const REDACTED = '[REDACTED]';
32
32
  /** Where the session ran: rows written through the global store keep the project the session was in. */
33
- export function compactionOrigin(hippoRoot, cwd) {
33
+ function compactionProject(hippoRoot, cwd) {
34
34
  if (!isGlobalStoreRoot(hippoRoot))
35
- return deriveOriginProject(path.dirname(hippoRoot));
35
+ return resolveProjectIdentity(path.dirname(hippoRoot));
36
36
  // No cwd means user-global, as stampOriginProject gives the global store; undefined would fall back to the hook's own cwd.
37
- return cwd === null ? '' : deriveOriginProject(cwd);
37
+ return cwd === null ? '' : resolveProjectIdentity(cwd);
38
+ }
39
+ function compactionOrigin(hippoRoot, cwd) {
40
+ return projectId(compactionProject(hippoRoot, cwd));
41
+ }
42
+ /** The names a record's held rows may carry: its origin, plus the folder name rows saved before project ids used. */
43
+ function heldOrigins(hippoRoot, cwd, originProject) {
44
+ const names = projectNames(compactionProject(hippoRoot, cwd));
45
+ return names.includes(originProject) ? names : [originProject];
38
46
  }
39
47
  function scrub(text) {
40
48
  return maskEmails(redactSecretsStrict(text));
@@ -158,12 +166,76 @@ function restates(item, held) {
158
166
  return next === item.length;
159
167
  }
160
168
  /** Live rows of one tenant and origin that default recall shows: a compaction often restates what an earlier one, or the user, already saved. */
161
- function heldRows(db, tenantId, originProject) {
169
+ function heldRows(db, tenantId, origins) {
162
170
  // SAFETY: the SELECT names the id, source_session_id and content columns.
163
- const rows = db.prepare(`SELECT id, source_session_id, content FROM memories WHERE tenant_id = ? AND origin_project = ? AND superseded_by IS NULL AND kind != 'raw'
164
- AND (scope IS NULL OR (scope != 'unknown:legacy' AND scope NOT LIKE '%:private:%'))`).all(tenantId, originProject);
171
+ const rows = db.prepare(`SELECT id, source_session_id, content FROM memories WHERE tenant_id = ? AND ${originInSql(origins)} AND superseded_by IS NULL AND kind != 'raw'
172
+ AND (scope IS NULL OR (scope != 'unknown:legacy' AND scope NOT LIKE '%:private:%'))`).all(tenantId, ...origins);
165
173
  return rows.map((r) => ({ id: r.id, sessionId: r.source_session_id, words: words(r.content) }));
166
174
  }
175
+ function compactionEntry(text, ctx, baseHalfLifeDays) {
176
+ return {
177
+ ...createMemory(text, {
178
+ layer: Layer.Episodic,
179
+ tags: [COMPACTION_MEMORY_TAG],
180
+ source: `${COMPACTION_SOURCE_PREFIX}${ctx.sessionId}`,
181
+ confidence: 'observed',
182
+ kind: 'distilled',
183
+ source_session_id: ctx.sessionId,
184
+ tenantId: ctx.tenantId,
185
+ baseHalfLifeDays,
186
+ }),
187
+ origin_project: ctx.originProject,
188
+ };
189
+ }
190
+ /** Runs inside saveItems' transaction: restatements of held rows are counted, the rest go through the write gate. */
191
+ function writeItemRows(db, hippoRoot, ctx, rows, baseHalfLifeDays) {
192
+ const out = { written: [], repeats: 0, refused: 0, restated: [] };
193
+ const held = heldRows(db, ctx.tenantId, heldOrigins(hippoRoot, ctx.cwd, ctx.originProject));
194
+ const restated = new Set();
195
+ for (const text of rows) {
196
+ const itemWords = words(text);
197
+ const match = held.find((h) => restates(itemWords, h.words));
198
+ if (match) {
199
+ out.repeats++;
200
+ if (match.id !== null && match.sessionId !== ctx.sessionId)
201
+ restated.add(match.id);
202
+ continue;
203
+ }
204
+ // createMemory throws below 3 chars, which would sink the whole transaction.
205
+ if (text.trim().length < 3) {
206
+ out.refused++;
207
+ continue;
208
+ }
209
+ const entry = compactionEntry(text, ctx, baseHalfLifeDays);
210
+ if (gatedWrite(db, hippoRoot, entry, { actor: 'post-compact' }) === 'written') {
211
+ out.written.push(entry);
212
+ held.push({ id: null, sessionId: ctx.sessionId, words: itemWords });
213
+ }
214
+ else {
215
+ out.refused++;
216
+ }
217
+ }
218
+ out.restated = [...restated];
219
+ return out;
220
+ }
221
+ /** After commit: report skips, mirror the new rows, bump the counter. */
222
+ function finishItemWrites(hippoRoot, writes, log) {
223
+ if (writes.repeats > 0)
224
+ log(`skipped ${writes.repeats} item(s) the store already holds`);
225
+ if (writes.refused > 0)
226
+ log(`skipped ${writes.refused} item(s) the write gate refused`);
227
+ for (const entry of writes.written)
228
+ writeEntryMirrors(hippoRoot, entry);
229
+ if (writes.written.length > 0) {
230
+ try {
231
+ updateStats(hippoRoot, { remembered: writes.written.length });
232
+ }
233
+ catch (err) {
234
+ // The rows are committed; a counter that could not be bumped must not turn that into a failed step.
235
+ log(`remembered counter not updated: ${errorMessage(err)}`);
236
+ }
237
+ }
238
+ }
167
239
  /** Items that become rows, then the record `done`, in one transaction so two sessions cannot both insert one text. Mirrors after commit. */
168
240
  export function saveItems(db, hippoRoot, ctx, log) {
169
241
  const usable = ctx.items.filter((item) => !item.includes(REDACTED));
@@ -175,9 +247,7 @@ export function saveItems(db, hippoRoot, ctx, log) {
175
247
  if (capped > 0)
176
248
  log(`capped: ${capped} more kept in the record only`);
177
249
  const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
178
- const written = [];
179
- let repeats = 0;
180
- let refused = 0;
250
+ let writes;
181
251
  db.exec('BEGIN IMMEDIATE');
182
252
  try {
183
253
  if (ctx.recordId !== null) {
@@ -190,47 +260,11 @@ export function saveItems(db, hippoRoot, ctx, log) {
190
260
  return current?.items_written ?? 0;
191
261
  }
192
262
  }
193
- const held = heldRows(db, ctx.tenantId, ctx.originProject);
194
- const restated = new Set();
195
- for (const text of rows) {
196
- const itemWords = words(text);
197
- const match = held.find((h) => restates(itemWords, h.words));
198
- if (match) {
199
- repeats++;
200
- if (match.id !== null && match.sessionId !== ctx.sessionId)
201
- restated.add(match.id);
202
- continue;
203
- }
204
- // createMemory throws below 3 chars, which would sink the whole transaction.
205
- if (text.trim().length < 3) {
206
- refused++;
207
- continue;
208
- }
209
- const entry = {
210
- ...createMemory(text, {
211
- layer: Layer.Episodic,
212
- tags: [COMPACTION_MEMORY_TAG],
213
- source: `${COMPACTION_SOURCE_PREFIX}${ctx.sessionId}`,
214
- confidence: 'observed',
215
- kind: 'distilled',
216
- source_session_id: ctx.sessionId,
217
- tenantId: ctx.tenantId,
218
- baseHalfLifeDays,
219
- }),
220
- origin_project: ctx.originProject,
221
- };
222
- if (gatedWrite(db, hippoRoot, entry, { actor: 'post-compact' }) === 'written') {
223
- written.push(entry);
224
- held.push({ id: null, sessionId: ctx.sessionId, words: itemWords });
225
- }
226
- else {
227
- refused++;
228
- }
229
- }
263
+ writes = writeItemRows(db, hippoRoot, ctx, rows, baseHalfLifeDays);
230
264
  // Said again by another session is the same signal as being recalled; the same session carrying it forward is not.
231
- strengthenRetrievedOn(db, [...restated], { tenantId: ctx.tenantId, recallBoostAblated: isRecallBoostAblated() });
265
+ strengthenRetrievedOn(db, writes.restated, { tenantId: ctx.tenantId, recallBoostAblated: isRecallBoostAblated() });
232
266
  if (ctx.recordId !== null) {
233
- db.prepare(`UPDATE compactions SET items_written = ?, status = 'done' WHERE tenant_id = ? AND id = ?`).run(written.length, ctx.tenantId, ctx.recordId);
267
+ db.prepare(`UPDATE compactions SET items_written = ?, status = 'done' WHERE tenant_id = ? AND id = ?`).run(writes.written.length, ctx.tenantId, ctx.recordId);
234
268
  }
235
269
  db.exec('COMMIT');
236
270
  }
@@ -241,22 +275,8 @@ export function saveItems(db, hippoRoot, ctx, log) {
241
275
  catch { /* already rolled back; keep the original error */ }
242
276
  throw err;
243
277
  }
244
- if (repeats > 0)
245
- log(`skipped ${repeats} item(s) the store already holds`);
246
- if (refused > 0)
247
- log(`skipped ${refused} item(s) the write gate refused`);
248
- for (const entry of written)
249
- writeEntryMirrors(hippoRoot, entry);
250
- if (written.length > 0) {
251
- try {
252
- updateStats(hippoRoot, { remembered: written.length });
253
- }
254
- catch (err) {
255
- // The rows are committed; a counter that could not be bumped must not turn that into a failed step.
256
- log(`remembered counter not updated: ${errorMessage(err)}`);
257
- }
258
- }
259
- return written.length;
278
+ finishItemWrites(hippoRoot, writes, log);
279
+ return writes.written.length;
260
280
  }
261
281
  /** null when the text is not a PostCompact payload naming a session. */
262
282
  export function parsePostCompactPayload(stdinText) {
@@ -320,6 +340,7 @@ export function saveCompaction(hippoRoot, payload, log) {
320
340
  recordId: record?.id ?? null,
321
341
  sessionId: payload.sessionId,
322
342
  originProject: record?.originProject ?? compactionOrigin(hippoRoot, payload.cwd),
343
+ cwd: payload.cwd,
323
344
  items: text.items,
324
345
  }, log);
325
346
  }
@@ -494,7 +515,7 @@ function importSpool(db, hippoRoot, tenantId, log, deadline) {
494
515
  // The record holds the items now, so the file is done even if the write below fails.
495
516
  fs.rmSync(claimed, { force: true });
496
517
  removed = true;
497
- saveItems(db, hippoRoot, { tenantId: spooled.tenantId, recordId: record.id, sessionId: record.sessionId, originProject: record.originProject, items: record.items }, log);
518
+ saveItems(db, hippoRoot, { tenantId: spooled.tenantId, recordId: record.id, sessionId: record.sessionId, originProject: record.originProject, cwd: record.cwd, items: record.items }, log);
498
519
  finished++;
499
520
  }
500
521
  catch (err) {
@@ -515,7 +536,7 @@ export function replayCompactions(db, hippoRoot, log, deadline = Number.POSITIVE
515
536
  if (Date.now() > deadline)
516
537
  break;
517
538
  try {
518
- saveItems(db, hippoRoot, { tenantId, recordId: record.id, sessionId: record.sessionId, originProject: record.originProject, items: record.items }, log);
539
+ saveItems(db, hippoRoot, { tenantId, recordId: record.id, sessionId: record.sessionId, originProject: record.originProject, cwd: record.cwd, items: record.items }, log);
519
540
  finished++;
520
541
  }
521
542
  catch (err) {
@@ -549,7 +570,7 @@ export function replayCompactions(db, hippoRoot, log, deadline = Number.POSITIVE
549
570
  log(`${record.id} was filled by another process`);
550
571
  continue;
551
572
  }
552
- saveItems(db, hippoRoot, { tenantId, recordId: record.id, sessionId: record.sessionId, originProject: record.originProject, items: text.items }, log);
573
+ saveItems(db, hippoRoot, { tenantId, recordId: record.id, sessionId: record.sessionId, originProject: record.originProject, cwd: record.cwd, items: text.items }, log);
553
574
  finished++;
554
575
  }
555
576
  catch (err) {
package/dist/compare.d.ts CHANGED
@@ -6,8 +6,7 @@
6
6
  * multihop.ts, rerankers/*). Every comparator here takes a structural param
7
7
  * type instead of an imported one, on purpose — a type-only import back to
8
8
  * search.ts would still create the search.ts <-> physics.ts ESM import
9
- * cycle this module exists to avoid (r2 critic HIGH,
10
- * docs/plans/2026-07-09-recall-determinism.md T2).
9
+ * cycle this module exists to avoid.
11
10
  *
12
11
  * Mirrors the deliberate-determinism comment style already established in
13
12
  * graph-stream.ts:88, :165-168, :233-236 — a sort with a documented,
@@ -32,11 +31,9 @@ export interface EntryIdentity {
32
31
  * independently-created stores (different directory name, different insert
33
32
  * order of everything else on disk) sorts identically. The metadata keys make
34
33
  * byte-identical twins order by what they carry instead of by `id`
35
- * (`crypto.randomUUID()`), which is per-instance random: before v1.38.1 the
36
- * dedupe survivor of two twins that differed only in tags or source was
37
- * whichever id sorted first, and a semantic/episodic pair always kept the
38
- * episodic copy because `mem_` sorts before `sem_`. `id` stays the terminal
39
- * key so the order is total within one store.
34
+ * (`crypto.randomUUID()`), which is per-instance random and would make the
35
+ * dedupe survivor arbitrary. `id` stays the terminal key so the order is
36
+ * total within one store.
40
37
  *
41
38
  * Layer rank puts semantic first: semantic rows are consolidation output, so
42
39
  * keeping that copy preserves the promotion instead of demoting the memory.
@@ -47,8 +44,8 @@ export interface EntryIdentity {
47
44
  * Plain `<`/`>`, NOT `localeCompare`: `localeCompare` is locale- and
48
45
  * ICU-version-dependent (a determinism leak in its own right) and needlessly
49
46
  * slow for a tiebreak that only needs a total order. The metadata keys are
50
- * only computed on a content tie, which is rare post-T1 (path-tag embedding
51
- * fix), so the per-compare Set/sort cost never lands on the hot path.
47
+ * only computed on a content tie, which is rare, so the per-compare Set/sort
48
+ * cost never lands on the hot path.
52
49
  */
53
50
  export declare function compareEntryIdentity(a: EntryIdentity, b: EntryIdentity): number;
54
51
  /** Minimal shape for score-primary sort sites (SearchResult and friends
@@ -72,13 +69,11 @@ export declare function compareScoredResults(a: ScoredEntryLike, b: ScoredEntryL
72
69
  *
73
70
  * `ScoredPhysicsResult` (physics.ts) carries `{ memoryId, baseScore,
74
71
  * clusterAmplification, finalScore }` -- NO `entry`/`content` in scope at
75
- * that layer, so `compareEntryIdentity` cannot apply directly (plan T2
76
- * shape (c)). With only the default memoryId key this is PER-INSTANCE-ONLY
77
- * determinism; callers that need CROSS-INGEST stability supply `tieKeyOf`
78
- * mapping the result to its memory CONTENT (codex review finding: the
79
- * baseScore tie order selects the cluster_top_k amplification set, which
80
- * MUTATES scores before the downstream content-aware merge sort runs -- so
81
- * the tie key must be content-stable at THIS layer, not just downstream).
72
+ * that layer, so `compareEntryIdentity` cannot apply directly. With only the
73
+ * default memoryId key this is PER-INSTANCE-ONLY determinism; callers that need
74
+ * CROSS-INGEST stability supply `tieKeyOf` mapping the result to its memory
75
+ * CONTENT, because the baseScore tie order selects the cluster_top_k
76
+ * amplification set, which MUTATES scores before the content-aware merge sort.
82
77
  *
83
78
  * A factory (not a fixed-field comparator) because physics.ts re-sorts the
84
79
  * same result array by two different score fields in sequence (`baseScore`
package/dist/compare.js CHANGED
@@ -6,8 +6,7 @@
6
6
  * multihop.ts, rerankers/*). Every comparator here takes a structural param
7
7
  * type instead of an imported one, on purpose — a type-only import back to
8
8
  * search.ts would still create the search.ts <-> physics.ts ESM import
9
- * cycle this module exists to avoid (r2 critic HIGH,
10
- * docs/plans/2026-07-09-recall-determinism.md T2).
9
+ * cycle this module exists to avoid.
11
10
  *
12
11
  * Mirrors the deliberate-determinism comment style already established in
13
12
  * graph-stream.ts:88, :165-168, :233-236 — a sort with a documented,
@@ -21,11 +20,9 @@
21
20
  * independently-created stores (different directory name, different insert
22
21
  * order of everything else on disk) sorts identically. The metadata keys make
23
22
  * byte-identical twins order by what they carry instead of by `id`
24
- * (`crypto.randomUUID()`), which is per-instance random: before v1.38.1 the
25
- * dedupe survivor of two twins that differed only in tags or source was
26
- * whichever id sorted first, and a semantic/episodic pair always kept the
27
- * episodic copy because `mem_` sorts before `sem_`. `id` stays the terminal
28
- * key so the order is total within one store.
23
+ * (`crypto.randomUUID()`), which is per-instance random and would make the
24
+ * dedupe survivor arbitrary. `id` stays the terminal key so the order is
25
+ * total within one store.
29
26
  *
30
27
  * Layer rank puts semantic first: semantic rows are consolidation output, so
31
28
  * keeping that copy preserves the promotion instead of demoting the memory.
@@ -36,8 +33,8 @@
36
33
  * Plain `<`/`>`, NOT `localeCompare`: `localeCompare` is locale- and
37
34
  * ICU-version-dependent (a determinism leak in its own right) and needlessly
38
35
  * slow for a tiebreak that only needs a total order. The metadata keys are
39
- * only computed on a content tie, which is rare post-T1 (path-tag embedding
40
- * fix), so the per-compare Set/sort cost never lands on the hot path.
36
+ * only computed on a content tie, which is rare, so the per-compare Set/sort
37
+ * cost never lands on the hot path.
41
38
  */
42
39
  export function compareEntryIdentity(a, b) {
43
40
  return (compareStrings(a.content, b.content) ||
@@ -90,13 +87,11 @@ export function compareScoredResults(a, b) {
90
87
  *
91
88
  * `ScoredPhysicsResult` (physics.ts) carries `{ memoryId, baseScore,
92
89
  * clusterAmplification, finalScore }` -- NO `entry`/`content` in scope at
93
- * that layer, so `compareEntryIdentity` cannot apply directly (plan T2
94
- * shape (c)). With only the default memoryId key this is PER-INSTANCE-ONLY
95
- * determinism; callers that need CROSS-INGEST stability supply `tieKeyOf`
96
- * mapping the result to its memory CONTENT (codex review finding: the
97
- * baseScore tie order selects the cluster_top_k amplification set, which
98
- * MUTATES scores before the downstream content-aware merge sort runs -- so
99
- * the tie key must be content-stable at THIS layer, not just downstream).
90
+ * that layer, so `compareEntryIdentity` cannot apply directly. With only the
91
+ * default memoryId key this is PER-INSTANCE-ONLY determinism; callers that need
92
+ * CROSS-INGEST stability supply `tieKeyOf` mapping the result to its memory
93
+ * CONTENT, because the baseScore tie order selects the cluster_top_k
94
+ * amplification set, which MUTATES scores before the content-aware merge sort.
100
95
  *
101
96
  * A factory (not a fixed-field comparator) because physics.ts re-sorts the
102
97
  * same result array by two different score fields in sequence (`baseScore`
package/dist/config.d.ts CHANGED
@@ -64,34 +64,33 @@ export interface HippoConfig {
64
64
  enabled: boolean;
65
65
  budget: number;
66
66
  /** Skip a block identical to the one already injected this session
67
- * (ROADMAP TE2). Default true. Needs a session id from the hook payload. */
67
+ * Default true. Needs a session id from the hook payload. */
68
68
  skipUnchanged: boolean;
69
69
  /** Resend an unchanged block after this many consecutive skips, so long
70
70
  * sessions still see pinned rules near the latest turn. Default 10; 0
71
71
  * never resends an unchanged block. */
72
72
  refreshTurns: number;
73
- /** Z1: gate the hook's backfill on the prompt's own content instead of
74
- * the five newest memories. Default true since 1.55.0: overlap tied but median
75
- * tokens fell 847 to 533 (docs/evals/2026-09-26-z1-prompt-recall-result.md). */
73
+ /** Gate the hook's backfill on the prompt's own content instead of the five
74
+ * newest memories. Default true: the same overlap for fewer tokens. */
76
75
  promptRecall: boolean;
77
- /** Z1: overlap metric for the prompt-recall gate. Default 'jaccard' (tuned, docs/evals/2026-09-26-z1-prompt-recall-result.md). */
76
+ /** Overlap metric for the prompt-recall gate. Default 'jaccard' (tuned). */
78
77
  promptRecallMetric: PromptRecallMetric;
79
- /** Z1: minimum overlap score to admit a candidate. Default 0.04 (tuned). */
78
+ /** Minimum overlap score to admit a candidate. Default 0.04 (tuned). */
80
79
  promptRecallThreshold: number;
81
- /** Z1: minimum shared tokens to admit a candidate. Default 2. */
80
+ /** Minimum shared tokens to admit a candidate. Default 2. */
82
81
  promptRecallMinShared: number;
83
- /** Z1: max prompt-recall entries injected per prompt. Default 5 (tuned). */
82
+ /** Max prompt-recall entries injected per prompt. Default 5 (tuned). */
84
83
  promptRecallMaxItems: number;
85
- /** Z1: FTS candidate pool size per store before gating. Default 100. */
84
+ /** FTS candidate pool size per store before gating. Default 100. */
86
85
  promptRecallCandidates: number;
87
86
  };
88
- /** Memory scope isolation (v39): when true (default), ambient context
87
+ /** Memory scope isolation: when true (default), ambient context
89
88
  * (`hippo context`, the UserPromptSubmit hook, /v1/context, MCP
90
89
  * hippo_context) excludes memories owned by OTHER projects; explicit
91
90
  * recall is unaffected. Set false to disable the ORIGIN PARTITION only -
92
91
  * the secret veto is unconditional for ambient surfaces and no config or
93
92
  * flag re-includes secret-flagged rows (explicit recall still returns
94
- * them). See docs/plans/2026-07-01-memory-scope-isolation.md. */
93
+ * them). */
95
94
  contextProjectIsolation: boolean;
96
95
  extraction: {
97
96
  enabled: boolean | 'auto';
@@ -110,11 +109,8 @@ export interface HippoConfig {
110
109
  ambient: {
111
110
  enabled: boolean;
112
111
  };
113
- /** LC2-E3: opt-in learned memory-value rescue veto on the sleep decay pass
114
- * (docs/plans/2026-08-10-lc2-e3-mv-wiring.md). Default OFF — the frozen
115
- * E2 weights (src/memory-value-weights.ts) only run when explicitly
116
- * enabled; no other knobs in v1 (the rescue budget is a code constant
117
- * tied to E2 evidence, not user-tunable). */
112
+ /** Opt-in learned memory-value rescue veto on the sleep decay pass. Default OFF: the frozen weights
113
+ * (src/memory-value-weights.ts) only run when enabled; the rescue budget is a code constant, not user-tunable. */
118
114
  memoryValue: {
119
115
  enabled: boolean;
120
116
  };
@@ -131,8 +127,8 @@ export interface HippoConfig {
131
127
  * Default 180. 0 keeps dormant memories forever. */
132
128
  retentionDays: number;
133
129
  };
134
- /** FE2: tags a memory `churn-stale` when its named file/symbol/script
135
- * changed since storage. Default OFF - FE3 measures before it flips. */
130
+ /** Tags a memory `churn-stale` when its named file/symbol/script
131
+ * changed since storage. Default OFF until a measurement justifies the flip. */
136
132
  churnStaleness: {
137
133
  enabled: boolean;
138
134
  };
@@ -150,6 +146,11 @@ export interface HippoConfig {
150
146
  pilot: {
151
147
  holdoutRateBp: number;
152
148
  };
149
+ /** How a folder's project is named. `remote: false` drops the origin-remote rule, leaving
150
+ * `.hippo-project.json` then the folder name. Read from the global store's config only, so every store agrees. */
151
+ projectIdentity: {
152
+ remote: boolean;
153
+ };
153
154
  }
154
155
  export declare function loadConfig(hippoRoot: string): HippoConfig;
155
156
  //# sourceMappingURL=config.d.ts.map
package/dist/config.js CHANGED
@@ -5,10 +5,11 @@ import * as fs from 'fs';
5
5
  import * as path from 'path';
6
6
  import { DEFAULT_PHYSICS_CONFIG, mergePhysicsConfig } from './physics-config.js';
7
7
  import { DEFAULT_HALF_LIFE_DAYS } from './memory.js';
8
+ import { DEFAULT_LOCAL_BUMP, DEFAULT_RECALL_BUDGET } from './search/types.js';
8
9
  import { log } from './log.js';
9
10
  const DEFAULT_CONFIG = {
10
11
  defaultHalfLifeDays: DEFAULT_HALF_LIFE_DAYS,
11
- defaultBudget: 4000,
12
+ defaultBudget: DEFAULT_RECALL_BUDGET,
12
13
  defaultContextBudget: 3000,
13
14
  decayBasis: 'adaptive',
14
15
  autoLearnOnSleep: true,
@@ -36,7 +37,7 @@ const DEFAULT_CONFIG = {
36
37
  lambda: 0.7,
37
38
  },
38
39
  search: {
39
- localBump: 1.2,
40
+ localBump: DEFAULT_LOCAL_BUMP,
40
41
  },
41
42
  replay: {
42
43
  count: 5,
@@ -92,6 +93,9 @@ const DEFAULT_CONFIG = {
92
93
  pilot: {
93
94
  holdoutRateBp: 0,
94
95
  },
96
+ projectIdentity: {
97
+ remote: true,
98
+ },
95
99
  };
96
100
  function isMemoryValueConfig(value) {
97
101
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -132,6 +136,17 @@ function pilotHoldoutRate(value) {
132
136
  `(got ${JSON.stringify(value)}) - using 0 (pilot off).`);
133
137
  return 0;
134
138
  }
139
+ // Only a real false turns the remote rule off; anything else malformed warns and keeps it on.
140
+ function projectIdentityRemote(value) {
141
+ const remote = value?.remote;
142
+ if (remote === true || remote === false)
143
+ return remote;
144
+ if (value === undefined || (remote === undefined && value !== null && value.constructor === Object))
145
+ return true;
146
+ log.warn(`config.json's "projectIdentity" must be an object like {"remote": false} ` +
147
+ `(got ${JSON.stringify(value)}) - using true (projects named by their origin remote).`);
148
+ return true;
149
+ }
135
150
  function agentMemoryTools(value) {
136
151
  if (value === undefined || value === null)
137
152
  return null;
@@ -141,6 +156,70 @@ function agentMemoryTools(value) {
141
156
  `(got ${JSON.stringify(value)}) - importing none.`);
142
157
  return [];
143
158
  }
159
+ function memoryValueOverride(raw) {
160
+ // Spreading a non-object raw.memoryValue (e.g. {"memoryValue": true}) silently leaves `enabled` false;
161
+ // this feature must never be silently off, so warn loudly and fall back to defaults instead.
162
+ const memoryValueRaw = raw.memoryValue;
163
+ const validMemoryValueConfig = memoryValueRaw === undefined || isMemoryValueConfig(memoryValueRaw);
164
+ if (!validMemoryValueConfig) {
165
+ log.warn(`config.json's "memoryValue" must be an object like {"enabled": true} ` +
166
+ `(got ${JSON.stringify(memoryValueRaw)}) - using defaults.`);
167
+ }
168
+ return memoryValueRaw !== undefined && isMemoryValueConfig(memoryValueRaw) ? memoryValueRaw : {};
169
+ }
170
+ function dormantSettings(raw) {
171
+ // Same rule as memoryValue above: a malformed value never silently
172
+ // changes what sleep does. Anything but a real object / boolean / number
173
+ // warns and falls back to the default, which keeps faded memories.
174
+ const dormantRaw = raw.dormant;
175
+ if (dormantRaw !== undefined && !isDormantConfig(dormantRaw)) {
176
+ log.warn(`config.json's "dormant" must be an object like {"enabled": false} ` +
177
+ `(got ${JSON.stringify(dormantRaw)}) - using the default (faded memories kept dormant).`);
178
+ }
179
+ const dormantOverride = dormantRaw !== undefined && isDormantConfig(dormantRaw) ? dormantRaw : {};
180
+ // Only a real boolean counts: {"enabled": "false"} is a truthy string.
181
+ let dormantEnabled = dormantOverride.enabled ?? DEFAULT_CONFIG.dormant.enabled;
182
+ if (dormantEnabled !== true && dormantEnabled !== false) {
183
+ log.warn(`config.json's "dormant.enabled" must be true or false ` +
184
+ `(got ${JSON.stringify(dormantEnabled)}) - using the default (faded memories kept dormant).`);
185
+ dormantEnabled = DEFAULT_CONFIG.dormant.enabled;
186
+ }
187
+ let dormantRetentionDays = dormantOverride.retentionDays ?? DEFAULT_CONFIG.dormant.retentionDays;
188
+ if (!Number.isFinite(dormantRetentionDays) || dormantRetentionDays < 0) {
189
+ log.warn(`config.json's "dormant.retentionDays" must be a number of days, 0 or more ` +
190
+ `(got ${JSON.stringify(dormantRetentionDays)}) - using ${DEFAULT_CONFIG.dormant.retentionDays}.`);
191
+ dormantRetentionDays = DEFAULT_CONFIG.dormant.retentionDays;
192
+ }
193
+ return { enabled: dormantEnabled, retentionDays: dormantRetentionDays };
194
+ }
195
+ function churnStalenessEnabled(raw) {
196
+ // Same "never silently wrong" rule as memoryValue/dormant above.
197
+ const churnStalenessRaw = raw.churnStaleness;
198
+ const validChurnStalenessConfig = churnStalenessRaw === undefined || isChurnStalenessConfig(churnStalenessRaw);
199
+ if (!validChurnStalenessConfig) {
200
+ log.warn(`config.json's "churnStaleness" must be an object like {"enabled": true} ` +
201
+ `(got ${JSON.stringify(churnStalenessRaw)}) - using defaults.`);
202
+ }
203
+ const enabled = churnStalenessRaw !== undefined && isChurnStalenessConfig(churnStalenessRaw)
204
+ ? churnStalenessRaw.enabled
205
+ : DEFAULT_CONFIG.churnStaleness.enabled;
206
+ if (enabled !== true && enabled !== false) {
207
+ log.warn(`config.json's "churnStaleness.enabled" must be true or false ` +
208
+ `(got ${JSON.stringify(enabled)}) - using false.`);
209
+ return false;
210
+ }
211
+ return enabled;
212
+ }
213
+ function defaultHalfLifeDays(raw) {
214
+ // Every writer starts a memory on this, and a zero or negative half-life scores zero strength, so sleep would retire it.
215
+ const days = raw.defaultHalfLifeDays ?? DEFAULT_CONFIG.defaultHalfLifeDays;
216
+ if (!Number.isFinite(days) || days <= 0) {
217
+ log.warn(`config.json's "defaultHalfLifeDays" must be a number of days above 0 ` +
218
+ `(got ${JSON.stringify(days)}) - using ${DEFAULT_CONFIG.defaultHalfLifeDays}.`);
219
+ return DEFAULT_CONFIG.defaultHalfLifeDays;
220
+ }
221
+ return days;
222
+ }
144
223
  export function loadConfig(hippoRoot) {
145
224
  const configPath = path.join(hippoRoot, 'config.json');
146
225
  if (!fs.existsSync(configPath))
@@ -149,66 +228,12 @@ export function loadConfig(hippoRoot) {
149
228
  const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
150
229
  const basis = raw.decayBasis;
151
230
  const validBasis = basis === 'clock' || basis === 'session' || basis === 'adaptive';
152
- // Review-round F6: {...DEFAULT_CONFIG.memoryValue, ...raw.memoryValue}
153
- // silently no-ops when raw.memoryValue is a non-object (e.g. the user
154
- // wrote {"memoryValue": true}) — spreading a boolean/primitive/array
155
- // contributes no enumerable own properties, so `enabled` stays at the
156
- // default `false` with zero indication anything was wrong. This
157
- // feature's whole point is "never silently off": warn loudly and fall
158
- // back to defaults instead of merging garbage.
159
- const memoryValueRaw = raw.memoryValue;
160
- const validMemoryValueConfig = memoryValueRaw === undefined || isMemoryValueConfig(memoryValueRaw);
161
- if (!validMemoryValueConfig) {
162
- log.warn(`config.json's "memoryValue" must be an object like {"enabled": true} ` +
163
- `(got ${JSON.stringify(memoryValueRaw)}) - using defaults.`);
164
- }
165
- const memoryValueOverride = memoryValueRaw !== undefined && isMemoryValueConfig(memoryValueRaw) ? memoryValueRaw : {};
166
- // Same rule as memoryValue above: a malformed value never silently
167
- // changes what sleep does. Anything but a real object / boolean / number
168
- // warns and falls back to the default, which keeps faded memories.
169
- const dormantRaw = raw.dormant;
170
- if (dormantRaw !== undefined && !isDormantConfig(dormantRaw)) {
171
- log.warn(`config.json's "dormant" must be an object like {"enabled": false} ` +
172
- `(got ${JSON.stringify(dormantRaw)}) - using the default (faded memories kept dormant).`);
173
- }
174
- const dormantOverride = dormantRaw !== undefined && isDormantConfig(dormantRaw) ? dormantRaw : {};
175
- // Only a real boolean counts: {"enabled": "false"} is a truthy string.
176
- let dormantEnabled = dormantOverride.enabled ?? DEFAULT_CONFIG.dormant.enabled;
177
- if (dormantEnabled !== true && dormantEnabled !== false) {
178
- log.warn(`config.json's "dormant.enabled" must be true or false ` +
179
- `(got ${JSON.stringify(dormantEnabled)}) - using the default (faded memories kept dormant).`);
180
- dormantEnabled = DEFAULT_CONFIG.dormant.enabled;
181
- }
182
- let dormantRetentionDays = dormantOverride.retentionDays ?? DEFAULT_CONFIG.dormant.retentionDays;
183
- if (!Number.isFinite(dormantRetentionDays) || dormantRetentionDays < 0) {
184
- log.warn(`config.json's "dormant.retentionDays" must be a number of days, 0 or more ` +
185
- `(got ${JSON.stringify(dormantRetentionDays)}) - using ${DEFAULT_CONFIG.dormant.retentionDays}.`);
186
- dormantRetentionDays = DEFAULT_CONFIG.dormant.retentionDays;
187
- }
188
- // Same "never silently wrong" rule as memoryValue/dormant above.
189
- const churnStalenessRaw = raw.churnStaleness;
190
- const validChurnStalenessConfig = churnStalenessRaw === undefined || isChurnStalenessConfig(churnStalenessRaw);
191
- if (!validChurnStalenessConfig) {
192
- log.warn(`config.json's "churnStaleness" must be an object like {"enabled": true} ` +
193
- `(got ${JSON.stringify(churnStalenessRaw)}) - using defaults.`);
194
- }
195
- let churnStalenessEnabled = churnStalenessRaw !== undefined && isChurnStalenessConfig(churnStalenessRaw)
196
- ? churnStalenessRaw.enabled
197
- : DEFAULT_CONFIG.churnStaleness.enabled;
198
- if (churnStalenessEnabled !== true && churnStalenessEnabled !== false) {
199
- log.warn(`config.json's "churnStaleness.enabled" must be true or false ` +
200
- `(got ${JSON.stringify(churnStalenessEnabled)}) - using false.`);
201
- churnStalenessEnabled = false;
202
- }
203
- // Every writer starts a memory on this, and a zero or negative half-life scores zero strength, so sleep would retire it.
204
- let defaultHalfLifeDays = raw.defaultHalfLifeDays ?? DEFAULT_CONFIG.defaultHalfLifeDays;
205
- if (!Number.isFinite(defaultHalfLifeDays) || defaultHalfLifeDays <= 0) {
206
- log.warn(`config.json's "defaultHalfLifeDays" must be a number of days above 0 ` +
207
- `(got ${JSON.stringify(defaultHalfLifeDays)}) - using ${DEFAULT_CONFIG.defaultHalfLifeDays}.`);
208
- defaultHalfLifeDays = DEFAULT_CONFIG.defaultHalfLifeDays;
209
- }
231
+ const memoryValue = memoryValueOverride(raw);
232
+ const dormant = dormantSettings(raw);
233
+ const churnEnabled = churnStalenessEnabled(raw);
234
+ const halfLifeDays = defaultHalfLifeDays(raw);
210
235
  return {
211
- defaultHalfLifeDays,
236
+ defaultHalfLifeDays: halfLifeDays,
212
237
  defaultBudget: raw.defaultBudget ?? DEFAULT_CONFIG.defaultBudget,
213
238
  defaultContextBudget: raw.defaultContextBudget ?? DEFAULT_CONFIG.defaultContextBudget,
214
239
  decayBasis: validBasis ? basis : DEFAULT_CONFIG.decayBasis,
@@ -232,18 +257,16 @@ export function loadConfig(hippoRoot) {
232
257
  ambient: { ...DEFAULT_CONFIG.ambient, ...(raw.ambient ?? {}) },
233
258
  memoryValue: {
234
259
  ...DEFAULT_CONFIG.memoryValue,
235
- ...memoryValueOverride,
236
- },
237
- dormant: {
238
- enabled: dormantEnabled,
239
- retentionDays: dormantRetentionDays,
260
+ ...memoryValue,
240
261
  },
262
+ dormant,
241
263
  churnStaleness: {
242
- enabled: churnStalenessEnabled,
264
+ enabled: churnEnabled,
243
265
  },
244
266
  agentMemories: { tools: agentMemoryTools(raw.agentMemories?.tools) },
245
267
  deliveryLedger: { enabled: deliveryLedgerEnabled(raw.deliveryLedger) },
246
268
  pilot: { holdoutRateBp: pilotHoldoutRate(raw.pilot) },
269
+ projectIdentity: { remote: projectIdentityRemote(raw.projectIdentity) },
247
270
  };
248
271
  }
249
272
  catch (err) {