hippo-memory 1.61.0 → 1.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) hide show
  1. package/README.md +37 -53
  2. package/dist/agent-memories/apply.d.ts +3 -1
  3. package/dist/agent-memories/apply.js +23 -1
  4. package/dist/agent-memories/claude-code.d.ts +4 -1
  5. package/dist/agent-memories/claude-code.js +53 -9
  6. package/dist/agent-memories/report.d.ts +1 -0
  7. package/dist/agent-memories/report.js +2 -2
  8. package/dist/agent-memories/sync.d.ts +3 -3
  9. package/dist/agent-memories/sync.js +46 -24
  10. package/dist/agent-memories/types.d.ts +0 -2
  11. package/dist/ambient-store.d.ts +4 -4
  12. package/dist/ambient-store.js +4 -3
  13. package/dist/api/assemble.d.ts +7 -10
  14. package/dist/api/assemble.js +62 -66
  15. package/dist/api/audit.d.ts +2 -2
  16. package/dist/api/audit.js +2 -2
  17. package/dist/api/auth.d.ts +6 -6
  18. package/dist/api/auth.js +5 -7
  19. package/dist/api/context-select.d.ts +50 -0
  20. package/dist/api/context-select.js +342 -0
  21. package/dist/api/context-types.d.ts +12 -17
  22. package/dist/api/context.d.ts +5 -4
  23. package/dist/api/context.js +208 -535
  24. package/dist/api/dormant.js +1 -1
  25. package/dist/api/drill-down.d.ts +4 -4
  26. package/dist/api/drill-down.js +56 -41
  27. package/dist/api/outcome.d.ts +8 -13
  28. package/dist/api/outcome.js +13 -17
  29. package/dist/api/promote.d.ts +5 -8
  30. package/dist/api/promote.js +55 -66
  31. package/dist/api/quarantine.js +3 -2
  32. package/dist/api/recall-types.d.ts +43 -55
  33. package/dist/api/recall.d.ts +3 -3
  34. package/dist/api/recall.js +310 -456
  35. package/dist/api/remember.d.ts +2 -2
  36. package/dist/api/sleep.d.ts +7 -35
  37. package/dist/api/sleep.js +206 -220
  38. package/dist/api/tokens.d.ts +2 -2
  39. package/dist/api/tokens.js +2 -2
  40. package/dist/api/types.d.ts +6 -10
  41. package/dist/api/types.js +2 -4
  42. package/dist/audit-prune.d.ts +4 -6
  43. package/dist/audit-prune.js +3 -5
  44. package/dist/audit.js +11 -33
  45. package/dist/auth.d.ts +8 -9
  46. package/dist/auth.js +5 -7
  47. package/dist/autolearn.d.ts +1 -1
  48. package/dist/autolearn.js +1 -1
  49. package/dist/availability.js +3 -5
  50. package/dist/capture/command.d.ts +5 -4
  51. package/dist/capture/command.js +8 -22
  52. package/dist/capture/compact.d.ts +2 -2
  53. package/dist/capture/compact.js +11 -15
  54. package/dist/capture/extract.js +37 -115
  55. package/dist/capture-error.d.ts +1 -1
  56. package/dist/capture-error.js +1 -1
  57. package/dist/churn-git.d.ts +1 -1
  58. package/dist/churn-git.js +1 -1
  59. package/dist/cli/audit.js +3 -4
  60. package/dist/cli/auth.js +3 -6
  61. package/dist/cli/briefs.js +324 -306
  62. package/dist/cli/context.js +44 -34
  63. package/dist/cli/continuity.js +283 -271
  64. package/dist/cli/curate.d.ts +1 -1
  65. package/dist/cli/curate.js +43 -58
  66. package/dist/cli/dag.js +5 -9
  67. package/dist/cli/decisions.js +334 -345
  68. package/dist/cli/explain.js +68 -61
  69. package/dist/cli/goals.js +1 -1
  70. package/dist/cli/init.js +1 -1
  71. package/dist/cli/maintenance.js +62 -51
  72. package/dist/cli/playbooks.js +391 -379
  73. package/dist/cli/projects.js +11 -6
  74. package/dist/cli/recall.js +30 -44
  75. package/dist/cli/remember.js +118 -87
  76. package/dist/cli/session-hooks.js +106 -115
  77. package/dist/cli/setup.d.ts +1 -1
  78. package/dist/cli/setup.js +267 -250
  79. package/dist/cli/shared.js +3 -3
  80. package/dist/cli/slack.js +1 -1
  81. package/dist/cli/sleep.js +27 -1
  82. package/dist/cli/status.d.ts +4 -4
  83. package/dist/cli/status.js +80 -76
  84. package/dist/cli/transfer.js +88 -107
  85. package/dist/cli/usage.js +9 -6
  86. package/dist/cli.d.ts +1 -1
  87. package/dist/cli.js +4 -9
  88. package/dist/compaction-record.d.ts +2 -2
  89. package/dist/compaction-record.js +89 -68
  90. package/dist/compare.d.ts +11 -16
  91. package/dist/compare.js +11 -16
  92. package/dist/config.d.ts +19 -18
  93. package/dist/config.js +90 -67
  94. package/dist/connectors/github/backfill.d.ts +2 -2
  95. package/dist/connectors/github/backfill.js +8 -15
  96. package/dist/connectors/github/cli-impl.js +3 -8
  97. package/dist/connectors/github/deletion.d.ts +5 -12
  98. package/dist/connectors/github/deletion.js +5 -12
  99. package/dist/connectors/github/dlq.d.ts +6 -9
  100. package/dist/connectors/github/dlq.js +2 -3
  101. package/dist/connectors/github/ingest.d.ts +5 -7
  102. package/dist/connectors/github/ingest.js +8 -12
  103. package/dist/connectors/github/octokit-client.d.ts +3 -5
  104. package/dist/connectors/github/octokit-client.js +5 -6
  105. package/dist/connectors/github/signature.d.ts +9 -39
  106. package/dist/connectors/github/signature.js +9 -39
  107. package/dist/connectors/github/tenant-routing.d.ts +1 -1
  108. package/dist/connectors/github/tenant-routing.js +1 -1
  109. package/dist/connectors/github/transform.js +2 -2
  110. package/dist/connectors/github/types.d.ts +2 -10
  111. package/dist/connectors/github/types.js +1 -3
  112. package/dist/connectors/slack/deletion.d.ts +3 -8
  113. package/dist/connectors/slack/deletion.js +3 -8
  114. package/dist/connectors/slack/dlq.d.ts +1 -1
  115. package/dist/connectors/slack/ingest.d.ts +1 -1
  116. package/dist/connectors/slack/ingest.js +7 -16
  117. package/dist/connectors/slack/signature.d.ts +1 -1
  118. package/dist/connectors/slack/tenant-routing.d.ts +3 -5
  119. package/dist/connectors/slack/tenant-routing.js +3 -5
  120. package/dist/connectors/slack/transform.d.ts +5 -6
  121. package/dist/connectors/slack/transform.js +5 -6
  122. package/dist/connectors/slack/types.d.ts +2 -6
  123. package/dist/connectors/slack/types.js +1 -3
  124. package/dist/connectors/slack/web-client.js +10 -3
  125. package/dist/connectors/slack/workspaces.d.ts +3 -5
  126. package/dist/connectors/slack/workspaces.js +3 -5
  127. package/dist/consolidate/conflicts.js +3 -14
  128. package/dist/consolidate/decay.js +9 -29
  129. package/dist/consolidate/llm-passes.js +4 -5
  130. package/dist/consolidate/merge.js +8 -23
  131. package/dist/consolidate/run.d.ts +1 -8
  132. package/dist/consolidate/run.js +3 -25
  133. package/dist/consolidate/sleep.js +5 -17
  134. package/dist/consolidate/traces.js +9 -21
  135. package/dist/customer-notes.d.ts +5 -7
  136. package/dist/customer-notes.js +82 -76
  137. package/dist/dag.d.ts +10 -21
  138. package/dist/dag.js +189 -203
  139. package/dist/db/continuity.js +2 -2
  140. package/dist/db/migrations/v14.js +1 -1
  141. package/dist/db/migrations/v15.js +1 -2
  142. package/dist/db/migrations/v16.js +3 -4
  143. package/dist/db/migrations/v17.js +2 -3
  144. package/dist/db/migrations/v19.js +1 -1
  145. package/dist/db/migrations/v20.js +1 -1
  146. package/dist/db/migrations/v21.js +2 -6
  147. package/dist/db/migrations/v22.js +2 -4
  148. package/dist/db/migrations/v23.js +1 -1
  149. package/dist/db/migrations/v24.js +4 -6
  150. package/dist/db/migrations/v25.js +2 -3
  151. package/dist/db/migrations/v26.js +3 -3
  152. package/dist/db/migrations/v27.js +2 -10
  153. package/dist/db/migrations/v28.js +5 -8
  154. package/dist/db/migrations/v29.js +3 -4
  155. package/dist/db/migrations/v30.js +2 -2
  156. package/dist/db/migrations/v31.js +1 -1
  157. package/dist/db/migrations/v32.js +1 -1
  158. package/dist/db/migrations/v33.js +3 -3
  159. package/dist/db/migrations/v34.js +1 -1
  160. package/dist/db/migrations/v35.js +3 -4
  161. package/dist/db/migrations/v36.js +3 -4
  162. package/dist/db/migrations/v37.js +5 -5
  163. package/dist/db/migrations/v38.js +7 -8
  164. package/dist/db/migrations/v39.js +1 -1
  165. package/dist/db/migrations/v40.js +4 -16
  166. package/dist/db/migrations/v41.js +3 -4
  167. package/dist/db/migrations/v42.js +3 -4
  168. package/dist/db/migrations/v45.js +1 -1
  169. package/dist/db/migrations/v46.js +1 -1
  170. package/dist/db/migrations/v47.js +1 -1
  171. package/dist/db/migrations/v48.js +1 -1
  172. package/dist/decisions.d.ts +2 -2
  173. package/dist/decisions.js +97 -80
  174. package/dist/dedupe.js +86 -61
  175. package/dist/delivery-recorder.js +154 -135
  176. package/dist/doctor.js +129 -110
  177. package/dist/dormant.js +1 -4
  178. package/dist/embedding-provider.d.ts +4 -8
  179. package/dist/embedding-provider.js +4 -8
  180. package/dist/embeddings.js +55 -47
  181. package/dist/env.d.ts +1 -1
  182. package/dist/env.js +12 -12
  183. package/dist/escape.d.ts +5 -0
  184. package/dist/escape.js +10 -0
  185. package/dist/eval-stats.d.ts +1 -2
  186. package/dist/eval-stats.js +1 -2
  187. package/dist/eval-suite.js +27 -21
  188. package/dist/extract.js +4 -9
  189. package/dist/failure-log.d.ts +3 -3
  190. package/dist/failure-log.js +1 -1
  191. package/dist/forward-claim-detector.d.ts +2 -4
  192. package/dist/forward-claim-detector.js +6 -11
  193. package/dist/goals.d.ts +3 -3
  194. package/dist/goals.js +103 -91
  195. package/dist/graph/read.d.ts +2 -2
  196. package/dist/graph/read.js +5 -6
  197. package/dist/graph/types.d.ts +8 -8
  198. package/dist/graph/write.d.ts +7 -14
  199. package/dist/graph/write.js +16 -23
  200. package/dist/graph-extract.d.ts +7 -8
  201. package/dist/graph-extract.js +62 -72
  202. package/dist/graph-recall.d.ts +2 -2
  203. package/dist/graph-recall.js +55 -49
  204. package/dist/graph-stream.d.ts +5 -6
  205. package/dist/graph-stream.js +66 -57
  206. package/dist/graph-view.d.ts +2 -2
  207. package/dist/graph-view.js +7 -7
  208. package/dist/half-life-migration.d.ts +1 -2
  209. package/dist/half-life-migration.js +2 -3
  210. package/dist/hooks/codex-session.js +1 -1
  211. package/dist/hooks/codex-wrapper.d.ts +1 -1
  212. package/dist/hooks/codex-wrapper.js +3 -2
  213. package/dist/hooks/json-hooks.d.ts +2 -2
  214. package/dist/hooks/json-hooks.js +5 -4
  215. package/dist/hooks/opencode.d.ts +1 -1
  216. package/dist/hooks/opencode.js +5 -4
  217. package/dist/hooks/shared.d.ts +3 -7
  218. package/dist/hooks/shared.js +1 -8
  219. package/dist/http-util.d.ts +2 -3
  220. package/dist/http-util.js +3 -0
  221. package/dist/importers/core.d.ts +2 -9
  222. package/dist/importers/core.js +15 -30
  223. package/dist/importers/sources.js +2 -1
  224. package/dist/importers/vault.js +2 -20
  225. package/dist/incidents.d.ts +1 -1
  226. package/dist/incidents.js +46 -39
  227. package/dist/instruction-detect.d.ts +1 -1
  228. package/dist/instruction-detect.js +1 -1
  229. package/dist/invalidation.d.ts +3 -0
  230. package/dist/invalidation.js +160 -114
  231. package/dist/json.d.ts +5 -0
  232. package/dist/json.js +4 -0
  233. package/dist/judgment.js +1 -2
  234. package/dist/local-embedding.js +1 -1
  235. package/dist/mcp/admin-tools.js +7 -17
  236. package/dist/mcp/format.js +1 -1
  237. package/dist/mcp/framing.js +3 -6
  238. package/dist/mcp/protocol.d.ts +2 -5
  239. package/dist/mcp/protocol.js +1 -3
  240. package/dist/mcp/recall-tools.js +12 -15
  241. package/dist/mcp/request.js +4 -3
  242. package/dist/mcp/session-state.js +2 -3
  243. package/dist/mcp/stdio.js +2 -1
  244. package/dist/mcp/tools.js +9 -6
  245. package/dist/memory-value-weights.d.ts +5 -8
  246. package/dist/memory-value-weights.js +5 -8
  247. package/dist/memory-value.d.ts +13 -13
  248. package/dist/memory-value.js +26 -37
  249. package/dist/memory.d.ts +20 -22
  250. package/dist/memory.js +24 -48
  251. package/dist/multihop.d.ts +1 -1
  252. package/dist/multihop.js +3 -2
  253. package/dist/owner-validation.d.ts +4 -5
  254. package/dist/owner-validation.js +4 -5
  255. package/dist/physics.d.ts +4 -4
  256. package/dist/physics.js +7 -9
  257. package/dist/policies.d.ts +9 -10
  258. package/dist/policies.js +96 -81
  259. package/dist/postinstall.js +3 -6
  260. package/dist/predictions/planning-fallacy.d.ts +9 -14
  261. package/dist/predictions/planning-fallacy.js +10 -16
  262. package/dist/predictions/store.d.ts +15 -23
  263. package/dist/predictions/store.js +36 -33
  264. package/dist/processes.d.ts +2 -7
  265. package/dist/processes.js +88 -72
  266. package/dist/project-briefs.d.ts +2 -3
  267. package/dist/project-briefs.js +141 -118
  268. package/dist/project-identity.d.ts +22 -9
  269. package/dist/project-identity.js +47 -12
  270. package/dist/project-merge.d.ts +28 -5
  271. package/dist/project-merge.js +213 -46
  272. package/dist/project-remote.d.ts +12 -0
  273. package/dist/project-remote.js +138 -0
  274. package/dist/prompt-recall.js +1 -2
  275. package/dist/rate-limit.d.ts +1 -1
  276. package/dist/rate-limit.js +1 -1
  277. package/dist/raw-archive.d.ts +9 -0
  278. package/dist/raw-archive.js +70 -53
  279. package/dist/recall-history.d.ts +19 -20
  280. package/dist/recall-history.js +24 -42
  281. package/dist/recall-pipeline.js +4 -28
  282. package/dist/recall-scope.d.ts +7 -8
  283. package/dist/recall-scope.js +7 -8
  284. package/dist/recall-trace.d.ts +5 -9
  285. package/dist/recall-trace.js +6 -10
  286. package/dist/refine-llm.d.ts +1 -1
  287. package/dist/refine-llm.js +2 -2
  288. package/dist/reject-flow.d.ts +3 -4
  289. package/dist/reject-flow.js +122 -117
  290. package/dist/rejection.d.ts +5 -6
  291. package/dist/rejection.js +7 -15
  292. package/dist/rerankers/clef.d.ts +1 -1
  293. package/dist/rerankers/jev.d.ts +1 -2
  294. package/dist/rerankers/jev.js +4 -5
  295. package/dist/rerankers/llm.d.ts +1 -2
  296. package/dist/rerankers/llm.js +1 -2
  297. package/dist/rerankers/types.d.ts +1 -2
  298. package/dist/rrf.d.ts +2 -2
  299. package/dist/rrf.js +2 -2
  300. package/dist/search/bm25-search.d.ts +1 -1
  301. package/dist/search/bm25-search.js +2 -1
  302. package/dist/search/boosts.js +2 -1
  303. package/dist/search/hybrid.d.ts +1 -1
  304. package/dist/search/hybrid.js +2 -1
  305. package/dist/search/physics-search.d.ts +1 -1
  306. package/dist/search/physics-search.js +2 -1
  307. package/dist/search/types.d.ts +2 -0
  308. package/dist/search/types.js +3 -1
  309. package/dist/secret-detect.d.ts +4 -5
  310. package/dist/secret-detect.js +6 -10
  311. package/dist/server/auth.js +5 -5
  312. package/dist/server/client-ip.js +1 -1
  313. package/dist/server/cursor.js +2 -1
  314. package/dist/server/mcp-http.js +4 -4
  315. package/dist/server/request.d.ts +3 -6
  316. package/dist/server/request.js +6 -7
  317. package/dist/server/routes/admin.js +5 -4
  318. package/dist/server/routes/customer-notes.js +6 -5
  319. package/dist/server/routes/decisions.js +4 -3
  320. package/dist/server/routes/incidents.js +7 -5
  321. package/dist/server/routes/memories.js +7 -7
  322. package/dist/server/routes/policies.js +3 -2
  323. package/dist/server/routes/predictions.js +12 -15
  324. package/dist/server/routes/processes.js +3 -2
  325. package/dist/server/routes/project-briefs.js +8 -7
  326. package/dist/server/routes/recall.js +95 -93
  327. package/dist/server/routes/skills.js +6 -5
  328. package/dist/server/types.d.ts +1 -1
  329. package/dist/server/validation.d.ts +1 -2
  330. package/dist/server/validation.js +7 -14
  331. package/dist/server-detect.js +72 -58
  332. package/dist/server.d.ts +2 -2
  333. package/dist/server.js +131 -117
  334. package/dist/shared.d.ts +26 -17
  335. package/dist/shared.js +102 -104
  336. package/dist/skills.d.ts +3 -3
  337. package/dist/skills.js +88 -72
  338. package/dist/store/audit-event.d.ts +2 -2
  339. package/dist/store/audit-event.js +1 -1
  340. package/dist/store/candidates.d.ts +2 -2
  341. package/dist/store/candidates.js +4 -3
  342. package/dist/store/conflicts.js +30 -22
  343. package/dist/store/delete-and-batch.d.ts +11 -14
  344. package/dist/store/delete-and-batch.js +40 -91
  345. package/dist/store/entry-reads.d.ts +17 -25
  346. package/dist/store/entry-reads.js +59 -40
  347. package/dist/store/entry-row.d.ts +6 -24
  348. package/dist/store/entry-row.js +6 -24
  349. package/dist/store/entry-writes.d.ts +6 -7
  350. package/dist/store/entry-writes.js +13 -11
  351. package/dist/store/handoffs.d.ts +1 -1
  352. package/dist/store/handoffs.js +7 -10
  353. package/dist/store/index-and-stats.d.ts +2 -6
  354. package/dist/store/index-and-stats.js +4 -10
  355. package/dist/store/mirrors.d.ts +6 -19
  356. package/dist/store/mirrors.js +14 -39
  357. package/dist/store/open.js +9 -31
  358. package/dist/store/rows.d.ts +5 -11
  359. package/dist/store/rows.js +6 -11
  360. package/dist/store/search-rows.d.ts +17 -34
  361. package/dist/store/search-rows.js +34 -56
  362. package/dist/store/sessions.d.ts +4 -5
  363. package/dist/store/sessions.js +5 -6
  364. package/dist/store/summaries.d.ts +13 -17
  365. package/dist/store/summaries.js +26 -70
  366. package/dist/support-bundle.js +4 -8
  367. package/dist/tenant.d.ts +1 -5
  368. package/dist/token-ledger.d.ts +1 -1
  369. package/dist/token-ledger.js +3 -5
  370. package/dist/trace.js +1 -3
  371. package/dist/version.d.ts +1 -1
  372. package/dist/version.js +1 -1
  373. package/dist/working-memory.d.ts +1 -1
  374. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  375. package/extensions/openclaw-plugin/package.json +1 -1
  376. package/openclaw.plugin.json +1 -1
  377. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
