hippo-memory 1.63.2 → 1.64.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 (275) hide show
  1. package/README.md +10 -4
  2. package/dist/agent-memories/claude-code.js +3 -3
  3. package/dist/ambient-store.d.ts +2 -0
  4. package/dist/ambient-store.js +4 -3
  5. package/dist/api/assemble.d.ts +2 -0
  6. package/dist/api/assemble.js +11 -7
  7. package/dist/api/auth.d.ts +15 -11
  8. package/dist/api/auth.js +116 -22
  9. package/dist/api/context-select.d.ts +5 -3
  10. package/dist/api/context-select.js +6 -11
  11. package/dist/api/context-types.d.ts +2 -1
  12. package/dist/api/context.d.ts +1 -1
  13. package/dist/api/context.js +79 -55
  14. package/dist/api/dormant.d.ts +5 -5
  15. package/dist/api/dormant.js +13 -9
  16. package/dist/api/drill-down.d.ts +2 -0
  17. package/dist/api/drill-down.js +12 -7
  18. package/dist/api/forget.d.ts +1 -1
  19. package/dist/api/forget.js +8 -4
  20. package/dist/api/learn.js +5 -3
  21. package/dist/api/outcome.d.ts +1 -1
  22. package/dist/api/outcome.js +2 -1
  23. package/dist/api/promote.js +8 -5
  24. package/dist/api/recall-types.d.ts +4 -2
  25. package/dist/api/recall.js +51 -36
  26. package/dist/api/remember.d.ts +7 -0
  27. package/dist/api/remember.js +26 -2
  28. package/dist/api/sleep-run.d.ts +28 -0
  29. package/dist/api/sleep-run.js +267 -0
  30. package/dist/api/sleep.d.ts +5 -45
  31. package/dist/api/sleep.js +5 -252
  32. package/dist/api/types.d.ts +5 -0
  33. package/dist/api/types.js +4 -0
  34. package/dist/audit-prune.js +1 -1
  35. package/dist/audit.d.ts +1 -3
  36. package/dist/audit.js +7 -103
  37. package/dist/auth.d.ts +48 -4
  38. package/dist/auth.js +111 -42
  39. package/dist/autolearn.js +2 -2
  40. package/dist/capture/caller-session.d.ts +16 -0
  41. package/dist/capture/caller-session.js +58 -0
  42. package/dist/capture/command.d.ts +19 -1
  43. package/dist/capture/command.js +28 -21
  44. package/dist/capture/compact-resume-caller.d.ts +11 -0
  45. package/dist/capture/compact-resume-caller.js +35 -0
  46. package/dist/capture/compact.d.ts +0 -14
  47. package/dist/capture/compact.js +7 -160
  48. package/dist/capture/compaction-items-caller.d.ts +16 -0
  49. package/dist/capture/compaction-items-caller.js +32 -0
  50. package/dist/capture/extract.d.ts +2 -14
  51. package/dist/capture/extract.js +135 -346
  52. package/dist/capture/failure-caller.d.ts +21 -0
  53. package/dist/capture/failure-caller.js +74 -0
  54. package/dist/capture/failure-reading.d.ts +44 -0
  55. package/dist/capture/failure-reading.js +78 -0
  56. package/dist/capture/pre-compact-caller.d.ts +16 -0
  57. package/dist/capture/pre-compact-caller.js +39 -0
  58. package/dist/capture/session-end-caller.d.ts +21 -0
  59. package/dist/capture/session-end-caller.js +30 -0
  60. package/dist/capture/session-texts.d.ts +14 -0
  61. package/dist/capture/session-texts.js +42 -0
  62. package/dist/capture/transcript.d.ts +6 -1
  63. package/dist/capture/transcript.js +20 -16
  64. package/dist/capture/working-state.d.ts +21 -0
  65. package/dist/capture/working-state.js +154 -0
  66. package/dist/capture-error.d.ts +8 -22
  67. package/dist/capture-error.js +9 -61
  68. package/dist/cli/audit.js +25 -0
  69. package/dist/cli/auth.js +4 -3
  70. package/dist/cli/context.js +13 -158
  71. package/dist/cli/continuity.js +2 -1
  72. package/dist/cli/init.js +5 -3
  73. package/dist/cli/last-sleep.d.ts +6 -0
  74. package/dist/cli/last-sleep.js +68 -0
  75. package/dist/cli/quality-repair-once.d.ts +3 -0
  76. package/dist/cli/quality-repair-once.js +36 -0
  77. package/dist/cli/recall.js +2 -1
  78. package/dist/cli/remember.d.ts +2 -0
  79. package/dist/cli/remember.js +4 -1
  80. package/dist/cli/session-hooks.d.ts +0 -3
  81. package/dist/cli/session-hooks.js +12 -49
  82. package/dist/cli/setup.js +10 -2
  83. package/dist/cli/shared.d.ts +7 -14
  84. package/dist/cli/shared.js +19 -78
  85. package/dist/cli/sleep.js +6 -3
  86. package/dist/cli/usage.js +18 -9
  87. package/dist/cli.d.ts +1 -1
  88. package/dist/cli.js +3 -2
  89. package/dist/client.js +1 -1
  90. package/dist/compaction-record.d.ts +19 -1
  91. package/dist/compaction-record.js +61 -132
  92. package/dist/compaction-spool.d.ts +45 -0
  93. package/dist/compaction-spool.js +454 -0
  94. package/dist/config.d.ts +8 -0
  95. package/dist/config.js +49 -0
  96. package/dist/connectors/github/deletion.d.ts +1 -1
  97. package/dist/connectors/github/deletion.js +5 -17
  98. package/dist/consolidate/conflicts.js +4 -0
  99. package/dist/consolidate/flush-units.d.ts +9 -0
  100. package/dist/consolidate/flush-units.js +63 -0
  101. package/dist/consolidate/llm-passes.js +5 -4
  102. package/dist/consolidate/merge.js +5 -1
  103. package/dist/consolidate/run.d.ts +2 -1
  104. package/dist/consolidate/sleep.d.ts +2 -0
  105. package/dist/consolidate/sleep.js +82 -15
  106. package/dist/context-render.d.ts +14 -0
  107. package/dist/context-render.js +28 -0
  108. package/dist/dag.js +6 -0
  109. package/dist/dashboard-actions.js +3 -2
  110. package/dist/dashboard.js +4 -3
  111. package/dist/db/backup.d.ts +6 -0
  112. package/dist/db/backup.js +29 -0
  113. package/dist/db/busy.d.ts +9 -0
  114. package/dist/db/busy.js +35 -0
  115. package/dist/db/continuity.d.ts +1 -0
  116. package/dist/db/continuity.js +31 -4
  117. package/dist/db/meta.d.ts +2 -0
  118. package/dist/db/meta.js +7 -0
  119. package/dist/db/migrate.js +2 -3
  120. package/dist/db/migrations/index.d.ts +1 -1
  121. package/dist/db/migrations/index.js +5 -1
  122. package/dist/db/migrations/v39.js +2 -7
  123. package/dist/db/migrations/v53.d.ts +3 -0
  124. package/dist/db/migrations/v53.js +16 -0
  125. package/dist/db/migrations/v54.d.ts +3 -0
  126. package/dist/db/migrations/v54.js +25 -0
  127. package/dist/db/open.d.ts +11 -0
  128. package/dist/db/open.js +27 -0
  129. package/dist/db/tables.d.ts +2 -0
  130. package/dist/db/tables.js +6 -2
  131. package/dist/db.d.ts +2 -2
  132. package/dist/db.js +2 -2
  133. package/dist/doctor.d.ts +1 -1
  134. package/dist/doctor.js +31 -15
  135. package/dist/dormant.d.ts +16 -20
  136. package/dist/dormant.js +24 -30
  137. package/dist/entry/json-hooks.d.ts +3 -0
  138. package/dist/entry/json-hooks.js +4 -0
  139. package/dist/entry/project-identity.d.ts +2 -0
  140. package/dist/entry/project-identity.js +3 -0
  141. package/dist/entry/session-text.d.ts +7 -0
  142. package/dist/entry/session-text.js +8 -0
  143. package/dist/extract.js +6 -0
  144. package/dist/failure-log.d.ts +10 -2
  145. package/dist/failure-log.js +19 -2
  146. package/dist/gated-write.js +1 -1
  147. package/dist/graph/delta.d.ts +44 -0
  148. package/dist/graph/delta.js +109 -0
  149. package/dist/graph/types.d.ts +5 -0
  150. package/dist/graph/write.d.ts +20 -20
  151. package/dist/graph/write.js +103 -36
  152. package/dist/graph-extract.d.ts +43 -9
  153. package/dist/graph-extract.js +140 -157
  154. package/dist/graph-recall.d.ts +1 -0
  155. package/dist/graph-recall.js +2 -2
  156. package/dist/graph-view.d.ts +1 -0
  157. package/dist/graph-view.js +25 -2
  158. package/dist/half-life-migration.js +5 -1
  159. package/dist/handoff-evidence.d.ts +5 -0
  160. package/dist/handoff-evidence.js +30 -0
  161. package/dist/home-path.d.ts +5 -0
  162. package/dist/home-path.js +24 -0
  163. package/dist/hooks/json-hooks.d.ts +13 -2
  164. package/dist/hooks/json-hooks.js +246 -117
  165. package/dist/hooks/opencode.js +1 -1
  166. package/dist/hooks/shared.d.ts +2 -0
  167. package/dist/hooks/shared.js +6 -1
  168. package/dist/http-util.d.ts +16 -7
  169. package/dist/http-util.js +65 -20
  170. package/dist/importers/vault.js +2 -0
  171. package/dist/index.d.ts +2 -1
  172. package/dist/index.js +1 -0
  173. package/dist/json.d.ts +2 -0
  174. package/dist/json.js +7 -0
  175. package/dist/ledger-db.d.ts +10 -0
  176. package/dist/ledger-db.js +42 -0
  177. package/dist/mcp/admin-tools.d.ts +2 -2
  178. package/dist/mcp/admin-tools.js +47 -5
  179. package/dist/mcp/memory-tools.js +7 -2
  180. package/dist/mcp/protocol.d.ts +6 -0
  181. package/dist/mcp/protocol.js +4 -2
  182. package/dist/mcp/recall-tools.js +19 -6
  183. package/dist/mcp/request.js +4 -0
  184. package/dist/mcp/session-state.d.ts +4 -0
  185. package/dist/mcp/session-state.js +15 -6
  186. package/dist/mcp/shared-gate.d.ts +4 -0
  187. package/dist/mcp/shared-gate.js +22 -0
  188. package/dist/mcp/tools.js +1 -0
  189. package/dist/memory-quality.d.ts +35 -0
  190. package/dist/memory-quality.js +191 -0
  191. package/dist/memory.d.ts +4 -4
  192. package/dist/physics-state.js +1 -1
  193. package/dist/pilot-arm.d.ts +14 -3
  194. package/dist/pilot-arm.js +32 -10
  195. package/dist/postinstall.js +2 -2
  196. package/dist/project-briefs.js +4 -4
  197. package/dist/project-identity.d.ts +14 -10
  198. package/dist/project-identity.js +29 -11
  199. package/dist/project-merge.d.ts +0 -2
  200. package/dist/project-merge.js +9 -13
  201. package/dist/prompt-hook.d.ts +65 -0
  202. package/dist/prompt-hook.js +213 -0
  203. package/dist/prompt-recall.js +1 -1
  204. package/dist/quality-repair.d.ts +27 -0
  205. package/dist/quality-repair.js +220 -0
  206. package/dist/rate-limit.d.ts +1 -0
  207. package/dist/rate-limit.js +5 -3
  208. package/dist/raw-archive.d.ts +1 -1
  209. package/dist/raw-archive.js +6 -16
  210. package/dist/recall-scope.d.ts +28 -5
  211. package/dist/recall-scope.js +52 -7
  212. package/dist/recall-trace.js +1 -1
  213. package/dist/reject-flow.d.ts +2 -0
  214. package/dist/reject-flow.js +20 -6
  215. package/dist/secret-detect.js +7 -8
  216. package/dist/server/auth.d.ts +8 -6
  217. package/dist/server/auth.js +85 -38
  218. package/dist/server/client-ip.d.ts +2 -0
  219. package/dist/server/client-ip.js +16 -1
  220. package/dist/server/mcp-http.d.ts +9 -3
  221. package/dist/server/mcp-http.js +56 -13
  222. package/dist/server/request.d.ts +1 -1
  223. package/dist/server/request.js +5 -5
  224. package/dist/server/routes/admin.js +12 -6
  225. package/dist/server/routes/memories.js +9 -1
  226. package/dist/server/routes/recall.js +14 -2
  227. package/dist/server/types.d.ts +42 -5
  228. package/dist/server/validation.d.ts +5 -2
  229. package/dist/server/validation.js +19 -2
  230. package/dist/server.d.ts +21 -4
  231. package/dist/server.js +207 -49
  232. package/dist/session-digest.d.ts +0 -1
  233. package/dist/session-digest.js +3 -12
  234. package/dist/session-owners.d.ts +7 -0
  235. package/dist/session-owners.js +54 -0
  236. package/dist/share-scrub.d.ts +3 -0
  237. package/dist/share-scrub.js +8 -0
  238. package/dist/shared.d.ts +1 -0
  239. package/dist/shared.js +26 -18
  240. package/dist/store/candidates.d.ts +14 -3
  241. package/dist/store/candidates.js +48 -26
  242. package/dist/store/conflicts.d.ts +15 -0
  243. package/dist/store/conflicts.js +68 -44
  244. package/dist/store/delete-and-batch.d.ts +25 -1
  245. package/dist/store/delete-and-batch.js +121 -69
  246. package/dist/store/entry-reads.d.ts +5 -6
  247. package/dist/store/entry-reads.js +24 -12
  248. package/dist/store/entry-row.d.ts +7 -10
  249. package/dist/store/entry-row.js +40 -18
  250. package/dist/store/entry-writes.d.ts +3 -3
  251. package/dist/store/entry-writes.js +9 -23
  252. package/dist/store/handoffs.d.ts +9 -6
  253. package/dist/store/handoffs.js +72 -59
  254. package/dist/store/index-and-stats.js +2 -2
  255. package/dist/store/open.d.ts +3 -1
  256. package/dist/store/open.js +3 -3
  257. package/dist/store/search-rows.d.ts +8 -5
  258. package/dist/store/search-rows.js +15 -18
  259. package/dist/store/sessions.d.ts +25 -6
  260. package/dist/store/sessions.js +61 -19
  261. package/dist/store/summaries.d.ts +1 -1
  262. package/dist/store/summaries.js +3 -18
  263. package/dist/store/tenant-lookup.d.ts +7 -3
  264. package/dist/store/tenant-lookup.js +9 -9
  265. package/dist/store-port.d.ts +13 -0
  266. package/dist/store-port.js +20 -0
  267. package/dist/version.d.ts +5 -1
  268. package/dist/version.js +5 -1
  269. package/dist/working-memory.js +1 -1
  270. package/dist/write-budget.d.ts +15 -0
  271. package/dist/write-budget.js +15 -0
  272. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  273. package/extensions/openclaw-plugin/package.json +1 -1
  274. package/openclaw.plugin.json +1 -1
  275. package/package.json +8 -4
