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
@@ -1,6 +1,5 @@
1
1
  /**
2
- * E2 project_brief first-class object
3
- * (docs/plans/2026-05-30-e2-project-brief-object.md).
2
+ * Project_brief first-class object.
4
3
  *
5
4
  * A `project_brief` is the living, repo-scoped summary of a repository's state: a
6
5
  * `summary` body scoped to a `repo`, evolving via the supersede delta lifecycle.
@@ -31,6 +30,7 @@ import { createMemory, Layer } from './memory.js';
31
30
  import { appendAuditEvent } from './audit.js';
32
31
  import { objectHalfLifeDays } from './half-life-migration.js';
33
32
  import { keysetAfter } from './keyset.js';
33
+ import { escapeLike } from './escape.js';
34
34
  export const VALID_BRIEF_STATES = new Set([
35
35
  'active',
36
36
  'superseded',
@@ -106,7 +106,7 @@ function buildBriefContent(repo, summary) {
106
106
  // Preflight the supersede target BEFORE inserting the new row (so the new
107
107
  // autoincrement id can never be its own supersede target); read the
108
108
  // predecessor version in the same SELECT for server-derived versioning.
109
- // Mirrors saveSkill / saveProcess (codex P1 2026-05-28).
109
+ // Mirrors saveSkill / saveProcess.
110
110
  function preflightBriefSupersede(db, tenantId, supersedesId) {
111
111
  // SAFETY: SELECT projects exactly status, version; .get() returns that
112
112
  // shape for the matching row, or undefined when no brief/tenant pair matches.
@@ -276,7 +276,7 @@ export function closeProjectBrief(hippoRoot, tenantId, id, actor = 'cli') {
276
276
  // Closing removes the object from the graph. Remove its rows DIRECTLY (deterministic),
277
277
  // not only via an enqueued rebuild whose queue item is lost if the mirror is later
278
278
  // forgotten (the queue row cascade-deletes with the memory), which would leave the closed
279
- // object stale and could block that forget (codex P1). Still enqueue when a mirror exists
279
+ // object stale and could block that forget. Still enqueue when a mirror exists
280
280
  // so a concurrent rebuild re-derives consistently (harmless if it also runs).
281
281
  removeGraphEntitiesForObject(hippoRoot, tenantId, 'project', closed.id);
282
282
  if (closed.memoryId) {
@@ -351,7 +351,7 @@ export function loadProjectBriefs(hippoRoot, tenantId, opts = {}) {
351
351
  /**
352
352
  * The repo's CURRENT active brief, or null. By convention there is one active brief
353
353
  * per (tenant, repo); if an operator created more than one (the DB does not prevent
354
- * it, consistent with every other E2 object), the MOST-RECENT active row wins.
354
+ * it, consistent with every other first-class object), the MOST-RECENT active row wins.
355
355
  */
