hippo-memory 1.62.0 → 1.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (370) hide show
  1. package/README.md +9 -0
  2. package/dist/agent-memories/apply.d.ts +2 -0
  3. package/dist/agent-memories/apply.js +23 -1
  4. package/dist/agent-memories/claude-code.d.ts +2 -1
  5. package/dist/agent-memories/claude-code.js +4 -4
  6. package/dist/agent-memories/report.d.ts +1 -0
  7. package/dist/agent-memories/report.js +2 -2
  8. package/dist/agent-memories/sync.js +37 -23
  9. package/dist/ambient-store.d.ts +4 -4
  10. package/dist/ambient-store.js +4 -3
  11. package/dist/api/assemble.d.ts +7 -10
  12. package/dist/api/assemble.js +7 -12
  13. package/dist/api/audit.d.ts +2 -2
  14. package/dist/api/audit.js +2 -2
  15. package/dist/api/auth.d.ts +6 -6
  16. package/dist/api/auth.js +5 -7
  17. package/dist/api/context-select.d.ts +4 -3
  18. package/dist/api/context-select.js +9 -11
  19. package/dist/api/context-types.d.ts +12 -17
  20. package/dist/api/context.d.ts +3 -2
  21. package/dist/api/context.js +20 -20
  22. package/dist/api/dormant.js +1 -1
  23. package/dist/api/drill-down.d.ts +4 -4
  24. package/dist/api/drill-down.js +22 -10
  25. package/dist/api/outcome.d.ts +8 -13
  26. package/dist/api/outcome.js +13 -17
  27. package/dist/api/promote.d.ts +5 -8
  28. package/dist/api/quarantine.js +3 -2
  29. package/dist/api/recall-types.d.ts +43 -55
  30. package/dist/api/recall.d.ts +3 -3
  31. package/dist/api/recall.js +7 -18
  32. package/dist/api/remember.d.ts +2 -2
  33. package/dist/api/sleep.d.ts +7 -35
  34. package/dist/api/sleep.js +3 -2
  35. package/dist/api/tokens.d.ts +2 -2
  36. package/dist/api/tokens.js +2 -2
  37. package/dist/api/types.d.ts +6 -10
  38. package/dist/api/types.js +2 -4
  39. package/dist/audit-prune.d.ts +4 -6
  40. package/dist/audit-prune.js +3 -5
  41. package/dist/audit.js +11 -33
  42. package/dist/auth.d.ts +8 -9
  43. package/dist/auth.js +5 -7
  44. package/dist/autolearn.d.ts +1 -1
  45. package/dist/autolearn.js +1 -1
  46. package/dist/availability.js +3 -5
  47. package/dist/capture/command.d.ts +5 -4
  48. package/dist/capture/command.js +8 -22
  49. package/dist/capture/compact.d.ts +1 -1
  50. package/dist/capture/compact.js +9 -13
  51. package/dist/capture/extract.js +37 -115
  52. package/dist/capture-error.d.ts +1 -1
  53. package/dist/capture-error.js +1 -1
  54. package/dist/churn-git.d.ts +1 -1
  55. package/dist/churn-git.js +1 -1
  56. package/dist/cli/audit.js +3 -4
  57. package/dist/cli/auth.js +3 -6
  58. package/dist/cli/curate.d.ts +1 -1
  59. package/dist/cli/curate.js +8 -24
  60. package/dist/cli/dag.js +5 -9
  61. package/dist/cli/decisions.js +10 -21
  62. package/dist/cli/explain.js +2 -1
  63. package/dist/cli/goals.js +1 -1
  64. package/dist/cli/init.js +1 -1
  65. package/dist/cli/playbooks.js +4 -9
  66. package/dist/cli/projects.js +3 -1
  67. package/dist/cli/recall.js +2 -1
  68. package/dist/cli/remember.js +6 -18
  69. package/dist/cli/session-hooks.js +21 -40
  70. package/dist/cli/setup.d.ts +1 -1
  71. package/dist/cli/setup.js +2 -6
  72. package/dist/cli/shared.js +3 -3
  73. package/dist/cli/slack.js +1 -1
  74. package/dist/cli/sleep.js +27 -1
  75. package/dist/cli/status.d.ts +4 -4
  76. package/dist/cli/status.js +7 -12
  77. package/dist/cli/transfer.js +8 -13
  78. package/dist/cli/usage.js +9 -6
  79. package/dist/cli.d.ts +1 -1
  80. package/dist/cli.js +4 -9
  81. package/dist/compaction-record.d.ts +2 -0
  82. package/dist/compaction-record.js +89 -68
  83. package/dist/compare.d.ts +11 -16
  84. package/dist/compare.js +11 -16
  85. package/dist/config.d.ts +19 -18
  86. package/dist/config.js +90 -67
  87. package/dist/connectors/github/backfill.d.ts +2 -2
  88. package/dist/connectors/github/backfill.js +8 -15
  89. package/dist/connectors/github/cli-impl.js +3 -8
  90. package/dist/connectors/github/deletion.d.ts +5 -12
  91. package/dist/connectors/github/deletion.js +5 -12
  92. package/dist/connectors/github/dlq.d.ts +6 -9
  93. package/dist/connectors/github/dlq.js +2 -3
  94. package/dist/connectors/github/ingest.d.ts +5 -7
  95. package/dist/connectors/github/ingest.js +8 -12
  96. package/dist/connectors/github/octokit-client.d.ts +3 -5
  97. package/dist/connectors/github/octokit-client.js +5 -6
  98. package/dist/connectors/github/signature.d.ts +9 -39
  99. package/dist/connectors/github/signature.js +9 -39
  100. package/dist/connectors/github/tenant-routing.d.ts +1 -1
  101. package/dist/connectors/github/tenant-routing.js +1 -1
  102. package/dist/connectors/github/transform.js +2 -2
  103. package/dist/connectors/github/types.d.ts +2 -10
  104. package/dist/connectors/github/types.js +1 -3
  105. package/dist/connectors/slack/deletion.d.ts +3 -8
  106. package/dist/connectors/slack/deletion.js +3 -8
  107. package/dist/connectors/slack/dlq.d.ts +1 -1
  108. package/dist/connectors/slack/ingest.d.ts +1 -1
  109. package/dist/connectors/slack/ingest.js +7 -16
  110. package/dist/connectors/slack/signature.d.ts +1 -1
  111. package/dist/connectors/slack/tenant-routing.d.ts +3 -5
  112. package/dist/connectors/slack/tenant-routing.js +3 -5
  113. package/dist/connectors/slack/transform.d.ts +5 -6
  114. package/dist/connectors/slack/transform.js +5 -6
  115. package/dist/connectors/slack/types.d.ts +2 -6
  116. package/dist/connectors/slack/types.js +1 -3
  117. package/dist/connectors/slack/web-client.js +10 -3
  118. package/dist/connectors/slack/workspaces.d.ts +3 -5
  119. package/dist/connectors/slack/workspaces.js +3 -5
  120. package/dist/consolidate/conflicts.js +3 -14
  121. package/dist/consolidate/decay.js +9 -29
  122. package/dist/consolidate/llm-passes.js +4 -5
  123. package/dist/consolidate/merge.js +8 -23
  124. package/dist/consolidate/run.d.ts +1 -8
  125. package/dist/consolidate/run.js +3 -25
  126. package/dist/consolidate/sleep.js +5 -17
  127. package/dist/consolidate/traces.js +9 -21
  128. package/dist/customer-notes.d.ts +5 -7
  129. package/dist/customer-notes.js +6 -9
  130. package/dist/dag.d.ts +10 -21
  131. package/dist/dag.js +23 -73
  132. package/dist/db/continuity.js +2 -2
  133. package/dist/db/migrations/v14.js +1 -1
  134. package/dist/db/migrations/v15.js +1 -2
  135. package/dist/db/migrations/v16.js +3 -4
  136. package/dist/db/migrations/v17.js +2 -3
  137. package/dist/db/migrations/v19.js +1 -1
  138. package/dist/db/migrations/v20.js +1 -1
  139. package/dist/db/migrations/v21.js +2 -6
  140. package/dist/db/migrations/v22.js +2 -4
  141. package/dist/db/migrations/v23.js +1 -1
  142. package/dist/db/migrations/v24.js +4 -6
  143. package/dist/db/migrations/v25.js +2 -3
  144. package/dist/db/migrations/v26.js +3 -3
  145. package/dist/db/migrations/v27.js +2 -10
  146. package/dist/db/migrations/v28.js +5 -8
  147. package/dist/db/migrations/v29.js +3 -4
  148. package/dist/db/migrations/v30.js +2 -2
  149. package/dist/db/migrations/v31.js +1 -1
  150. package/dist/db/migrations/v32.js +1 -1
  151. package/dist/db/migrations/v33.js +3 -3
  152. package/dist/db/migrations/v34.js +1 -1
  153. package/dist/db/migrations/v35.js +3 -4
  154. package/dist/db/migrations/v36.js +3 -4
  155. package/dist/db/migrations/v37.js +5 -5
  156. package/dist/db/migrations/v38.js +7 -8
  157. package/dist/db/migrations/v39.js +1 -1
  158. package/dist/db/migrations/v40.js +4 -16
  159. package/dist/db/migrations/v41.js +3 -4
  160. package/dist/db/migrations/v42.js +3 -4
  161. package/dist/db/migrations/v45.js +1 -1
  162. package/dist/db/migrations/v46.js +1 -1
  163. package/dist/db/migrations/v47.js +1 -1
  164. package/dist/db/migrations/v48.js +1 -1
  165. package/dist/decisions.d.ts +2 -2
  166. package/dist/decisions.js +6 -6
  167. package/dist/dedupe.js +86 -61
  168. package/dist/delivery-recorder.js +154 -135
  169. package/dist/doctor.js +119 -104
  170. package/dist/dormant.js +1 -4
  171. package/dist/embedding-provider.d.ts +4 -8
  172. package/dist/embedding-provider.js +4 -8
  173. package/dist/embeddings.js +55 -47
  174. package/dist/env.d.ts +1 -1
  175. package/dist/env.js +12 -12
  176. package/dist/escape.d.ts +5 -0
  177. package/dist/escape.js +10 -0
  178. package/dist/eval-stats.d.ts +1 -2
  179. package/dist/eval-stats.js +1 -2
  180. package/dist/eval-suite.js +27 -21
  181. package/dist/extract.js +4 -9
  182. package/dist/failure-log.d.ts +3 -3
  183. package/dist/failure-log.js +1 -1
  184. package/dist/forward-claim-detector.d.ts +2 -4
  185. package/dist/forward-claim-detector.js +6 -11
  186. package/dist/goals.d.ts +3 -3
  187. package/dist/goals.js +5 -6
  188. package/dist/graph/read.d.ts +2 -2
  189. package/dist/graph/read.js +5 -6
  190. package/dist/graph/types.d.ts +8 -8
  191. package/dist/graph/write.d.ts +7 -14
  192. package/dist/graph/write.js +16 -23
  193. package/dist/graph-extract.d.ts +7 -8
  194. package/dist/graph-extract.js +62 -72
  195. package/dist/graph-recall.d.ts +2 -2
  196. package/dist/graph-recall.js +55 -49
  197. package/dist/graph-stream.d.ts +5 -6
  198. package/dist/graph-stream.js +66 -57
  199. package/dist/graph-view.d.ts +2 -2
  200. package/dist/graph-view.js +7 -7
  201. package/dist/half-life-migration.d.ts +1 -2
  202. package/dist/half-life-migration.js +2 -3
  203. package/dist/hooks/codex-session.js +1 -1
  204. package/dist/hooks/codex-wrapper.d.ts +1 -1
  205. package/dist/hooks/codex-wrapper.js +3 -2
  206. package/dist/hooks/json-hooks.d.ts +2 -2
  207. package/dist/hooks/json-hooks.js +5 -4
  208. package/dist/hooks/opencode.d.ts +1 -1
  209. package/dist/hooks/opencode.js +5 -4
  210. package/dist/hooks/shared.d.ts +3 -7
  211. package/dist/hooks/shared.js +1 -8
  212. package/dist/http-util.d.ts +2 -3
  213. package/dist/http-util.js +3 -0
  214. package/dist/importers/core.d.ts +2 -9
  215. package/dist/importers/core.js +15 -30
  216. package/dist/importers/sources.js +2 -1
  217. package/dist/importers/vault.js +2 -20
  218. package/dist/incidents.d.ts +1 -1
  219. package/dist/incidents.js +1 -1
  220. package/dist/instruction-detect.d.ts +1 -1
  221. package/dist/instruction-detect.js +1 -1
  222. package/dist/invalidation.d.ts +3 -0
  223. package/dist/invalidation.js +160 -114
  224. package/dist/json.d.ts +5 -0
  225. package/dist/json.js +4 -0
  226. package/dist/judgment.js +1 -2
  227. package/dist/local-embedding.js +1 -1
  228. package/dist/mcp/admin-tools.js +7 -17
  229. package/dist/mcp/format.js +1 -1
  230. package/dist/mcp/framing.js +3 -6
  231. package/dist/mcp/protocol.d.ts +2 -5
  232. package/dist/mcp/protocol.js +1 -3
  233. package/dist/mcp/recall-tools.js +12 -15
  234. package/dist/mcp/request.js +4 -3
  235. package/dist/mcp/session-state.js +2 -3
  236. package/dist/mcp/stdio.js +2 -1
  237. package/dist/mcp/tools.js +9 -6
  238. package/dist/memory-value-weights.d.ts +5 -8
  239. package/dist/memory-value-weights.js +5 -8
  240. package/dist/memory-value.d.ts +13 -13
  241. package/dist/memory-value.js +26 -37
  242. package/dist/memory.d.ts +20 -22
  243. package/dist/memory.js +24 -48
  244. package/dist/multihop.d.ts +1 -1
  245. package/dist/multihop.js +3 -2
  246. package/dist/owner-validation.d.ts +4 -5
  247. package/dist/owner-validation.js +4 -5
  248. package/dist/physics.d.ts +4 -4
  249. package/dist/physics.js +7 -9
  250. package/dist/policies.d.ts +9 -10
  251. package/dist/policies.js +12 -14
  252. package/dist/postinstall.js +3 -6
  253. package/dist/predictions/planning-fallacy.d.ts +9 -14
  254. package/dist/predictions/planning-fallacy.js +10 -16
  255. package/dist/predictions/store.d.ts +15 -23
  256. package/dist/predictions/store.js +36 -33
  257. package/dist/processes.d.ts +2 -7
  258. package/dist/processes.js +3 -3
  259. package/dist/project-briefs.d.ts +2 -3
  260. package/dist/project-briefs.js +9 -13
  261. package/dist/project-identity.d.ts +22 -9
  262. package/dist/project-identity.js +47 -12
  263. package/dist/project-merge.d.ts +17 -2
  264. package/dist/project-merge.js +109 -27
  265. package/dist/project-remote.d.ts +12 -0
  266. package/dist/project-remote.js +138 -0
  267. package/dist/prompt-recall.js +1 -2
  268. package/dist/rate-limit.d.ts +1 -1
  269. package/dist/rate-limit.js +1 -1
  270. package/dist/raw-archive.d.ts +9 -0
  271. package/dist/raw-archive.js +70 -53
  272. package/dist/recall-history.d.ts +19 -20
  273. package/dist/recall-history.js +24 -42
  274. package/dist/recall-pipeline.js +4 -28
  275. package/dist/recall-scope.d.ts +7 -8
  276. package/dist/recall-scope.js +7 -8
  277. package/dist/recall-trace.d.ts +5 -9
  278. package/dist/recall-trace.js +6 -10
  279. package/dist/refine-llm.d.ts +1 -1
  280. package/dist/refine-llm.js +2 -2
  281. package/dist/reject-flow.d.ts +3 -4
  282. package/dist/reject-flow.js +122 -117
  283. package/dist/rejection.d.ts +5 -6
  284. package/dist/rejection.js +7 -15
  285. package/dist/rerankers/clef.d.ts +1 -1
  286. package/dist/rerankers/jev.d.ts +1 -2
  287. package/dist/rerankers/jev.js +4 -5
  288. package/dist/rerankers/llm.d.ts +1 -2
  289. package/dist/rerankers/llm.js +1 -2
  290. package/dist/rerankers/types.d.ts +1 -2
  291. package/dist/rrf.d.ts +2 -2
  292. package/dist/rrf.js +2 -2
  293. package/dist/search/bm25-search.d.ts +1 -1
  294. package/dist/search/bm25-search.js +2 -1
  295. package/dist/search/boosts.js +2 -1
  296. package/dist/search/hybrid.d.ts +1 -1
  297. package/dist/search/hybrid.js +2 -1
  298. package/dist/search/physics-search.d.ts +1 -1
  299. package/dist/search/physics-search.js +2 -1
  300. package/dist/search/types.d.ts +2 -0
  301. package/dist/search/types.js +3 -1
  302. package/dist/secret-detect.d.ts +4 -5
  303. package/dist/secret-detect.js +6 -10
  304. package/dist/server/auth.js +5 -5
  305. package/dist/server/client-ip.js +1 -1
  306. package/dist/server/cursor.js +2 -1
  307. package/dist/server/mcp-http.js +4 -4
  308. package/dist/server/request.d.ts +3 -6
  309. package/dist/server/request.js +6 -7
  310. package/dist/server/routes/admin.js +5 -4
  311. package/dist/server/routes/customer-notes.js +6 -5
  312. package/dist/server/routes/decisions.js +4 -3
  313. package/dist/server/routes/incidents.js +7 -5
  314. package/dist/server/routes/memories.js +7 -7
  315. package/dist/server/routes/policies.js +3 -2
  316. package/dist/server/routes/predictions.js +12 -15
  317. package/dist/server/routes/processes.js +3 -2
  318. package/dist/server/routes/project-briefs.js +8 -7
  319. package/dist/server/routes/recall.js +95 -93
  320. package/dist/server/routes/skills.js +6 -5
  321. package/dist/server/types.d.ts +1 -1
  322. package/dist/server/validation.d.ts +1 -2
  323. package/dist/server/validation.js +7 -14
  324. package/dist/server-detect.js +72 -58
  325. package/dist/server.d.ts +2 -2
  326. package/dist/server.js +131 -117
  327. package/dist/shared.d.ts +17 -17
  328. package/dist/shared.js +93 -97
  329. package/dist/skills.d.ts +3 -3
  330. package/dist/skills.js +8 -8
  331. package/dist/store/audit-event.d.ts +2 -2
  332. package/dist/store/audit-event.js +1 -1
  333. package/dist/store/candidates.d.ts +2 -2
  334. package/dist/store/candidates.js +4 -3
  335. package/dist/store/conflicts.js +30 -22
  336. package/dist/store/delete-and-batch.d.ts +11 -14
  337. package/dist/store/delete-and-batch.js +40 -91
  338. package/dist/store/entry-reads.d.ts +17 -25
  339. package/dist/store/entry-reads.js +59 -40
  340. package/dist/store/entry-row.d.ts +6 -24
  341. package/dist/store/entry-row.js +6 -24
  342. package/dist/store/entry-writes.d.ts +6 -7
  343. package/dist/store/entry-writes.js +13 -11
  344. package/dist/store/handoffs.d.ts +1 -1
  345. package/dist/store/handoffs.js +7 -10
  346. package/dist/store/index-and-stats.d.ts +2 -6
  347. package/dist/store/index-and-stats.js +4 -10
  348. package/dist/store/mirrors.d.ts +6 -19
  349. package/dist/store/mirrors.js +14 -39
  350. package/dist/store/open.js +9 -31
  351. package/dist/store/rows.d.ts +5 -11
  352. package/dist/store/rows.js +6 -11
  353. package/dist/store/search-rows.d.ts +17 -34
  354. package/dist/store/search-rows.js +31 -59
  355. package/dist/store/sessions.d.ts +4 -5
  356. package/dist/store/sessions.js +5 -6
  357. package/dist/store/summaries.d.ts +13 -17
  358. package/dist/store/summaries.js +26 -70
  359. package/dist/support-bundle.js +4 -8
  360. package/dist/tenant.d.ts +1 -5
  361. package/dist/token-ledger.d.ts +1 -1
  362. package/dist/token-ledger.js +3 -5
  363. package/dist/trace.js +1 -3
  364. package/dist/version.d.ts +1 -1
  365. package/dist/version.js +1 -1
  366. package/dist/working-memory.d.ts +1 -1
  367. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  368. package/extensions/openclaw-plugin/package.json +1 -1
  369. package/openclaw.plugin.json +1 -1
  370. package/package.json +1 -1