- import { writeEntry } from './store/entry-writes.js';
2
- import { loadAllEntries, readEntry } from './store/entry-reads.js';
1
+ import { writeEntryOn } from './store/entry-writes.js';
2
+ import { loadAllEntries, selectEntriesByIds } from './store/entry-reads.js';
3
+ import { openStore } from './store/open.js';
3
4
  import { openHippoDb, closeHippoDb } from './db.js';
4
5
  import { CHURN_STALE_TAG } from './memory.js';
5
6
  import { GitReadError, gitLsFilesAtHead, fetchChurnWindowLog, gitGrepPresence, resolveCommitBefore, packageScriptsAt, } from './churn-git.js';
@@ -73,6 +74,7 @@ export function invalidateMatching(hippoRoot, target, tenantId, options) {
73
74
  dryRun,
74
75
  preview: [],
75
76
  };
77
+ const weakened = [];
76
78
  for (const entry of entries) {
77
79
  if (options?.onlyId !== undefined) {
78
80
  if (entry.id !== options.onlyId)
@@ -106,10 +108,24 @@ export function invalidateMatching(hippoRoot, target, tenantId, options) {
106
108
  if (!entry.tags.includes('invalidated')) {
107
109
  entry.tags.push('invalidated');
108
110
  }
109
- writeEntry(hippoRoot, entry);
111
+ weakened.push(entry);
110
112
  }
113
+ writeEntriesOnOneHandle(hippoRoot, weakened);
111
114
  return result;
112
115
  }
116
+ /** Each row commits alone, as a run of writeEntry calls would, but the store opens once. */
117
+ function writeEntriesOnOneHandle(hippoRoot, entries) {
118
+ if (entries.length === 0)
119
+ return;
120
+ const db = openStore(hippoRoot);
121
+ try {
122
+ for (const entry of entries)
123
+ writeEntryOn(db, hippoRoot, entry);
124
+ }
125
+ finally {
126
+ closeHippoDb(db);
127
+ }
128
+ }
113
129
  const STOPWORDS = new Set([
114
130
  'the', 'an', 'is', 'it', 'in', 'on', 'at', 'to', 'for', 'of', 'by',
115
131
  'and', 'or', 'but', 'not', 'with', 'from', 'that', 'this', 'was', 'are',
@@ -225,16 +241,11 @@ function emptyChurnResult(dryRun, skippedPinned = [], error) {
225
241
  result.error = error;
226
242
  return result;
227
243
  }
228
- /** Flags memories whose named file/symbol/script changed or disappeared since storage (or last confirmation); only adds/removes CHURN_STALE_TAG, never confidence/half-life/strength. */
229
- export function detectChurnStale(hippoRoot, repoRoot, opts) {
230
- const dryRun = opts.dryRun === true;
231
- if (!opts.projectName)
232
- return emptyChurnResult(dryRun);
233
- const entries = loadAllEntries(hippoRoot, opts.tenantId);
244
+ function collectChurnCandidates(entries, opts) {
234
245
  const skippedPinned = [];
235
246
  const candidates = [];
236
247
  for (const entry of entries) {
237
- if (!entry.origin_project || entry.origin_project !== opts.projectName)
248
+ if (!entry.origin_project || (entry.origin_project !== opts.projectName && entry.origin_project !== opts.legacyName))
238
249
  continue;
239
250
  if (entry.superseded_by)
240
251
  continue;
@@ -249,6 +260,135 @@ export function detectChurnStale(hippoRoot, repoRoot, opts) {
249
260
  continue;
250
261
  candidates.push({ entry, refs: extractChurnRefs(entry.content) });
251
262
  }
263
+ return { candidates, skippedPinned };
264
+ }
265
+ function loadChurnGitView(repoRoot, projectName, candidates, anchorOf, needs) {
266
+ const headFiles = gitLsFilesAtHead(repoRoot);
267
+ let windowLog = [];
268
+ if (needs.paths) {
269
+ const minAnchor = candidates.reduce((min, c) => {
270
+ const a = anchorOf(c.entry);
271
+ return min === '' || a < min ? a : min;
272
+ }, '');
273
+ windowLog = fetchChurnWindowLog(repoRoot, minAnchor);
274
+ }
275
+ const touchedInWindow = new Set();
276
+ for (const c of windowLog)
277
+ for (const f of c.files)
278
+ touchedInWindow.add(f.path);
279
+ const trackedIndex = buildTrackedIndex(new Set([...headFiles, ...touchedInWindow]));
280
+ const headIndex = buildTrackedIndex(headFiles);
281
+ const presentAtHeadSymbols = needs.symbols
282
+ ? gitGrepPresence(repoRoot, [...new Set(candidates.flatMap((c) => c.refs.symbols))], 'HEAD')
283
+ : new Set();
284
+ const anchorCommitCache = new Map();
285
+ const resolveAnchorCommit = (anchorIso) => {
286
+ if (!anchorCommitCache.has(anchorIso)) {
287
+ anchorCommitCache.set(anchorIso, resolveCommitBefore(repoRoot, anchorIso));
288
+ }
289
+ return anchorCommitCache.get(anchorIso) ?? null;
290
+ };
291
+ const symbolsAtCommit = new Map();
292
+ const symbolsPresentAt = (hash, symbols) => {
293
+ const known = symbolsAtCommit.get(hash) ?? new Set();
294
+ const missing = symbols.filter((s) => !known.has(s));
295
+ if (missing.length > 0) {
296
+ for (const s of gitGrepPresence(repoRoot, missing, hash))
297
+ known.add(s);
298
+ symbolsAtCommit.set(hash, known);
299
+ }
300
+ return known;
301
+ };
302
+ const headScripts = needs.scripts ? packageScriptsAt(repoRoot, 'HEAD') : null;
303
+ const scriptsAtCommit = new Map();
304
+ const scriptsAt = (hash) => {
305
+ if (!scriptsAtCommit.has(hash)) {
306
+ scriptsAtCommit.set(hash, packageScriptsAt(repoRoot, hash));
307
+ }
308
+ return scriptsAtCommit.get(hash);
309
+ };
310
+ return { repoRoot, projectName, windowLog, trackedIndex, headIndex, presentAtHeadSymbols, headScripts, resolveAnchorCommit, symbolsPresentAt, scriptsAt };
311
+ }
312
+ function pathEvidence(git, paths, anchorTime) {
313
+ for (const rawPath of paths) {
314
+ const resolved = resolveTrackedPath(rawPath, git.repoRoot, git.projectName, git.trackedIndex);
315
+ if (!resolved)
316
+ continue;
317
+ if (isTrackedPath(git.headIndex, resolved)) {
318
+ const changed = git.windowLog.some((c) => new Date(c.date).getTime() > anchorTime && c.files.some((f) => f.path === resolved));
319
+ if (changed)
320
+ return `file-changed: ${resolved}`;
321
+ }
322
+ else {
323
+ // --no-renames means a rename shows as a D + A pair, so this also fires for renames.
324
+ const deleted = git.windowLog.some((c) => new Date(c.date).getTime() > anchorTime &&
325
+ c.files.some((f) => f.status === 'D' && f.path === resolved));
326
+ if (deleted)
327
+ return `file-deleted: ${resolved}`;
328
+ }
329
+ }
330
+ return null;
331
+ }
332
+ function symbolEvidence(git, symbols, anchor) {
333
+ const absent = symbols.filter((s) => !git.presentAtHeadSymbols.has(s));
334
+ if (absent.length === 0)
335
+ return null;
336
+ const anchorCommit = git.resolveAnchorCommit(anchor);
337
+ if (!anchorCommit)
338
+ return null;
339
+ const presentAtAnchor = git.symbolsPresentAt(anchorCommit, absent);
340
+ const hit = absent.find((s) => presentAtAnchor.has(s));
341
+ return hit ? `symbol-gone: \`${hit}\`` : null;
342
+ }
343
+ function scriptEvidence(git, scripts, anchor, headScripts) {
344
+ const anchorCommit = git.resolveAnchorCommit(anchor);
345
+ if (!anchorCommit)
346
+ return null;
347
+ const anchorScripts = git.scriptsAt(anchorCommit);
348
+ const hit = anchorScripts
349
+ ? scripts.find((s) => anchorScripts[s] !== undefined && headScripts[s] === undefined)
350
+ : undefined;
351
+ return hit ? `script-gone: ${hit}` : null;
352
+ }
353
+ /** The first churn evidence against one candidate: a changed or deleted file, then a gone symbol, then a gone script. */
354
+ function churnEvidence(git, refs, anchor) {
355
+ let evidence = pathEvidence(git, refs.paths, new Date(anchor).getTime());
356
+ if (!evidence && refs.symbols.length > 0)
357
+ evidence = symbolEvidence(git, refs.symbols, anchor);
358
+ if (!evidence && refs.scripts.length > 0 && git.headScripts !== null) {
359
+ evidence = scriptEvidence(git, refs.scripts, anchor, git.headScripts);
360
+ }
361
+ return evidence;
362
+ }
363
+ function tagChurnStale(hippoRoot, tenantId, toTag, confirmedAt) {
364
+ // The git calls above can take seconds; a good outcome landing meanwhile moves the anchor past the evidence.
365
+ const confirmedNow = queryConfirmedAt(hippoRoot, tenantId);
366
+ const unconfirmed = toTag.filter((stale) => confirmedNow.get(stale.id) === confirmedAt.get(stale.id));
367
+ if (unconfirmed.length === 0)
368
+ return;
369
+ const db = openStore(hippoRoot);
370
+ try {
371
+ const live = selectEntriesByIds(db, unconfirmed.map((stale) => stale.id), tenantId);
372
+ for (const stale of unconfirmed) {
373
+ const entry = live.get(stale.id);
374
+ if (!entry || entry.tags.includes(CHURN_STALE_TAG))
375
+ continue;
376
+ const tagged = { ...entry, tags: [...entry.tags, CHURN_STALE_TAG] };
377
+ writeEntryOn(db, hippoRoot, tagged);
378
+ live.set(stale.id, tagged); // a repeated id is already tagged, as a fresh read would show
379
+ }
380
+ }
381
+ finally {
382
+ closeHippoDb(db);
383
+ }
384
+ }
385
+ /** Flags memories whose named file/symbol/script changed or disappeared since storage (or last confirmation); only adds/removes CHURN_STALE_TAG, never confidence/half-life/strength. */
386
+ export function detectChurnStale(hippoRoot, repoRoot, opts) {
387
+ const dryRun = opts.dryRun === true;
388
+ if (!opts.projectName)
389
+ return emptyChurnResult(dryRun);
390
+ const entries = loadAllEntries(hippoRoot, opts.tenantId);
391
+ const { candidates, skippedPinned } = collectChurnCandidates(entries, opts);
252
392
  if (candidates.length === 0)
253
393
  return emptyChurnResult(dryRun, skippedPinned);
254
394
  const confirmedAt = queryConfirmedAt(hippoRoot, opts.tenantId);
@@ -258,108 +398,24 @@ export function detectChurnStale(hippoRoot, repoRoot, opts) {
258
398
  const confirmedMs = confirmed ? Date.parse(confirmed) : NaN;
259
399
  return confirmedMs > Date.parse(entry.created) ? new Date(confirmedMs).toISOString() : new Date(entry.created).toISOString();
260
400
  };
261
- const needsPaths = candidates.some((c) => c.refs.paths.length > 0);
262
- const needsSymbols = candidates.some((c) => c.refs.symbols.length > 0);
263
- const needsScripts = candidates.some((c) => c.refs.scripts.length > 0);
264
- if (!needsPaths && !needsSymbols && !needsScripts) {
401
+ const needs = {
402
+ paths: candidates.some((c) => c.refs.paths.length > 0),
403
+ symbols: candidates.some((c) => c.refs.symbols.length > 0),
404
+ scripts: candidates.some((c) => c.refs.scripts.length > 0),
405
+ };
406
+ if (!needs.paths && !needs.symbols && !needs.scripts) {
265
407
  return { checked: candidates.length, marked: 0, alreadyMarked: 0, skippedPinned, dryRun, preview: [] };
266
408
  }
267
409
  const result = { checked: 0, marked: 0, alreadyMarked: 0, skippedPinned, dryRun, preview: [] };
268
410
  try {
269
- const headFiles = gitLsFilesAtHead(repoRoot);
270
- let windowLog = [];
271
- if (needsPaths) {
272
- const minAnchor = candidates.reduce((min, c) => {
273
- const a = anchorOf(c.entry);
274
- return min === '' || a < min ? a : min;
275
- }, '');
276
- windowLog = fetchChurnWindowLog(repoRoot, minAnchor);
277
- }
278
- const touchedInWindow = new Set();
279
- for (const c of windowLog)
280
- for (const f of c.files)
281
- touchedInWindow.add(f.path);
282
- const trackedIndex = buildTrackedIndex(new Set([...headFiles, ...touchedInWindow]));
283
- const headIndex = buildTrackedIndex(headFiles);
284
- const presentAtHeadSymbols = needsSymbols
285
- ? gitGrepPresence(repoRoot, [...new Set(candidates.flatMap((c) => c.refs.symbols))], 'HEAD')
286
- : new Set();
287
- const anchorCommitCache = new Map();
288
- const resolveAnchorCommit = (anchorIso) => {
289
- if (!anchorCommitCache.has(anchorIso)) {
290
- anchorCommitCache.set(anchorIso, resolveCommitBefore(repoRoot, anchorIso));
291
- }
292
- return anchorCommitCache.get(anchorIso) ?? null;
293
- };
294
- const symbolsAtCommit = new Map();
295
- const symbolsPresentAt = (hash, symbols) => {
296
- const known = symbolsAtCommit.get(hash) ?? new Set();
297
- const missing = symbols.filter((s) => !known.has(s));
298
- if (missing.length > 0) {
299
- for (const s of gitGrepPresence(repoRoot, missing, hash))
300
- known.add(s);
301
- symbolsAtCommit.set(hash, known);
302
- }
303
- return known;
304
- };
305
- const headScripts = needsScripts ? packageScriptsAt(repoRoot, 'HEAD') : null;
306
- const scriptsAtCommit = new Map();
411
+ const git = loadChurnGitView(repoRoot, opts.legacyName ?? opts.projectName, candidates, anchorOf, needs);
307
412
  // Collected here, written only after every candidate's evidence is
308
413
  // computed: a GitReadError thrown mid-loop must never leave an earlier
309
414
  // candidate tagged while a later one aborts the run untagged.
310
415
  const toTag = [];
311
416
  for (const { entry, refs } of candidates) {
312
417
  result.checked++;
313
- const anchor = anchorOf(entry);
314
- const anchorTime = new Date(anchor).getTime();
315
- let evidence = null;
316
- for (const rawPath of refs.paths) {
317
- const resolved = resolveTrackedPath(rawPath, repoRoot, opts.projectName, trackedIndex);
318
- if (!resolved)
319
- continue;
320
- if (isTrackedPath(headIndex, resolved)) {
321
- const changed = windowLog.some((c) => new Date(c.date).getTime() > anchorTime && c.files.some((f) => f.path === resolved));
322
- if (changed) {
323
- evidence = `file-changed: ${resolved}`;
324
- break;
325
- }
326
- }
327
- else {
328
- // --no-renames means a rename shows as a D + A pair, so this also fires for renames.
329
- const deleted = windowLog.some((c) => new Date(c.date).getTime() > anchorTime &&
330
- c.files.some((f) => f.status === 'D' && f.path === resolved));
331
- if (deleted) {
332
- evidence = `file-deleted: ${resolved}`;
333
- break;
334
- }
335
- }
336
- }
337
- if (!evidence && refs.symbols.length > 0) {
338
- const absent = refs.symbols.filter((s) => !presentAtHeadSymbols.has(s));
339
- if (absent.length > 0) {
340
- const anchorCommit = resolveAnchorCommit(anchor);
341
- if (anchorCommit) {
342
- const presentAtAnchor = symbolsPresentAt(anchorCommit, absent);
343
- const hit = absent.find((s) => presentAtAnchor.has(s));
344
- if (hit)
345
- evidence = `symbol-gone: \`${hit}\``;
346
- }
347
- }
348
- }
349
- if (!evidence && refs.scripts.length > 0 && headScripts !== null) {
350
- const anchorCommit = resolveAnchorCommit(anchor);
351
- if (anchorCommit) {
352
- if (!scriptsAtCommit.has(anchorCommit)) {
353
- scriptsAtCommit.set(anchorCommit, packageScriptsAt(repoRoot, anchorCommit));
354
- }
355
- const anchorScripts = scriptsAtCommit.get(anchorCommit);
356
- const hit = anchorScripts
357
- ? refs.scripts.find((s) => anchorScripts[s] !== undefined && headScripts[s] === undefined)
358
- : undefined;
359
- if (hit)
360
- evidence = `script-gone: ${hit}`;
361
- }
362
- }
418
+ const evidence = churnEvidence(git, refs, anchorOf(entry));
363
419
  if (!evidence)
364
420
  continue;
365
421
  const headline = entry.content.replace(/\s+/g, ' ').slice(0, 60);
@@ -372,18 +428,8 @@ export function detectChurnStale(hippoRoot, repoRoot, opts) {
372
428
  result.preview.push({ id: entry.id, headline, evidence, already: false });
373
429
  toTag.push(entry);
374
430
  }
375
- if (!dryRun && toTag.length > 0) {
376
- // The git calls above can take seconds; a good outcome landing meanwhile moves the anchor past the evidence.
377
- const confirmedNow = queryConfirmedAt(hippoRoot, opts.tenantId);
378
- for (const stale of toTag) {
379
- if (confirmedNow.get(stale.id) !== confirmedAt.get(stale.id))
380
- continue;
381
- const entry = readEntry(hippoRoot, stale.id, opts.tenantId);
382
- if (!entry || entry.tags.includes(CHURN_STALE_TAG))
383
- continue;
384
- writeEntry(hippoRoot, { ...entry, tags: [...entry.tags, CHURN_STALE_TAG] });
385
- }
386
- }
431
+ if (!dryRun && toTag.length > 0)
432
+ tagChurnStale(hippoRoot, opts.tenantId, toTag, confirmedAt);
387
433
  }
388
434
  catch (err) {
389
435
  if (err instanceof GitReadError)
package/dist/json.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
2
+ [key: string]: JsonValue;
3
+ };
4
+ export declare function isJsonString(value: JsonValue | undefined): value is string;
5
+ //# sourceMappingURL=json.d.ts.map
package/dist/json.js ADDED
@@ -0,0 +1,4 @@
1
+ export function isJsonString(value) {
2
+ return typeof value === 'string';
3
+ }
4
+ //# sourceMappingURL=json.js.map
package/dist/judgment.js CHANGED
@@ -38,8 +38,7 @@ const KINDS = ['error', 'decision', 'convention', 'preference', 'trivia'];
38
38
  const VALENCES = ['critical', 'negative', 'positive', 'neutral'];
39
39
  /** Absent key means hippo keeps its pre-Jev behaviour and makes no HTTP call. */
40
40
  export function judgmentApiKey() {
41
- const key = envTypesafeApiKey()?.trim();
42
- return key ? key : undefined;
41
+ return envTypesafeApiKey();
43
42
  }
44
43
  function oneOf(value, allowed) {
45
44
  return allowed.find((option) => option === value) ?? null;
@@ -104,7 +104,7 @@ async function loadPipeline(model) {
104
104
  if (!pipelineFn)
105
105
  return null;
106
106
  // The offline bundle used in egress-blocked sandboxes ships only the FP32 model, so use whichever file is on disk.
107
- const cacheRoot = envModelCache()?.trim();
107
+ const cacheRoot = envModelCache();
108
108
  const quantized = !cacheRoot
109
109
  || fs.existsSync(path.join(cacheRoot, model, 'onnx', 'model_quantized.onnx'));
110
110
  try {
@@ -5,13 +5,10 @@ import { loadStrengthRows } from '../store/candidates.js';
5
5
  import { listMemoryConflicts, resolveConflict } from '../store/conflicts.js';
6
6
  import { shareMemory, listPeers } from '../shared.js';
7
7
  import { computePredictionBaserate } from '../predictions/store.js';
8
- import { isJsonString } from './protocol.js';
8
+ import { isJsonString } from '../json.js';
9
9
  export function runPredictBaserateTool({ args, ctx, hippoRoot, tenantId }) {
10
- // J3 reference-class / planning-fallacy detector. Reads from the E2
11
- // predictions table; returns text-only response matching the existing
12
- // MCP tool convention (no structured JSON over the wire). Direct call
13
- // to computePredictionBaserate; helper opens its own db + emits audit
14
- // (single source of truth, no caller-site drift).
10
+ // Text-only reply, matching the other MCP tools; the helper opens its own db
11
+ // and emits the audit, so call sites cannot drift.
15
12
  const classTag = String(args.class_tag || '').trim();
16
13
  if (!classTag)
17
14
  return 'No class_tag provided. Usage: pass class_tag matching a class used in past predictions (e.g. "migration-effort").';
@@ -68,8 +65,7 @@ export function runResolveTool({ args, ctx, hippoRoot, tenantId }) {
68
65
  const conflictId = Number(args.conflict_id);
69
66
  const keepId = String(args.keep || '');
70
67
  const forget = Boolean(args.forget);
71
- // AT1: optional rejectLoser + reason, threaded straight through to
72
- // resolveConflict's opts (plan §5 — mirrors the CLI's --reject-loser).
68
+ // Optional rejectLoser + reason pass straight to resolveConflict, as the CLI's --reject-loser does.
73
69
  const rejectLoser = Boolean(args.rejectLoser);
74
70
  const reason = isJsonString(args.reason) ? args.reason : undefined;
75
71
  if (isNaN(conflictId) || !keepId)
@@ -77,12 +73,8 @@ export function runResolveTool({ args, ctx, hippoRoot, tenantId }) {
77
73
  const result = resolveConflict(hippoRoot, conflictId, keepId, forget, tenantId, {
78
74
  rejectLoserValue: rejectLoser,
79
75
  reason,
80
- // P2 fix: resolveConflict's opts.rejectedBy defaults to 'cli' when
81
- // omitted — this call site never passed it, so the tombstone's
82
- // rejected_by AND the conflict_resolve audit's actor both landed as
83
- // 'cli' even though the caller was MCP. ctx.actor carries the
84
- // auth-resolved actor for HTTP-MCP (see McpContext above); stdio
85
- // callers pass no ctx, so 'mcp' is the honest fallback there.
76
+ // rejectedBy defaults to 'cli', so name the real actor for the tombstone and audit:
77
+ // ctx.actor for HTTP-MCP, 'mcp' for stdio callers, which pass no ctx.
86
78
  rejectedBy: ctx?.actor ?? 'mcp',
87
79
  });
88
80
  if (!result)
@@ -105,9 +97,7 @@ export function runShareTool({ args, hippoRoot, tenantId }) {
105
97
  return `Shared [${shared.id}] to global store. Source: ${shared.source}`;
106
98
  }
107
99
  export function runPeersTool({ tenantId }) {
108
- // D4 v1.12.10: tenant-scope the cross-project peer discovery.
109
- // tenantId is the caller's tenant (matches hippo_share above);
110
- // passing undefined would restore the pre-D4 host-wide behaviour.
100
+ // Tenant-scope peer discovery to the caller, as hippo_share does; undefined would list host-wide.
111
101
  const peers = listPeers(undefined, tenantId);
112
102
  if (peers.length === 0)
113
103
  return 'No peers found.';
@@ -120,7 +120,7 @@ export function tailSection(rows) {
120
120
  }
121
121
  return '\n' + lines.join('\n');
122
122
  }
123
- // J3.2: the hint depends on the query alone, so api.retrieve's copy is the one shown; JSON.stringify fences the phrase.
123
+ // The hint depends on the query alone, so api.retrieve's copy is the one shown; JSON.stringify fences the phrase.
124
124
  export function planningSection(r) {
125
125
  if (r.planningFallacyHint) {
126
126
  const h = r.planningFallacyHint;
@@ -1,9 +1,6 @@
1
- // MCP stdio transport framing.
2
- //
3
- // Spec: messages are newline-delimited JSON-RPC, with no embedded newlines.
4
- // https://modelcontextprotocol.io/specification/.../basic/transports#stdio
5
- // We also accept legacy LSP-style `Content-Length` framing so the
6
- // printf-and-pipe smoke test from issue #13 still works.
1
+ // MCP stdio transport framing: newline-delimited JSON-RPC, no embedded newlines
2
+ // (https://modelcontextprotocol.io/specification/.../basic/transports#stdio). Legacy LSP-style
3
+ // `Content-Length` framing is also accepted so a printf-and-pipe smoke test still works.
7
4
  const HEADER_DELIM = Buffer.from('\r\n\r\n');
8
5
  export function parseFrame(buffer) {
9
6
  // Strip leading whitespace/newlines between messages.
@@ -1,6 +1,7 @@
1
1
  import { loadConfig } from '../config.js';
2
2
  import type { Actor as ApiActor } from '../api.js';
3
3
  import { type ResolveProjectIdentityOpts } from '../project-identity.js';
4
+ import type { JsonValue } from '../json.js';
4
5
  /** Same bounded walk as the CLI (ends at home, so HIPPO_HOME wins over ~/.hippo); cwd/opts are the test seam. */
5
6
  export declare function findHippoRoot(cwd?: string, opts?: ResolveProjectIdentityOpts): string | null;
6
7
  interface McpRequest {
@@ -44,7 +45,7 @@ export interface McpContext {
44
45
  * assuming admin, or a member key over HTTP-MCP would act as admin.
45
46
  */
46
47
  role?: 'admin' | 'member';
47
- /** EI2: scope grants for the HTTP-MCP caller's key. Absent for stdio (admin, needs none). */
48
+ /** Scope grants for the HTTP-MCP caller's key. Absent for stdio (admin, needs none). */
48
49
  scopes?: readonly string[];
49
50
  viaAuthResolver?: true;
50
51
  /** Set by the HTTP transport for the host's operator; a context without a role is in-process and implies it. */
@@ -63,10 +64,6 @@ export interface McpContext {
63
64
  * a member key never acts as admin through MCP.
64
65
  */
65
66
  export declare function mcpActor(ctx: McpContext | undefined): ApiActor;
66
- export type JsonValue = string | number | boolean | null | JsonValue[] | {
67
- [key: string]: JsonValue;
68
- };
69
- export declare function isJsonString(v: JsonValue | undefined): v is string;
70
67
  export declare function isJsonBoolean(v: JsonValue | undefined): v is boolean;
71
68
  export declare function isJsonObjectRecord(v: JsonValue | undefined): v is {
72
69
  [key: string]: JsonValue;
@@ -43,9 +43,7 @@ export function mcpActor(ctx) {
43
43
  actor.hostAdmin = true;
44
44
  return actor;
45
45
  }
46
- export function isJsonString(v) {
47
- return typeof v === 'string';
48
- }
46
+ // ── JSON-ish domain type for untrusted MCP tool-call arguments ──
49
47
  export function isJsonBoolean(v) {
50
48
  return typeof v === 'boolean';
51
49
  }
@@ -11,9 +11,11 @@ import { detectAvailabilityBias } from '../availability.js';
11
11
  import { openHippoDb, closeHippoDb } from '../db.js';
12
12
  import { estimateTokens } from '../token-ledger.js';
13
13
  import { assembleCost, assembleText, drillCost, drillText } from '../context-render.js';
14
- import { mcpActor, isJsonString, isJsonBoolean } from './protocol.js';
14
+ import { mcpActor, isJsonBoolean } from './protocol.js';
15
15
  import { sessionRecallHistoryMcp, lastRecalledIds, resolveClientKey } from './session-state.js';
16
16
  import { formatContinuityBlock, formatMemories, memoryCost, memoriesReserve, snapshotPiece, handoffPiece, trailPiece, contextCost, tailSection, planningSection, } from './format.js';
17
+ import { isJsonString } from '../json.js';
18
+ import { MAX_ID_LEN } from '../http-util.js';
17
19
  function parseRecallArgs(args, defaultBudget) {
18
20
  const query = String(args.query || '');
19
21
  const budget = Number(args.budget) || defaultBudget;
@@ -35,9 +37,9 @@ function parseRecallArgs(args, defaultBudget) {
35
37
  const scorerWindow = args.scorer_window === undefined
36
38
  ? undefined
37
39
  : Number(args.scorer_window);
38
- // session_id drives the goal-stack boost inside api.retrieve; same trim and 256-char cap as fresh_tail_session_id.
40
+ // session_id drives the goal-stack boost inside api.retrieve; same trim and MAX_ID_LEN cap as fresh_tail_session_id.
39
41
  const sessionIdRaw = isJsonString(args.session_id) ? args.session_id.trim() : '';
40
- const sessionId = sessionIdRaw.length > 0 && sessionIdRaw.length <= 256
42
+ const sessionId = sessionIdRaw.length > 0 && sessionIdRaw.length <= MAX_ID_LEN
41
43
  ? sessionIdRaw
42
44
  : undefined;
43
45
  const recallExtra = {};
@@ -71,7 +73,7 @@ function recallPresenter(budget, includeContinuity, anchorRing, queryHash, out)
71
73
  const showTail = tailRows.length > 0 && pays(tailSection(tailRows));
72
74
  const continuityPiece = includeContinuity && apiResult.continuity ? `\n\n${formatContinuityBlock(apiResult.continuity)}` : '';
73
75
  const showContinuity = continuityPiece !== '' && pays(continuityPiece);
74
- // J1, J2 and C5: the hints and Cutoff block describe the list MCP shows, not the window band in apiResult.
76
+ // The hints and Cutoff block describe the list MCP shows, not the window band in apiResult.
75
77
  const render = (cut) => {
76
78
  const list = dropHeldCopies(cut, (r) => r.entry); // after every cut, so a merged row cut here never hides its sources
77
79
  const anchoring = anchorRing ? detectAnchoring(snapshotRing(anchorRing), queryHash, list[0]?.entry.id ?? null) : null;
@@ -168,11 +170,8 @@ function auditRecallHints(call, query, anchorRing, queryHash, rendered) {
168
170
  }
169
171
  }
170
172
  else {
171
- // Telemetry: caller had no sessionId so ring tracking skipped.
172
- // Per the recall-audit convention at api.ts:854, use SHA-256/16
173
- // for prompt hashing (NOT hashQueryText which is FNV-1a 32-bit
174
- // for recall matching; brute-force trivial for low-entropy
175
- // queries). Codex round-2 P2 catch.
173
+ // No sessionId, so no ring. Hash the prompt with SHA-256/16 as the recall audit does (api.ts:854):
174
+ // hashQueryText is FNV-1a 32-bit, trivial to brute-force on low-entropy queries.
176
175
  appendRecallAudit(hippoRoot, {
177
176
  tenantId,
178
177
  actor: ctx?.actor ?? 'mcp',
@@ -278,10 +277,8 @@ export function runDrillTool({ args, ctx, hippoRoot, tenantId }) {
278
277
  drillExtra.depth = depth;
279
278
  const r = apiDrillDown(apiCtx, summaryId, { ...drillExtra, cost: drillCost });
280
279
  if ('failure' in r) {
281
- // v1.6.4: only not_drillable is caller-actionable. not_found
282
- // intentionally collapses cross-tenant + scope-blocked + missing
283
- // (codex round 3 P1: distinguishing scope_blocked would leak
284
- // private-row existence on this surface).
280
+ // Only not_drillable is caller-actionable. not_found merges cross-tenant, scope-blocked and
281
+ // missing, because telling scope_blocked apart would leak private-row existence.
285
282
  if (r.failure === 'not_drillable') {
286
283
  return `Id ${summaryId} is a leaf row, not a level-2+ summary; nothing to drill into.`;
287
284
  }
@@ -303,12 +300,12 @@ export async function runContextTool({ args, ctx, hippoRoot, config, tenantId })
303
300
  ? args.scope
304
301
  : undefined;
305
302
  // The served store names the project (an HTTP daemon runs from anywhere); the global root names none, so stdio falls back to its launch cwd.
306
- const storeProject = resolveProjectIdentity(path.dirname(path.resolve(hippoRoot))).name;
303
+ const storeProject = resolveProjectIdentity(path.dirname(path.resolve(hippoRoot)));
307
304
  const result = await apiGetContext({ hippoRoot, tenantId, actor: mcpActor(ctx) }, {
308
305
  q: autoDetectContext(),
309
306
  budget,
310
307
  exactScope,
311
- currentProject: storeProject !== '' ? storeProject : resolveProjectIdentity(process.cwd()).name,
308
+ currentProject: storeProject.name !== '' ? storeProject : resolveProjectIdentity(process.cwd()),
312
309
  cost: contextCost,
313
310
  });
314
311
  lastRecalledIds.set(resolveClientKey(ctx), result.entries.map((r) => r.entry.id));
@@ -7,11 +7,12 @@ import { openHippoDb, closeHippoDb } from '../db.js';
7
7
  import { estimateTokens, recordTokenUse } from '../token-ledger.js';
8
8
  import { PACKAGE_VERSION } from '../version.js';
9
9
  import { validateToolArgs } from './tool-args.js';
10
- import { findHippoRoot, isJsonString, isJsonObjectRecord } from './protocol.js';
10
+ import { findHippoRoot, isJsonObjectRecord } from './protocol.js';
11
11
  import { TOOLS, TOOLS_BY_NAME, ARGS_CHECKED_BY_API } from './tools.js';
12
12
  import { runRecallTool, runAssembleTool, runDrillTool, runContextTool } from './recall-tools.js';
13
13
  import { runRememberTool, runOutcomeTool, runLearnTool } from './memory-tools.js';
14
14
  import { runPredictBaserateTool, runStatusTool, runConflictsTool, runResolveTool, runShareTool, runPeersTool } from './admin-tools.js';
15
+ import { isJsonString } from '../json.js';
15
16
  /**
16
17
  * Zero-install first run (`npx -y hippo-memory mcp` with no store anywhere):
17
18
  * create the global store instead of failing every tool call, and say so on
@@ -24,7 +25,7 @@ function createGlobalStoreOnFirstRun() {
24
25
  log.warn(`no memory store found; created the global store at ${root}. Run \`hippo init\` in a project for a project store.`);
25
26
  return root;
26
27
  }
27
- // ── Token ledger (ROADMAP TE0) ──
28
+ // ── Token ledger ──
28
29
  const MCP_TOKEN_SURFACES = new Map([
29
30
  ['hippo_recall', 'mcp_recall'],
30
31
  ['hippo_context', 'mcp_context'],
@@ -83,7 +84,7 @@ async function executeTool(name, args, ctx) {
83
84
  // resolve tenant from HIPPO_TENANT.
84
85
  const hippoRoot = ctx?.hippoRoot ?? findHippoRoot() ?? createGlobalStoreOnFirstRun();
85
86
  const config = loadConfig(hippoRoot);
86
- // A5: every store read in this server returns to the caller and is
87
+ // Every store read in this server returns to the caller and is
87
88
  // tenant-isolated. Resolved once per tool call: prefer the transport's
88
89
  // ctx.tenantId so an HTTP Bearer for tenant B doesn't drop to HIPPO_TENANT.
89
90
  const tenantId = ctx?.tenantId ?? resolveTenantId({});
@@ -1,7 +1,6 @@
1
1
  // Per-process state the MCP tools share across calls: recall rings, last recalled ids, auto-sleep runs.
2
- // v0.33 / J1 — Module-level per-(tenant, session) recall-history ring map
3
- // for the MCP pipeline. Separate from CLI/HTTP rings per plan v3
4
- // architecture (per-pipeline rings; no IPC).
2
+ // Per-(tenant, session) recall-history rings for the MCP pipeline, kept apart from
3
+ // the CLI/HTTP rings: each pipeline owns its rings, with no IPC between them.
5
4
  export const sessionRecallHistoryMcp = new Map();
6
5
  /** Test-only: reset the module-level recall-history Map. Call from beforeEach. */
7
6
  export function __resetSessionRecallHistoryMcp() {
package/dist/mcp/stdio.js CHANGED
@@ -1,8 +1,9 @@
1
1
  // Stdio transport: newline-delimited JSON-RPC frames in on stdin, replies out on stdout.
2
2
  import { errorFields, log } from '../log.js';
3
3
  import { parseFrame } from './framing.js';
4
- import { mcpErrorResponse, isJsonObjectRecord, isJsonString } from './protocol.js';
4
+ import { mcpErrorResponse, isJsonObjectRecord } from './protocol.js';
5
5
  import { handleMcpRequest } from './request.js';
6
+ import { isJsonString } from '../json.js';
6
7
  // MCP stdio transport spec: messages are newline-delimited JSON-RPC, no embedded newlines.
7
8
  // https://modelcontextprotocol.io/specification/.../basic/transports#stdio
8
9
  function send(msg) {