356
356
  export function loadActiveBriefForRepo(hippoRoot, tenantId, repo) {
357
357
  assertTenantId('loadActiveBriefForRepo', tenantId);
@@ -375,10 +375,6 @@ export function loadActiveBriefForRepo(hippoRoot, tenantId, repo) {
375
375
  // ---------------------------------------------------------------------------
376
376
  // Refresh assembler (the distinguishing deliverable)
377
377
  // ---------------------------------------------------------------------------
378
- /** Escape LIKE wildcards in operator-supplied text (mirror of store/search-rows.ts). */
379
- function escapeLike(term) {
380
- return term.replace(/[%_\\]/g, '\\$&');
381
- }
382
378
  /** Single-line headline for a receipt: first non-empty line, newline-stripped,
383
379
  * truncated. Deterministic + safe for the markdown bullet list. */
384
380
  function receiptHeadline(content) {
@@ -414,9 +410,9 @@ function loadBriefReceipts(hippoRoot, tenantId, normalizedRepo) {
414
410
  // NOTE on ordering: the `id DESC` tiebreak is lexical on a random-ish memory id
415
411
  // (e.g. `sem_<hex>`), NOT chronological — within the same `created` timestamp the
416
412
  // order is stable-but-arbitrary, not insertion order. `created DESC` is the real
417
- // recency ordering. (plan-eng-critic 2026-05-30, med.)
413
+ // recency ordering.
418
414
  //
419
- // Budget-aware assembly (codex-review-critic 2026-05-30, P2): the digest is the
415
+ // Budget-aware assembly: the digest is the
420
416
  // brief `summary`, which saveProjectBrief caps at MAX_BRIEF_SUMMARY_LEN. The
421
417
  // receipt/headline caps (50 x ~200) could otherwise build an ~11KB body that the
422
418
  // store then REJECTS, breaking refresh for inputs within the advertised caps. So
@@ -518,8 +514,8 @@ export function refreshBrief(hippoRoot, tenantId, repo, actor = 'cli') {
518
514
  supersedesBriefId: active ? active.id : undefined,
519
515
  refreshReceiptCount: receiptCount,
520
516
  // Tag the refreshed brief's mirror as repo-local so path-aware recall boosts
521
- // it like the manual `brief new`/`supersede` paths do (codex-review 2026-05-30,
522
- // P2). Safe vs self-recursion: assembleBriefFromReceipts excludes
517
+ // it like the manual `brief new`/`supersede` paths do.
518
+ // Safe vs self-recursion: assembleBriefFromReceipts excludes
523
519
  // source='project_brief', so the brief never becomes its own receipt.
524
520
  extraTags: [`path:${normalizedRepo.toLowerCase()}`],
525
521
  }, actor);
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Project identity resolution for memory scope isolation (ROADMAP.md Part I
3
- * [Committed] "Memory scope isolation"; plan docs/plans/2026-07-01-memory-scope-isolation.md S1).
2
+ * Project identity resolution for memory scope isolation.
4
3
  *
5
4
  * Resolution rules:
6
5
  * - The nearest ancestor of cwd (including cwd itself) containing a `.hippo`
@@ -11,6 +10,9 @@
11
10
  * - A directory with no marker anywhere up the walk is NOT a project: it
12
11
  * resolves to the user-global identity (empty name), so memories written
13
12
  * there stay injectable everywhere (matches pre-isolation behavior).
13
+ * - A project's name is its id: `.hippo-project.json` at the root, else the
14
+ * checkout's normalised `origin` remote, else the folder rule (legacyName),
15
+ * so two repos in folders of one name stay apart.
14
16
  *
15
17
  * NOTE: this module must stay free of imports from shared.ts / store.ts /
16
18
  * api.ts so any of them can import it without creating a cycle.
@@ -19,8 +21,12 @@
19
21
  export interface ProjectIdentity {
20
22
  /** Realpath-resolved root directory of the project (the start dir when not in a project). */
21
23
  root: string;
22
- /** Lowercased basename of the project root, or of its repo for a linked worktree with no `.hippo` (a store keeps the name its rows carry); '' outside a project. */
24
+ /** The project id rows are stamped with: the project file's id, the origin remote, or legacyName; '' outside a project. */
23
25
  name: string;
26
+ /** The folder rule rows written before ids carry: the root's lowercased basename, or its repo's for a linked worktree with no `.hippo`. */
27
+ legacyName: string;
28
+ /** Every other name this root resolves to (the project file id, the origin remote even with the rule off, legacyName), so rows written under an earlier id stay readable. */
29
+ aliases?: readonly string[];
24
30
  /** True when the directory resolves to the user home working set. */
25
31
  isHome: boolean;
26
32
  }
@@ -33,7 +39,7 @@ export interface ResolveProjectIdentityOpts {
33
39
  homeDir?: string;
34
40
  stopDir?: string;
35
41
  }
36
- /** Clear the per-process identity cache (test seam). */
42
+ /** Clear the per-process identity cache and the remote switch it read (test seam). */
37
43
  export declare function clearProjectIdentityCache(): void;
38
44
  /**
39
45
  * Canonicalize a path via realpath, falling back to path.resolve when the
@@ -46,17 +52,25 @@ export declare function realpathOrResolve(p: string): string;
46
52
  */
47
53
  export declare function resolveProjectIdentity(cwd?: string, opts?: ResolveProjectIdentityOpts): ProjectIdentity;
48
54
  /** Nearest ancestor `.hippo` below home and the temp root (never projects; on Windows the temp root sits inside home).
49
- * Everything is realpath'd so a symlinked temp root or cwd still matches its bound. Design notes: docs/plans/2026-09-05-*.md */
55
+ * Everything is realpath'd so a symlinked temp root or cwd still matches its bound. */
50
56
  export declare function findHippoStoreDir(cwd?: string, opts?: ResolveProjectIdentityOpts): string | null;
57
+ /** A reader's project: a bare name, or an identity whose own rows may also carry its legacy folder name. */
58
+ export type ProjectRef = string | Pick<ProjectIdentity, 'name' | 'legacyName' | 'aliases'>;
59
+ /** The id a reader's new rows are stamped with; '' outside a project. */
60
+ export declare function projectId(project: ProjectRef): string;
61
+ /** Every origin_project value the reader's own rows carry: the id first, then each alias and the legacy name. */
62
+ export declare function projectNames(project: ProjectRef): readonly string[];
63
+ /** `origin_project IN (?, ...)` with one placeholder per name; an empty list matches nothing. */
64
+ export declare function originInSql(names: readonly string[], column?: string): string;
51
65
  /**
52
66
  * v39 memory scope isolation: classify a memory's origin_project against the
53
- * active project. `currentName === ''` means the session is not in a project
67
+ * active project. An empty current id means the session is not in a project
54
68
  * (home dir or markerless cwd) - everything is in scope there, matching
55
69
  * pre-isolation behavior. NULL/undefined origin is a legacy pre-v39 row and
56
70
  * is treated as cross-project (deny by default) - the safe direction for a
57
71
  * security partition.
58
72
  */
59
- export declare function classifyOriginProject(origin: string | null | undefined, currentName: string): 'project' | 'user-global' | 'cross-project';
73
+ export declare function classifyOriginProject(origin: string | null | undefined, current: ProjectRef): 'project' | 'user-global' | 'cross-project';
60
74
  /**
61
75
  * The global Hippo store directory, resolved the same way shared.ts does:
62
76
  * $HIPPO_HOME > $XDG_DATA_HOME/hippo > ~/.hippo. Lives here (leaf module) so
@@ -85,8 +99,7 @@ export declare function originFromSource(source: string | null | undefined, home
85
99
  * Returns the project name, or '' for user-global (written at/under home or
86
100
  * in a markerless directory) - injectable everywhere. Write sites must always
87
101
  * persist this value; a NULL origin_project column is reserved for legacy
88
- * pre-migration rows, which ambient context treats as deny (see plan
89
- * docs/plans/2026-07-01-memory-scope-isolation.md "Origin model").
102
+ * pre-migration rows, which ambient context treats as deny.
90
103
  */
91
104
  export declare function deriveOriginProject(cwd?: string, opts?: ResolveProjectIdentityOpts): string;
92
105
  //# sourceMappingURL=project-identity.d.ts.map
@@ -2,12 +2,31 @@ import { envHippoHome, envXdgDataHome } from './env.js';
2
2
  import * as fs from 'fs';
3
3
  import * as os from 'os';
4
4
  import * as path from 'path';
5
+ import { loadConfig } from './config.js';
5
6
  import { log } from './log.js';
7
+ import { originRemoteId, projectFileId } from './project-remote.js';
6
8
  const MAX_WALK_DEPTH = 64;
7
9
  const identityCache = new Map();
8
- /** Clear the per-process identity cache (test seam). */
10
+ let remoteSwitch = null;
11
+ /** Clear the per-process identity cache and the remote switch it read (test seam). */
9
12
  export function clearProjectIdentityCache() {
10
13
  identityCache.clear();
14
+ remoteSwitch = null;
15
+ }
16
+ // The global store's config only: a per-store switch would stamp one checkout two ways.
17
+ function remoteRuleOn() {
18
+ const globalRoot = resolveGlobalRootDir();
19
+ if (remoteSwitch?.globalRoot !== globalRoot)
20
+ remoteSwitch = { globalRoot, on: loadConfig(globalRoot).projectIdentity.remote };
21
+ return remoteSwitch.on;
22
+ }
23
+ /** The id a root names itself by plus every rung that resolves; only the root's own `.git` is read, so a store nested in another checkout keeps its folder name. */
24
+ function namesAt(root, legacyName) {
25
+ const fileId = projectFileId(root);
26
+ const remoteId = originRemoteId(root);
27
+ const name = fileId ?? (remoteRuleOn() ? remoteId : null) ?? legacyName;
28
+ const aliases = [...new Set([fileId, remoteId, legacyName])].filter((n) => n !== null && n !== name);
29
+ return aliases.length > 0 ? { name, aliases } : { name };
11
30
  }
12
31
  /**
13
32
  * Canonicalize a path via realpath, falling back to path.resolve when the
@@ -72,16 +91,16 @@ export function resolveProjectIdentity(cwd, opts) {
72
91
  let identity;
73
92
  const root = hippoRoot ?? gitRoot;
74
93
  if (root !== null) {
75
- const name = (hippoRoot === null ? linkedWorktreeRepoName(root, home) : null) ?? path.basename(root).toLowerCase();
76
- identity = { root, name, isHome: false };
94
+ const legacyName = (hippoRoot === null ? linkedWorktreeRepoName(root, home) : null) ?? path.basename(root).toLowerCase();
95
+ identity = { root, ...namesAt(root, legacyName), legacyName, isHome: false };
77
96
  }
78
97
  else if (reachedHome || isUnder(start, home)) {
79
- identity = { root: home, name: '', isHome: true };
98
+ identity = { root: home, name: '', legacyName: '', isHome: true };
80
99
  }
81
100
  else {
82
101
  // No markers anywhere: not a project. Empty name keeps these memories
83
102
  // user-global rather than fabricating an origin from a basename.
84
- identity = { root: start, name: '', isHome: false };
103
+ identity = { root: start, name: '', legacyName: '', isHome: false };
85
104
  }
86
105
  if (cacheable)
87
106
  identityCache.set(startInput, identity);
@@ -137,7 +156,7 @@ function walkProjectMarkers(start, home, stopDirs) {
137
156
  return { hippoRoot, gitRoot, reachedHome };
138
157
  }
139
158
  /** Nearest ancestor `.hippo` below home and the temp root (never projects; on Windows the temp root sits inside home).
140
- * Everything is realpath'd so a symlinked temp root or cwd still matches its bound. Design notes: docs/plans/2026-09-05-*.md */
159
+ * Everything is realpath'd so a symlinked temp root or cwd still matches its bound. */
141
160
  export function findHippoStoreDir(cwd, opts) {
142
161
  const home = realpathOrResolve(opts?.homeDir ?? os.homedir());
143
162
  const stops = [realpathOrResolve(os.tmpdir())];
@@ -147,22 +166,39 @@ export function findHippoStoreDir(cwd, opts) {
147
166
  const { hippoRoot } = walkProjectMarkers(start, home, stops);
148
167
  return hippoRoot === null ? null : path.join(hippoRoot, '.hippo');
149
168
  }
169
+ function isBareName(project) {
170
+ return typeof project === 'string';
171
+ }
172
+ /** The id a reader's new rows are stamped with; '' outside a project. */
173
+ export function projectId(project) {
174
+ return isBareName(project) ? project : project.name;
175
+ }
176
+ /** Every origin_project value the reader's own rows carry: the id first, then each alias and the legacy name. */
177
+ export function projectNames(project) {
178
+ if (isBareName(project) || project.name === '')
179
+ return [projectId(project)];
180
+ return [...new Set([project.name, ...(project.aliases ?? []), project.legacyName])].filter((n) => n !== '');
181
+ }
182
+ /** `origin_project IN (?, ...)` with one placeholder per name; an empty list matches nothing. */
183
+ export function originInSql(names, column = 'origin_project') {
184
+ return names.length === 0 ? '0' : `${column} IN (${names.map(() => '?').join(', ')})`;
185
+ }
150
186
  /**
151
187
  * v39 memory scope isolation: classify a memory's origin_project against the
152
- * active project. `currentName === ''` means the session is not in a project
188
+ * active project. An empty current id means the session is not in a project
153
189
  * (home dir or markerless cwd) - everything is in scope there, matching
154
190
  * pre-isolation behavior. NULL/undefined origin is a legacy pre-v39 row and
155
191
  * is treated as cross-project (deny by default) - the safe direction for a
156
192
  * security partition.
157
193
  */
158
- export function classifyOriginProject(origin, currentName) {
159
- if (currentName === '')
194
+ export function classifyOriginProject(origin, current) {
195
+ if (projectId(current) === '')
160
196
  return 'project';
161
197
  if (origin === undefined || origin === null)
162
198
  return 'cross-project';
163
199
  if (origin === '')
164
200
  return 'user-global';
165
- return origin === currentName ? 'project' : 'cross-project';
201
+ return projectNames(current).includes(origin) ? 'project' : 'cross-project';
166
202
  }
167
203
  /**
168
204
  * The global Hippo store directory, resolved the same way shared.ts does:
@@ -221,8 +257,7 @@ export function originFromSource(source, homeName) {
221
257
  * Returns the project name, or '' for user-global (written at/under home or
222
258
  * in a markerless directory) - injectable everywhere. Write sites must always
223
259
  * persist this value; a NULL origin_project column is reserved for legacy
224
- * pre-migration rows, which ambient context treats as deny (see plan
225
- * docs/plans/2026-07-01-memory-scope-isolation.md "Origin model").
260
+ * pre-migration rows, which ambient context treats as deny.
226
261
  */
227
262
  export function deriveOriginProject(cwd, opts) {
228
263
  return resolveProjectIdentity(cwd, opts).name;
@@ -11,7 +11,7 @@ export interface ProjectSummary {
11
11
  export interface MergeResult {
12
12
  readonly from: string;
13
13
  readonly into: string;
14
- /** Imported note copies moved to dormant storage; the next sync under `into` imports the notes still on disk. */
14
+ /** Imports `into` already holds for the same note, moved to dormant storage; the rest are re-tagged and the next sync files them under `into`. */
15
15
  readonly setAside: readonly string[];
16
16
  readonly restamped: readonly string[];
17
17
  readonly dormantRestamped: readonly string[];
@@ -22,11 +22,18 @@ export interface ProjectFold {
22
22
  readonly from: string;
23
23
  readonly into: string;
24
24
  }
25
+ /** An old name whose session folders now resolve to several projects: no fold can say whose rows are whose. */
26
+ export interface ProjectCollision {
27
+ readonly name: string;
28
+ readonly ids: readonly string[];
29
+ }
25
30
  export interface RepairResult {
26
31
  /** Imported notes filed under the wrong project, or under a project name when a user-global import holds the same text: set aside. */
27
32
  readonly copies: readonly string[];
28
33
  /** Names whose recorded session folders all resolve to one other project today, folded as `merge` would. */
29
34
  readonly folds: readonly ProjectFold[];
35
+ /** Names left unfolded because their folders now resolve to more than one project; only `merge` by hand can split them. */
36
+ readonly collisions: readonly ProjectCollision[];
30
37
  readonly toProject: ReadonlyArray<{
31
38
  readonly id: string;
32
39
  readonly origin: string;
@@ -50,11 +57,19 @@ export declare function mergeProjects(db: DatabaseSyncLike, hippoRoot: string, o
50
57
  into: string;
51
58
  dryRun: boolean;
52
59
  }): MergeResult;
60
+ /** Names folded, directly or through others, into one of `names`; the sync moves imports still filed under them. */
61
+ export declare function namesFoldedInto(db: DatabaseSyncLike, tenantId: string, names: readonly string[]): string[];
53
62
  /** Reads only, so doctor and a dry run take no write lock; merged rows are planned before any fold, so a few may re-tag differently once folds apply. */
54
- export declare function planProjectRepair(db: DatabaseSyncLike, hippoRoot: string, tenantId: string): Omit<RepairResult, 'backup'>;
63
+ export declare function planProjectRepair(db: DatabaseSyncLike, hippoRoot: string, tenantId: string, globalFolds?: boolean): Omit<RepairResult, 'backup'>;
55
64
  /** Sets aside stray imports, folds the names the resolver now maps elsewhere, then re-tags sleep's user-global merges by their parents; a dry run only plans. */
56
65
  export declare function repairProjects(db: DatabaseSyncLike, hippoRoot: string, opts: {
57
66
  tenantId: string;
58
67
  dryRun: boolean;
68
+ globalFolds?: boolean;
59
69
  }): RepairResult;
70
+ /**
71
+ * Sleep runs repair once per store, so an upgrade needs no command. Global-store folds stay
72
+ * with `hippo projects repair`: their evidence is compaction folders, blind to a same-named repo that never compacted.
73
+ */
74
+ export declare function repairOnceOnSleep(db: DatabaseSyncLike, hippoRoot: string, tenantId: string): RepairResult | null;
60
75
  //# sourceMappingURL=project-merge.d.ts.map
@@ -2,14 +2,16 @@
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import { setAsideRow } from './agent-memories/apply.js';
5
- import { transcriptNotesOrigin } from './agent-memories/claude-code.js';
5
+ import { transcriptNotesProject } from './agent-memories/claude-code.js';
6
6
  import { containerId, containerPrefix } from './agent-memories/source.js';
7
7
  import { AGENT_MEMORY_SOURCE_PREFIX, AGENT_MEMORY_TOOLS, toolSourcePrefix } from './agent-memories/tools.js';
8
8
  import { appendAuditEvent, queryAuditEvents } from './audit.js';
9
+ import { getMeta, setMeta } from './db/meta.js';
9
10
  import { insertDormantRow, listDormantSnapshots, replaceDormantEntry } from './dormant.js';
10
11
  import { processEnv } from './env.js';
11
12
  import { calculateStrength } from './memory.js';
12
- import { deriveOriginProject, isGlobalStoreRoot } from './project-identity.js';
13
+ import { isObjectLike, isStringValue } from './capture-contract.js';
14
+ import { isGlobalStoreRoot, projectNames, resolveProjectIdentity } from './project-identity.js';
13
15
  import { duplicateKey } from './same-text.js';
14
16
  import { removeEntryMirrors } from './store/mirrors.js';
15
17
  import { deleteEntryRowInTx, writeEntryMirrors } from './store/entry-writes.js';
@@ -93,11 +95,14 @@ export function mergeProjects(db, hippoRoot, opts) {
93
95
  return result;
94
96
  }
95
97
  function foldInTx(db, tenantId, from, into) {
96
- const rows = selectAllEntries(db, tenantId).filter((e) => e.origin_project === from);
98
+ const all = selectAllEntries(db, tenantId);
99
+ const rows = all.filter((e) => e.origin_project === from);
100
+ // The sync renames the rest to `into`'s prefix and keeps their ids; only a note `into` already holds would show twice.
101
+ const held = new Set(all.filter((e) => e.origin_project === into && !e.superseded_by && isImport(e)).map(noteKey));
97
102
  const setAside = [];
98
103
  for (const row of rows) {
99
104
  const tag = toolTag(row.source);
100
- if (!isImport(row) || row.superseded_by || row.kind === 'raw' || tag === null)
105
+ if (!isImport(row) || row.superseded_by || row.kind === 'raw' || tag === null || !held.has(noteKey(row)))
101
106
  continue;
102
107
  if (setAsideRow(db, tag, { ...row, origin_project: into }, 'project-merge').kind === 'dormant')
103
108
  setAside.push(row.id);
@@ -117,6 +122,11 @@ function foldInTx(db, tenantId, from, into) {
117
122
  .run(into, tenantId, from).changes ?? 0);
118
123
  return { setAside, restamped, dormantRestamped, compactions };
119
124
  }
125
+ /** Tool, note key and hash of an import, without the container, whose id hashes the project name. */
126
+ function noteKey(entry) {
127
+ const rest = entry.source.slice(AGENT_MEMORY_SOURCE_PREFIX.length);
128
+ return `${rest.split(':')[0]}:${rest.slice(rest.indexOf('/') + 1)}`;
129
+ }
120
130
  /** Live imports under a project name whose exact text a user-global import holds: a session folder's notes stamped with the folder it ended in. */
121
131
  function importCopies(db, tenantId) {
122
132
  const live = selectAllEntries(db, tenantId).filter((e) => !e.superseded_by && isImport(e));
@@ -135,51 +145,106 @@ function strayImports(db, hippoRoot, tenantId) {
135
145
  .all(tenantId);
136
146
  const owners = new Map();
137
147
  for (const { transcript, cwd } of sessions) {
138
- const origin = transcriptNotesOrigin(transcript, cwd, machine);
148
+ const project = transcriptNotesProject(transcript, cwd, machine);
139
149
  const dir = path.join(path.dirname(transcript), 'memory');
140
- if (origin !== null)
141
- owners.set(dir, (owners.get(dir) ?? new Set()).add(origin));
150
+ if (project === null)
151
+ continue;
152
+ const ids = owners.get(dir) ?? new Map();
153
+ owners.set(dir, ids.set(project.name, [...new Set([...(ids.get(project.name) ?? []), ...projectNames(project)])]));
142
154
  }
143
155
  const tool = toolSourcePrefix('claude-code');
144
156
  const live = selectLiveEntriesBySourcePrefix(db, tenantId, tool).filter((e) => e.kind !== 'raw' && !e.pinned);
145
157
  const origins = new Set(live.map((e) => e.origin_project ?? ''));
158
+ const edges = foldEdges(db, tenantId);
146
159
  const right = new Set();
147
160
  const wrong = new Set();
148
- for (const [dir, owner] of owners) {
149
- if (owner.size !== 1)
161
+ for (const [dir, ids] of owners) {
162
+ if (ids.size !== 1)
150
163
  continue;
151
- for (const origin of origins)
152
- (owner.has(origin) ? right : wrong).add(containerPrefix('claude-code', containerId(dir, 'project', platform, origin)));
164
+ // Its names before the id, and names folded into it, are the owner's too: the next sync moves those prefixes under the id.
165
+ const [names] = ids.values();
166
+ const own = new Set([...names, ...foldedInto(edges, names)]);
167
+ for (const origin of new Set([...origins, ...own]))
168
+ (own.has(origin) ? right : wrong).add(containerPrefix('claude-code', containerId(dir, 'project', platform, origin)));
153
169
  }
154
170
  const prefix = (e) => e.source.slice(0, e.source.indexOf('/', tool.length) + 1);
155
171
  const misfiled = live.filter((e) => wrong.has(prefix(e)));
156
172
  const seen = new Set(misfiled.map((e) => e.id));
157
173
  return [...misfiled, ...copies.filter((e) => !seen.has(e.id) && !right.has(prefix(e)))];
158
174
  }
159
- /** Global store only, where a compaction's name came from its cwd: a name whose every cwd still on disk resolves to one other project today. */
160
- function planFolds(db, tenantId) {
175
+ /** The global store reads each name's recorded session folders, where a compaction's name came from its cwd; a project store folds only its own folder name. */
176
+ function planFolds(db, hippoRoot, tenantId, globalFolds) {
177
+ // A name someone merged into by hand stays: undoing their choice would rest on the resolver alone.
178
+ const chosen = new Set(queryAuditEvents(db, { tenantId, op: 'project_merge', limit: 10000 }).map((e) => e.metadata.into));
179
+ if (!isGlobalStoreRoot(hippoRoot))
180
+ return { folds: ownLegacyFold(db, hippoRoot, tenantId).filter((f) => !chosen.has(f.from)), collisions: [] };
161
181
  // SAFETY: the SELECT names the two columns of the row type.
162
182
  const rows = db.prepare(`SELECT DISTINCT origin_project AS origin, cwd FROM compactions WHERE tenant_id = ? AND origin_project <> '' AND cwd IS NOT NULL`)
163
183
  .all(tenantId);
164
184
  const today = new Map();
165
185
  for (const { origin, cwd } of rows) {
166
186
  if (fs.existsSync(cwd))
167
- today.set(origin, (today.get(origin) ?? new Set()).add(deriveOriginProject(cwd)));
187
+ today.set(origin, (today.get(origin) ?? new Set()).add(resolveProjectIdentity(cwd).name));
168
188
  }
169
- // A name someone merged into by hand stays: undoing their choice would rest on the resolver alone.
170
- const chosen = new Set(queryAuditEvents(db, { tenantId, op: 'project_merge', limit: 10000 }).map((e) => e.metadata.into));
171
- const folds = [...today].flatMap(([from, names]) => {
189
+ const folds = [];
190
+ const collisions = [];
191
+ for (const [from, names] of today) {
172
192
  const [into] = names;
173
- return names.size === 1 && into !== '' && into !== from && !chosen.has(from) ? [{ from, into }] : [];
174
- });
193
+ if (chosen.has(from))
194
+ continue;
195
+ if (names.size > 1)
196
+ collisions.push({ name: from, ids: [...names].sort() });
197
+ else if (into !== '' && into !== from)
198
+ folds.push({ from, into });
199
+ }
175
200
  // A fold into a name that itself folds would land rows by run order; the next repair takes the rest of the chain.
176
201
  const sources = new Set(folds.map((f) => f.from));
177
- return folds.filter((f) => !sources.has(f.into));
202
+ return { folds: globalFolds ? folds.filter((f) => !sources.has(f.into)) : [], collisions };
203
+ }
204
+ /** A project store's rows, dormant snapshots and compaction records written before its id existed carry its folder name. */
205
+ function ownLegacyFold(db, hippoRoot, tenantId) {
206
+ const { name, legacyName } = resolveProjectIdentity(path.dirname(path.resolve(hippoRoot)));
207
+ if (legacyName === '' || legacyName === name)
208
+ return [];
209
+ const heldIn = (table) => db.prepare(`SELECT 1 FROM ${table} WHERE tenant_id = ? AND origin_project = ? LIMIT 1`).get(tenantId, legacyName) !== undefined;
210
+ const held = heldIn('memories') || heldIn('compactions') || listDormantSnapshots(db, tenantId).some((s) => s.entry.origin_project === legacyName);
211
+ return held ? [{ from: legacyName, into: name }] : [];
212
+ }
213
+ function foldOf(value) {
214
+ if (!isObjectLike(value) || Array.isArray(value))
215
+ return [];
216
+ const { from, into } = value;
217
+ return isStringValue(from) && isStringValue(into) ? [{ from, into }] : [];
218
+ }
219
+ function foldEdges(db, tenantId) {
220
+ return [
221
+ ...queryAuditEvents(db, { tenantId, op: 'project_merge', limit: 10000 }).flatMap((e) => foldOf(e.metadata)),
222
+ ...queryAuditEvents(db, { tenantId, op: 'project_repair', limit: 10000 })
223
+ .flatMap((e) => (Array.isArray(e.metadata.folds) ? e.metadata.folds.flatMap(foldOf) : [])),
224
+ ];
225
+ }
226
+ function foldedInto(edges, names) {
227
+ const seen = new Set(names);
228
+ let grew = true;
229
+ while (grew) {
230
+ grew = false;
231
+ for (const { from, into } of edges) {
232
+ if (!seen.has(into) || seen.has(from))
233
+ continue;
234
+ seen.add(from);
235
+ grew = true;
236
+ }
237
+ }
238
+ return [...seen].filter((n) => !names.includes(n));
239
+ }
240
+ /** Names folded, directly or through others, into one of `names`; the sync moves imports still filed under them. */
241
+ export function namesFoldedInto(db, tenantId, names) {
242
+ return foldedInto(foldEdges(db, tenantId), names);
178
243
  }
179
244
  /** Reads only, so doctor and a dry run take no write lock; merged rows are planned before any fold, so a few may re-tag differently once folds apply. */
180
- export function planProjectRepair(db, hippoRoot, tenantId) {
181
- const folds = isGlobalStoreRoot(hippoRoot) ? planFolds(db, tenantId) : [];
182
- return { copies: strayImports(db, hippoRoot, tenantId).map((e) => e.id), folds, ...planUserGlobalRepair(db, tenantId, folds) };
245
+ export function planProjectRepair(db, hippoRoot, tenantId, globalFolds = true) {
246
+ const { folds, collisions } = planFolds(db, hippoRoot, tenantId, globalFolds);
247
+ return { copies: strayImports(db, hippoRoot, tenantId).map((e) => e.id), folds, collisions, ...planUserGlobalRepair(db, tenantId, folds) };
183
248
  }
184
249
  /** Parents read with `folds` already applied, so a plan matches what apply does after folding. */
185
250
  function planUserGlobalRepair(db, tenantId, folds) {
@@ -210,9 +275,9 @@ function planUserGlobalRepair(db, tenantId, folds) {
210
275
  }
211
276
  /** Sets aside stray imports, folds the names the resolver now maps elsewhere, then re-tags sleep's user-global merges by their parents; a dry run only plans. */
212
277
  export function repairProjects(db, hippoRoot, opts) {
213
- const { tenantId, dryRun } = opts;
278
+ const { tenantId, dryRun, globalFolds = true } = opts;
214
279
  if (dryRun)
215
- return { ...planProjectRepair(db, hippoRoot, tenantId), backup: null };
280
+ return { ...planProjectRepair(db, hippoRoot, tenantId, globalFolds), backup: null };
216
281
  const backup = backupStore(db, hippoRoot, 'before-repair');
217
282
  const { result, rewrite, purge } = inTransaction(db, false, () => {
218
283
  const copies = [];
@@ -221,7 +286,7 @@ export function repairProjects(db, hippoRoot, opts) {
221
286
  if (tag !== null && setAsideRow(db, tag, row, 'project-repair').kind === 'dormant')
222
287
  copies.push(row.id);
223
288
  }
224
- const folds = isGlobalStoreRoot(hippoRoot) ? planFolds(db, tenantId) : [];
289
+ const { folds, collisions } = planFolds(db, hippoRoot, tenantId, globalFolds);
225
290
  const folded = folds.map((f) => foldInTx(db, tenantId, f.from, f.into));
226
291
  const plan = planUserGlobalRepair(db, tenantId, []);
227
292
  const stamp = db.prepare(`UPDATE memories SET origin_project = ?, updated_at = datetime('now') WHERE tenant_id = ? AND id = ?`);
@@ -235,7 +300,7 @@ export function repairProjects(db, hippoRoot, opts) {
235
300
  }
236
301
  appendAuditEvent(db, { tenantId, actor: ACTOR, op: 'project_repair', metadata: { backup, copies, folds, folded, ...plan } });
237
302
  return {
238
- result: { copies, folds, ...plan, backup },
303
+ result: { copies, folds, collisions, ...plan, backup },
239
304
  rewrite: [...plan.toProject.map((r) => r.id), ...folded.flatMap((f) => f.restamped)],
240
305
  purge: [...copies, ...plan.setAside, ...folded.flatMap((f) => f.setAside)],
241
306
  };
@@ -243,6 +308,23 @@ export function repairProjects(db, hippoRoot, opts) {
243
308
  refreshMirrors(db, hippoRoot, tenantId, rewrite, purge);
244
309
  return result;
245
310
  }
311
+ const AUTO_REPAIR_META_KEY = 'project_repair_auto';
312
+ /** True when a repair would change nothing. */
313
+ function repairIsEmpty(r) {
314
+ return r.copies.length + r.folds.length + r.toProject.length + r.setAside.length === 0;
315
+ }
316
+ /**
317
+ * Sleep runs repair once per store, so an upgrade needs no command. Global-store folds stay
318
+ * with `hippo projects repair`: their evidence is compaction folders, blind to a same-named repo that never compacted.
319
+ */
320
+ export function repairOnceOnSleep(db, hippoRoot, tenantId) {
321
+ if (getMeta(db, AUTO_REPAIR_META_KEY) === '1')
322
+ return null;
323
+ const plan = planProjectRepair(db, hippoRoot, tenantId, false);
324
+ const result = repairIsEmpty(plan) ? null : repairProjects(db, hippoRoot, { tenantId, dryRun: false, globalFolds: false });
325
+ setMeta(db, AUTO_REPAIR_META_KEY, '1');
326
+ return result;
327
+ }
246
328
  /** After commit, as the agent memory sync does: a stale mirror would bring the old tag back on the next rebuild. */
247
329
  function refreshMirrors(db, hippoRoot, tenantId, rewrite, purge) {
248
330
  const ids = new Set(rewrite);
@@ -0,0 +1,12 @@
1
+ /** The config file git reads for a checkout: `.git/config`, a linked worktree's `<commondir>/config`, a submodule's `<gitdir>/config`. */
2
+ export declare function gitConfigPath(gitRoot: string): string | null;
3
+ /** `remote.origin.url` from a config file's text, the first value as git fetch uses; the subsection name is case-sensitive. */
4
+ export declare function originUrlFromConfig(text: string): string | null;
5
+ /** `host/path` for a network remote, so ssh, scp and https forms agree; null for a local path, which would put a user path into rows. */
6
+ export declare function normaliseRemote(url: string): string | null;
7
+ /** The normalised origin of the checkout rooted at gitRoot, or null when it has none a project can be named by. */
8
+ export declare function originRemoteId(gitRoot: string): string | null;
9
+ export declare const PROJECT_FILE = ".hippo-project.json";
10
+ /** The `id` a team committed in `.hippo-project.json` at root, lowercased like every project name; null when absent or unusable. */
11
+ export declare function projectFileId(root: string): string | null;
12
+ //# sourceMappingURL=project-remote.d.ts.map