@@ -8,24 +8,11 @@ export interface VerifyOpts {
8
8
  }
9
9
  export declare function verifyGitHubSignature(opts: VerifyOpts): boolean;
10
10
  /**
11
- * Source-aware idempotency key. v1.3.1 hotfix (codex round 1 P0 #3 + claude
12
- * round 2 P0 #3).
13
- *
14
- * Round 1 design: sha256(eventName + ':' + rawBody) so an attacker rotating
15
- * X-GitHub-Delivery cannot bypass dedupe.
16
- *
17
- * Round 2 found that key produced different hashes for the SAME source event
18
- * delivered via webhook vs via REST backfill, because backfill rawBody is the
19
- * REST list-item shape while webhook rawBody is the envelope. Result:
20
- * backfill + later webhook of the same issue created two `kind='raw'` rows
21
- * with the same artifact_ref. Combined with the deletion bug, deletion could
22
- * not archive both.
23
- *
24
- * v1.3.1 fix: key from the SOURCE-NORMALIZED identifier — artifact_ref plus
25
- * the source-side updated_at timestamp. Same artifact + same revision = same
26
- * key, regardless of which path delivered it. Different revisions of the same
27
- * issue (an edit) get different keys, which is correct: each edit IS a new
28
- * memory revision.
11
+ * Source-aware idempotency key: artifact_ref plus the source-side updated_at.
12
+ * A raw-body key would hash a webhook envelope and a REST backfill item of the
13
+ * SAME source event differently and ingest it twice. Same artifact + same
14
+ * revision = same key, whichever path delivered it; an edit gets a new key,
15
+ * which is correct because each edit IS a new memory revision.
29
16
  *
30
17
  * Both inputs are upstream-derived from the parsed event, not from the
31
18
  * unsigned delivery header — replay attacks still cannot bypass dedupe.
@@ -36,30 +23,13 @@ export declare function verifyGitHubSignature(opts: VerifyOpts): boolean;
36
23
  * - updatedAt: source-side ISO timestamp (issue.updated_at,
37
24
  * comment.updated_at, pull_request.updated_at). Empty string when the
38
25
  * payload omits it (rare; older REST shapes).
39
- *
40
- * Migration note for v1.3.0 → v1.3.1: existing github_event_log rows from
41
- * v1.3.0 used the round-1 key shape and will not collide with v1.3.1 keys.
42
- * The first webhook delivery after upgrading creates a new log row with the
43
- * new key. This is acceptable for a hotfix (no production users on v1.3.0)
44
- * and correct semantics going forward.
45
26
  */
