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/README.md CHANGED
@@ -547,6 +547,15 @@ hippo projects merge hippo-wt-fix hippo --global # fold an old worktree name i
547
547
  hippo projects repair --global # set aside note copies, fold old worktree names, re-tag merges
548
548
  ```
549
549
 
550
+ A project's name is the `id` in a committed `.hippo-project.json`, else its `origin` remote
551
+ (`github.com/acme/api`), else its folder name. So two repos both called `api` no longer share
552
+ memories in the global store. Rows saved under the old folder name stay visible to the
553
+ project, and `hippo projects repair` folds that name into the id unless two projects claim it.
554
+ `hippo sleep` runs that repair once per store after an upgrade, with a backup; in the global store
555
+ it leaves the name folds to you, since only `hippo projects repair --global` lists them for review.
556
+ Set `{"projectIdentity":{"remote":false}}` in the global `config.json` to keep folder names.
557
+ A long-running MCP or HTTP server reads a new project file or remote after a restart.
558
+
550
559
  **See what memory costs in tokens.** Every block of memory text hippo hands an agent (the
551
560
  per-prompt hook, the block `hippo compact-resume` restores after compaction, `hippo context`,
552
561
  `hippo recall`, the MCP tools, the HTTP API) is recorded in a token ledger: counts, surface
@@ -24,6 +24,8 @@ export interface ContainerWork {
24
24
  readonly adopt: ReadonlyMap<string, readonly MemoryEntry[]>;
25
25
  /** Legacy rows a new row of the key supersedes, by key (second round). */
26
26
  readonly replace: ReadonlyMap<string, readonly MemoryEntry[]>;
27
+ /** This container's prefixes under the project's earlier names; their rows move here, so a new id imports nothing twice. */
28
+ readonly legacyPrefixes: readonly string[];
27
29
  }
28
30
  export interface ContainerOutcome {
29
31
  readonly tally: Tally;
@@ -1,6 +1,6 @@
1
1
  // One container's sync in one transaction on the caller's handle: lookup, plan, then every write (plan designs 6 to 8).
2
2
  import { appendAuditEvent } from '../audit.js';
3
- import { deleteDormantRow, dormantSnapshotsBySourcePrefix, insertDormantRow, readDormantSnapshot } from '../dormant.js';
3
+ import { deleteDormantRow, dormantSnapshotsBySourcePrefix, insertDormantRow, readDormantSnapshot, replaceDormantEntry } from '../dormant.js';
4
4
  import { gatedWrite } from '../gated-write.js';
5
5
  import { Layer, calculateStrength, createMemory } from '../memory.js';
6
6
  import { findRejectedValue, rejectionDigest } from '../rejection.js';
@@ -65,6 +65,8 @@ class ContainerRun {
65
65
  }
66
66
  run() {
67
67
  this.adoptLegacy();
68
+ for (const old of this.w.legacyPrefixes)
69
+ this.adoptPrefix(old);
68
70
  const live = selectLiveEntriesBySourcePrefix(this.s.db, this.s.tenantId, this.w.prefix);
69
71
  for (const row of [...live, ...[...this.w.replace.values()].flat()])
70
72
  this.rows.set(row.id, row);
@@ -116,6 +118,26 @@ class ContainerRun {
116
118
  }
117
119
  }
118
120
  }
121
+ /** Rows filed under an earlier project name keep their id and history; dormant ones move too, scanned only until this prefix holds live rows. */
122
+ adoptPrefix(old) {
123
+ const origin = this.s.originProject ?? null;
124
+ let moved = 0;
125
+ for (const row of selectLiveEntriesBySourcePrefix(this.s.db, this.s.tenantId, old)) {
126
+ const source = this.w.prefix + row.source.slice(old.length);
127
+ const done = this.s.db.prepare(`UPDATE memories SET source = ?, origin_project = COALESCE(?, origin_project) WHERE id = ? AND tenant_id = ? AND source = ?`).run(source, origin, row.id, row.tenantId, row.source);
128
+ if (Number(done.changes ?? 0) === 0)
129
+ continue;
130
+ moved++;
131
+ this.mirror.push({ ...row, source, origin_project: origin ?? row.origin_project });
132
+ }
133
+ this.tally.renamed += moved;
134
+ if (moved === 0 && (this.w.container.items.length === 0 || selectLiveEntriesBySourcePrefix(this.s.db, this.s.tenantId, this.w.prefix).length > 0))
135
+ return;
136
+ for (const snap of dormantSnapshotsBySourcePrefix(this.s.db, this.s.tenantId, old)) {
137
+ const source = this.w.prefix + snap.entry.source.slice(old.length);
138
+ replaceDormantEntry(this.s.db, this.s.tenantId, snap.entry.id, { ...snap.entry, source, origin_project: origin ?? snap.entry.origin_project });
139
+ }
140
+ }
119
141
  refusals() {
120
142
  const out = new Map();
121
143
  for (const item of this.w.container.items) {
@@ -1,3 +1,4 @@
1
+ import { type ProjectIdentity } from '../project-identity.js';
1
2
  import type { Adapter, AdapterContext, Listing } from './types.js';
2
3
  /** Claude Code's auto memory folder names for a project: its checkout, which subfolders share, or the folder itself outside a repository. */
3
4
  export declare function claudeMemoryFolderNames(projectRoot: string, platform: NodeJS.Platform): Set<string>;
@@ -9,5 +10,5 @@ export declare const claudeCodeAdapter: Adapter;
9
10
  /** A session's own notes folder and nothing else, with no git call, so post-compact can read it inside the hook's time limit. */
10
11
  export declare function claudeTranscriptListing(ctx: AdapterContext, transcriptPath: string): Listing;
11
12
  /** The project a session folder's notes belong to: the one Claude named the folder for, the session's start folder, else cwd or a parent; null when none matches. */
12
- export declare function transcriptNotesOrigin(transcriptPath: string, cwd: string | null, machine: Pick<AdapterContext, 'platform' | 'env'>): string | null;
13
+ export declare function transcriptNotesProject(transcriptPath: string, cwd: string | null, machine: Pick<AdapterContext, 'platform' | 'env'>): ProjectIdentity | null;
13
14
  //# sourceMappingURL=claude-code.d.ts.map
@@ -1,7 +1,7 @@
1
1
  // Claude Code's auto memory: frontmatter `.md` notes in a per-project folder, plus the `autoMemoryDirectory` user folder.
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { deriveOriginProject, realpathOrResolve } from '../project-identity.js';
4
+ import { realpathOrResolve, resolveProjectIdentity } from '../project-identity.js';
5
5
  import { isStringValue } from '../capture-contract.js';
6
6
  import { isJsonObject } from '../hooks/shared.js';
7
7
  import { expandHome, frontmatterField, itemTime, readTextFile, splitFrontmatter } from './files.js';
@@ -52,7 +52,7 @@ export function claudeTranscriptListing(ctx, transcriptPath) {
52
52
  return { tool: 'claude-code', home: config, containers: readFolders([folder], 'project', ctx.platform), warnings: [] };
53
53
  }
54
54
  /** The project a session folder's notes belong to: the one Claude named the folder for, the session's start folder, else cwd or a parent; null when none matches. */
55
- export function transcriptNotesOrigin(transcriptPath, cwd, machine) {
55
+ export function transcriptNotesProject(transcriptPath, cwd, machine) {
56
56
  const fold = (name) => (machine.platform === 'win32' ? name.toLowerCase() : name);
57
57
  const folder = fold(path.basename(path.dirname(transcriptPath)));
58
58
  const start = transcriptStartCwd(transcriptPath);
@@ -60,13 +60,13 @@ export function transcriptNotesOrigin(transcriptPath, cwd, machine) {
60
60
  if (pinned !== null && fold(pinned) === folder) {
61
61
  // A pinned name stands for whatever project the session ran in, so its start folder decides.
62
62
  const from = start ?? cwd;
63
- return from !== null && fs.existsSync(from) ? deriveOriginProject(from) : null;
63
+ return from !== null && fs.existsSync(from) ? resolveProjectIdentity(from) : null;
64
64
  }
65
65
  for (const from of [start, cwd]) {
66
66
  for (let dir = from === null ? null : path.resolve(from); dir !== null; dir = path.dirname(dir) === dir ? null : path.dirname(dir)) {
67
67
  // A folder gone from disk resolves to its bare name, never the project it was in, so it decides nothing.
68
68
  if ([dir, realpathOrResolve(dir)].some((d) => fold(claudeFolderName(d)) === folder))
69
- return fs.existsSync(dir) ? deriveOriginProject(dir) : null;
69
+ return fs.existsSync(dir) ? resolveProjectIdentity(dir) : null;
70
70
  }
71
71
  }
72
72
  return null;
@@ -8,6 +8,7 @@ export interface Tally {
8
8
  untagged: number;
9
9
  unchanged: number;
10
10
  adopted: number;
11
+ renamed: number;
11
12
  collapsed: number;
12
13
  retagged: number;
13
14
  handedOver: number;
@@ -2,7 +2,7 @@
2
2
  import { AGENT_MEMORY_TOOLS } from './tools.js';
3
3
  export function emptyTally() {
4
4
  return {
5
- imported: 0, replaced: 0, restored: 0, setAside: 0, untagged: 0, unchanged: 0, adopted: 0, collapsed: 0, retagged: 0,
5
+ imported: 0, replaced: 0, restored: 0, setAside: 0, untagged: 0, unchanged: 0, adopted: 0, renamed: 0, collapsed: 0, retagged: 0,
6
6
  handedOver: 0, duplicate: 0, secret: 0, short: 0, rejected: 0, unread: 0, unreadable: 0, busy: 0,
7
7
  };
8
8
  }
@@ -47,7 +47,7 @@ export function totalTally(report) {
47
47
  const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
48
48
  const TALLY_WORDS = {
49
49
  imported: 'new', replaced: 'replaced', restored: 'brought back', setAside: 'set aside', untagged: 'pinned and kept',
50
- unchanged: 'unchanged', adopted: 'taken over from the old Claude import', collapsed: 'duplicates folded', retagged: 'retagged',
50
+ unchanged: 'unchanged', adopted: 'taken over from the old Claude import', renamed: 'moved to the project id', collapsed: 'duplicates folded', retagged: 'retagged',
51
51
  handedOver: 'handed over from the global store', duplicate: 'already stored', secret: 'skipped for a secret', short: 'too short',
52
52
  rejected: 'skipped as rejected', unread: 'files skipped', unreadable: 'unreadable folders', busy: 'busy folders',
53
53
  };
@@ -6,7 +6,8 @@ import path from 'node:path';
6
6
  import { errorMessage } from '../log.js';
7
7
  import { loadConfig } from '../config.js';
8
8
  import { closeHippoDb, isSqliteBusy, openHippoDb } from '../db.js';
9
- import { deriveOriginProject, isGlobalStoreRoot, resolveGlobalRootDir } from '../project-identity.js';
9
+ import { namesFoldedInto } from '../project-merge.js';
10
+ import { isGlobalStoreRoot, originInSql, projectNames, resolveGlobalRootDir, resolveProjectIdentity } from '../project-identity.js';
10
11
  import { duplicateKey, heldTextKeys } from '../same-text.js';
11
12
  import { removeEntryMirrors } from '../store/mirrors.js';
12
13
  import { initStore, isInitialized } from '../store/open.js';
@@ -15,7 +16,7 @@ import { selectLiveEntriesBySourcePrefix } from '../store/entry-reads.js';
15
16
  import { updateStats } from '../store/index-and-stats.js';
16
17
  import { resolveTenantId } from '../tenant.js';
17
18
  import { setAsideRow, syncContainer } from './apply.js';
18
- import { claudeCodeAdapter, claudeTranscriptListing, transcriptNotesOrigin } from './claude-code.js';
19
+ import { claudeCodeAdapter, claudeTranscriptListing, transcriptNotesProject } from './claude-code.js';
19
20
  import { codexAdapter } from './codex.js';
20
21
  import { copilotAdapter } from './copilot.js';
21
22
  import { geminiAdapter } from './gemini.js';
@@ -43,14 +44,14 @@ export function importForStore(hippoRoot, opts) {
43
44
  export function importProjectMemories(hippoRoot, opts) {
44
45
  const ctx = context(opts.machine, { projectRoot: path.dirname(hippoRoot) });
45
46
  return runPass({
46
- scope: 'project', target: hippoRoot, invoking: hippoRoot, list: (a) => a.list(ctx, 'project'), legacy: true, originProject: undefined, handover: true,
47
+ scope: 'project', target: hippoRoot, invoking: hippoRoot, list: (a) => a.list(ctx, 'project'), legacy: true, originProject: undefined, legacyOrigins: [], handover: true,
47
48
  }, opts);
48
49
  }
49
50
  /** Every tool's user-level memory into the global store, created on demand, with no origin. */
50
51
  export function importUserMemories(invokingRoot, opts) {
51
52
  const ctx = context(opts.machine, {});
52
53
  return runPass({
53
- scope: 'user', target: resolveGlobalRootDir(), invoking: invokingRoot, list: (a) => a.list(ctx, 'user'), legacy: false, originProject: '', handover: false,
54
+ scope: 'user', target: resolveGlobalRootDir(), invoking: invokingRoot, list: (a) => a.list(ctx, 'user'), legacy: false, originProject: '', legacyOrigins: [], handover: false,
54
55
  }, opts);
55
56
  }
56
57
  /** Session end in a folder with no store of its own: the session's project into the global store with its origin, the session folder's notes, then the user pass. */
@@ -58,7 +59,7 @@ export function importAtSessionEnd(cwd, transcriptPath, opts) {
58
59
  const globalRoot = resolveGlobalRootDir();
59
60
  const ctx = context(opts.machine, { projectRoot: cwd });
60
61
  const report = runPass({
61
- scope: 'project', target: globalRoot, invoking: globalRoot, list: (a) => a.list(ctx, 'project'), legacy: false, originProject: deriveOriginProject(cwd), handover: false,
62
+ scope: 'project', target: globalRoot, invoking: globalRoot, list: (a) => a.list(ctx, 'project'), legacy: false, ...projectOrigins(resolveProjectIdentity(cwd)), handover: false,
62
63
  }, opts);
63
64
  if (transcriptPath !== undefined)
64
65
  mergeReports(report, importSessionFolder(globalRoot, transcriptPath, cwd, opts));
@@ -67,18 +68,21 @@ export function importAtSessionEnd(cwd, transcriptPath, opts) {
67
68
  }
68
69
  /** The session folder's notes under the project Claude filed them for, never cwd's: a session begun at home keeps its home notes user-global wherever it ends. No git call, so post-compact can run it. */
69
70
  export function importSessionFolder(hippoRoot, transcriptPath, cwd, opts) {
70
- const origin = transcriptNotesOrigin(transcriptPath, cwd, opts.machine);
71
- if (origin === null)
71
+ const project = transcriptNotesProject(transcriptPath, cwd, opts.machine);
72
+ if (project === null)
72
73
  return emptyReport();
73
74
  // A project store takes its own project's notes; another project's, or home's, go to the global store, which parts them by origin.
74
- const own = isGlobalStoreRoot(hippoRoot) || origin === deriveOriginProject(path.dirname(hippoRoot));
75
+ const own = isGlobalStoreRoot(hippoRoot) || project.name === resolveProjectIdentity(path.dirname(hippoRoot)).name;
75
76
  const target = own ? hippoRoot : resolveGlobalRootDir();
76
77
  const ctx = context(opts.machine, {});
77
78
  return runPass({
78
- scope: 'project', target, invoking: hippoRoot, legacy: false, originProject: origin, handover: false,
79
+ scope: 'project', target, invoking: hippoRoot, legacy: false, ...projectOrigins(project), handover: false,
79
80
  list: (a) => (a.tool === 'claude-code' ? claudeTranscriptListing(ctx, transcriptPath) : null),
80
81
  }, opts);
81
82
  }
83
+ function projectOrigins(project) {
84
+ return { originProject: project.name, legacyOrigins: projectNames(project).slice(1) };
85
+ }
82
86
  function context(machine, extra) {
83
87
  return { home: machine.home, env: machine.env, platform: machine.platform, ...extra };
84
88
  }
@@ -183,11 +187,12 @@ function syncStore(pass, listings, store, opts, report) {
183
187
  tenantId,
184
188
  baseHalfLifeDays: loadConfig(store.root).defaultHalfLifeDays,
185
189
  originProject: pass.originProject,
186
- isDuplicate: duplicateCheck(store, tenantId, pass.originProject, legacy?.adopted ?? new Set()),
190
+ isDuplicate: duplicateCheck(store, tenantId, [pass.originProject ?? '', ...pass.legacyOrigins], legacy?.adopted ?? new Set()),
187
191
  dryRun: opts.dryRun === true,
188
192
  };
189
193
  // A project's rows in the global store are parted by origin: a worktree and its main checkout share a Claude folder.
190
194
  const partition = store.global && pass.scope === 'project' ? pass.originProject ?? '' : null;
195
+ const earlier = partition === null || partition === '' ? [] : earlierNames(store.db, tenantId, partition, pass.legacyOrigins);
191
196
  const synced = [];
192
197
  let remembered = 0;
193
198
  for (const listing of listings) {
@@ -198,7 +203,7 @@ function syncStore(pass, listings, store, opts, report) {
198
203
  out.tally.unreadable++;
199
204
  continue;
200
205
  }
201
- const work = containerWork(tool, container, opts.machine.platform, legacy, partition ?? '');
206
+ const work = containerWork(tool, container, opts.machine.platform, legacy, partition ?? '', earlier);
202
207
  const outcome = syncOne(session, work, out, report);
203
208
  if (outcome === null)
204
209
  continue;
@@ -208,21 +213,28 @@ function syncStore(pass, listings, store, opts, report) {
208
213
  afterCommit(store.root, outcome, report);
209
214
  }
210
215
  if (session.dryRun)
211
- out.unlisted += unlistedRows(store.db, tenantId, tool, pass.scope, listing, opts.machine.platform, partition);
216
+ out.unlisted += unlistedRows(store.db, tenantId, tool, pass.scope, listing, opts.machine.platform, partition === null ? null : [partition, ...earlier]);
212
217
  }
213
218
  if (remembered > 0 && !session.dryRun)
214
219
  bumpRemembered(store.root, remembered, report);
215
220
  return synced;
216
221
  }
217
- function containerWork(tool, container, platform, legacy, origin) {
222
+ /** The legacy names plus every name a merge or repair folded into the project; the audit read runs once per pass. */
223
+ function earlierNames(db, tenantId, id, legacyOrigins) {
224
+ const names = [id, ...legacyOrigins];
225
+ return [...legacyOrigins, ...namesFoldedInto(db, tenantId, names)].filter((n) => n !== '' && n !== id);
226
+ }
227
+ function containerWork(tool, container, platform, legacy, origin, earlier) {
218
228
  const none = new Map();
219
229
  const own = tool.id === 'claude-code' ? legacy?.byContainer.get(container.path) : undefined;
230
+ const prefixOf = (name) => containerPrefix(tool.id, containerId(container.path, container.scope, platform, name));
220
231
  return {
221
232
  tool,
222
233
  container,
223
- prefix: containerPrefix(tool.id, containerId(container.path, container.scope, platform, origin)),
234
+ prefix: prefixOf(origin),
224
235
  adopt: own?.adopt ?? none,
225
236
  replace: own?.replace ?? none,
237
+ legacyPrefixes: earlier.map(prefixOf),
226
238
  };
227
239
  }
228
240
  /** Null when the container was skipped: a busy store waits for the next sync, any other failure is a warning and the sync goes on. */
@@ -265,27 +277,27 @@ function bumpRemembered(root, remembered, report) {
265
277
  }
266
278
  }
267
279
  /** Design 6: only text stored by another path counts, and in the global store only rows visible where the new row goes. */
268
- function duplicateCheck(store, tenantId, origin, adopted) {
280
+ function duplicateCheck(store, tenantId, origins, adopted) {
269
281
  let keys = null;
270
282
  return (text) => {
271
- keys ??= otherPathKeys(store, tenantId, origin ?? '', adopted);
283
+ keys ??= otherPathKeys(store, tenantId, origins, adopted);
272
284
  return keys.has(duplicateKey(text));
273
285
  };
274
286
  }
275
- function otherPathKeys(store, tenantId, origin, adopted) {
276
- const visible = store.global ? ` AND (origin_project = '' OR origin_project = ?)` : '';
277
- const params = store.global ? [tenantId, AGENT_MEMORY_SOURCE_PREFIX, origin] : [tenantId, AGENT_MEMORY_SOURCE_PREFIX];
287
+ function otherPathKeys(store, tenantId, origins, adopted) {
288
+ const visible = store.global ? ` AND (origin_project = '' OR ${originInSql(origins)})` : '';
289
+ const params = store.global ? [tenantId, AGENT_MEMORY_SOURCE_PREFIX, ...origins] : [tenantId, AGENT_MEMORY_SOURCE_PREFIX];
278
290
  // SAFETY: the SELECT names the three columns of the row type.
279
291
  const rows = store.db.prepare(`SELECT id, content, source FROM memories