@@ -3,11 +3,11 @@
3
3
  *
4
4
  * Populates the graph from the already-structured consolidated first-class object tables -
5
5
  * NO NLP, no precision gate for entities + supersedes. The graph is a pure derived
6
- * function of the current object state, so `extractGraph` is an idempotent REBUILD: clear
7
- * the tenant's graph, then re-derive entities + `supersedes` relations from decisions /
6
+ * function of the current object state, so `extractGraph` is an idempotent REBUILD: derive
7
+ * entities + `supersedes` relations from decisions /
8
8
  * policies / customer_notes / project_briefs (the four object types whose kind maps to the
9
- * `entity_type` enum). All writes go through the src/graph.ts consolidated-source guard
10
- * (insertEntity / insertRelation / clearGraph); this module issues no raw SQL.
9
+ * `entity_type` enum), then write only their difference from the stored rows. All writes go
10
+ * through the src/graph/write.ts consolidated-source guard; this module issues no raw SQL.
11
11
  *
12
12
  * Pass 3 adds the
13
13
  * first CROSS-OBJECT relations: a deterministic NAME-MATCH heuristic that emits a
@@ -19,8 +19,11 @@
19
19
  * skill/incident/process entities (not in the entity_type enum - needs a migration); the
20
20
  * `hippo sleep` enqueue-hook.