46
27
  export declare function computeIdempotencyKey(artifactRef: string, updatedAt: string | null | undefined): string;
47
28
  /**
48
- * v1.3.2: deletion-specific idempotency key. Distinct namespace from
49
- * computeIdempotencyKey so an ingest's row in github_event_log doesn't make
50
- * a deletion event return 'duplicate' before it gets a chance to archive.
51
- *
52
- * The codex round 3 P0 fix on server.ts was to call computeIdempotencyKey
53
- * with the right (artifactRef, updatedAt) shape for deletions. That made
54
- * the deletion key MATCH the ingest key — which collapsed to a "deletion
55
- * always returns duplicate" bug because both shared github_event_log.
56
- *
57
- * v1.3.2 splits the namespace: deletion key = sha256('deleted:' +
58
- * artifactRef + ':' + updatedAt). Two retries of the SAME deletion event
59
- * still dedupe (same artifact + same updatedAt + same prefix → same key).
60
- * Ingest of the same artifact + same updatedAt produces a DIFFERENT key,
61
- * so a deletion event does not get short-circuited by the ingest's prior
62
- * log row.
29
+ * Deletion-specific idempotency key: sha256('deleted:' + artifactRef + ':' +
30
+ * updatedAt). Distinct namespace from computeIdempotencyKey so an ingest's row
31
+ * in github_event_log doesn't make a deletion return 'duplicate' before it
32
+ * gets a chance to archive; retries of the SAME deletion still dedupe.
63
33
  *
64
34
  * Kept as a separate exported function so the namespace prefix is explicit
65
35
  * at every call site (server.ts deletion branches, deletion.ts, DLQ replay).
@@ -17,24 +17,11 @@ export function verifyGitHubSignature(opts) {
17
17
  return false;
18
18
  }
19
19
  /**
20
- * Source-aware idempotency key. v1.3.1 hotfix (codex round 1 P0 #3 + claude
21
- * round 2 P0 #3).
22
- *
23
- * Round 1 design: sha256(eventName + ':' + rawBody) so an attacker rotating
24
- * X-GitHub-Delivery cannot bypass dedupe.
25
- *
26
- * Round 2 found that key produced different hashes for the SAME source event
27
- * delivered via webhook vs via REST backfill, because backfill rawBody is the
28
- * REST list-item shape while webhook rawBody is the envelope. Result:
29
- * backfill + later webhook of the same issue created two `kind='raw'` rows
30
- * with the same artifact_ref. Combined with the deletion bug, deletion could
31
- * not archive both.
32
- *
33
- * v1.3.1 fix: key from the SOURCE-NORMALIZED identifier — artifact_ref plus
34
- * the source-side updated_at timestamp. Same artifact + same revision = same
35
- * key, regardless of which path delivered it. Different revisions of the same
36
- * issue (an edit) get different keys, which is correct: each edit IS a new
37
- * memory revision.
20
+ * Source-aware idempotency key: artifact_ref plus the source-side updated_at.
21
+ * A raw-body key would hash a webhook envelope and a REST backfill item of the
22
+ * SAME source event differently and ingest it twice. Same artifact + same
23
+ * revision = same key, whichever path delivered it; an edit gets a new key,
24
+ * which is correct because each edit IS a new memory revision.
38
25
  *
39
26
  * Both inputs are upstream-derived from the parsed event, not from the
40
27
  * unsigned delivery header — replay attacks still cannot bypass dedupe.
@@ -45,32 +32,15 @@ export function verifyGitHubSignature(opts) {
45
32
  * - updatedAt: source-side ISO timestamp (issue.updated_at,
46
33
  * comment.updated_at, pull_request.updated_at). Empty string when the
47
34
  * payload omits it (rare; older REST shapes).
48
- *
49
- * Migration note for v1.3.0 → v1.3.1: existing github_event_log rows from
50
- * v1.3.0 used the round-1 key shape and will not collide with v1.3.1 keys.
51
- * The first webhook delivery after upgrading creates a new log row with the
52
- * new key. This is acceptable for a hotfix (no production users on v1.3.0)
53
- * and correct semantics going forward.
54
35
  */