280
292
  WHERE tenant_id = ? AND superseded_by IS NULL AND substr(source, 1, ${AGENT_MEMORY_SOURCE_PREFIX.length}) != ?${visible}`).all(...params);
281
293
  return new Set(rows.filter((r) => !adopted.has(r.id)).flatMap(heldTextKeys));
282
294
  }
283
- /** Dry run only: kept rows of this scope in containers this run did not list (a moved project's old folder); `partition` limits it to one origin. */
295
+ /** Dry run only: kept rows of this scope in containers this run did not list (a moved project's old folder); `partition` limits it to one project's names. */
284
296
  function unlistedRows(db, tenantId, tool, scope, listing, platform, partition) {
285
- const listed = listing.containers.map((c) => containerPrefix(tool.id, containerId(c.path, c.scope, platform, partition ?? '')));
297
+ const listed = listing.containers.flatMap((c) => (partition ?? ['']).map((name) => containerPrefix(tool.id, containerId(c.path, c.scope, platform, name))));
286
298
  return selectLiveEntriesBySourcePrefix(db, tenantId, `${toolSourcePrefix(tool.id)}${scope === 'project' ? 'p' : 'u'}-`)
287
299
  .filter((row) => row.tags.includes(tool.tag) && !listed.some((p) => row.source.startsWith(p)))
288
- .filter((row) => partition === null || (row.origin_project ?? '') === partition).length;
300
+ .filter((row) => partition === null || partition.includes(row.origin_project ?? '')).length;
289
301
  }
290
302
  /** Design 2's handover: rows the store-less hook path left in the global store, under this project's origin, for containers its store now syncs. */
291
303
  function handOver(synced, projectRoot, opts, report) {
@@ -297,7 +309,9 @@ function handOver(synced, projectRoot, opts, report) {
297
309
  db = openHippoDb(globalRoot, { busyWaitMs: opts.busyWaitMs });
298
310
  const tenantId = resolveTenantId({});
299
311
  // A folder with no git and no marker wrote as '' before its store existed, and as its own name after.
300
- const origins = [...new Set([deriveOriginProject(projectRoot), ''])];
312
+ // Names folded into this project's in the global store were its rows too.
313
+ const names = projectNames(resolveProjectIdentity(projectRoot));
314
+ const origins = [...new Set([...names, ...namesFoldedInto(db, tenantId, names), ''])];
301
315
  for (const work of synced)
302
316
  handOverContainer(db, globalRoot, tenantId, work, origins, opts.machine.platform, report);
303
317
  }
@@ -2,10 +2,10 @@ import { type AmbientTallies } from './ambient.js';
2
2
  /** The rows an ambient summary describes: a context read's envelope, origin partition and tag secret veto. */
3
3
  export interface AmbientStoreFilter {
4
4
  exactScope?: string;
5
- /** Rows of this project and user-global rows pass; absent admits every origin. */
6
- project?: string;
7
- /** The reader's project: a secret-tagged row counts only inside its own non-empty origin project. */
8
- currentProject: string;
5
+ /** Rows carrying one of these project names, and user-global rows, pass; absent admits every origin. */
6
+ project?: readonly string[];
7
+ /** The reader's project names: a secret-tagged row counts only inside its own non-empty origin project. */
8
+ currentProject: readonly string[];
9
9
  now: Date;
10
10
  }
11
11
  /** Whole-store ambient tallies in one aggregate pass, no row loaded. Content-pattern secrets and session-local
@@ -3,6 +3,7 @@ import { strengthSql } from './memory.js';
3
3
  import { RECALL_DEFAULT_DENY_SCOPES } from './recall-scope.js';
4
4
  import { SECRET_TAGS } from './secret-detect.js';
5
5
  import { openStore } from './store/open.js';
6
+ import { originInSql } from './project-identity.js';
6
7
  import { isErrorTagged } from './ambient.js';
7
8
  // A malformed or non-array JSON list reads as empty, as parseJsonArray does, instead of failing json_each.
8
9
  const jsonList = (column) => `(CASE WHEN json_valid(${column}) AND json_type(${column}) = 'array' THEN ${column} ELSE '[]' END)`;
@@ -19,8 +20,8 @@ function contextRowsWhere(tenantId, filter) {
19
20
  params.push(...RECALL_DEFAULT_DENY_SCOPES);
20
21
  }
21
22
  if (filter.project !== undefined) {
22
- where.push(`(origin_project = '' OR origin_project = ?)`);
23
- params.push(filter.project);
23
+ where.push(`(origin_project = '' OR ${originInSql(filter.project)})`);
24
+ params.push(...filter.project);
24
25
  }
25
26
  return { where, params };
26
27
  }
@@ -57,7 +58,7 @@ export function loadAmbientTallies(hippoRoot, tenantId, filter) {
57
58
  '[' || COALESCE(group_concat(${jsonList('tags_json')}, ','), '') || ']' AS tagLists
58
59
  FROM memories
59
60
  WHERE ${where.join(' AND ')}
60
- AND ((origin_project = ? AND origin_project != '') OR NOT ${secret.sql})`).get(sevenDaysAgo, ...params, filter.currentProject, ...secret.params);
61
+ AND ((${originInSql(filter.currentProject)} AND origin_project != '') OR NOT ${secret.sql})`).get(sevenDaysAgo, ...params, ...filter.currentProject, ...secret.params);
61
62
  const tagCounts = new Map();
62
63
  let errors = 0;
63
64
  const lists = /* SAFETY: jsonList yields an array per row, joined into one array */ JSON.parse(row.tagLists);
@@ -1,6 +1,7 @@
1
1
  import type { Context } from './types.js';
2
+ export declare const DEFAULT_ASSEMBLE_BUDGET = 4000;
2
3
  export interface AssembleOpts {
3
- /** Token budget. Default 4000. */
4
+ /** Token budget. Default DEFAULT_ASSEMBLE_BUDGET. */
4
5
  budget?: number;
5
6
  /** Recent raw rows always kept verbatim. Default 10. */
6
7
  freshTailCount?: number;
@@ -8,7 +9,7 @@ export interface AssembleOpts {
8
9
  * ancestor. Default true. */
9
10
  summarizeOlder?: boolean;
10
11
  /**
11
- * Restrict to a specific scope. v1.6.1 senior-review P1 #3 parity with
12
+ * Restrict to a specific scope, same rule as
12
13
  * `recall`: when set, exact match required (so an authorised caller can
13
14
  * assemble a `slack:private:CSEC` session by passing scope explicitly).
14
15
  * When undefined, default-deny applies to ANY `<source>:private:*` and
@@ -49,19 +50,15 @@ export interface AssembleResult {
49
50
  items: AssembledContextItem[];
50
51
  tokens: number;
51
52
  /**
52
- * Tenant + scope-filtered raw row count for the session — what the caller
53
- * could have seen given their grant. Pre-v1.6.1 was pre-filter (confusing
54
- * for all-private sessions); pre-v1.6.3 was capped (under-reported on
55
- * sessions > rowCap). v1.6.3 reports the FULL post-filter count via a
56
- * separate COUNT(*) query so consumers can render "session has N msgs"
57
- * accurately even when items[] is the windowed view.
53
+ * Tenant + scope-filtered raw row count for the session, uncapped by `rowCap`, so
54
+ * "session has N msgs" stays accurate when items[] is the windowed view.
58
55
  */
59
56
  totalRaw: number;
60
57
  summarized: number;
61
58
  evicted: number;
62
59
  /**
63
- * True when `rowCap` truncated the loaded window. With v1.6.2's NEWEST-cap
64
- * semantics, the items[] array represents the freshest tail of the session;
60
+ * True when `rowCap` truncated the loaded window. The cap keeps the NEWEST
61
+ * rows, so the items[] array represents the freshest tail of the session;
65
62
  * older rows beyond the cap are silently absent. Use `totalRaw - items.length
