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
@@ -0,0 +1,138 @@
1
+ // A checkout's `origin` remote, read from its git config file and normalised into a project id; no git process, so the prompt hook can call it.
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { isObjectLike, isStringValue } from './capture-contract.js';
5
+ import { log } from './log.js';
6
+ /** The config file git reads for a checkout: `.git/config`, a linked worktree's `<commondir>/config`, a submodule's `<gitdir>/config`. */
7
+ export function gitConfigPath(gitRoot) {
8
+ const marker = path.join(gitRoot, '.git');
9
+ try {
10
+ if (fs.statSync(marker).isDirectory())
11
+ return path.join(marker, 'config');
12
+ const gitDir = /^gitdir:\s*(.+)$/m.exec(fs.readFileSync(marker, 'utf8'))?.[1]?.trim();
13
+ if (!gitDir)
14
+ return null;
15
+ const linkDir = path.resolve(gitRoot, gitDir);
16
+ const commondir = path.join(linkDir, 'commondir');
17
+ if (!fs.existsSync(commondir))
18
+ return path.join(linkDir, 'config');
19
+ return path.join(path.resolve(linkDir, fs.readFileSync(commondir, 'utf8').trim()), 'config');
20
+ }
21
+ catch (err) {
22
+ log.debug(`project remote: no git config for ${gitRoot}: ${err instanceof Error ? err.message : String(err)}`);
23
+ return null;
24
+ }
25
+ }
26
+ /** One config value with git's quoting: `"` toggles quoting, backslash escapes, and an unquoted `#` or `;` starts a comment. */
27
+ function configValue(raw) {
28
+ let out = '';
29
+ let quoted = false;
30
+ for (let i = 0; i < raw.length; i++) {
31
+ const c = raw[i];
32
+ if (c === '\\' && i + 1 < raw.length) {
33
+ const next = raw[++i];
34
+ out += next === 'n' ? '\n' : next === 't' ? '\t' : next === 'b' ? '' : next;
35
+ }
36
+ else if (c === '"') {
37
+ quoted = !quoted;
38
+ }
39
+ else if (!quoted && (c === '#' || c === ';')) {
40
+ break;
41
+ }
42
+ else {
43
+ out += c;
44
+ }
45
+ }
46
+ return out.trim();
47
+ }
48
+ const SECTION = /^\[\s*([A-Za-z0-9.-]+)(?:\s+"((?:[^"\\]|\\.)*)")?\s*\](.*)$/;
49
+ const ENTRY = /^([A-Za-z][A-Za-z0-9-]*)\s*(?:=(.*))?$/;
50
+ /** `remote.origin.url` from a config file's text, the first value as git fetch uses; the subsection name is case-sensitive. */
51
+ export function originUrlFromConfig(text) {
52
+ let inOrigin = false;
53
+ for (const raw of text.split(/\r?\n/)) {
54
+ let line = raw.trim();
55
+ const section = SECTION.exec(line);
56
+ if (section) {
57
+ const [, name, sub, rest] = section;
58
+ inOrigin = sub === undefined
59
+ ? name.toLowerCase() === 'remote.origin'
60
+ : name.toLowerCase() === 'remote' && sub.replace(/\\(.)/g, '$1') === 'origin';
61
+ line = rest.trim();
62
+ }
63
+ const entry = inOrigin ? ENTRY.exec(line) : null;
64
+ if (entry && entry[1].toLowerCase() === 'url' && entry[2] !== undefined) {
65
+ const value = configValue(entry[2]);
66
+ if (value !== '')
67
+ return value;
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ /** Azure DevOps names one repo three ways; all become `dev.azure.com/org/project/repo`. */
73
+ function azureDevOps(hostPath) {
74
+ const ssh = /^(?:ssh\.dev\.azure\.com|vs-ssh\.visualstudio\.com)\/v3\/(.+)$/.exec(hostPath);
75
+ if (ssh)
76
+ return `dev.azure.com/${ssh[1]}`;
77
+ const legacy = /^([^/.]+)\.visualstudio\.com\/(?:defaultcollection\/)?(.+)$/.exec(hostPath);
78
+ const https = legacy ? `dev.azure.com/${legacy[1]}/${legacy[2]}` : hostPath;
79
+ return https.startsWith('dev.azure.com/') ? https.replace('/_git/', '/') : https;
80
+ }
81
+ /** `host/path` for a network remote, so ssh, scp and https forms agree; null for a local path, which would put a user path into rows. */
82
+ export function normaliseRemote(url) {
83
+ // A query or fragment can carry a token (`?private_token=`) and never names the repo.
84
+ const u = url.trim().replace(/[?#].*$/, '');
85
+ if (u === '' || u.includes('::') || /^file:/i.test(u) || /^[A-Za-z]:[\\/]/.test(u) || /^[./\\~]/.test(u))
86
+ return null;
87
+ let hostPath;
88
+ const withScheme = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/]*)(.*)$/.exec(u);
89
+ if (withScheme) {
90
+ // Userinfo can hold a token, and a port differs between ssh and https.
91
+ hostPath = withScheme[1].replace(/^.*@/, '').replace(/:\d*$/, '') + withScheme[2];
92
+ }
93
+ else {
94
+ const scp = /^(?:[^@/]+@)?([^:/]+):(.*)$/.exec(u);
95
+ if (!scp)
96
+ return null;
97
+ hostPath = `${scp[1]}/${scp[2]}`;
98
+ }
99
+ const tidy = hostPath.toLowerCase().replace(/\\/g, '/').replace(/\/{2,}/g, '/').replace(/\/+$/, '').replace(/\.git$/, '').replace(/\/+$/, '');
100
+ const id = azureDevOps(tidy);
101
+ // A colon would break the `shared:<project>:` source format; whitespace means this was no URL; an `@` left in a path may be a credential.
102
+ return /^[^/]+\/./.test(id) && !/[:\s@]/.test(id) ? id : null;
103
+ }
104
+ /** The normalised origin of the checkout rooted at gitRoot, or null when it has none a project can be named by. */
105
+ export function originRemoteId(gitRoot) {
106
+ const file = gitConfigPath(gitRoot);
107
+ if (file === null)
108
+ return null;
109
+ try {
110
+ const url = originUrlFromConfig(fs.readFileSync(file, 'utf8'));
111
+ return url === null ? null : normaliseRemote(url);
112
+ }
113
+ catch (err) {
114
+ log.debug(`project remote: ${file} not read: ${err instanceof Error ? err.message : String(err)}`);
115
+ return null;
116
+ }
117
+ }
118
+ export const PROJECT_FILE = '.hippo-project.json';
119
+ /** The `id` a team committed in `.hippo-project.json` at root, lowercased like every project name; null when absent or unusable. */
120
+ export function projectFileId(root) {
121
+ const file = path.join(root, PROJECT_FILE);
122
+ if (!fs.existsSync(file))
123
+ return null;
124
+ try {
125
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
126
+ const raw = isObjectLike(parsed) && 'id' in parsed ? parsed.id : undefined;
127
+ const id = isStringValue(raw) ? raw.trim().toLowerCase() : '';
128
+ // No slash: a committed file in an untrusted clone must not claim a remote-shaped id like `github.com/acme/api`.
129
+ if (id !== '' && id.length <= 200 && !/[:\s/\\]/.test(id))
130
+ return id;
131
+ log.warn(`${file}: "id" must be a non-empty string with no colon, slash or spaces; using the remote or folder name instead`);
132
+ }
133
+ catch (err) {
134
+ log.warn(`${file} not read: ${err instanceof Error ? err.message : String(err)}; using the remote or folder name instead`);
135
+ }
136
+ return null;
137
+ }
138
+ //# sourceMappingURL=project-remote.js.map
@@ -1,5 +1,4 @@
1
- /** Z1: recall gated on the hook prompt, not the five newest memories (pure, no I/O).
2
- * See docs/plans/2026-09-26-z1-prompt-recall.md. */
1
+ /** Recall gated on the hook prompt, not the five newest memories (pure, no I/O). */
3
2
  import { tokenize } from './tokenize.js';
4
3
  import { STOP_WORDS } from './audit.js';
5
4
  // Latency bound, not tuned: fixed in the prereg regardless of gate config.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Per-key token-bucket rate limiter for inbound /v1/* requests.
3
3
  *
4
- * Bounds api-key-id enumeration (the v0.40 follow-up noted in auth.ts): a
4
+ * Bounds api-key-id enumeration: a
5
5
  * client that drains its bucket is denied until it refills. Dependency-free
6
6
  * and unit-testable in isolation via the injectable `now`.
7
7
  */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Per-key token-bucket rate limiter for inbound /v1/* requests.
3
3
  *
4
- * Bounds api-key-id enumeration (the v0.40 follow-up noted in auth.ts): a
4
+ * Bounds api-key-id enumeration: a
5
5
  * client that drains its bucket is denied until it refills. Dependency-free
6
6
  * and unit-testable in isolation via the injectable `now`.
7
7
  */
@@ -11,5 +11,14 @@ export interface ArchiveOpts {
11
11
  */
12
12
  afterArchive?: (db: DatabaseSyncLike, archivedMemoryId: string) => void;
13
13
  }
14
+ /**
15
+ * The only legitimate path to remove a `kind='raw'` row from `memories`.
16
+ *
17
+ * Snapshots the full row into `raw_archive`, flips `kind` to `'archived'` so the
18
+ * append-only trigger lets the delete through, then deletes the row. All in one
19
+ * SAVEPOINT so it can be nested inside an outer transaction (e.g. batchWriteAndDelete).
20
+ *
21
+ * Throws if the row does not exist or is not `kind='raw'`.
22
+ */
14
23
  export declare function archiveRawMemory(db: DatabaseSyncLike, id: string, opts: ArchiveOpts): void;
15
24
  //# sourceMappingURL=raw-archive.d.ts.map
@@ -2,7 +2,7 @@ import { BadRequestError, NotFoundError } from './api-errors.js';
2
2
  import { isFtsAvailable } from './db.js';
3
3
  import { appendAuditEvent, reportAuditWriteFailure } from './audit.js';
4
4
  import { markSummaryDirtyInTx } from './summary-dirty.js';
5
- export function archiveRawMemory(db, id, opts) {
5
+ function loadRawRow(db, id) {
6
6
  // SAFETY: SELECT * FROM memories returns every column of the memories table; only
7
7
  // kind, tenant_id, and dag_parent_id are read below, all guaranteed present (possibly
8
8
  // null) by the memories schema.
@@ -12,65 +12,82 @@ export function archiveRawMemory(db, id, opts) {
12
12
  if (row.kind !== 'raw') {
13
13
  throw new BadRequestError(`memory ${id} is not raw (kind=${String(row.kind)})`);
14
14
  }
15
+ return row;
16
+ }
17
+ function moveRowToArchive(db, id, row, opts) {
18
+ // GDPR: raw_archive stores ONLY metadata, not the original
19
+ // memory content. The audit_log row appended below carries op='archive_raw'
20
+ // for the compliance audit trail. True right-to-be-forgotten — the original
21
+ // content is unrecoverable from raw_archive after this point.
22
+ const archivedAt = new Date().toISOString();
23
+ const redactedPayload = JSON.stringify({
24
+ redacted: true,
25
+ archived_at: archivedAt,
26
+ tenant_id: row.tenant_id ?? 'default',
27
+ kind: row.kind,
28
+ reason: opts.reason,
29
+ });
30
+ db.prepare(`INSERT INTO raw_archive (memory_id, archived_at, reason, archived_by, payload_json) VALUES (?, ?, ?, ?, ?)`).run(id, archivedAt, opts.reason, opts.who, redactedPayload);
31
+ // Flip kind to 'archived' so the BEFORE DELETE trigger no longer fires, then delete.
32
+ db.prepare(`UPDATE memories SET kind = 'archived' WHERE id = ?`).run(id);
33
+ db.prepare(`DELETE FROM memories WHERE id = ?`).run(id);
34
+ // FTS5 is a virtual table — no FK CASCADE applies. Purge the FTS row so the
35
+ // archived content is not searchable after archive. Without this the original
36
+ // raw text remains in memories_fts until the next DB-open backfill, defeating
37
+ // GDPR right-to-be-forgotten.
38
+ if (isFtsAvailable(db)) {
39
+ try {
40
+ db.prepare(`DELETE FROM memories_fts WHERE id = ?`).run(id);
41
+ }
42
+ catch {
43
+ // Best effort only. The DELETE on memories already succeeded; FTS will
44
+ // self-heal on next DB open via backfillFtsIndex.
45
+ }
46
+ }
47
+ }
48
+ // Emit the archive_raw audit event inside the SAVEPOINT so the audit row is
49
+ // committed atomically with the row deletion. Use the row's own tenant_id
50
+ // (fetched above as part of SELECT *), not the env. Archives must be
51
+ // attributed to the tenant that owns the row, not whatever HIPPO_TENANT
52
+ // happens to be set to in the calling shell.
53
+ function auditArchive(db, id, row, opts) {
54
+ try {
55
+ appendAuditEvent(db, {
56
+ tenantId: String(row.tenant_id ?? 'default'),
57
+ actor: opts.who || 'cli',
58
+ op: 'archive_raw',
59
+ targetId: id,
60
+ metadata: { reason: opts.reason },
61
+ });
62
+ }
63
+ catch (error) {
64
+ // The archive itself has already succeeded; an unwritable audit table must not undo it.
65
+ reportAuditWriteFailure('archive_raw', String(error), id);
66
+ }
67
+ }
68
+ /**
69
+ * The only legitimate path to remove a `kind='raw'` row from `memories`.
70
+ *
71
+ * Snapshots the full row into `raw_archive`, flips `kind` to `'archived'` so the
72
+ * append-only trigger lets the delete through, then deletes the row. All in one
73
+ * SAVEPOINT so it can be nested inside an outer transaction (e.g. batchWriteAndDelete).
74
+ *
75
+ * Throws if the row does not exist or is not `kind='raw'`.
76
+ */
77
+ export function archiveRawMemory(db, id, opts) {
78
+ const row = loadRawRow(db, id);
15
79
  // SAVEPOINT (not BEGIN) so this works whether or not we're already inside a
16
80
  // transaction. SQLite refuses BEGIN within a transaction; SAVEPOINT nests safely.
17
81
  db.exec('SAVEPOINT archive_raw');
18
82
  try {
19
- // GDPR Path A (v0.39): raw_archive stores ONLY metadata, not the original
20
- // memory content. The audit_log row appended below carries op='archive_raw'
21
- // for the compliance audit trail. True right-to-be-forgotten — the original
22
- // content is unrecoverable from raw_archive after this point.
23
- const archivedAt = new Date().toISOString();
24
- const redactedPayload = JSON.stringify({
25
- redacted: true,
26
- archived_at: archivedAt,
27
- tenant_id: row.tenant_id ?? 'default',
28
- kind: row.kind,
29
- reason: opts.reason,
30
- });
31
- db.prepare(`INSERT INTO raw_archive (memory_id, archived_at, reason, archived_by, payload_json) VALUES (?, ?, ?, ?, ?)`).run(id, archivedAt, opts.reason, opts.who, redactedPayload);
32
- // Flip kind to 'archived' so the BEFORE DELETE trigger no longer fires, then delete.
33
- db.prepare(`UPDATE memories SET kind = 'archived' WHERE id = ?`).run(id);
34
- db.prepare(`DELETE FROM memories WHERE id = ?`).run(id);
35
- // FTS5 is a virtual table — no FK CASCADE applies. Purge the FTS row so the
36
- // archived content is not searchable after archive. Without this the original
37
- // raw text remains in memories_fts until the next DB-open backfill, defeating
38
- // GDPR right-to-be-forgotten.
39
- if (isFtsAvailable(db)) {
40
- try {
41
- db.prepare(`DELETE FROM memories_fts WHERE id = ?`).run(id);
42
- }
43
- catch {
44
- // Best effort only. The DELETE on memories already succeeded; FTS will
45
- // self-heal on next DB open via backfillFtsIndex.
46
- }
47
- }
48
- // A5 audit: emit archive_raw event inside the SAVEPOINT so the audit row is
49
- // committed atomically with the row deletion. Use the row's own tenant_id
50
- // (fetched above as part of SELECT *), not the env. Archives must be
51
- // attributed to the tenant that owns the row, not whatever HIPPO_TENANT
52
- // happens to be set to in the calling shell.
53
- try {
54
- appendAuditEvent(db, {
55
- tenantId: String(row.tenant_id ?? 'default'),
56
- actor: opts.who || 'cli',
57
- op: 'archive_raw',
58
- targetId: id,
59
- metadata: { reason: opts.reason },
60
- });
61
- }
62
- catch (error) {
63
- // The archive itself has already succeeded; an unwritable audit table must not undo it.
64
- reportAuditWriteFailure('archive_raw', String(error), id);
65
- }
66
- // v0.30 / E2 — DAG live-coupling: archive of a child under a level-2
67
- // summary marks parent dirty. Inside the SAVEPOINT so the dirty-mark
68
- // commits atomically with the archive. row.dag_parent_id was fetched
69
- // via SELECT * at L28 (schema v28 includes it).
83
+ moveRowToArchive(db, id, row, opts);
84
+ auditArchive(db, id, row, opts);
85
+ // Archiving a child under a level-2 summary marks the parent dirty, inside the
86
+ // SAVEPOINT so the dirty-mark commits atomically with the archive.
70
87
  if (row.dag_parent_id) {
71
88
  markSummaryDirtyInTx(db, String(row.dag_parent_id), String(row.tenant_id ?? 'default'), opts.who || 'cli');
72
89
  }
73
- // afterArchive hook (v0.39 commit 3): connector-level idempotency markers
90
+ // afterArchive hook: connector-level idempotency markers
74
91
  // (e.g. slack_event_log) must commit atomically with the archive itself.
75
92
  // Throwing here rolls back the entire SAVEPOINT — both the archive and any
76
93
  // hook side effects. The hook runs INSIDE the SAVEPOINT so its writes
@@ -1,21 +1,20 @@
1
1
  /**
2
- * J1 anchoring detector (recall-recurrence) — pure module.
2
+ * Anchoring detector (recall-recurrence), pure module.
3
3
  *
4
- * Implements two detection rules from ROADMAP-RESEARCH.md L546:
5
- * R1 query_repeat: same queryHash within recentRepeatWindow returned
4
+ * Implements two detection rules:
5
+ * query_repeat: same queryHash within recentRepeatWindow returned
6
6
  * same topMemoryId (caller is re-asking the same question).
7
- * R2 memory_dominance: same topMemoryId across >= minDominance distinct
7
+ * memory_dominance: same topMemoryId across >= minDominance distinct
8
8
  * queryHashes (memory acts as a fixed-point anchor regardless of what
9
9
  * the agent asks).
10
10
  *
11
- * Per the plan v3 architectural decision: each pipeline (api.recall via
11
+ * Each pipeline (api.recall via
12
12
  * HTTP, cmdRecall, MCP hippo_recall) owns its OWN ring buffer Map keyed
13
13
  * by (tenant, session). No cross-pipeline sharing (the typical multi-
14
14
  * process deployment makes IPC ring-sharing impractical; per-pipeline
15
15
  * is correct because each pipeline has its own top-1 ranking anyway).
16
16
  *
17
- * Plan: docs/plans/2026-05-26-j1-anchoring-detector.md.
18
- * Composes with J3.2: AnchoringHint + PlanningFallacyHint are independent
17
+ * AnchoringHint + PlanningFallacyHint are independent
19
18
  * signals; both can fire on the same recall.
20
19
  */
21
20
  export type AnchoringReason = 'query_repeat' | 'memory_dominance';
@@ -36,20 +35,20 @@ export interface RecallHistoryEntry {
36
35
  queryHash: number;
37
36
  /** Top-1 memory id this recall surfaced; null if zero results. */
38
37
  topMemoryId: string | null;
39
- /** ISO-8601 timestamp; advisory, not used by R1/R2 logic. */
38
+ /** ISO-8601 timestamp; advisory, not used by the detection rules. */
40
39
  ts: string;
41
40
  /** Memory id of the AnchoringHint that fired on this recall, if any.
42
- * Used by R1/R2 cooldown logic to prevent re-emitting the same hint on
41
+ * Used by the cooldown logic to prevent re-emitting the same hint on
43
42
  * consecutive recalls within the dominance window. Caller-written
44
43
  * AFTER detectAnchoring returns; reads next time detectAnchoring runs. */
45
44
  anchoredOn?: string;
46
45
  }
47
46
  export type RecallHistorySnapshot = readonly RecallHistoryEntry[];
48
47
  export interface DetectAnchoringOpts {
49
- /** R2 threshold: number of distinct queryHashes that must have returned
48
+ /** memory_dominance threshold: number of distinct queryHashes that must have returned
50
49
  * the same topMemoryId. Default 3. */
51
50
  minDominance?: number;
52
- /** R1 window: how many recent history entries to scan for query repeat.
51
+ /** query_repeat window: how many recent history entries to scan for query repeat.
53
52
  * Default 5. */
54
53
  recentRepeatWindow?: number;
55
54
  /** Cooldown: if the immediately-prior fire (per `anchoredOn`) was for
@@ -63,8 +62,8 @@ export interface DetectAnchoringOpts {
63
62
  * sort tokens → join → FNV-1a 32-bit.
64
63
  *
65
64
  * Token sort + dedup means semantically-equivalent queries with reordered
66
- * words collide intentionally (the roadmap's "semantically-distinct" v1
67
- * uses textual normalization; embedding-based distinctness is J1-v2).
65
+ * words collide intentionally ("semantically-distinct" is approximated by
66
+ * textual normalization, not embeddings).
68
67
  *
69
68
  * Deterministic across processes; stable across Node + V8 versions.
70
69
  */
@@ -73,17 +72,17 @@ export declare function hashQueryText(query: string): number;
73
72
  * Detect anchoring patterns in the recall history against the current
74
73
  * recall's (queryHash, topMemoryId).
75
74
  *
76
- * Rule precedence: R2 (memory_dominance) wins on tie. When both R1 and R2
77
- * fire on the same recall, return only the R2 hint — R2's signal is the
78
- * cognitively stronger one (a memory dominating multiple DIFFERENT queries
79
- * is a fixed-point anchor; R1 alone is just a literal re-ask).
75
+ * Rule precedence: memory_dominance wins on tie. When both rules fire on the
76
+ * same recall, return only memory_dominance, the stronger signal (a memory
77
+ * dominating multiple DIFFERENT queries is a fixed-point anchor; query_repeat
78
+ * alone is just a literal re-ask).
80
79
  *
81
80
  * Cooldown: if the immediately-prior recall fired a hint on the SAME
82
81
  * topMemoryId within `cooldown=3` history entries, suppress. Prevents
83
82
  * spam when the agent repeatedly recalls within the dominance window.
84
- * Cooldown is per-memory, not per-rule: if R2 fired on M (cooldown
85
- * engaged for M), and the next recall has top=N + repeated query →
86
- * R1 fires on N (different memory, not in cooldown).
83
+ * Cooldown is per-memory, not per-rule: if memory_dominance fired on M
84
+ * (cooldown engaged for M), and the next recall has top=N + repeated query,
85
+ * query_repeat fires on N (different memory, not in cooldown).
87
86
  *
88
87
  * @returns AnchoringHint when a pattern fires; null otherwise.
89
88
  */
@@ -1,21 +1,20 @@
1
1
  /**
2
- * J1 anchoring detector (recall-recurrence) — pure module.
2
+ * Anchoring detector (recall-recurrence), pure module.
3
3
  *
4
- * Implements two detection rules from ROADMAP-RESEARCH.md L546:
5
- * R1 query_repeat: same queryHash within recentRepeatWindow returned
4
+ * Implements two detection rules:
5
+ * query_repeat: same queryHash within recentRepeatWindow returned
6
6
  * same topMemoryId (caller is re-asking the same question).
7
- * R2 memory_dominance: same topMemoryId across >= minDominance distinct
7
+ * memory_dominance: same topMemoryId across >= minDominance distinct
8
8
  * queryHashes (memory acts as a fixed-point anchor regardless of what
9
9
  * the agent asks).
10
10
  *
11
- * Per the plan v3 architectural decision: each pipeline (api.recall via
11
+ * Each pipeline (api.recall via
12
12
  * HTTP, cmdRecall, MCP hippo_recall) owns its OWN ring buffer Map keyed
13
13
  * by (tenant, session). No cross-pipeline sharing (the typical multi-
14
14
  * process deployment makes IPC ring-sharing impractical; per-pipeline
15
15
  * is correct because each pipeline has its own top-1 ranking anyway).
16
16
  *
17
- * Plan: docs/plans/2026-05-26-j1-anchoring-detector.md.
18
- * Composes with J3.2: AnchoringHint + PlanningFallacyHint are independent
17
+ * AnchoringHint + PlanningFallacyHint are independent
19
18
  * signals; both can fire on the same recall.
20
19
  */
21
20
  import { envAnchoringOff, envAvailabilityOff } from './env.js';
@@ -31,44 +30,27 @@ const DEFAULT_COOLDOWN = 3;
31
30
  * sort tokens → join → FNV-1a 32-bit.
32
31
  *
33
32
  * Token sort + dedup means semantically-equivalent queries with reordered
34
- * words collide intentionally (the roadmap's "semantically-distinct" v1
35
- * uses textual normalization; embedding-based distinctness is J1-v2).
33
+ * words collide intentionally ("semantically-distinct" is approximated by
34
+ * textual normalization, not embeddings).
36
35
  *
37
36
  * Deterministic across processes; stable across Node + V8 versions.
38
37
  */
39
38
  export function hashQueryText(query) {
40
39
  if (!query)
41
40
  return 0;
42
- // Token dedup before join: the R2 contract says "distinct queries"
43
- // means semantically distinct, so `foo bar` and `foo foo bar` should
44
- // collapse to the same hash. Without dedup, simple phrasing
45
- // variations (typos, doubled tokens, intensifiers) would inflate
46
- // distinct-query counts and trip R2 on essentially the same question.
47
- // Codex round-1 catch.
48
- // Unicode-aware tokenization (codex round-3 P2 catch): the prior
49
- // ASCII-only [^a-z0-9\s] pattern stripped every non-Latin letter, so
50
- // Japanese, Arabic, Cyrillic, accented-Latin etc. queries collapsed
51
- // to empty token set -> hash 0 -> false R1 collisions across distinct
52
- // non-English queries. \p{L} = any Unicode letter, \p{N} = any Unicode
53
- // number, \p{M} = combining marks (preserve composed accented chars).
54
- // Requires the /u flag and Node >= 12.
41
+ // Tokens are deduped before join so `foo foo bar` and `foo bar` count as one query for memory_dominance.
42
+ // Unicode classes (\p{L} letter, \p{N} number, \p{M} combining mark; needs /u) keep
43
+ // non-Latin queries from collapsing to an empty token set and colliding at hash 0.
55
44
  const normalized = query
56
45
  .toLowerCase()
57
46
  .replace(/[^\p{L}\p{N}\p{M}\s]/gu, ' ')
58
47
  .split(/\s+/)
59
48
  .filter((t) => t.length > 0);
60
- // Drop tokens shorter than 3 chars to match the normalizer contract
61
- // (filler / stop words). Without this, `a login bug` vs `login bug`
62
- // hash differently and inflate R2 distinct-query counts, firing
63
- // memory_dominance on repeated phrasings of the same question.
64
- // Codex round-4 P2 catch. Matches the same >=3 filter in
65
- // src/forward-claim-detector.ts for class-resolver tokens.
49
+ // Drop tokens under 3 chars (filler words) so `a login bug` and `login bug` hash
50
+ // alike; matches the >=3 filter in src/forward-claim-detector.ts.
66
51
  const filtered = normalized.filter((t) => t.length >= 3);
67
- // Fallback when the >=3 filter would collapse the entire query to
68
- // empty: CJK queries like `测试` / `环境` are 2-char tokens; English
69
- // acronyms like `AI` / `UI` are 2 chars. Without this fallback they
70
- // all hash to fnv1a32(''), producing false R1 collisions across
71
- // distinct short-token queries. Codex round-5 P2 catch.
52
+ // Fall back to all tokens when the filter empties the query (CJK 2-char words,
53
+ // acronyms like `AI`) so distinct short queries do not all hash to fnv1a32('').
72
54
  const tokens = filtered.length > 0 ? filtered : normalized;
73
55
  const deduped = Array.from(new Set(tokens)).sort();
74
56
  return fnv1a32(deduped.join(' '));
@@ -91,17 +73,17 @@ function fnv1a32(text) {
91
73
  * Detect anchoring patterns in the recall history against the current
92
74
  * recall's (queryHash, topMemoryId).
93
75
  *
94
- * Rule precedence: R2 (memory_dominance) wins on tie. When both R1 and R2
95
- * fire on the same recall, return only the R2 hint — R2's signal is the
96
- * cognitively stronger one (a memory dominating multiple DIFFERENT queries
97
- * is a fixed-point anchor; R1 alone is just a literal re-ask).
76
+ * Rule precedence: memory_dominance wins on tie. When both rules fire on the
77
+ * same recall, return only memory_dominance, the stronger signal (a memory
78
+ * dominating multiple DIFFERENT queries is a fixed-point anchor; query_repeat
79
+ * alone is just a literal re-ask).
98
80
  *
99
81
  * Cooldown: if the immediately-prior recall fired a hint on the SAME
100
82
  * topMemoryId within `cooldown=3` history entries, suppress. Prevents
101
83
  * spam when the agent repeatedly recalls within the dominance window.
102
- * Cooldown is per-memory, not per-rule: if R2 fired on M (cooldown
103
- * engaged for M), and the next recall has top=N + repeated query →
104
- * R1 fires on N (different memory, not in cooldown).
84
+ * Cooldown is per-memory, not per-rule: if memory_dominance fired on M
85
+ * (cooldown engaged for M), and the next recall has top=N + repeated query,
86
+ * query_repeat fires on N (different memory, not in cooldown).
105
87
  *
106
88
  * @returns AnchoringHint when a pattern fires; null otherwise.
107
89
  */
@@ -119,7 +101,7 @@ export function detectAnchoring(history, currentQueryHash, currentTopMemoryId, o
119
101
  return null;
120
102
  }
121
103
  }
122
- // R2 check FIRST (wins on tie). Count distinct queryHashes in history
104
+ // memory_dominance check FIRST (wins on tie). Count distinct queryHashes in history
123
105
  // where topMemoryId === currentTopMemoryId (excluding null tops).
124
106
  const matchingQueryHashes = new Set();
125
107
  for (const entry of history) {
@@ -139,7 +121,7 @@ export function detectAnchoring(history, currentQueryHash, currentTopMemoryId, o
139
121
  source: 'j1-recurrence',
140
122
  };
141
123
  }
142
- // R1 check: is currentQueryHash present in the last `recentRepeatWindow`
124
+ // query_repeat check: is currentQueryHash present in the last `recentRepeatWindow`
143
125
  // entries AND was that entry's topMemoryId === currentTopMemoryId?
144
126
  const r1Slice = history.slice(-recentRepeatWindow);
145
127
  for (const entry of r1Slice) {
@@ -11,6 +11,7 @@ import { DEFAULT_GRAPH_STREAM_WEIGHT } from './graph-stream.js';
11
11
  import { Layer } from './memory.js';
12
12
  import { multihopSearch } from './multihop.js';
13
13
  import { passesCliRecallScopeFilter } from './recall-scope.js';
14
+ import { currentEntries } from './search/as-of.js';
14
15
  import { hybridSearch } from './search/hybrid.js';
15
16
  import { physicsSearch } from './search/physics-search.js';
16
17
  import { searchBothHybrid } from './shared.js';
@@ -72,36 +73,11 @@ function loadRecallPool(ctx, opts) {
72
73
  const passes = (e) => passesCliRecallScopeFilter(e.scope ?? null, requested);
73
74
  local = local.filter(passes);
74
75
  global = global.filter(passes);
75
- if (opts.asOf) {
76
- local = currentAsOf(local, opts.asOf);
77
- global = currentAsOf(global, opts.asOf);
78
- }
79
- else if (!opts.includeSuperseded) {
80
- local = local.filter((e) => !e.superseded_by);
81
- global = global.filter((e) => !e.superseded_by);
82
- }
76
+ const currentness = { asOf: opts.asOf, includeSuperseded: opts.includeSuperseded };
77
+ local = currentEntries(local, currentness);
78
+ global = currentEntries(global, currentness);
83
79
  return { local, global, total, dropped: total - (local.length + global.length) };
84
80
  }
85
- /** Rows that were true at `asOf`: valid by then, and not yet replaced by a successor in the same pool. */
86
- function currentAsOf(entries, asOf) {
87
- const asOfDate = new Date(asOf);
88
- const successorValidFrom = new Map();
89
- for (const e of entries) {
90
- if (e.superseded_by) {
91
- const successor = entries.find(s => s.id === e.superseded_by);
92
- if (successor)
93
- successorValidFrom.set(e.id, successor.valid_from);
94
- }
95
- }
96
- return entries.filter(e => {
97
- if (new Date(e.valid_from) > asOfDate)
98
- return false;
99
- if (!e.superseded_by)
100
- return true;
101
- const succVf = successorValidFrom.get(e.id);
102
- return succVf ? new Date(succVf) > asOfDate : true;
103
- });
104
- }
105
81
  async function searchPool(ctx, opts, pool) {
106
82
  const { query, budget, cost, includeSuperseded, asOf, activeScope: scope, search } = opts;
107
83
  const { mmr, mmrLambda, minResults, explain } = search;
@@ -1,8 +1,7 @@
1
1
  /**
2
- * v1.25.0 — recall-side scope predicates, extracted from api.ts into a leaf
3
- * module so shared.ts (which api.ts imports) can apply the same default-deny
4
- * rule to searchBothHybrid's internal candidate loads without an import
5
- * cycle. Mirrors the v39 `project-identity.ts` precedent. api.ts imports
2
+ * Recall-side scope predicates, in a leaf module so shared.ts (which api.ts
3
+ * imports) can apply the same default-deny rule to searchBothHybrid's internal
4
+ * candidate loads without an import cycle. api.ts imports
6
5
  * these for its own call sites AND re-exports them for back-compat
7
6
  * (`api.isPrivateScope`, test imports of `passesScopeFilterForRecall`).
8
7
  */
@@ -30,7 +29,7 @@ export declare const RECALL_DEFAULT_DENY_SCOPES: readonly ["unknown:legacy"];
30
29
  */
31
30
  export declare function assertNonEmpty<T>(arr: readonly T[], name: string): void;
32
31
  /**
33
- * v1.2.1: source-agnostic private-scope detector. A scope string is treated
32
+ * Source-agnostic private-scope detector. A scope string is treated
34
33
  * as private when it has the shape `<lowercase-source>:private:<rest>`.
35
34
  *
36
35
  * Examples that match:
@@ -56,14 +55,14 @@ export declare function isPrivateScope(scope: string | null | undefined): boolea
56
55
  * `<source>:private:*` scope and on the `RECALL_DEFAULT_DENY_SCOPES`
57
56
  * quarantine buckets. `null` and public scopes pass.
58
57
  *
59
- * @internal v1.7.2 — exported for test parity with
58
+ * @internal Exported for test parity with
60
59
  * `RECALL_DEFAULT_DENY_SCOPES` (single-source-of-truth verification). NOT part
61
60
  * of the public API surface; not re-exported from `src/index.ts`. Subject to
62
61
  * change without semver bump.
63
62
  */
64
63
  export declare function passesScopeFilterForRecall(scope: string | null, requested: string | undefined): boolean;
65
64
  /**
66
- * v1.25.0 — the CLI `--scope` variant of the recall filter (JS half of the
65
+ * The CLI `--scope` variant of the recall filter (JS half of the
67
66
  * SQL 'default-deny-or-exact' mode in loadSearchRows).
68
67
  *
69
68
  * The CLI flag predates the envelope column as a TAG-boost ranking hint
@@ -101,7 +100,7 @@ export interface ScopeActor {
101
100
  }
102
101
  /** True when `actor` may read `scope`: admin always; member needs an exact grant on a restricted scope. */
103
102
  export declare function canReadScope(actor: ScopeActor, scope: string): boolean;
104
- /** Authorize an explicitly requested scope before any read honours it (ROADMAP Part VIII EI2: member scope grants). */
103
+ /** Authorize an explicitly requested scope before any read honours it (member scope grants). */
105
104
  export declare function assertScopeRequestAllowed(actor: ScopeActor, requested: string | undefined): void;
106
105
  /** Scope a derived row keeps from one source: the restricted scope itself, else null. */
107
106
  export declare function derivationScope(scope: string | null | undefined): string | null;