55
36
  export function computeIdempotencyKey(artifactRef, updatedAt) {
56
37
  return createHash('sha256').update(`${artifactRef}:${updatedAt ?? ''}`).digest('hex');
57
38
  }
58
39
  /**
59
- * v1.3.2: deletion-specific idempotency key. Distinct namespace from
60
- * computeIdempotencyKey so an ingest's row in github_event_log doesn't make
61
- * a deletion event return 'duplicate' before it gets a chance to archive.
62
- *
63
- * The codex round 3 P0 fix on server.ts was to call computeIdempotencyKey
64
- * with the right (artifactRef, updatedAt) shape for deletions. That made
65
- * the deletion key MATCH the ingest key — which collapsed to a "deletion
66
- * always returns duplicate" bug because both shared github_event_log.
67
- *
68
- * v1.3.2 splits the namespace: deletion key = sha256('deleted:' +
69
- * artifactRef + ':' + updatedAt). Two retries of the SAME deletion event
70
- * still dedupe (same artifact + same updatedAt + same prefix → same key).
71
- * Ingest of the same artifact + same updatedAt produces a DIFFERENT key,
72
- * so a deletion event does not get short-circuited by the ingest's prior
73
- * log row.
40
+ * Deletion-specific idempotency key: sha256('deleted:' + artifactRef + ':' +
41
+ * updatedAt). Distinct namespace from computeIdempotencyKey so an ingest's row
42
+ * in github_event_log doesn't make a deletion return 'duplicate' before it
43
+ * gets a chance to archive; retries of the SAME deletion still dedupe.
74
44
  *
75
45
  * Kept as a separate exported function so the namespace prefix is explicit
76
46
  * at every call site (server.ts deletion branches, deletion.ts, DLQ replay).
@@ -20,7 +20,7 @@ export interface ResolveArgs {
20
20
  * is non-empty (multi-tenant install with foreign installation)
21
21
  * - `installationId` is missing AND either routing table is non-empty
22
22
  * AND no `repository.full_name` match (PAT-mode webhook from a foreign
23
- * account — codex P0 #4 regression target)
23
+ * account)
24
24
  *
25
25
  * Escape hatch: `GITHUB_ALLOW_UNKNOWN_INSTALLATION_FALLBACK=1` restores the
26
26
  * env fallback for emergency rollback only. Mirrors the Slack equivalent
@@ -14,7 +14,7 @@ import { envGithubAllowUnknownInstallationFallback, envTenant } from '../../env.
14
14
  * is non-empty (multi-tenant install with foreign installation)
15
15
  * - `installationId` is missing AND either routing table is non-empty
16
16
  * AND no `repository.full_name` match (PAT-mode webhook from a foreign
17
- * account — codex P0 #4 regression target)
17
+ * account)
18
18
  *
19
19
  * Escape hatch: `GITHUB_ALLOW_UNKNOWN_INSTALLATION_FALLBACK=1` restores the
20
20
  * env fallback for emergency rollback only. Mirrors the Slack equivalent
@@ -5,13 +5,13 @@ import { scopeFromRepository } from './scope.js';
5
5
  * caller can mark the delivery seen for idempotency and skip the insert.
6
6
  *
7
7
  * Contract (mirrors src/connectors/slack/transform.ts):
8
- * - kind is the literal 'raw' (E1.x connector boundary, see src/importers.ts).
8
+ * - kind is the literal 'raw' (connector boundary, see src/importers.ts).
9
9
  * - artifact_ref formats are stable; deletion paths look up by these strings.
10
10
  * - issue: github://<owner/repo>/issue/<number>
11
11
  * - issue_comment: github://<owner/repo>/issue/<number>/comment/<id>
12
12
  * - pull_request: github://<owner/repo>/pull/<number>
13
13
  * - pr_review_comment: github://<owner/repo>/pull/<number>/review_comment/<id>
14
- * - owner is `user:github:<login>`. Required by the v0.40.0 provenance gate.
14
+ * - owner is `user:github:<login>`. Required by the provenance gate.
15
15
  * - scope is derived from repository.private via scopeFromRepository (default
16
16
  * private when undetermined).
17
17
  */