66
63
  * - summarized + ...` to estimate how much you didn't see, or widen `rowCap`.
67
64
  */
@@ -2,6 +2,7 @@
2
2
  import { loadEntriesByIds, loadSessionRawMemories, countSessionRawMemories } from '../store/entry-reads.js';
3
3
  import { estimateTokens } from '../token-ledger.js';
4
4
  import { passesScopeFilterForRecall, assertScopeRequestAllowed } from '../recall-scope.js';
5
+ export const DEFAULT_ASSEMBLE_BUDGET = 4000;
5
6
  /**
6
7
  * Build a chronologically-ordered context window for a session. Adapts the
7
8
  * lossless-claw context-engine pattern to Hippo's score-ranked memory store.
@@ -26,7 +27,7 @@ import { passesScopeFilterForRecall, assertScopeRequestAllowed } from '../recall
26
27
  */
27
28
  export function assemble(ctx, sessionId, opts = {}) {
28
29
  assertScopeRequestAllowed(ctx.actor, opts.scope);
29
- const budget = opts.budget ?? 4000;
30
+ const budget = opts.budget ?? DEFAULT_ASSEMBLE_BUDGET;
30
31
  const freshTailCount = opts.freshTailCount ?? 10;
31
32
  const summarizeOlder = opts.summarizeOlder ?? true;
32
33
  const rowCap = opts.rowCap ?? 5000;
@@ -35,17 +36,12 @@ export function assemble(ctx, sessionId, opts = {}) {
35
36
  }
36
37
  const rows = loadSessionRawMemories(ctx.hippoRoot, sessionId, ctx.tenantId, rowCap);
37
38
  const truncated = rows.length === rowCap;
38
- // v1.6.3 senior-review P0-1: report the FULL post-filter row count even
39
- // when the cap windows the loaded set. Pre-v1.6.3 used `scoped.length`
40
- // which under-reported on long sessions and made consumers render
41
- // wrong "session has N msgs" UX.
39
+ // `scoped.length` under-counts a capped session, so totalRaw falls back to a COUNT below.
42
40
  const scoped = rows.filter((r) => passesScopeFilterForRecall(r.scope ?? null, opts.scope));
43
41
  let totalRaw;
44
42
  if (truncated) {
45
- // v1.6.3 codex P1 / senior P0: scope-aware unbounded COUNT. The helper
46
- // SQL-encodes the same default-deny rule passesScopeFilterForRecall
47
- // applies in TS, so a no-scope caller cannot infer private rows by
48
- // comparing totalRaw to items.length on a truncated session.
43
+ // The COUNT applies the same default-deny scope rule in SQL, so a no-scope
44
+ // caller cannot infer private rows by comparing totalRaw to items.length.
49
45
  totalRaw = countSessionRawMemories(ctx.hippoRoot, sessionId, ctx.tenantId, opts.scope);
50
46
  }
51
47
  else {
@@ -69,9 +65,8 @@ export function assemble(ctx, sessionId, opts = {}) {
69
65
  isFreshTail: true,
70
66
  strength: r.strength,
71
67
  }));
72
- // F4 (v1.6.5): byte compare canonical UTC ISO timestamps. ~50× faster than
73
- // localeCompare and chronological by virtue of the timestamp invariant
74
- // documented in src/memory.ts above MemoryEntry.
68
+ // Byte compare is chronological for the fixed-form UTC ISO timestamps
69
+ // (invariant documented in src/memory.ts above MemoryEntry).
75
70
  const cmpIso = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
76
71
  olderItems.sort((a, b) => cmpIso(a.createdAt, b.createdAt));
77
72
  tailItems.sort((a, b) => cmpIso(a.createdAt, b.createdAt));
@@ -10,8 +10,8 @@ export interface AuditListOpts {
10
10
  after?: KeysetPosition;
11
11
  }
12
12
  /**
13
- * Read audit events scoped to `ctx.tenantId`. Read-only — no audit emit (matches
14
- * A5: cmdAuditList does not record a 'recall'-style read event).
13
+ * Read audit events scoped to `ctx.tenantId`. Read-only, no audit emit (matches
14
+ * cmdAuditList, which does not record a 'recall'-style read event).
15
15
  */
16
16
  export declare function auditList(ctx: Context, opts: AuditListOpts): AuditEvent[];
17
17
  //# sourceMappingURL=audit.d.ts.map
package/dist/api/audit.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import { openHippoDb, closeHippoDb } from '../db.js';
3
3
  import { queryAuditEvents } from '../audit.js';
4
4
  /**
5
- * Read audit events scoped to `ctx.tenantId`. Read-only — no audit emit (matches
6
- * A5: cmdAuditList does not record a 'recall'-style read event).
5
+ * Read audit events scoped to `ctx.tenantId`. Read-only, no audit emit (matches
6
+ * cmdAuditList, which does not record a 'recall'-style read event).
7
7
  */
8
8
  export function auditList(ctx, opts) {
9
9
  const db = openHippoDb(ctx.hippoRoot);
@@ -4,8 +4,8 @@ import type { Context } from './types.js';
4
4
  export interface AuthCreateOpts {
5
5
  label?: string;
6
6
  /**
7
- * v1.12.3: authorization role for the new key. Defaults to `'admin'` for
8
- * back-compat with v1.12.0-v1.12.2 (the api_keys.role column DEFAULT also
7
+ * Authorization role for the new key. Defaults to `'admin'` for
8
+ * back-compat with keys minted before roles existed (the api_keys.role column DEFAULT also
9
9
  * resolves to 'admin' if omitted from the INSERT). Member keys are
10
10
  * 403-blocked from admin-gated routes (e.g. `POST /v1/sleep`).
11
11
  */
@@ -15,7 +15,7 @@ export interface AuthCreateResult {
15
15
  keyId: string;
16
16
  plaintext: string;
17
17
  tenantId: string;
18
- /** v1.12.3: the role bound to the new key (admin | member). */
18
+ /** The role bound to the new key (admin | member). */
19
19
  role: 'admin' | 'member';
20
20
  }
21
21
  /**
@@ -36,7 +36,7 @@ export declare function authCreate(ctx: Context, opts: AuthCreateOpts): AuthCrea
36
36
  * Divergence from `cmdAuthList` in src/cli.ts: the CLI today returns ALL keys
37
37
  * regardless of tenant (single-tenant deployments). The API surface is tenant-
38
38
  * scoped because future multi-tenant deployments will share a hippoRoot, and
39
- * tenant A must not see tenant B's keys. Read-only — no audit emit (matches A5).
39
+ * tenant A must not see tenant B's keys. Read-only, so no audit emit.
40
40
  */
41
41
  export declare function authList(ctx: Context, opts: {
42
42
  active: boolean;
@@ -55,7 +55,7 @@ export declare function authListRows(ctx: Context, opts: {
55
55
  * revoke only its own key (checked first), so no caller can probe other key_ids.
56
56
  *
57
57
  * Audit: emits 'auth_revoke' with `tenantId` set to the KEY ROW's tenant_id
58
- * (M1 fix from A5 review, mirrors src/cli.ts:cmdAuthRevoke). Skipped on no-op
58
+ * (mirrors src/cli.ts:cmdAuthRevoke). Skipped on no-op
59
59
  * revoke (already revoked) so re-running doesn't pad the audit log.
60
60
  */
61
61
  export interface AuthRevokeResult {
@@ -72,7 +72,7 @@ export declare function authKeyTenant(ctx: Context, keyId: string): string | und
72
72
  export interface AuthGrantResult {
73
73
  ok: true;
74
74
  }
75
- /** Grant `keyId` read access to one restricted `scope` (ROADMAP Part VIII EI2). Admin only. */
75
+ /** Grant `keyId` read access to one restricted `scope`. Admin only. */
76
76
  export declare function authGrant(ctx: Context, keyId: string, scope: string): AuthGrantResult;
77
77
  /** Revoke `keyId`'s grant on `scope`. Same authorization and lookup rules as authGrant. */
78
78
  export declare function authUngrant(ctx: Context, keyId: string, scope: string): AuthGrantResult;
package/dist/api/auth.js CHANGED
@@ -27,10 +27,8 @@ export function authCreate(ctx, opts) {
27
27
  try {
28
28
  const role = opts.role ?? (ctx.actor.viaAuthResolver ? 'member' : 'admin');
29
29
  const result = createApiKey(db, { tenantId: ctx.tenantId, label: opts.label, role });
30
- // v1.12.4: audit emit (closes the gap v1.12.3 CHANGELOG flagged as deferred).
31
- // Mirrors the auth_revoke pattern at authRevoke — same try/catch so audit
32
- // failure can't crash a successful mint. The plaintext is NEVER logged;
33
- // metadata carries label + role + the keyId (which is non-secret).
30
+ // Same try/catch as authRevoke so audit failure can't crash a successful mint.
31
+ // The plaintext is NEVER logged; metadata carries label + role, keyId is non-secret.
34
32
  try {
35
33
  appendAuditEvent(db, {
36
34
  tenantId: ctx.tenantId,
@@ -59,7 +57,7 @@ export function authCreate(ctx, opts) {
59
57
  * Divergence from `cmdAuthList` in src/cli.ts: the CLI today returns ALL keys
60
58
  * regardless of tenant (single-tenant deployments). The API surface is tenant-
61
59
  * scoped because future multi-tenant deployments will share a hippoRoot, and
62
- * tenant A must not see tenant B's keys. Read-only — no audit emit (matches A5).
60
+ * tenant A must not see tenant B's keys. Read-only, so no audit emit.
63
61
  */
64
62
  export function authList(ctx, opts) {
65
63
  return authListRows(ctx, opts).map((r) => r.key);
@@ -101,7 +99,7 @@ export function authRevoke(ctx, keyId) {
101
99
  if (!alreadyRevoked) {
102
100
  try {
103
101
  appendAuditEvent(db, {
104
- tenantId: row.tenantId, // M1: KEY's tenant, not ctx.tenantId.
102
+ tenantId: row.tenantId, // KEY's tenant, not ctx.tenantId.
105
103
  actor: ctx.actor.subject,
106
104
  op: 'auth_revoke',
107
105
  targetId: keyId,
@@ -134,7 +132,7 @@ export function authKeyTenant(ctx, keyId) {
134
132
  closeHippoDb(db);
135
133
  }
136
134
  }
137
- /** Grant `keyId` read access to one restricted `scope` (ROADMAP Part VIII EI2). Admin only. */
135
+ /** Grant `keyId` read access to one restricted `scope`. Admin only. */
138
136
  export function authGrant(ctx, keyId, scope) {
139
137
  return changeScopeGrant(ctx, keyId, scope, 'auth_grant');
140
138
  }
@@ -1,6 +1,7 @@
1
1
  import type { AmbientLoadResult } from '../store/candidates.js';
2
2
  import { type MemoryEntry } from '../memory.js';
3
3
  import { type HippoConfig } from '../config.js';
4
+ import type { ProjectRef } from '../project-identity.js';
4
5
  import type { DeliveryObserver } from '../delivery-recorder.js';
5
6
  import type { ContextCost, ContextOpts, ContextResultEntry } from './context-types.js';
6
7
  import type { Context } from './types.js';
@@ -18,9 +19,9 @@ export interface ContextPlan {
18
19
  primaryIsGlobal: boolean;
19
20
  hasLocalTaskState: boolean;
20
21
  config: HippoConfig;
21
- currentProjectName: string;
22
+ currentProject: ProjectRef;
22
23
  includeCrossProject: boolean;
23
- originProject: string | undefined;
24
+ originProject: readonly string[] | undefined;
24
25
  promptRecallPending: boolean;
25
26
  cost: ContextCost | undefined;
26
27
  price: (entry: MemoryEntry, isGlobal: boolean, promptRecall?: boolean) => number;
@@ -41,7 +42,7 @@ export interface ContextPools {
41
42
  export declare function oneCopyPerMemory(local: readonly MemoryEntry[], global: readonly MemoryEntry[], now: Date): [MemoryEntry[], MemoryEntry[]];
42
43
  export declare const finiteOr: (v: number, dflt: number, min: number) => number;
43
44
  /** Pins plus the prompt-recall or recent-N backfill; null means the block is empty. */
44
- export declare function selectPinned(ctx: Context, opts: ContextOpts, plan: ContextPlan, left: number, pools: ContextPools, admission: ContextAdmission): ContextResultEntry[] | null;
45
+ export declare function selectPinned(opts: ContextOpts, plan: ContextPlan, left: number, pools: ContextPools, admission: ContextAdmission): ContextResultEntry[] | null;
45
46
  /** No query: the strongest memories by strength, up to budget. */
46
47
  export declare function selectStrongest(plan: ContextPlan, left: number, pools: ContextPools): ContextResultEntry[];
47
48
  /** Real query: hybrid search over both stores, or physics/hybrid over the local rows; emits the 'recall' audit row. */