21
21
  */
22
- import { clearGraph, insertEntity, insertRelation, runGraphRebuildTransaction } from './graph/write.js';
22
+ import { applyGraphOps, runGraphRebuildTransaction } from './graph/write.js';
23
23
  import { MAX_ENTITY_NAME_LEN } from './graph/types.js';
24
+ import { graphDelta, readGraphDelta } from './graph/delta.js';
25
+ import { WRITE_BUDGET } from './write-budget.js';
26
+ import { SLEEP_DB_WAIT_MS } from './db.js';
24
27
  import { loadDecisions } from './decisions.js';
25
28
  import { loadPolicies } from './policies.js';
26
29
  import { loadCustomerNotes } from './customer-notes.js';
@@ -64,7 +67,7 @@ function sourceObjectOf(entityType, e2Id) {
64
67
  throw new Error(`graph-extract: entityType '${entityType}' has no source_object_type mapping`);
65
68
  return { type, id: e2Id };
66
69
  }
67
- /** Unordered entity-id pair key, so a relation between a,b is found in either direction. */
70
+ /** Unordered entity-key pair, so a relation between a,b is found in either direction. */
68
71
  function pairKey(a, b) {
69
72
  return a < b ? `${a}:${b}` : `${b}:${a}`;
70
73
  }
@@ -88,7 +91,7 @@ textOf) {
88
91
  hitCap = true;
89
92
  for (const r of loaded) {
90
93
  // SAFETY: loadFn is always one of loadDecisions/loadPolicies/loadCustomerNotes/
91
- // loadProjectBriefs (wired at each extractGraph call site); every one of their row
94
+ // loadProjectBriefs (wired in GRAPH_SOURCES); every one of their row
92
95
  // types declares id: number, memoryId: string | null, supersededBy: number | null.
93
96
  // `any` here is the deliberate type-erasure boundary (see eslint-disable above)
94
97
  // that lets one loop handle all four object row shapes.
@@ -102,144 +105,97 @@ textOf) {
102
105
  });
103
106
  }
104
107
  }
105
- return { rows, hitCap };
108
+ return { entityType, rows, hitCap };
106
109
  }