@@ -7,8 +7,9 @@
7
7
  * header value so a malicious or misrouted payload cannot satisfy a guard
8
8
  * for the wrong event type.
9
9
  */
10
+ import { type JsonValue } from '../../json.js';
10
11
  /**
11
- * Codex P1 #7: `private` MUST be optional, not required. The Slack-style
12
+ * `private` MUST be optional, not required. The Slack-style
12
13
  * fail-safe in scope.ts requires an envelope with `private: undefined` to
13
14
  * map to private; a strict boolean type would reject the payload before
14
15
  * scope can fail closed.
@@ -79,15 +80,6 @@ export interface GitHubPullRequestReviewCommentEvent extends GitHubWebhookEnvelo
79
80
  updated_at?: string;
80
81
  };
81
82
  }
82
- /**
83
- * The full value space `JSON.parse` can produce. Boundary-guard functions in
84
- * this file accept `JsonValue` (never `unknown`) so a value's origin as
85
- * unparsed external JSON stays visible in its type, then narrow it via the
86
- * `isJson*` predicates below.
87
- */
88
- export type JsonValue = string | number | boolean | null | JsonValue[] | {
89
- [key: string]: JsonValue;
90
- };
91
83
  export declare function isGitHubWebhookEnvelope(x: JsonValue): x is JsonValue & GitHubWebhookEnvelope;
92
84
  export declare function isGitHubIssueEvent(x: JsonValue, evtHeader: string): x is JsonValue & GitHubIssueEvent;
93
85
  export declare function isGitHubIssueCommentEvent(x: JsonValue, evtHeader: string): x is JsonValue & GitHubIssueCommentEvent;
@@ -7,12 +7,10 @@
7
7
  * header value so a malicious or misrouted payload cannot satisfy a guard
8
8
  * for the wrong event type.
9
9
  */
10
+ import { isJsonString } from '../../json.js';
10
11
  function isJsonObject(x) {
11
12
  return x !== null && typeof x === 'object';
12
13
  }
13
- function isJsonString(x) {
14
- return typeof x === 'string';
15
- }
16
14
  function isJsonNumber(x) {
17
15
  return typeof x === 'number';
18
16
  }
@@ -11,14 +11,9 @@ export interface DeletionResult {
11
11
  memoryId: string | null;
12
12
  }
13
13
  /**
14
- * Handle Slack `message_deleted`. v0.39 commit 3 closes the prior race where
15
- * the archive committed but `markEventSeen` ran on a second db handle — a
16
- * crash between them left the deletion event un-acked, and the next retry
17
- * hit a now-archived row and returned `not_found` instead of `duplicate`.
18
- *
19
- * Fix: pass `afterArchive` to `archiveRaw`, which runs inside the same
20
- * SAVEPOINT as the archive itself. The slack_event_log row commits with the
21
- * archive or not at all.
14
+ * Handle Slack `message_deleted`. `afterArchive` runs inside the archive's own
15
+ * SAVEPOINT, so the slack_event_log row commits with the archive or not at all
16
+ * and a crash cannot leave a retry hitting `not_found` instead of `duplicate`.
22
17
  */
23
18
  export declare function handleMessageDeleted(ctx: Context, input: DeletionInput): DeletionResult;
24
19
  //# sourceMappingURL=deletion.d.ts.map
@@ -2,14 +2,9 @@ import { archiveRaw } from '../../api.js';
2
2
  import { openHippoDb, closeHippoDb } from '../../db.js';
3
3
  import { markEventSeen, hasSeenEvent } from './idempotency.js';
4
4
  /**
5
- * Handle Slack `message_deleted`. v0.39 commit 3 closes the prior race where
6
- * the archive committed but `markEventSeen` ran on a second db handle — a
7
- * crash between them left the deletion event un-acked, and the next retry
8
- * hit a now-archived row and returned `not_found` instead of `duplicate`.
9
- *
10
- * Fix: pass `afterArchive` to `archiveRaw`, which runs inside the same
11
- * SAVEPOINT as the archive itself. The slack_event_log row commits with the
12
- * archive or not at all.
5
+ * Handle Slack `message_deleted`. `afterArchive` runs inside the archive's own
6
+ * SAVEPOINT, so the slack_event_log row commits with the archive or not at all
7
+ * and a crash cannot leave a retry hitting `not_found` instead of `duplicate`.
13
8
  */
14
9
  export function handleMessageDeleted(ctx, input) {
15
10
  const db = openHippoDb(ctx.hippoRoot);
@@ -15,7 +15,7 @@ export interface DlqItem {
15
15
  slackTimestamp: string | null;
16
16
  }
17
17
  /**
18
- * v0.39 commit 3: writeToDlq is now bucket-aware. `bucket` defaults to
18
+ * writeToDlq is bucket-aware. `bucket` defaults to
19
19
  * 'parse_error' to preserve the legacy single-arg call sites while letting
20
20
  * new callers tag rows for `hippo slack dlq replay` triage.
21
21
  *
@@ -25,7 +25,7 @@ export interface IngestResult {
25
25
  * and the SAVEPOINT, we throw `DuplicateEventError` to roll back the memory
26
26
  * write. The pre-check `hasSeenEvent` stays as a fast path for the common
27
27
  * already-seen case, but the afterWrite throw is what makes idempotency
28
- * correct under two-worker concurrency (v0.39 commit 3 fix).
28
+ * correct under two-worker concurrency.
29
29
  * - Empty-body messages return 'skipped' but still mark seen so a replay
30
30
  * returns 'duplicate' rather than re-running the transform.
31
31
  */
@@ -15,7 +15,7 @@ import { messageToRememberOpts } from './transform.js';
15
15
  * and the SAVEPOINT, we throw `DuplicateEventError` to roll back the memory
16
16
  * write. The pre-check `hasSeenEvent` stays as a fast path for the common
17
17
  * already-seen case, but the afterWrite throw is what makes idempotency
18
- * correct under two-worker concurrency (v0.39 commit 3 fix).
18
+ * correct under two-worker concurrency.
19
19
  * - Empty-body messages return 'skipped' but still mark seen so a replay
20
20
  * returns 'duplicate' rather than re-running the transform.
21
21
  */
@@ -25,12 +25,8 @@ export function ingestMessage(ctx, input) {
25
25
  const db = openHippoDb(ctx.hippoRoot);
26
26
  try {
27
27
  if (hasSeenEvent(db, input.eventId)) {
28
- // v1.12.6 fix (B3): empty-body events mark seen with memory_id=NULL on
29
- // first call (see line further down). Their replay should return the
30
- // same 'skipped' status they originally returned, not 'duplicate' —
31
- // the asymmetry was a paper-cut for callers that switch/case on
32
- // status. memory_id=NULL is the discriminator. Non-NULL memory_id
33
- // means an actual memory was ingested before, so 'duplicate' is correct.
28
+ // Empty-body events are marked seen with memory_id=NULL, so their replay returns the same 'skipped'
29
+ // they first returned; a non-NULL memory_id means a memory was ingested, so 'duplicate'.
34
30
  const cachedId = lookupMemoryByEvent(db, input.eventId);
35
31
  return { status: cachedId === null ? 'skipped' : 'duplicate', memoryId: cachedId };
36
32
  }
@@ -65,11 +61,8 @@ export function ingestMessage(ctx, input) {
65
61
  }
66
62
  }
67
63
  function rememberWithEventLog(ctx, eventId, opts) {
68
- // v1.12.0: drop the legacy `|| 'connector:slack'` fallback — ctx is always
69
- // provided by server.ts:1039 which constructs it with the connector subject.
70
- // Under the new object-shaped Context.actor, an OR-fallback would evaluate
71
- // an object as truthy and skip the fallback anyway (logic bug if ctx were
72
- // ever missing); explicit reliance on the caller is safer.
64
+ // No `|| 'connector:slack'` fallback: the caller always builds ctx with the connector subject, and with
65
+ // an object-shaped Context.actor an OR-fallback would never fire anyway.
73
66
  const result = remember(ctx, {
74
67
  ...opts,
75
68
  untrusted: true,
@@ -101,10 +94,8 @@ function lostRaceResult(ctx, eventId) {
101
94
  }
102
95
  }
103
96
  function rejectedValueResult(ctx, eventId) {
104
- // AT1 (plan §3 containment): a tombstone hit is a PERMANENT skip, not
105
- // a transient failure — never DLQ-retry it (retrying would just hit
106
- // the same refusal forever). Mark the event seen exactly like the
107
- // empty-body branch above so a Slack retry acks as done, not error.
97
+ // A tombstone hit is a PERMANENT skip: a DLQ retry would hit the same refusal forever, so mark the
98
+ // event seen like the empty-body branch above and let a Slack retry ack as done, not error.
108
99
  const db4 = openHippoDb(ctx.hippoRoot);
109
100
  try {
110
101
  markEventSeen(db4, eventId, null);
@@ -4,7 +4,7 @@ export interface VerifyOpts {
4
4
  signature: string;
5
5
  signingSecret: string;
6
6
  /**
7
- * Previous signing secret during a rotation. v0.39 commit 3: deploy with
7
+ * Previous signing secret during a rotation. Deploy with
8
8
  * both `SLACK_SIGNING_SECRET` (new) and `SLACK_SIGNING_SECRET_PREVIOUS` (old)
9
9
  * set, verify both work, drop previous after rollover. The verifier tries
10
10
  * `signingSecret` first, then `previousSecret` if that fails.
@@ -11,11 +11,9 @@ import type { DatabaseSyncLike } from '../../db.js';
11
11
  * deployment's tenant). Escape hatch: SLACK_ALLOW_UNKNOWN_TEAM_FALLBACK=1
12
12
  * restores the env fallback for emergency rollback only.
13
13
  *
14
- * v0.39 commit 3 (CRITICAL #5): the previous version returned null on miss
15
- * unconditionally, and the route handler then fell back to HIPPO_TENANT —
16
- * which silently routed events from a foreign workspace into the deployment
17
- * tenant. The fail-closed contract lives here so every caller (route handler,
18
- * CLI replay, future MCP) gets the same protection.
14
+ * The fail-closed contract lives here so every caller (route handler, CLI
15
+ * replay, future MCP) gets the same protection against routing a foreign
16
+ * workspace's events into the deployment tenant.
19
17
  */
20
18
  export declare function resolveTenantForTeam(db: DatabaseSyncLike, teamId: string): string | null;
21
19
  //# sourceMappingURL=tenant-routing.d.ts.map
@@ -11,11 +11,9 @@ import { envSlackAllowUnknownTeamFallback, envTenant } from '../../env.js';
11
11
  * deployment's tenant). Escape hatch: SLACK_ALLOW_UNKNOWN_TEAM_FALLBACK=1
12
12
  * restores the env fallback for emergency rollback only.
13
13
  *
14
- * v0.39 commit 3 (CRITICAL #5): the previous version returned null on miss
15
- * unconditionally, and the route handler then fell back to HIPPO_TENANT —
16
- * which silently routed events from a foreign workspace into the deployment
17
- * tenant. The fail-closed contract lives here so every caller (route handler,
18
- * CLI replay, future MCP) gets the same protection.
14
+ * The fail-closed contract lives here so every caller (route handler, CLI
15
+ * replay, future MCP) gets the same protection against routing a foreign
16
+ * workspace's events into the deployment tenant.
19
17
  */
20
18
  export function resolveTenantForTeam(db, teamId) {
21
19
  // SAFETY: query selects only `tenant_id`, so a returned row has that shape;
@@ -12,18 +12,17 @@ export interface TransformInput {
12
12
  * without text). Caller treats null as "skip but mark idempotency seen".
13
13
  *
14
14
  * Contract:
15
- * - kind is the literal 'raw' (E1.x connector boundary, see src/importers.ts).
15
+ * - kind is the literal 'raw' (connector boundary, see src/importers.ts).
16
16
  * - artifact_ref format MUST be exactly `slack://${teamId}/${channelId}/${ts}`;
17
- * the deletion path (Task 9) looks up by this string.
18
- * - owner is non-null whenever a row is written. Required by the v0.40.0
17
+ * the deletion path looks up by this string.
18
+ * - owner is non-null whenever a row is written. Required by the
19
19
  * provenance gate (`hippo provenance --strict`).
20
20
  * - `user:<slack_user_id>` when the event carries a `user`.
21
21
  * - `bot:<bot_id>` for the `bot_message` subtype (or any userless+text event
22
22
  * that supplies bot_id).
23
23
  * - `bot:unknown` only as a last-resort sentinel so the gate never sees null.
24
- * Codex round 1 P1: skipping userless messages instead of stamping a bot
25
- * owner would silently drop existing bot ingestion via the
26
- * "skipped but seen" path at ingest.ts:54-65.
24
+ * Skipping userless messages instead would silently drop bot ingestion
25
+ * via the "skipped but seen" path in ingest.ts.
27
26
  */
28
27
  export declare function messageToRememberOpts(input: TransformInput): RememberOpts | null;
29
28
  //# sourceMappingURL=transform.d.ts.map
@@ -5,18 +5,17 @@ import { scopeFromChannel } from './scope.js';
5
5
  * without text). Caller treats null as "skip but mark idempotency seen".
6
6
  *
7
7
  * Contract:
8
- * - kind is the literal 'raw' (E1.x connector boundary, see src/importers.ts).
8
+ * - kind is the literal 'raw' (connector boundary, see src/importers.ts).
9
9
  * - artifact_ref format MUST be exactly `slack://${teamId}/${channelId}/${ts}`;
10
- * the deletion path (Task 9) looks up by this string.
11
- * - owner is non-null whenever a row is written. Required by the v0.40.0
10
+ * the deletion path looks up by this string.
11
+ * - owner is non-null whenever a row is written. Required by the
12
12
  * provenance gate (`hippo provenance --strict`).
13
13
  * - `user:<slack_user_id>` when the event carries a `user`.
14
14
  * - `bot:<bot_id>` for the `bot_message` subtype (or any userless+text event
15
15
  * that supplies bot_id).
16
16
  * - `bot:unknown` only as a last-resort sentinel so the gate never sees null.
17
- * Codex round 1 P1: skipping userless messages instead of stamping a bot
18
- * owner would silently drop existing bot ingestion via the
19
- * "skipped but seen" path at ingest.ts:54-65.
17
+ * Skipping userless messages instead would silently drop bot ingestion
18
+ * via the "skipped but seen" path in ingest.ts.
20
19
  */
21
20
  export function messageToRememberOpts(input) {
22
21
  const text = input.message.text?.trim();
@@ -2,6 +2,7 @@
2
2
  * Slack Events API envelope shapes used by the ingestion connector.
3
3
  * Spec: https://api.slack.com/events-api
4
4
  */
5
+ import { type JsonValue } from '../../json.js';
5
6
  export interface SlackUrlVerification {
6
7
  type: 'url_verification';
7
8
  challenge: string;
@@ -18,18 +19,13 @@ export interface SlackMessageEvent {
18
19
  thread_ts?: string;
19
20
  /**
20
21
  * Slack `bot_message` subtype carries `bot_id` instead of `user`. The
21
- * v0.40.0 provenance gate requires a non-null `owner`, so transform.ts
22
+ * provenance gate requires a non-null `owner`, so transform.ts
22
23
  * derives `owner: bot:<bot_id>` when `user` is absent.
23
24
  */
24
25
  bot_id?: string;
25
26
  /** Present on subtype='message_deleted'. */
26
27
  deleted_ts?: string;
27
28
  }
28
- /** JSON value shape for the parts of an inbound Slack payload not yet
29
- * narrowed to a known envelope/event shape. */
30
- export type JsonValue = string | number | boolean | null | JsonValue[] | {
31
- [key: string]: JsonValue;
32
- };
33
29
  export interface SlackEventEnvelope {
34
30
  type: 'event_callback';
35
31
  team_id: string;
@@ -2,9 +2,7 @@
2
2
  * Slack Events API envelope shapes used by the ingestion connector.
3
3
  * Spec: https://api.slack.com/events-api
4
4
  */
5
- function isJsonString(value) {
6
- return typeof value === 'string';
7
- }
5
+ import { isJsonString } from '../../json.js';
8
6
  function isJsonNumber(value) {
9
7
  return typeof value === 'number';
10
8
  }
@@ -1,7 +1,14 @@
1
1
  import { fetchWithRetry, isRetryableStatus } from '../../http-retry.js';
2
- function isJsonString(value) {
3
- return typeof value === 'string';
4
- }
2
+ import { isJsonString } from '../../json.js';
3
+ /**
4
+ * Build a SlackHistoryFetcher that pages `conversations.history` over real
5
+ * HTTP. Wraps `fetchWithRetry` so 429 and 5xx handling is automatic. The returned
6
+ * fetcher is the one `backfillChannel` consumes.
7
+ *
8
+ * Slack omits `channel` from messages in the history response, so we stamp
9
+ * the request channel id onto each parsed message — downstream ingest needs
10
+ * it on every event.
11
+ */
5
12
  const SLACK_TIMEOUT_MS = 30_000;
6
13
  export function slackHistoryFetcher(token, fetchImpl) {
7
14
  return async ({ channelId, cursor, oldest }) => {
@@ -1,15 +1,13 @@
1
1
  /**
2
- * Slack workspace registration helpers (T2B follow-up, 2026-05-24).
2
+ * Slack workspace registration helpers.
3
3
  *
4
4
  * The `slack_workspaces` table maps Slack `team_id` → hippo `tenant_id`.
5
5
  * Empty table = single-tenant install (HIPPO_TENANT fallback).
6
6
  * Non-empty = multi-workspace install (fail-closed routing via
7
7
  * `resolveTenantForTeam`).
8
8
  *
9
- * Before T2B, populating this table required direct SQL — fine for a
10
- * single-machine deployment, awkward for operators with multiple
11
- * workspaces. These helpers give the CLI (`hippo slack workspaces
12
- * add|list|remove`) a clean surface.
9
+ * These helpers give the CLI (`hippo slack workspaces add|list|remove`) a
10
+ * surface, so operators with multiple workspaces need no direct SQL.
13
11
  *
14
12
  * Design choices:
15
13
  * - `add` is an upsert (ON CONFLICT UPDATE). Re-registering an
@@ -1,15 +1,13 @@
1
1
  /**
2
- * Slack workspace registration helpers (T2B follow-up, 2026-05-24).
2
+ * Slack workspace registration helpers.
3
3
  *
4
4
  * The `slack_workspaces` table maps Slack `team_id` → hippo `tenant_id`.
5
5
  * Empty table = single-tenant install (HIPPO_TENANT fallback).
6
6
  * Non-empty = multi-workspace install (fail-closed routing via
7
7
  * `resolveTenantForTeam`).
8
8
  *
9
- * Before T2B, populating this table required direct SQL — fine for a
10
- * single-machine deployment, awkward for operators with multiple
11
- * workspaces. These helpers give the CLI (`hippo slack workspaces
12
- * add|list|remove`) a clean surface.
9
+ * These helpers give the CLI (`hippo slack workspaces add|list|remove`) a
10
+ * surface, so operators with multiple workspaces need no direct SQL.
13
11
  *
14
12
  * Design choices:
15
13
  * - `add` is an upsert (ON CONFLICT UPDATE). Re-registering an
@@ -28,15 +28,11 @@ const CONFLICT_STOPWORDS = new Set([
28
28
  ]);
29
29
  /** Pairs of live non-semantic memories that contradict each other, in survivor order. */
30
30
  export function detectConflicts(entries, now, decayOpts = {},
31
- // LC2-E3 (opt-in, default off): ids rescued by this cycle's decay pass.
32
- // detectConflicts recomputes its own strength>=DECAY_THRESHOLD survivor
33
- // filter independently of the decay pass above; without this bypass,
34
- // rescued entries would be silently re-excluded from conflict detection
35
- // every cycle even though the decay pass just decided to keep them.
36
- // Default empty set: flag-off behavior is unchanged.
31
+ // Ids this cycle's decay pass rescued: the survivor filter below is recomputed independently, so
32
+ // without this bypass rescued entries would be silently re-excluded from conflict detection.
37
33
  rescuedIds = new Set()) {
38
34
  const survivors = entries.filter((entry) => entry.layer !== Layer.Semantic
39
- // CD5: an unreviewed quarantined row must not taint a visible memory as conflicted.
35
+ // An unreviewed quarantined row must not taint a visible memory as conflicted.
40
36
  && !isQuarantineScope(entry.scope ?? null)
41
37
  && (rescuedIds.has(entry.id) || calculateStrength(entry, now, decayOpts) >= DECAY_THRESHOLD));
42
38
  const detected = [];
@@ -165,13 +161,6 @@ function inferConflictPolarity(text) {
165
161
  return 'positive';
166
162
  return 'neutral';
167
163
  }
168
- function stripConflictPolarity(text) {
169
- return text
170
- .toLowerCase()
171
- .replace(/\b(?:not|never|no|don['’]?t|do\s+not|doesn['’]?t|does\s+not|can['’]?t|cannot|shouldn['’]?t|should\s+not|enabled|enable|disabled|disable|on|off|true|false|always|must|must\s+not|works?|working|missing|broken|failed|available|present)\b/g, ' ')
172
- .replace(/\s+/g, ' ')
173
- .trim();
174
- }
175
164
  function containsAny(text, needles) {
176
165
  return needles.some((needle) => text.includes(needle));
177
166
  }
@@ -38,17 +38,8 @@ function keepSurvivor(run, entry, strength) {
38
38
  // -------------------------------------------------------------------------
39
39
  // 1. Decay pass
40
40
  // -------------------------------------------------------------------------
41
- // LC2-E3 (opt-in, default off; docs/plans/2026-08-10-lc2-e3-mv-wiring.md):
42
- // flag OFF keeps the single-phase loop below byte-identical to pre-E3
43
- // behavior (pre-registered gate G2). Flag ON restructures into two phases:
44
- // phase 1 classifies every entry (condemned vs survivor) with ZERO
45
- // commits; phase 2 runs rescueSet over the per-tenant candidate groups,
46
- // then commits — rescued entries get the standard survivor bookkeeping
47
- // refresh (stored strength + effective confidence; no half-life edits, no
48
- // rank-derived writes) and are pushed to survivors so they fully
49
- // participate in this cycle's merge/physics/conflict passes; non-rescued
50
- // condemned entries follow the existing pendingDeletes/result.removed/
51
- // details path.
41
+ // Memory-value flag OFF runs the single-phase loop below. Flag ON classifies every entry with ZERO commits,
42
+ // runs rescueSet per tenant, then commits; rescued entries stay full survivors for this cycle's later passes.
52
43
  export function decayPass(run) {
53
44
  if (run.config.memoryValue.enabled)
54
45
  return decayWithMemoryValue(run);
@@ -76,10 +67,8 @@ function reportNonFiniteScores(result, rankById) {
76
67
  }
77
68
  function decayWithMemoryValue(run) {
78
69
  const { all, now, result } = run;
79
- // Carried forward to logRun, where the mv_rescue audit rows are actually
80
- // written (code-review fix: writing them here, before batchWriteAndDelete,
81
- // would assert rescues for a cycle whose effects might never land if a
82
- // later phase throws).
70
+ // Carried forward to logRun, where the mv_rescue audit rows are written: writing them here, before
71
+ // batchWriteAndDelete, would assert rescues whose effects might never land if a later phase throws.
83
72
  const rescuedEntries = [];
84
73
  let rescuedIds = new Set();
85
74
  let rankById = new Map();
@@ -98,31 +87,22 @@ function decayWithMemoryValue(run) {
98
87
  // logRun stay !dryRun-gated), so the preview matches what a
99
88
  // real run would decide.
100
89
  const condemnedIds = new Set(condemned.map((e) => e.id));
101
- // Fail-loud must not depend on condemnation traffic (round-2 code-review
102
- // P2-2): validate the frozen weights constant unconditionally, even on a
103
- // sleep with nothing condemned.
90
+ // Fail-loud must not depend on condemnation traffic: validate the frozen weights even when nothing is condemned.
104
91
  validateWeights();
105
92
  if (condemnedIds.size > 0) {
106
- // Compute the per-tenant ranking ONCE (round-2 code-review P2-2):
107
- // rankById feeds both rescueSet's decision (via precomputedRanks,
108
- // skipping its own internal rankNonPinnedByTenant call) and the
109
- // detail/audit rank context below, so the whole-store ranking pass
110
- // runs a single time per sleep instead of twice, and only when there
111
- // is actually something condemned to rank against.
93
+ // Rank per tenant ONCE: rankById feeds both rescueSet (via precomputedRanks) and the detail/audit
94
+ // context below, so the whole-store ranking runs once per sleep and only when something is condemned.
112
95
  rankById = rankNonPinnedByTenant(all, now);
113
96
  rescuedIds = rescueSet(all, condemnedIds, now, MEMORY_VALUE_WEIGHTS, SOURCE_ARTIFACT_SHA256, rankById);
114
97
  reportNonFiniteScores(result, rankById);
115
98
  }
116
99
  // --- Phase 2b: commit, one pass over `all` in ITS ORIGINAL ORDER ---
117
- // (review-round F4: rescued entries used to be appended at the tail of
118
- // survivors, systematically starving them in downstream order-sensitive
119
- // passes like extraction's slice(0,20) — a single pass over `all`
120
- // preserves flag-off's ordering semantics exactly.)
100
+ // Rescued entries appended at the tail would be starved by order-sensitive passes like extraction's slice(0,20).
121
101
  for (const entry of all) {
122
102
  const strength = strengthById.get(entry.id);
123
103
  if (run.retirable(entry) && strength < DECAY_THRESHOLD) {
124
104
  if (rescuedIds.has(entry.id)) {
125
- // Rescued (D1): standard survivor stored-strength refresh (P2-1).
105
+ // Rescued: standard survivor stored-strength refresh.
126
106
  // Confidence is left alone here: it is an epistemic tier, not a
127
107
  // cached computation, so resolveConfidence derives it on read.
128
108
  rescuedEntries.push(keepSurvivor(run, entry, strength));