107
- /**
108
- * Idempotent rebuild of the tenant's deterministic graph from its consolidated first-class
109
- * objects. Returns the entity/relation counts (+ which types were truncated at the
110
- * per-type cap). Safe to re-run: output is a pure function of the current object state.
111
- */
112
- export function extractGraph(hippoRoot, tenantId) {
113
- assertTenantId('extractGraph', tenantId);
114
- const sources = [
115
- { entityType: 'decision', loadFn: loadDecisions, nameOf: (r) => r.decisionText, textOf: (r) => [r.decisionText, r.context].filter(Boolean).join(' ') },
116
- { entityType: 'policy', loadFn: loadPolicies, nameOf: (r) => r.policyName, textOf: (r) => [r.policyName, r.policyText].filter(Boolean).join(' ') },
117
- { entityType: 'customer', loadFn: loadCustomerNotes, nameOf: (r) => r.customer, textOf: (r) => [r.customer, r.note].filter(Boolean).join(' ') },
118
- { entityType: 'project', loadFn: loadProjectBriefs, nameOf: (r) => r.repo, textOf: (r) => [r.repo, r.summary].filter(Boolean).join(' ') },
119
- ];
120
- // READ PHASE: load every source's rows on their own connections BEFORE the
121
- // write transaction. Holding the rebuild's BEGIN IMMEDIATE write lock while
122
- // opening a second connection for these reads dead-locks ('database is
123
- // locked'); preloading keeps the transaction single-connection.
124
- const loaded = sources.map((src) => {
125
- const { rows, hitCap } = loadType(hippoRoot, tenantId, src.entityType, src.loadFn, src.nameOf, src.textOf);
126
- return { entityType: src.entityType, rows, hitCap };
127
- });
128
- // WRITE PHASE: clear + every insert run in ONE transaction, so two
129
- // concurrent rebuilds serialize on the SQLite write lock (no duplicate rows)
130
- // and a throw mid-rebuild rolls back the clear (no bricked graph). No second
131
- // connection is opened inside.
132
- return runGraphRebuildTransaction(hippoRoot, tenantId, (txDb) => rebuildGraphRows(txDb, hippoRoot, tenantId, loaded));
110
+ const GRAPH_SOURCES = [
111
+ { entityType: 'decision', loadFn: loadDecisions, nameOf: (r) => r.decisionText, textOf: (r) => [r.decisionText, r.context].filter(Boolean).join(' ') },
112
+ { entityType: 'policy', loadFn: loadPolicies, nameOf: (r) => r.policyName, textOf: (r) => [r.policyName, r.policyText].filter(Boolean).join(' ') },
113
+ { entityType: 'customer', loadFn: loadCustomerNotes, nameOf: (r) => r.customer, textOf: (r) => [r.customer, r.note].filter(Boolean).join(' ') },
114
+ { entityType: 'project', loadFn: loadProjectBriefs, nameOf: (r) => r.repo, textOf: (r) => [r.repo, r.summary].filter(Boolean).join(' ') },
115
+ ];
116
+ /** Every source type's rows, each read on its own connection; callers load before any write lock is taken. */
117
+ export function loadGraphSources(hippoRoot, tenantId) {
118
+ assertTenantId('loadGraphSources', tenantId);
119
+ return GRAPH_SOURCES.map((src) => loadType(hippoRoot, tenantId, src.entityType, src.loadFn, src.nameOf, src.textOf));
120
+ }
121
+ /** The tenant's graph as a pure function of its loaded objects; the write side diffs it against what is stored. */
122
+ export function deriveGraph(loaded) {
123
+ const pass = deriveEntities(loaded);
124
+ const supersedes = deriveSupersedes(pass);
125
+ const references = deriveReferences(pass.created, supersedes.supersededPairs, pass.truncated);
126
+ return {
127
+ entities: pass.created.map((e) => ({ entityType: e.entityType, sourceObject: e.sourceObject, name: e.name, memoryId: e.memoryId })),
128
+ relations: [...supersedes.relations, ...references],
129
+ byType: pass.byType,
130
+ truncated: pass.truncated,
131
+ references: references.length,
132
+ };
133
133
  }
134
- // Pass 1: entities. Every ACTIVE/SUPERSEDED object row becomes an entity ANCHORED to its
135
- // authoritative object (source_object_type/id) - it survives a forgotten mirror
136
- // (memory_id NULL). The mirror memory is passed through only when it still exists
137
- // (it remains a recall pointer until forgotten/pruned).
138
- function insertEntityRows(txDb, hippoRoot, tenantId, loaded) {
139
- const pass = { byType: {}, truncated: [], allRows: [], entityIdByKey: new Map(), memoryIdByKey: new Map(), created: [] };
134
+ const naturalKeyOf = (e) => ({ entityType: e.entityType, sourceObject: e.sourceObject });
135
+ // Pass 1: every active or superseded object row becomes an entity anchored to its object, so it survives a
136
+ // forgotten mirror; the mirror memory rides along only while it exists.
137
+ function deriveEntities(loaded) {
138
+ const pass = { byType: {}, truncated: [], allRows: [], byKey: new Map(), created: [] };
140
139
  for (const { entityType, rows, hitCap } of loaded) {
141
140
  if (hitCap)
142
141
  pass.truncated.push(entityType);
143
142
  pass.byType[entityType] = 0;
144
143
  for (const row of rows) {
145
144
  pass.allRows.push(row);
146
- // Name fields are uncapped at source and insertEntity rejects an over-cap or empty name,
147
- // which after clearGraph would brick the rebuild. Trim before capping so leading
148
- // whitespace cannot slice to ''; skip a still-empty label rather than throw.
149
- const name = (row.name ?? '').trim().slice(0, MAX_ENTITY_NAME_LEN);
150
- if (name.length === 0)
145
+ // Names are uncapped at source; trimming on both sides of the cap gives the exact name insertEntity stores.
146
+ const name = (row.name ?? '').trim().slice(0, MAX_ENTITY_NAME_LEN).trim();
147
+ const key = keyOf(row.entityType, row.e2Id);
148
+ // A row seen twice was superseded between the two status loads; its first sighting stands.
149
+ if (name.length === 0 || pass.byKey.has(key))
151
150
  continue;
152
- const sourceObject = sourceObjectOf(row.entityType, row.e2Id);
153
- const entity = insertEntity(hippoRoot, tenantId, {
151
+ const entity = {
154
152
  entityType: row.entityType,
153
+ sourceObject: sourceObjectOf(row.entityType, row.e2Id),
155
154
  name,
156
155
  memoryId: row.memoryId,
157
- sourceObject,
158
- }, txDb);
159
- const k = keyOf(row.entityType, row.e2Id);
160
- pass.entityIdByKey.set(k, entity.id);
161
- pass.memoryIdByKey.set(k, row.memoryId);
162
- pass.created.push({ entityId: entity.id, entityType: row.entityType, memoryId: row.memoryId, sourceObject, name, searchText: row.searchText ?? '', superseded: row.supersededBy !== null });
156
+ key,
157
+ searchText: row.searchText ?? '',
158
+ superseded: row.supersededBy !== null,
159
+ };
160
+ pass.byKey.set(key, entity);
161
+ pass.created.push(entity);
163
162
  pass.byType[entityType] += 1;
164
163
  }
165
164
  }
166
165
  return pass;
167
166
  }
168
- // "Y supersedes X" - but only when BOTH X and Y were EXTRACTED (e.g. Y may be closed
169
- // and absent). The emit guard is ENTITY presence (entityIdByKey), not memory presence:
170
- // a forgotten successor mirror must still emit the edge. The relation is anchored to Y's
171
- // authoritative object; Y's mirror memory is passed only when it still lives.
172
- function insertSupersedesRelations(txDb, hippoRoot, tenantId, pass) {
173
- let relations = 0;
174
- // Entity-id pairs already related by supersedes (unordered). Pass 3 skips a references
175
- // edge for such a pair: a version-extends-its-predecessor's-name containment (e.g.
176
- // "Adopt X (managed)" contains "Adopt X") is a name artifact, not a cross-reference,
177
- // and supersedes already captures their relationship.
167
+ // Pass 2: "Y supersedes X" when both were derived (Y may be closed and absent). The edge is anchored to the
168
+ // successor's object and carries its mirror memory only while that lives.
169
+ function deriveSupersedes(pass) {
170
+ const relations = [];
171
+ // Pass 3 skips these pairs: "Adopt X (managed)" containing "Adopt X" is a version, not a cross-reference.
178
172
  const supersededPairs = new Set();
179
173
  for (const row of pass.allRows) {
180
174
  if (row.supersededBy === null)
181
175
  continue;
182
- const xKey = keyOf(row.entityType, row.e2Id);
183
- const yKey = keyOf(row.entityType, row.supersededBy);
184
- const fromId = pass.entityIdByKey.get(yKey); // successor Y
185
- const toId = pass.entityIdByKey.get(xKey); // superseded X
186
- if (fromId === undefined || toId === undefined)
176
+ const successor = pass.byKey.get(keyOf(row.entityType, row.supersededBy));
177
+ const superseded = pass.byKey.get(keyOf(row.entityType, row.e2Id));
178
+ if (!successor || !superseded)
187
179
  continue;
188
- const yMemoryId = pass.memoryIdByKey.get(yKey) ?? null; // successor's mirror, null if gone
189
- insertRelation(hippoRoot, tenantId, {
190
- fromEntityId: fromId,
191
- toEntityId: toId,
180
+ relations.push({
181
+ from: naturalKeyOf(successor),
182
+ to: naturalKeyOf(superseded),
192
183
  relType: 'supersedes',
193
- memoryId: yMemoryId,
194
- sourceObject: sourceObjectOf(row.entityType, row.supersededBy), // successor Y's object
195
- }, txDb);
196
- supersededPairs.add(pairKey(fromId, toId));
197
- relations += 1;
184
+ memoryId: successor.memoryId,
185
+ sourceObject: successor.sourceObject,
186
+ });
187
+ supersededPairs.add(pairKey(successor.key, superseded.key));
198
188
  }
199
189
  return { relations, supersededPairs };
200
190
  }
201
- /**
202
- * The deterministic rebuild WRITES, run inside `runGraphRebuildTransaction`'s
203
- * transaction (`txDb` is its connection). Clears the tenant's graph then
204
- * re-derives entities + `supersedes` + `references` from the preloaded rows. All
205
- * DB access here is on `txDb` (or in-memory) — no other connection is opened.
206
- */
207
- function rebuildGraphRows(txDb, hippoRoot, tenantId, loaded) {
208
- // Rebuild from scratch: the graph is derived, so clear then re-derive.
209
- clearGraph(hippoRoot, tenantId, txDb);
210
- const pass = insertEntityRows(txDb, hippoRoot, tenantId, loaded);
211
- const { byType, truncated, created } = pass;
212
- const supersedes = insertSupersedesRelations(txDb, hippoRoot, tenantId, pass);
213
- let relations = supersedes.relations;
214
- // Pass 3: cross-object `references` edges via conservative name matching. A source's
215
- // text containing a target entity's name -> "source references target". Sources +
216
- // targets are CREATED entities only, each anchored to its source object (so the
217
- // edge survives a forgotten source mirror).
218
- const references = extractReferences(hippoRoot, tenantId, created, supersedes.supersededPairs, truncated, txDb);
219
- relations += references;
220
- const entities = created.length;
221
- return { entities, relations, references, byType, truncated };
222
- }
223
- /**
224
- * Pass 3. Build a target-name index from the created entities (names within the length
225
- * bounds, ambiguous names dropped), scan each created entity's text once with one
226
- * combined word-boundary regex, and emit `references` edges (self-skipped, deduped,
227
- * per-source capped). Each edge is anchored to the source entity's object (memoryId
228
- * passed through only when the mirror lives). Returns the number of references edges written.
229
- */
230
- function extractReferences(hippoRoot, tenantId, created, supersededPairs, truncated, txDb) {
231
- // Build the target index: normalised name -> single entity id. A name is a target only
232
- // if its length is in bounds; a name shared by >1 entity is AMBIGUOUS and dropped.
233
- const nameToId = new Map();
191
+ /** Normalised name to its single target; a name two entities share is ambiguous and dropped. */
192
+ function referenceTargets(created) {
193
+ const byName = new Map();
234
194
  const ambiguous = new Set();
235
195
  for (const e of created) {
236
- // References are among ACTIVE entities only: a superseded (outdated) row is not a
237
- // current cross-reference target.
238
196
  if (e.superseded)
239
197
  continue;
240
- // Decisions are SOURCE-only: their name is decision prose, referenced by supersedes,
241
- // not by name-mention. Excluding them as targets also prevents a decision's own name
242
- // (== its searchText) from whole-string self-matching and shadowing embedded targets.
198
+ // Decisions are sources only: their name is their prose, which would self-match and shadow embedded targets.
243
199
  if (e.entityType === 'decision')
244
200
  continue;
245
201
  const norm = e.name.trim().toLowerCase();
@@ -247,59 +203,86 @@ function extractReferences(hippoRoot, tenantId, created, supersededPairs, trunca
247
203
  continue;
248
204
  if (ambiguous.has(norm))
249
205
  continue;
250
- if (nameToId.has(norm)) {
251
- // Second distinct entity with this name (and not its own id repeated) -> ambiguous.
252
- if (nameToId.get(norm) !== e.entityId) {
253
- nameToId.delete(norm);
254
- ambiguous.add(norm);
255
- }
256
- continue;
206
+ const seen = byName.get(norm);
207
+ if (seen === undefined)
208
+ byName.set(norm, e);
209
+ else if (seen.key !== e.key) {
210
+ byName.delete(norm);
211
+ ambiguous.add(norm);
257
212
  }
258
- nameToId.set(norm, e.entityId);
259
213
  }
260
- if (nameToId.size === 0)
261
- return 0;
262
- // Record the truncation (observability, mirroring MAX_EXTRACT_PER_TYPE) so a >cap
263
- // store's under-matched references are not silent.
264
- if (nameToId.size > MAX_TARGET_NAMES)
214
+ return byName;
215
+ }
216
+ // Pass 3: a `references` edge when one object's text names another entity, matched on word boundaries,
217
+ // self-skipped, deduped and capped per source; the edge is anchored to the source object.
218
+ function deriveReferences(created, supersededPairs, truncated) {
219
+ const byName = referenceTargets(created);
220
+ if (byName.size === 0)
221
+ return [];
222
+ if (byName.size > MAX_TARGET_NAMES)
265
223
  truncated.push('references-targets');
266
- // LONGEST name first, then alphabetical: JS regex alternation is leftmost-first, so
267
- // ordering longer names before their prefixes makes the match longest-at-position
268
- // (`postgres pro` wins over `postgres`). Deterministic, so truncation is stable.
269
- const targetNames = [...nameToId.keys()]
224
+ // Longest first, then alphabetical: regex alternation is leftmost-first, so `postgres pro` beats `postgres`.
225
+ const targetNames = [...byName.keys()]
270
226
  .sort((a, b) => b.length - a.length || (a < b ? -1 : a > b ? 1 : 0))
271
227
  .slice(0, MAX_TARGET_NAMES);
272
- const pattern = `\\b(?:${targetNames.map(escapeRegex).join('|')})\\b`;
273
- const re = new RegExp(pattern, 'gi');
274
- let references = 0;
228
+ const re = new RegExp(`\\b(?:${targetNames.map(escapeRegex).join('|')})\\b`, 'gi');
229
+ const relations = [];
275
230
  for (const src of created) {
276
- if (src.superseded)
277
- continue; // superseded sources hold only stale references
278
- if (!src.searchText)
231
+ if (src.superseded || !src.searchText)
279
232
  continue;
280
- const targets = new Set();
233
+ const targets = new Map();
281
234
  for (const m of src.searchText.matchAll(re)) {
282
- const targetId = nameToId.get(m[0].toLowerCase());
283
- if (targetId === undefined || targetId === src.entityId)
284
- continue; // miss / self
285
- if (supersededPairs.has(pairKey(src.entityId, targetId)))
286
- continue; // already version-related
287
- targets.add(targetId);
235
+ const target = byName.get(m[0].toLowerCase());
236
+ if (target === undefined || target.key === src.key)
237
+ continue;
238
+ if (supersededPairs.has(pairKey(src.key, target.key)))
239
+ continue;
240
+ targets.set(target.key, target);
288
241
  if (targets.size >= MAX_REFERENCES_PER_OBJECT)
289
- break; // per-source cap
242
+ break;
290
243
  }
291
- for (const targetId of targets) {
292
- insertRelation(hippoRoot, tenantId, {
293
- fromEntityId: src.entityId,
294
- toEntityId: targetId,
295
- relType: 'references',
296
- // Anchored to the source object; its mirror memory is passed only when it lives.
297
- memoryId: src.memoryId,
298
- sourceObject: src.sourceObject,
299
- }, txDb);
300
- references += 1;
244
+ for (const target of targets.values()) {
245
+ relations.push({ from: naturalKeyOf(src), to: naturalKeyOf(target), relType: 'references', memoryId: src.memoryId, sourceObject: src.sourceObject });
301
246
  }
302
247
  }
303
- return references;
248
+ return relations;
249
+ }
250
+ function resultOf(derived, skipped) {
251
+ const { entities, relations, references, byType, truncated } = derived;
252
+ const result = { entities: entities.length, relations: relations.length, references, byType, truncated };
253
+ if (skipped > 0)
254
+ result.skipped = skipped;
255
+ return result;
256
+ }
257
+ /** Idempotent rebuild of the tenant's graph from its consolidated objects, in one transaction; safe to re-run.
258
+ * Returns the derived counts and which types hit the per-type cap. */
259
+ export function extractGraph(hippoRoot, tenantId) {
260
+ assertTenantId('extractGraph', tenantId);
261
+ // Loaded first: opening a second connection under the rebuild's BEGIN IMMEDIATE dead-locks ('database is locked').
262
+ const derived = deriveGraph(loadGraphSources(hippoRoot, tenantId));
263
+ // Diffed and applied under one lock, so two concurrent rebuilds serialize instead of both inserting a missing row.
264
+ const skipped = runGraphRebuildTransaction(hippoRoot, tenantId, (txDb) => applyGraphOps(txDb, hippoRoot, tenantId, graphDelta(txDb, tenantId, derived), 0, { holdMs: Infinity }).skipped);
265
+ return resultOf(derived, skipped);
266
+ }
267
+ /** extractGraph for sleep: diffs outside the lock, then applies in transactions of about `budget.holdMs` with a pause
268
+ * between them. A run stopped between chunks leaves a mixed graph that the next run's diff finishes. */
269
+ export async function extractGraphChunked(hippoRoot, tenantId, budget = WRITE_BUDGET) {
270
+ assertTenantId('extractGraphChunked', tenantId);
271
+ const derived = deriveGraph(loadGraphSources(hippoRoot, tenantId));
272
+ const ops = readGraphDelta(hippoRoot, tenantId, derived);
273
+ let next = 0;
274
+ let skipped = 0;
275
+ let committedAt = 0;
276
+ while (next < ops.length) {
277
+ if (next > 0)
278
+ await budget.pause(committedAt);
279
+ const from = next;
280
+ // Sleep's wait, not a server request's 250 ms: each chunk is a fresh BEGIN IMMEDIATE a hook's write could otherwise fail.
281
+ const chunk = runGraphRebuildTransaction(hippoRoot, tenantId, (txDb) => applyGraphOps(txDb, hippoRoot, tenantId, ops, from, { holdMs: budget.holdMs, clock: budget.clock }), { busyWaitMs: SLEEP_DB_WAIT_MS });
282
+ committedAt = budget.clock();
283
+ next = chunk.next;
284
+ skipped += chunk.skipped;
285
+ }
286
+ return resultOf(derived, skipped);
304
287
  }
305
288
  //# sourceMappingURL=graph-extract.js.map
@@ -38,6 +38,7 @@ export interface GraphExpandOpts {
38
38
  recallScope?: {
39
39
  requested?: string;
40
40
  additive?: boolean;
41
+ ownScope?: string;
41
42
  };
42
43
  }
43
44
  /**
@@ -137,8 +137,8 @@ function passesRecallFilters(mem, via, successorValidFrom, opts) {
137
137
  return false; // default recall drops superseded
138
138
  }
139
139
  return recallScope.additive
140
- ? passesCliRecallScopeFilter(mem.scope ?? null, recallScope.requested)
141
- : passesScopeFilterForRecall(mem.scope ?? null, recallScope.requested);
140
+ ? passesCliRecallScopeFilter(mem.scope ?? null, recallScope.requested) || passesScopeFilterForRecall(mem.scope ?? null, undefined, recallScope.ownScope)
141
+ : passesScopeFilterForRecall(mem.scope ?? null, recallScope.requested, recallScope.ownScope);
142
142
  }
143
143
  /** Traverse one store's graph from its seeds into `hitsByOrigin`. Pure reads; mutates `seenMemoryIds`
144
144
  * and `seenContent` so a memory, or a share/promote copy of it, surfaces at most once across stores. */
@@ -36,6 +36,7 @@ export declare const DEFAULT_VIEW_LIMIT = 500;
36
36
  export declare function buildGraphModel(hippoRoot: string, tenantId: string, opts?: {
37
37
  entity?: string;
38
38
  limit?: number;
39
+ canRead?: (scope: string | null) => boolean;
39
40
  }): GraphModel;
40
41
  /**
41
42
  * Deterministic Fruchterman-Reingold-style force layout. Seeded circular init +
@@ -10,6 +10,25 @@
10
10
  import { loadEntities, loadEntitiesByName, loadEntitiesByIds, loadRelations, loadNeighborRelations, loadRelationsAmong, withGraphReadSnapshot, } from './graph/read.js';
11
11
  /** Default bound for the CLI viewer / show so an unbounded set is never laid out. */
12
12
  export const DEFAULT_VIEW_LIMIT = 500;
13
+ // graph/read.ts's IN-list chunk, so one statement stays far under SQLite's bound-parameter limit.
14
+ const MEMORY_ID_CHUNK = 400;
15
+ /** The items whose source memory `canRead` admits: a NULL memoryId has no scope to hide, and a missing memory fails closed. */
16
+ function readableOnly(db, tenantId, items, canRead) {
17
+ if (!canRead)
18
+ return items;
19
+ const ids = [...new Set(items.flatMap((i) => (i.memoryId === null ? [] : [i.memoryId])))];
20
+ const readable = new Set();
21
+ for (let i = 0; i < ids.length; i += MEMORY_ID_CHUNK) {
22
+ const slice = ids.slice(i, i + MEMORY_ID_CHUNK);
23
+ // SAFETY: rows' shape matches the id and scope columns named in the SELECT.
24
+ const rows = db.prepare(`SELECT id, scope FROM memories WHERE tenant_id = ? AND id IN (${slice.map(() => '?').join(',')})`)
25
+ .all(tenantId, ...slice);
26
+ for (const r of rows)
27
+ if (canRead(r.scope))
28
+ readable.add(r.id);
29
+ }
30
+ return items.filter((i) => i.memoryId === null || readable.has(i.memoryId));
31
+ }
13
32
  /**
14
33
  * Build the view-model from the graph (reads only). With `opts.entity`, returns a
15
34
  * focus subgraph: every entity whose name === entity, plus their 1-hop neighbours,
@@ -32,7 +51,9 @@ export function buildGraphModel(hippoRoot, tenantId, opts = {}) {
32
51
  // cap the focus matches. (3) Discover 1-hop neighbours and cap the UNION to
33
52
  // `limit` nodes. (4) Load ALL edges AMONG the union so neighbour-to-neighbour
34
53
  // edges that don't touch the focus are included too.
35
- const focus = loadEntitiesByName(hippoRoot, tenantId, opts.entity, { limit }, db);
54
+ const named = loadEntitiesByName(hippoRoot, tenantId, opts.entity, { limit }, db);
55
+ // A hidden focus must not answer through its neighbours that the name exists.
56
+ const focus = readableOnly(db, tenantId, named, opts.canRead);
36
57
  if (focus.length === 0)
37
58
  return { nodes: [], edges: [], truncated: false };
38
59
  const focusIds = focus.map((e) => e.id);
@@ -54,7 +75,7 @@ export function buildGraphModel(hippoRoot, tenantId, opts = {}) {
54
75
  // edge in favour of out-of-union rows.
55
76
  relations = loadRelationsAmong(hippoRoot, tenantId, unionIds, { limit }, db);
56
77
  truncated =
57
- focus.length >= limit ||
78
+ named.length >= limit ||
58
79
  hop.length >= limit || // neighbour scan capped -> a 1-hop neighbour may be omitted
59
80
  neighboursCapped || // node cap filled before all neighbours were consumed
60
81
  union.size > unionIds.length ||
@@ -65,6 +86,8 @@ export function buildGraphModel(hippoRoot, tenantId, opts = {}) {
65
86
  relations = loadRelations(hippoRoot, tenantId, { limit }, db);
66
87
  truncated = nodeEntities.length >= limit || relations.length >= limit;
67
88
  }
89
+ nodeEntities = readableOnly(db, tenantId, nodeEntities, opts.canRead);
90
+ relations = readableOnly(db, tenantId, relations, opts.canRead);
68
91
  const nodeIds = new Set(nodeEntities.map((e) => e.id));
69
92
  const nodes = nodeEntities.map((e) => ({
70
93
  id: e.id,
@@ -169,7 +169,11 @@ function writePlan(db, plan, old, move) {
169
169
  const byTenant = new Map();
170
170
  for (const e of plan) {
171
171
  update.run(e.half_life_days, e.id);
172
- byTenant.set(e.tenantId, { ...byTenant.get(e.tenantId), [e.id]: old.get(e.id) });
172
+ // One record per tenant, filled in place: copying it per row made the write grow with the square of the store.
173
+ let record = byTenant.get(e.tenantId);
174
+ if (!record)
175
+ byTenant.set(e.tenantId, (record = {}));
176
+ record[e.id] = old.get(e.id);
173
177
  }
174
178
  for (const [tenantId, oldHalfLives] of byTenant) {
175
179
  appendAuditEvent(db, { tenantId, actor: move.actor, op: 'half_life_migrate', metadata: { from: move.from, to: move.to, ids: Object.keys(oldHalfLives), oldHalfLives } });
@@ -0,0 +1,5 @@
1
+ import type { HandoffEvidence } from './handoff.js';
2
+ export declare function collectHandoffEvidence(cwd: string, testStatus: HandoffEvidence['testStatus'], options?: {
3
+ timeoutMs?: number;
4
+ }): HandoffEvidence;
5
+ //# sourceMappingURL=handoff-evidence.d.ts.map
@@ -0,0 +1,30 @@
1
+ // A handoff's git evidence, read with no store, so a hook with no store can send it with the session's end.
2
+ import { execFileSync } from 'child_process';
3
+ // Best-effort git state; a missing git, non-repo cwd, or a call past timeoutMs (each call, default 2 s)
4
+ // yields null fields rather than throw, so a hook with a hard deadline can cap a slow repo.
5
+ export function collectHandoffEvidence(cwd, testStatus, options = {}) {
6
+ const timeoutMs = options.timeoutMs ?? 2000;
7
+ let gitRef = null;
8
+ try {
9
+ gitRef = execFileSync('git', ['rev-parse', 'HEAD'], {
10
+ cwd, encoding: 'utf8', timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
11
+ }).trim() || null;
12
+ }
13
+ catch {
14
+ // No git, not a repo, or timed out: evidence is optional, so the field stays null.
15
+ gitRef = null;
16
+ }
17
+ let dirtyTree = null;
18
+ try {
19
+ const status = execFileSync('git', ['status', '--porcelain'], {
20
+ cwd, encoding: 'utf8', timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
21
+ });
22
+ dirtyTree = status.trim().length > 0;
23
+ }
24
+ catch {
25
+ // Same as gitRef: unknown tree state is reported as null, never as an error.
26
+ dirtyTree = null;
27
+ }
28
+ return { gitRef, dirtyTree, testStatus };
29
+ }
30
+ //# sourceMappingURL=handoff-evidence.js.map
@@ -0,0 +1,5 @@
1
+ /** One pattern per path shape that carries a user name: Windows, Git Bash and WSL mounts, extended-length, UNC home shares, Linux, macOS and 8.3 short names. */
2
+ export declare const USER_SEGMENT: readonly RegExp[];
3
+ /** `text` with each home-directory segment replaced by `[home]`. */
4
+ export declare function maskHomePaths(text: string): string;
5
+ //# sourceMappingURL=home-path.d.ts.map
@@ -0,0 +1,24 @@
1
+ // Home-directory paths name their user, so text bound for a shared store tests or masks them here; no imports keep it light.
2
+ // VS Code writes file links URL-encoded, so a separator may be %5C or %2F.
3
+ const WIN_SEP = String.raw `(?:[\\/]|%5C|%2F)`;
4
+ const WIN_CHAR = String.raw `(?:(?!%5C|%2F)[^\\/:*?"<>|\s])`;
5
+ // A Windows name never holds a quote, so JSON stops it; one with spaces runs to a separator, quote, closing mark or line end, leaving the mark, as stopping at its first word leaks the surname.
6
+ const WIN_USER = String.raw `(?:${WIN_CHAR}+(?: ${WIN_CHAR}+){0,3}(?=${WIN_SEP}|["'\`)\]>,;]|\.(?=\s|$)|(?<!\.)(?:[^\S ]|$))(?<![\`)\],;'])|${WIN_CHAR}+)`;
7
+ /** One pattern per path shape that carries a user name: Windows, Git Bash and WSL mounts, extended-length, UNC home shares, Linux, macOS and 8.3 short names. */
8
+ export const USER_SEGMENT = [
9
+ new RegExp(String.raw `[A-Za-z](?::|%3A)${WIN_SEP}+(?:Users|Documents and Settings)${WIN_SEP}+${WIN_USER}`, 'i'),
10
+ new RegExp(String.raw `(?<![\w.])(?:\/mnt)?\/[A-Za-z]\/Users\/${WIN_USER}`, 'i'),
11
+ /\\\\\?\\/,
12
+ new RegExp(String.raw `\\\\[^\\/\s]+\\+(?:homes?|users|profiles?)\$?\\+${WIN_USER}`, 'i'),
13
+ /(?<![\w.~-])(?:\/var)?\/home\/[^/\s]+/,
14
+ /(?<![\w.~-])(?:\/var)?\/root(?![\w.-])/,
15
+ /(?<![\w.~-])\/Users\/[^/\s]+/,
16
+ /[\\/][A-Z0-9_$]{1,6}~\d{1,6}(?:\.[A-Z0-9]{1,3})?(?![\w~])|\b[A-Z0-9_$]{1,6}~\d{1,6}(?:\.[A-Z0-9]{1,3})?[\\/]/,
17
+ ];
18
+ // Global copies, so the shared test patterns keep their lastIndex at 0.
19
+ const USER_SEGMENT_ALL = USER_SEGMENT.map((re) => new RegExp(re.source, `${re.flags}g`));
20
+ /** `text` with each home-directory segment replaced by `[home]`. */
21
+ export function maskHomePaths(text) {
22
+ return USER_SEGMENT_ALL.reduce((out, re) => out.replace(re, '[home]'), text);
23
+ }
24
+ //# sourceMappingURL=home-path.js.map
@@ -10,8 +10,9 @@
10
10
  * sequence, writing both outputs to the log file. The parent returns in
11
11
  * <100ms so the TUI teardown can't kill the child before it finishes.
12
12
  * - SessionStart: `hippo last-sleep --path <path>` - prints the log
13
- * written by the previous session's detached worker to stderr, which
14
- * keeps it out of the model's context, and then clears it.
13
+ * written by the previous session's detached worker on stderr and
14
+ * clears it. Stdout carries only a `systemMessage` problems line,
15
+ * because Claude Code shows that to the user, not the model.
15
16
  * Earlier Claude Code forms are detected and migrated automatically:
16
17
  * - < 0.20.2: `Stop` hook firing `hippo sleep` on every assistant turn.
17
18
  * - < 0.21.0: bare `hippo sleep` in SessionEnd, no `--log-file`.
@@ -33,11 +34,14 @@
33
34
  * `hooks` block from opencode.json.
34
35
  */
35
36
  import { type JsonHookTarget } from './shared.js';
37
+ import { type JsonValue } from '../json.js';
38
+ /** A target's hook settings file, the log its SessionEnd hook writes, and the tool's display name. */
36
39
  export interface JsonHookPaths {
37
40
  settings: string;
38
41
  logFile: string;
39
42
  display: string;
40
43
  }
44
+ /** What installJsonHooks wrote, migrated or left alone in one target's settings file. */
41
45
  export interface InstallResult {
42
46
  target: JsonHookTarget;
43
47
  settingsPath: string;
@@ -57,7 +61,14 @@ export interface InstallResult {
57
61
  /** The file exists but is not JSON hippo can merge into, so it was left untouched. */
58
62
  invalidJson: boolean;
59
63
  }
64
+ /** The paths for `target` under the current home directory; reads and writes nothing. */
60
65
  export declare function resolveJsonHookPaths(target: JsonHookTarget): JsonHookPaths;
66
+ /** Adds hippo's hooks to `target`'s settings file and migrates older hippo entries; a file that is not JSON stays untouched. */
61
67
  export declare function installJsonHooks(target: JsonHookTarget): InstallResult;
68
+ /** Swaps the file in with one rename, so a crash mid-write cannot leave a truncated settings.json that Claude Code cannot parse. */
69
+ export declare function writeSettingsFile(file: string, settings: JsonValue): void;
70
+ /** Removes hippo's own hook handlers from `target`'s settings file and keeps every other handler; true when the file changed. */
62
71
  export declare function uninstallJsonHooks(target: JsonHookTarget): boolean;
72
+ /** Whether uninstall can edit settings.json; `invalidJson` marks a file that exists but is not JSON hippo can edit, which `uninstallJsonHooks` reports only as false. */
73
+ export declare function checkUninstallable(target: JsonHookTarget): Pick<InstallResult, 'settingsPath' | 'invalidJson'>;
63
74
  //# sourceMappingURL=json-hooks.d.ts.map