@illuma-ai/agents 1.1.25 → 1.3.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 (272) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +20 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/spawnPath.cjs +104 -0
  4. package/dist/cjs/common/spawnPath.cjs.map +1 -0
  5. package/dist/cjs/graphs/Graph.cjs +87 -31
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/HandoffRegistry.cjs +143 -0
  8. package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -0
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs +587 -184
  10. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  11. package/dist/cjs/graphs/phases/flushLoop.cjs +214 -0
  12. package/dist/cjs/graphs/phases/flushLoop.cjs.map +1 -0
  13. package/dist/cjs/graphs/phases/memoryFlushPhase.cjs +102 -0
  14. package/dist/cjs/graphs/phases/memoryFlushPhase.cjs.map +1 -0
  15. package/dist/cjs/llm/bedrock/index.cjs +4 -3
  16. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  17. package/dist/cjs/main.cjs +115 -0
  18. package/dist/cjs/main.cjs.map +1 -1
  19. package/dist/cjs/memory/citations.cjs +69 -0
  20. package/dist/cjs/memory/citations.cjs.map +1 -0
  21. package/dist/cjs/memory/compositeBackend.cjs +60 -0
  22. package/dist/cjs/memory/compositeBackend.cjs.map +1 -0
  23. package/dist/cjs/memory/constants.cjs +232 -0
  24. package/dist/cjs/memory/constants.cjs.map +1 -0
  25. package/dist/cjs/memory/embeddings.cjs +151 -0
  26. package/dist/cjs/memory/embeddings.cjs.map +1 -0
  27. package/dist/cjs/memory/factory.cjs +95 -0
  28. package/dist/cjs/memory/factory.cjs.map +1 -0
  29. package/dist/cjs/memory/migrate.cjs +81 -0
  30. package/dist/cjs/memory/migrate.cjs.map +1 -0
  31. package/dist/cjs/memory/mmr.cjs +138 -0
  32. package/dist/cjs/memory/mmr.cjs.map +1 -0
  33. package/dist/cjs/memory/paths.cjs +217 -0
  34. package/dist/cjs/memory/paths.cjs.map +1 -0
  35. package/dist/cjs/memory/pgvectorStore.cjs +225 -0
  36. package/dist/cjs/memory/pgvectorStore.cjs.map +1 -0
  37. package/dist/cjs/memory/recallTracking.cjs +98 -0
  38. package/dist/cjs/memory/recallTracking.cjs.map +1 -0
  39. package/dist/cjs/memory/schema.sql +51 -0
  40. package/dist/cjs/memory/temporalDecay.cjs +118 -0
  41. package/dist/cjs/memory/temporalDecay.cjs.map +1 -0
  42. package/dist/cjs/nodes/ApprovalGateNode.cjs +1 -1
  43. package/dist/cjs/nodes/ApprovalGateNode.cjs.map +1 -1
  44. package/dist/cjs/prompts/memoryFlushPrompt.cjs +49 -0
  45. package/dist/cjs/prompts/memoryFlushPrompt.cjs.map +1 -0
  46. package/dist/cjs/run.cjs +16 -3
  47. package/dist/cjs/run.cjs.map +1 -1
  48. package/dist/cjs/stream.cjs +4 -4
  49. package/dist/cjs/stream.cjs.map +1 -1
  50. package/dist/cjs/tools/AskUser.cjs +6 -1
  51. package/dist/cjs/tools/AskUser.cjs.map +1 -1
  52. package/dist/cjs/tools/BrowserTools.cjs +1 -1
  53. package/dist/cjs/tools/BrowserTools.cjs.map +1 -1
  54. package/dist/cjs/tools/ToolNode.cjs +127 -10
  55. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  56. package/dist/cjs/tools/approval/constants.cjs +2 -2
  57. package/dist/cjs/tools/approval/constants.cjs.map +1 -1
  58. package/dist/cjs/tools/memory/index.cjs +58 -0
  59. package/dist/cjs/tools/memory/index.cjs.map +1 -0
  60. package/dist/cjs/tools/memory/memoryAppendTool.cjs +69 -0
  61. package/dist/cjs/tools/memory/memoryAppendTool.cjs.map +1 -0
  62. package/dist/cjs/tools/memory/memoryGetTool.cjs +49 -0
  63. package/dist/cjs/tools/memory/memoryGetTool.cjs.map +1 -0
  64. package/dist/cjs/tools/memory/memorySearchTool.cjs +65 -0
  65. package/dist/cjs/tools/memory/memorySearchTool.cjs.map +1 -0
  66. package/dist/cjs/tools/memory/shared.cjs +106 -0
  67. package/dist/cjs/tools/memory/shared.cjs.map +1 -0
  68. package/dist/cjs/types/graph.cjs.map +1 -1
  69. package/dist/cjs/utils/childAgentContext.cjs +242 -0
  70. package/dist/cjs/utils/childAgentContext.cjs.map +1 -0
  71. package/dist/cjs/utils/events.cjs +36 -4
  72. package/dist/cjs/utils/events.cjs.map +1 -1
  73. package/dist/cjs/utils/finishReasons.cjs +44 -0
  74. package/dist/cjs/utils/finishReasons.cjs.map +1 -0
  75. package/dist/cjs/utils/llm.cjs.map +1 -1
  76. package/dist/cjs/utils/logging.cjs +34 -0
  77. package/dist/cjs/utils/logging.cjs.map +1 -0
  78. package/dist/cjs/utils/toolCallNormalization.cjs +250 -0
  79. package/dist/cjs/utils/toolCallNormalization.cjs.map +1 -0
  80. package/dist/esm/agents/AgentContext.mjs +20 -3
  81. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  82. package/dist/esm/common/spawnPath.mjs +95 -0
  83. package/dist/esm/common/spawnPath.mjs.map +1 -0
  84. package/dist/esm/graphs/Graph.mjs +87 -31
  85. package/dist/esm/graphs/Graph.mjs.map +1 -1
  86. package/dist/esm/graphs/HandoffRegistry.mjs +141 -0
  87. package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -0
  88. package/dist/esm/graphs/MultiAgentGraph.mjs +587 -184
  89. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  90. package/dist/esm/graphs/phases/flushLoop.mjs +209 -0
  91. package/dist/esm/graphs/phases/flushLoop.mjs.map +1 -0
  92. package/dist/esm/graphs/phases/memoryFlushPhase.mjs +99 -0
  93. package/dist/esm/graphs/phases/memoryFlushPhase.mjs.map +1 -0
  94. package/dist/esm/llm/bedrock/index.mjs +4 -3
  95. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  96. package/dist/esm/main.mjs +21 -0
  97. package/dist/esm/main.mjs.map +1 -1
  98. package/dist/esm/memory/citations.mjs +64 -0
  99. package/dist/esm/memory/citations.mjs.map +1 -0
  100. package/dist/esm/memory/compositeBackend.mjs +58 -0
  101. package/dist/esm/memory/compositeBackend.mjs.map +1 -0
  102. package/dist/esm/memory/constants.mjs +198 -0
  103. package/dist/esm/memory/constants.mjs.map +1 -0
  104. package/dist/esm/memory/embeddings.mjs +148 -0
  105. package/dist/esm/memory/embeddings.mjs.map +1 -0
  106. package/dist/esm/memory/factory.mjs +93 -0
  107. package/dist/esm/memory/factory.mjs.map +1 -0
  108. package/dist/esm/memory/migrate.mjs +78 -0
  109. package/dist/esm/memory/migrate.mjs.map +1 -0
  110. package/dist/esm/memory/mmr.mjs +130 -0
  111. package/dist/esm/memory/mmr.mjs.map +1 -0
  112. package/dist/esm/memory/paths.mjs +207 -0
  113. package/dist/esm/memory/paths.mjs.map +1 -0
  114. package/dist/esm/memory/pgvectorStore.mjs +223 -0
  115. package/dist/esm/memory/pgvectorStore.mjs.map +1 -0
  116. package/dist/esm/memory/recallTracking.mjs +94 -0
  117. package/dist/esm/memory/recallTracking.mjs.map +1 -0
  118. package/dist/esm/memory/schema.sql +51 -0
  119. package/dist/esm/memory/temporalDecay.mjs +110 -0
  120. package/dist/esm/memory/temporalDecay.mjs.map +1 -0
  121. package/dist/esm/nodes/ApprovalGateNode.mjs +1 -1
  122. package/dist/esm/nodes/ApprovalGateNode.mjs.map +1 -1
  123. package/dist/esm/prompts/memoryFlushPrompt.mjs +44 -0
  124. package/dist/esm/prompts/memoryFlushPrompt.mjs.map +1 -0
  125. package/dist/esm/run.mjs +16 -3
  126. package/dist/esm/run.mjs.map +1 -1
  127. package/dist/esm/stream.mjs +4 -4
  128. package/dist/esm/stream.mjs.map +1 -1
  129. package/dist/esm/tools/AskUser.mjs +6 -1
  130. package/dist/esm/tools/AskUser.mjs.map +1 -1
  131. package/dist/esm/tools/BrowserTools.mjs +1 -1
  132. package/dist/esm/tools/BrowserTools.mjs.map +1 -1
  133. package/dist/esm/tools/ToolNode.mjs +128 -11
  134. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  135. package/dist/esm/tools/approval/constants.mjs +2 -2
  136. package/dist/esm/tools/approval/constants.mjs.map +1 -1
  137. package/dist/esm/tools/memory/index.mjs +46 -0
  138. package/dist/esm/tools/memory/index.mjs.map +1 -0
  139. package/dist/esm/tools/memory/memoryAppendTool.mjs +67 -0
  140. package/dist/esm/tools/memory/memoryAppendTool.mjs.map +1 -0
  141. package/dist/esm/tools/memory/memoryGetTool.mjs +47 -0
  142. package/dist/esm/tools/memory/memoryGetTool.mjs.map +1 -0
  143. package/dist/esm/tools/memory/memorySearchTool.mjs +63 -0
  144. package/dist/esm/tools/memory/memorySearchTool.mjs.map +1 -0
  145. package/dist/esm/tools/memory/shared.mjs +98 -0
  146. package/dist/esm/tools/memory/shared.mjs.map +1 -0
  147. package/dist/esm/types/graph.mjs.map +1 -1
  148. package/dist/esm/utils/childAgentContext.mjs +237 -0
  149. package/dist/esm/utils/childAgentContext.mjs.map +1 -0
  150. package/dist/esm/utils/events.mjs +36 -5
  151. package/dist/esm/utils/events.mjs.map +1 -1
  152. package/dist/esm/utils/finishReasons.mjs +41 -0
  153. package/dist/esm/utils/finishReasons.mjs.map +1 -0
  154. package/dist/esm/utils/llm.mjs.map +1 -1
  155. package/dist/esm/utils/logging.mjs +31 -0
  156. package/dist/esm/utils/logging.mjs.map +1 -0
  157. package/dist/esm/utils/toolCallNormalization.mjs +247 -0
  158. package/dist/esm/utils/toolCallNormalization.mjs.map +1 -0
  159. package/dist/types/common/index.d.ts +1 -0
  160. package/dist/types/common/spawnPath.d.ts +59 -0
  161. package/dist/types/graphs/HandoffRegistry.d.ts +97 -0
  162. package/dist/types/graphs/MultiAgentGraph.d.ts +58 -18
  163. package/dist/types/graphs/index.d.ts +1 -0
  164. package/dist/types/graphs/phases/flushLoop.d.ts +106 -0
  165. package/dist/types/graphs/phases/memoryFlushPhase.d.ts +100 -0
  166. package/dist/types/index.d.ts +7 -0
  167. package/dist/types/memory/__tests__/mockBackend.d.ts +40 -0
  168. package/dist/types/memory/citations.d.ts +39 -0
  169. package/dist/types/memory/compositeBackend.d.ts +30 -0
  170. package/dist/types/memory/constants.d.ts +121 -0
  171. package/dist/types/memory/embeddings.d.ts +15 -0
  172. package/dist/types/memory/factory.d.ts +23 -0
  173. package/dist/types/memory/index.d.ts +21 -0
  174. package/dist/types/memory/migrate.d.ts +14 -0
  175. package/dist/types/memory/mmr.d.ts +50 -0
  176. package/dist/types/memory/paths.d.ts +107 -0
  177. package/dist/types/memory/pgvectorStore.d.ts +56 -0
  178. package/dist/types/memory/recallTracking.d.ts +30 -0
  179. package/dist/types/memory/temporalDecay.d.ts +53 -0
  180. package/dist/types/memory/types.d.ts +182 -0
  181. package/dist/types/prompts/memoryFlushPrompt.d.ts +54 -0
  182. package/dist/types/run.d.ts +1 -0
  183. package/dist/types/tools/AskUser.d.ts +1 -1
  184. package/dist/types/tools/BrowserTools.d.ts +2 -2
  185. package/dist/types/tools/approval/constants.d.ts +2 -2
  186. package/dist/types/tools/memory/index.d.ts +39 -0
  187. package/dist/types/tools/memory/memoryAppendTool.d.ts +27 -0
  188. package/dist/types/tools/memory/memoryGetTool.d.ts +22 -0
  189. package/dist/types/tools/memory/memorySearchTool.d.ts +22 -0
  190. package/dist/types/tools/memory/shared.d.ts +106 -0
  191. package/dist/types/types/graph.d.ts +16 -3
  192. package/dist/types/utils/childAgentContext.d.ts +99 -0
  193. package/dist/types/utils/events.d.ts +21 -0
  194. package/dist/types/utils/finishReasons.d.ts +32 -0
  195. package/dist/types/utils/logging.d.ts +2 -0
  196. package/dist/types/utils/toolCallNormalization.d.ts +44 -0
  197. package/package.json +6 -4
  198. package/src/agents/AgentContext.ts +26 -3
  199. package/src/common/__tests__/enum.test.ts +4 -2
  200. package/src/common/__tests__/spawnPath.test.ts +110 -0
  201. package/src/common/index.ts +1 -0
  202. package/src/common/spawnPath.ts +101 -0
  203. package/src/graphs/Graph.ts +94 -43
  204. package/src/graphs/HandoffRegistry.ts +199 -0
  205. package/src/graphs/MultiAgentGraph.ts +694 -226
  206. package/src/graphs/__tests__/HandoffRegistry.test.ts +410 -0
  207. package/src/graphs/__tests__/multi-agent-delegate.test.ts +61 -16
  208. package/src/graphs/__tests__/multi-agent-edges.test.ts +4 -2
  209. package/src/graphs/__tests__/multi-agent-nested-subgraph.test.ts +221 -0
  210. package/src/graphs/__tests__/structured-output.integration.test.ts +212 -118
  211. package/src/graphs/contextManagement.e2e.test.ts +1 -1
  212. package/src/graphs/index.ts +1 -0
  213. package/src/graphs/phases/__tests__/flushLoop.test.ts +264 -0
  214. package/src/graphs/phases/__tests__/memoryFlushPhase.test.ts +37 -0
  215. package/src/graphs/phases/__tests__/runMemoryFlush.test.ts +150 -0
  216. package/src/graphs/phases/flushLoop.ts +303 -0
  217. package/src/graphs/phases/memoryFlushPhase.ts +209 -0
  218. package/src/index.ts +30 -1
  219. package/src/llm/bedrock/index.ts +4 -5
  220. package/src/memory/__tests__/citations.test.ts +61 -0
  221. package/src/memory/__tests__/compositeBackend.test.ts +79 -0
  222. package/src/memory/__tests__/isolation.test.ts +206 -0
  223. package/src/memory/__tests__/mmr.test.ts +148 -0
  224. package/src/memory/__tests__/mockBackend.ts +161 -0
  225. package/src/memory/__tests__/paths.test.ts +168 -0
  226. package/src/memory/__tests__/recallTracking.test.ts +96 -0
  227. package/src/memory/__tests__/temporalDecay.test.ts +151 -0
  228. package/src/memory/citations.ts +80 -0
  229. package/src/memory/compositeBackend.ts +99 -0
  230. package/src/memory/constants.ts +229 -0
  231. package/src/memory/embeddings.ts +188 -0
  232. package/src/memory/factory.ts +111 -0
  233. package/src/memory/index.ts +46 -0
  234. package/src/memory/migrate.ts +116 -0
  235. package/src/memory/mmr.ts +161 -0
  236. package/src/memory/paths.ts +258 -0
  237. package/src/memory/pgvectorStore.ts +324 -0
  238. package/src/memory/recallTracking.ts +127 -0
  239. package/src/memory/schema.sql +51 -0
  240. package/src/memory/temporalDecay.ts +134 -0
  241. package/src/memory/types.ts +185 -0
  242. package/src/nodes/ApprovalGateNode.ts +4 -10
  243. package/src/nodes/__tests__/ApprovalGateNode.test.ts +11 -20
  244. package/src/prompts/memoryFlushPrompt.ts +78 -0
  245. package/src/run.ts +17 -6
  246. package/src/scripts/test-bedrock-handoff-autonomous.ts +56 -20
  247. package/src/specs/agent-handoffs-bedrock.integration.test.ts +8 -5
  248. package/src/specs/agent-handoffs.test.ts +8 -2
  249. package/src/stream.ts +4 -6
  250. package/src/tools/AskUser.ts +7 -2
  251. package/src/tools/BrowserTools.ts +3 -5
  252. package/src/tools/ToolNode.ts +150 -13
  253. package/src/tools/__tests__/ToolApproval.test.ts +22 -9
  254. package/src/tools/approval/__tests__/constants.test.ts +4 -4
  255. package/src/tools/approval/constants.ts +2 -2
  256. package/src/tools/memory/__tests__/memoryTools.test.ts +205 -0
  257. package/src/tools/memory/index.ts +96 -0
  258. package/src/tools/memory/memoryAppendTool.ts +101 -0
  259. package/src/tools/memory/memoryGetTool.ts +53 -0
  260. package/src/tools/memory/memorySearchTool.ts +80 -0
  261. package/src/tools/memory/shared.ts +169 -0
  262. package/src/tools/search/search.test.ts +6 -1
  263. package/src/types/graph.ts +16 -3
  264. package/src/utils/__tests__/childAgentContext.test.ts +217 -0
  265. package/src/utils/__tests__/finishReasons.test.ts +55 -0
  266. package/src/utils/__tests__/toolCallNormalization.test.ts +181 -0
  267. package/src/utils/childAgentContext.ts +259 -0
  268. package/src/utils/events.ts +37 -4
  269. package/src/utils/finishReasons.ts +40 -0
  270. package/src/utils/llm.ts +0 -1
  271. package/src/utils/logging.ts +45 -8
  272. package/src/utils/toolCallNormalization.ts +271 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Autonomous memory — shared constants.
3
+ *
4
+ * Single source of truth for defaults, limits, and magic strings used across
5
+ * the memory store, tools, flush phase, and tests. Changing a value here
6
+ * changes it everywhere — no hunting through files.
7
+ */
8
+
9
+ /** Default embedding provider when {@link process.env.MEMORY_EMBEDDINGS_PROVIDER} is unset. */
10
+ export const DEFAULT_MEMORY_PROVIDER = 'bedrock' as const;
11
+
12
+ /** Default embedding model (Titan v2 — AWS-native, 6× cheaper than Cohere v4). */
13
+ export const DEFAULT_MEMORY_MODEL = 'amazon.titan-embed-text-v2:0';
14
+
15
+ /** Default vector width (Titan v2 supports 256 / 512 / 1024). */
16
+ export const DEFAULT_MEMORY_DIMENSIONS = 1024;
17
+
18
+ /** Default Postgres table name; can be overridden via env for dev/prod sharing. */
19
+ export const DEFAULT_MEMORY_TABLE = 'agent_memories';
20
+
21
+ /** Default Postgres schema. */
22
+ export const DEFAULT_MEMORY_SCHEMA = 'public';
23
+
24
+ /** Phase values used by the flush-phase gate. */
25
+ export const MEMORY_PHASE_NORMAL = 'normal';
26
+ export const MEMORY_PHASE_FLUSHING = 'memory_flushing';
27
+
28
+ /**
29
+ * Search defaults — aligned with upstream's upstream defaults.
30
+ *
31
+ * Sources:
32
+ * - `upstream reference` → maxResults=6
33
+ * - `upstream reference` → maxInjectedChars=4000
34
+ *
35
+ * Keeping these in lockstep with upstream means the mandatory-recall tool
36
+ * description, budget clamps, and eval corpora line up with upstream's
37
+ * tuning — we inherit their calibration instead of re-tuning from scratch.
38
+ */
39
+ export const DEFAULT_MAX_SEARCH_RESULTS = 6;
40
+ export const DEFAULT_MIN_SCORE = 0.1;
41
+ export const DEFAULT_MAX_INJECTED_CHARS = 4000;
42
+
43
+ /** Hybrid retrieval weights — 70% vector cosine, 30% BM25 / ts_rank text score. */
44
+ export const HYBRID_VECTOR_WEIGHT = 0.7;
45
+ export const HYBRID_TEXT_WEIGHT = 0.3;
46
+
47
+ /**
48
+ * Phase 2 rerank defaults — ported from upstream.
49
+ *
50
+ * Sources:
51
+ * - `upstream reference` → lambda=0.7
52
+ * - `upstream reference` → halfLifeDays=30
53
+ *
54
+ * Both features are opt-in (enabled=false by default) — the Phase 2
55
+ * features are layered on top of hybrid search and don't change default
56
+ * behavior for callers that upgrade in place.
57
+ */
58
+ export const DEFAULT_MMR_ENABLED = false;
59
+ export const DEFAULT_MMR_LAMBDA = 0.7;
60
+ export const DEFAULT_TEMPORAL_DECAY_ENABLED = false;
61
+ export const DEFAULT_TEMPORAL_DECAY_HALF_LIFE_DAYS = 30;
62
+ export const DEFAULT_RECALL_TRACKING_ENABLED = false;
63
+ export const DEFAULT_CITATIONS_MODE = 'auto' as const;
64
+
65
+ /**
66
+ * Flush trigger margins (token counts) — aligned with upstream upstream.
67
+ *
68
+ * Sources:
69
+ * - `upstream reference` → softThreshold=4000
70
+ * - `upstream reference` → reserveFloor=20000
71
+ */
72
+ export const DEFAULT_FLUSH_SOFT_THRESHOLD_TOKENS = 4000;
73
+ export const DEFAULT_FLUSH_RESERVE_FLOOR_TOKENS = 20000;
74
+
75
+ /** Hard cap on append calls per flush phase — prevents runaway writes. */
76
+ export const DEFAULT_MAX_APPENDS_PER_FLUSH = 20;
77
+
78
+ /**
79
+ * Hard cap on agentic loop iterations inside {@link runMemoryFlush}.
80
+ *
81
+ * Each iteration = one model.invoke() followed by execution of any
82
+ * `memory_append` tool_calls it emits. Mirrors upstream's flush-plan
83
+ * loop cap; 8 is enough for ~2–3 reflections of batched notes while
84
+ * protecting against runaway cycles if the model refuses to stop.
85
+ */
86
+ export const DEFAULT_MAX_FLUSH_ITERATIONS = 8;
87
+
88
+ /** Path prefix enforced on every append. Paths outside this are rejected. */
89
+ export const MEMORY_PATH_PREFIX = 'memory/';
90
+
91
+ /** Tool names — kept as constants so server code + tests never drift. */
92
+ export const MEMORY_SEARCH_TOOL_NAME = 'memory_search';
93
+ export const MEMORY_GET_TOOL_NAME = 'memory_get';
94
+ export const MEMORY_APPEND_TOOL_NAME = 'memory_append';
95
+
96
+ /**
97
+ * Mandatory-recall description — the single most load-bearing line in the
98
+ * whole memory system. Do not soften, shorten, or reword without an eval run.
99
+ *
100
+ * Ported VERBATIM from upstream `extensions/memory-core/src/tools.ts:186`.
101
+ * The wiki/corpus clause is retained even though Phase 1 doesn't ship
102
+ * compiled-wiki supplements — keeping the string identical means upstream's
103
+ * eval corpora remain drop-in valid.
104
+ */
105
+ export const MEMORY_SEARCH_DESCRIPTION =
106
+ 'Mandatory recall step: semantically search MEMORY.md + memory/*.md ' +
107
+ '(and optional session transcripts) before answering questions about ' +
108
+ 'prior work, decisions, dates, people, preferences, or todos. Optional ' +
109
+ '`corpus=wiki` or `corpus=all` also searches registered compiled-wiki ' +
110
+ 'supplements. If response has disabled=true, memory retrieval is ' +
111
+ 'unavailable and should be surfaced to the user.';
112
+
113
+ /**
114
+ * Ported VERBATIM from upstream `extensions/memory-core/src/tools.ts:322`.
115
+ */
116
+ export const MEMORY_GET_DESCRIPTION =
117
+ 'Safe snippet read from MEMORY.md or memory/*.md with optional from/lines; ' +
118
+ '`corpus=wiki` reads from registered compiled-wiki supplements. Use after ' +
119
+ 'search to pull only the needed lines and keep context small.';
120
+
121
+ /**
122
+ * `memory_append` tool description.
123
+ *
124
+ * Phase 1 historically wrote to a single date-keyed file
125
+ * (`memory/YYYY-MM-DD.md`), ported verbatim from upstream. That scheme
126
+ * is now replaced by an 8-path canonical whitelist — see
127
+ * {@link ./paths.MEMORY_ALL_PATHS}. The tool description no longer
128
+ * names a specific file; the flush-turn prompt renders the full rubric
129
+ * inline so the model sees every writable path at inference time.
130
+ */
131
+ export const MEMORY_APPEND_DESCRIPTION =
132
+ "Append a durable note to one of the agent's canonical memory documents. " +
133
+ 'The `path` argument MUST be one of the whitelisted paths listed in the ' +
134
+ 'flush prompt rubric — unknown paths are rejected. Content is merged into ' +
135
+ 'the existing row for that document via UPSERT, so the same document ' +
136
+ 'accumulates across sessions.';
137
+
138
+ /**
139
+ * Reply token that signals the flush turn produced no user-visible output.
140
+ * Ported VERBATIM from upstream `src/auto-reply/tokens.ts:4`.
141
+ */
142
+ export const SILENT_REPLY_TOKEN = 'NO_REPLY';
143
+
144
+ /**
145
+ * Placeholder replaced at flush time with the rendered path-rubric for
146
+ * the caller's scope. See `renderPathsRubric()` in `./paths.ts` and
147
+ * `resolveFlushPrompts()` in `../prompts/memoryFlushPrompt.ts`.
148
+ *
149
+ * Kept as a unique sentinel so `replaceAll` is safe even if the rubric
150
+ * content happens to contain regex metacharacters.
151
+ */
152
+ export const FLUSH_PROMPT_RUBRIC_PLACEHOLDER = '{{MEMORY_PATHS_RUBRIC}}';
153
+
154
+ /**
155
+ * Memory-flush prompts — canonical-document model.
156
+ *
157
+ * Every durable memory routes into one of 8 stable canonical documents
158
+ * (4 agent-tier + 4 user-tier). The rubric is injected at flush time so
159
+ * the model reads the authoritative path list with descriptions, and for
160
+ * isolated/autonomous agents the user-tier rows are transparently omitted
161
+ * from the rubric — making "user-tier writes require a scoped caller" a
162
+ * compile-time guarantee rather than a runtime check alone.
163
+ *
164
+ * Two-tier semantics the prompt enforces:
165
+ * - **agent/** — shared operational knowledge; every user of this agent
166
+ * benefits from rows written here. Do NOT put personal facts here.
167
+ * - **user/** — personalization for the specific caller only. Row is
168
+ * private to that user; other users never see it.
169
+ */
170
+ const MEMORY_FLUSH_ROUTING_HINT =
171
+ 'Route every note into exactly one of the canonical documents below by ' +
172
+ 'picking the best match. Do NOT invent new paths; do NOT create date-keyed ' +
173
+ 'files; unknown paths are rejected by the store.';
174
+
175
+ const MEMORY_FLUSH_TIER_HINT =
176
+ 'Two tiers: `memory/agent/*` is SHARED operational knowledge visible to ' +
177
+ 'every user of this agent — put successful patterns, pitfalls, domain ' +
178
+ 'facts, and house-style there. `memory/user/*` is PRIVATE to the specific ' +
179
+ 'caller — put their identity, preferences, projects, and references there. ' +
180
+ 'Never put user-specific facts in an agent/* document.';
181
+
182
+ const MEMORY_FLUSH_READ_ONLY_HINT =
183
+ 'Treat workspace bootstrap/reference files such as MEMORY.md, DREAMS.md, SOUL.md, TOOLS.md, and AGENTS.md as read-only during this flush; never overwrite, replace, or edit them.';
184
+
185
+ /**
186
+ * Learning hint — steers the flush turn to capture the things that
187
+ * matter most for future turns: reusable patterns, tool failures
188
+ * (so the same mistake is not repeated), explicit corrections, and
189
+ * durable user-specific facts. Append-only via `memory_append`; one
190
+ * note per lesson.
191
+ */
192
+ const MEMORY_FLUSH_LEARNING_HINT =
193
+ 'Capture durable lessons the agent (and future turns) should retain: ' +
194
+ '(a) successful task patterns and workflows → memory/agent/playbook.md; ' +
195
+ '(b) tool failures and schema mistakes → memory/agent/pitfalls.md; ' +
196
+ '(c) stable domain facts about the systems/APIs → memory/agent/domain.md; ' +
197
+ "(d) this user's preferences, tone, and corrections → memory/user/preferences.md; " +
198
+ "(e) this user's identity and role → memory/user/profile.md. " +
199
+ 'Write one note per distinct lesson. Do not log conversation summaries ' +
200
+ 'or anything derivable from the code or recent history.';
201
+
202
+ const MEMORY_FLUSH_RUBRIC_BLOCK =
203
+ 'Canonical documents available for this turn (path — tag — description):\n' +
204
+ FLUSH_PROMPT_RUBRIC_PLACEHOLDER;
205
+
206
+ export const DEFAULT_MEMORY_FLUSH_PROMPT = [
207
+ 'Pre-compaction memory flush.',
208
+ MEMORY_FLUSH_ROUTING_HINT,
209
+ MEMORY_FLUSH_TIER_HINT,
210
+ MEMORY_FLUSH_READ_ONLY_HINT,
211
+ MEMORY_FLUSH_LEARNING_HINT,
212
+ 'Call the `memory_append` tool for every note you want to persist, passing one of the whitelisted paths as the `path` argument. Do NOT describe what you are about to write; just call the tool.',
213
+ `If nothing worth storing, reply with exactly ${SILENT_REPLY_TOKEN}.`,
214
+ '',
215
+ MEMORY_FLUSH_RUBRIC_BLOCK,
216
+ ].join('\n');
217
+
218
+ export const DEFAULT_MEMORY_FLUSH_SYSTEM_PROMPT = [
219
+ 'Pre-compaction memory flush turn.',
220
+ 'The session is near auto-compaction; capture durable memories to disk.',
221
+ MEMORY_FLUSH_ROUTING_HINT,
222
+ MEMORY_FLUSH_TIER_HINT,
223
+ MEMORY_FLUSH_READ_ONLY_HINT,
224
+ MEMORY_FLUSH_LEARNING_HINT,
225
+ 'Use the `memory_append` tool for every durable note, with `path` set to one of the whitelisted canonical documents. Never claim you wrote a note without actually calling the tool.',
226
+ `If there is nothing worth storing, reply with exactly ${SILENT_REPLY_TOKEN}.`,
227
+ '',
228
+ MEMORY_FLUSH_RUBRIC_BLOCK,
229
+ ].join('\n');
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Provider-agnostic embedding client for autonomous memory.
3
+ *
4
+ * Factory pattern mirrors host's `getSharedSummaryBedrockClient()` in
5
+ * `api/server/controllers/agents/client.js` — lazy init, shared instance,
6
+ * testable reset. Same AWS credential chain as host's existing Bedrock
7
+ * calls, so no new IAM policy or secret.
8
+ */
9
+ import {
10
+ BedrockRuntimeClient,
11
+ InvokeModelCommand,
12
+ } from '@aws-sdk/client-bedrock-runtime';
13
+ import {
14
+ DEFAULT_MEMORY_DIMENSIONS,
15
+ DEFAULT_MEMORY_MODEL,
16
+ DEFAULT_MEMORY_PROVIDER,
17
+ } from './constants';
18
+
19
+ export type EmbeddingProviderKind = 'bedrock' | 'openai';
20
+
21
+ export interface EmbeddingProvider {
22
+ readonly kind: EmbeddingProviderKind;
23
+ readonly model: string;
24
+ readonly dimensions: number;
25
+ embed(text: string): Promise<number[]>;
26
+ embedBatch(texts: string[]): Promise<number[][]>;
27
+ }
28
+
29
+ interface ResolvedEmbedderConfig {
30
+ provider: EmbeddingProviderKind;
31
+ model: string;
32
+ dimensions: number;
33
+ }
34
+
35
+ /**
36
+ * Read embedder config from environment. Called once per factory invocation;
37
+ * fresh reads make testing override-friendly via {@link resetMemoryEmbedder}.
38
+ */
39
+ function resolveConfig(): ResolvedEmbedderConfig {
40
+ const rawProvider = (
41
+ process.env.MEMORY_EMBEDDINGS_PROVIDER ?? DEFAULT_MEMORY_PROVIDER
42
+ ).toLowerCase();
43
+ if (rawProvider !== 'bedrock' && rawProvider !== 'openai') {
44
+ throw new Error(
45
+ `Unsupported MEMORY_EMBEDDINGS_PROVIDER: "${rawProvider}". Supported: bedrock, openai.`
46
+ );
47
+ }
48
+ const model = process.env.MEMORY_EMBEDDINGS_MODEL ?? DEFAULT_MEMORY_MODEL;
49
+ const dimensions =
50
+ Number(process.env.MEMORY_EMBEDDINGS_DIMENSIONS) ||
51
+ DEFAULT_MEMORY_DIMENSIONS;
52
+ return { provider: rawProvider, model, dimensions };
53
+ }
54
+
55
+ class BedrockEmbedder implements EmbeddingProvider {
56
+ readonly kind = 'bedrock' as const;
57
+ readonly model: string;
58
+ readonly dimensions: number;
59
+ private client: BedrockRuntimeClient;
60
+
61
+ constructor(params: { model: string; dimensions: number }) {
62
+ this.model = params.model;
63
+ this.dimensions = params.dimensions;
64
+ const region =
65
+ process.env.BEDROCK_AWS_DEFAULT_REGION ??
66
+ process.env.AWS_REGION ??
67
+ 'us-east-1';
68
+ // Default credential provider chain — identical to host's existing
69
+ // Bedrock client. In ECS this resolves to the task role.
70
+ this.client = new BedrockRuntimeClient({ region });
71
+ }
72
+
73
+ async embed(text: string): Promise<number[]> {
74
+ const input = text.trim();
75
+ if (!input) {
76
+ throw new Error('Cannot embed empty text');
77
+ }
78
+ const body = {
79
+ inputText: input,
80
+ dimensions: this.dimensions,
81
+ normalize: true,
82
+ };
83
+ const command = new InvokeModelCommand({
84
+ modelId: this.model,
85
+ contentType: 'application/json',
86
+ accept: 'application/json',
87
+ body: new TextEncoder().encode(JSON.stringify(body)),
88
+ });
89
+ const response = await this.client.send(command);
90
+ const decoded = JSON.parse(new TextDecoder().decode(response.body)) as {
91
+ embedding: number[];
92
+ };
93
+ if (!Array.isArray(decoded.embedding)) {
94
+ throw new Error('Bedrock embedding response missing `embedding` field');
95
+ }
96
+ return decoded.embedding;
97
+ }
98
+
99
+ async embedBatch(texts: string[]): Promise<number[][]> {
100
+ // Titan embed v2 has no native batch API — sequential calls are the
101
+ // documented pattern. Parallelism here can trip per-account TPS ceilings,
102
+ // so we keep it serial. For Phase 4 we can introduce a bounded queue.
103
+ const out: number[][] = [];
104
+ for (const text of texts) {
105
+ out.push(await this.embed(text));
106
+ }
107
+ return out;
108
+ }
109
+ }
110
+
111
+ class OpenAIEmbedder implements EmbeddingProvider {
112
+ readonly kind = 'openai' as const;
113
+ readonly model: string;
114
+ readonly dimensions: number;
115
+ private apiKey: string;
116
+ private baseUrl: string;
117
+
118
+ constructor(params: { model: string; dimensions: number }) {
119
+ this.model = params.model;
120
+ this.dimensions = params.dimensions;
121
+ const apiKey = process.env.OPENAI_API_KEY;
122
+ if (!apiKey) {
123
+ throw new Error(
124
+ 'OPENAI_API_KEY is not set — required when MEMORY_EMBEDDINGS_PROVIDER=openai'
125
+ );
126
+ }
127
+ this.apiKey = apiKey;
128
+ this.baseUrl = process.env.OPENAI_BASEURL ?? 'https://api.openai.com/v1';
129
+ }
130
+
131
+ private async call(texts: string[]): Promise<number[][]> {
132
+ const res = await fetch(`${this.baseUrl}/embeddings`, {
133
+ method: 'POST',
134
+ headers: {
135
+ 'Content-Type': 'application/json',
136
+ Authorization: `Bearer ${this.apiKey}`,
137
+ },
138
+ body: JSON.stringify({ model: this.model, input: texts }),
139
+ });
140
+ if (!res.ok) {
141
+ const errText = await res.text();
142
+ throw new Error(`OpenAI embeddings ${res.status}: ${errText}`);
143
+ }
144
+ const json = (await res.json()) as { data: Array<{ embedding: number[] }> };
145
+ return json.data.map((d) => d.embedding);
146
+ }
147
+
148
+ async embed(text: string): Promise<number[]> {
149
+ const [vec] = await this.call([text]);
150
+ return vec;
151
+ }
152
+
153
+ async embedBatch(texts: string[]): Promise<number[][]> {
154
+ if (texts.length === 0) return [];
155
+ return this.call(texts);
156
+ }
157
+ }
158
+
159
+ let sharedEmbedder: EmbeddingProvider | null = null;
160
+
161
+ /**
162
+ * Lazy singleton accessor. Same idiom as host's
163
+ * `getSharedSummaryBedrockClient()`.
164
+ */
165
+ export function getMemoryEmbedder(): EmbeddingProvider {
166
+ if (sharedEmbedder) return sharedEmbedder;
167
+ const cfg = resolveConfig();
168
+ switch (cfg.provider) {
169
+ case 'bedrock':
170
+ sharedEmbedder = new BedrockEmbedder({
171
+ model: cfg.model,
172
+ dimensions: cfg.dimensions,
173
+ });
174
+ break;
175
+ case 'openai':
176
+ sharedEmbedder = new OpenAIEmbedder({
177
+ model: cfg.model,
178
+ dimensions: cfg.dimensions,
179
+ });
180
+ break;
181
+ }
182
+ return sharedEmbedder;
183
+ }
184
+
185
+ /** Test hook — drops the cached embedder so the next call re-reads env. */
186
+ export function resetMemoryEmbedder(): void {
187
+ sharedEmbedder = null;
188
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Factory for building a memory backend from environment variables.
3
+ *
4
+ * Called once at startup by host's singleton (`api/server/services/memoryStore.js`).
5
+ * Returns `null` — never throws — when required env vars are missing, so
6
+ * agents with `memory_enabled=true` still run, just without memory. Host
7
+ * logs a single warning on boot instead of crashing.
8
+ */
9
+ import { Pool } from 'pg';
10
+ import { DEFAULT_MEMORY_TABLE } from './constants';
11
+ import { PgvectorMemoryStore } from './pgvectorStore';
12
+ import { runMemoryMigration } from './migrate';
13
+ import { PgvectorRecallTracker, type RecallTracker } from './recallTracking';
14
+ import type { MemoryBackend } from './types';
15
+
16
+ export interface BuildMemoryBackendResult {
17
+ backend: MemoryBackend;
18
+ pool: Pool;
19
+ /** Phase 2 — shared recall tracker bound to the same pool/migration. */
20
+ recallTracker: RecallTracker;
21
+ }
22
+
23
+ interface PgConfig {
24
+ host: string;
25
+ port: number;
26
+ user: string;
27
+ password: string;
28
+ database: string;
29
+ ssl: boolean;
30
+ }
31
+
32
+ function readPgConfig(): PgConfig | null {
33
+ // Dev shortcut: single URL overrides discrete fields if present.
34
+ const url = process.env.MEMORY_DATABASE_URL;
35
+ if (url) {
36
+ try {
37
+ const u = new URL(url);
38
+ return {
39
+ host: u.hostname,
40
+ port: Number(u.port || 5432),
41
+ user: decodeURIComponent(u.username),
42
+ password: decodeURIComponent(u.password),
43
+ database: u.pathname.replace(/^\//, ''),
44
+ ssl: (process.env.MEMORY_POSTGRES_SSL ?? 'disable') === 'require',
45
+ };
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ const host = process.env.MEMORY_POSTGRES_HOST;
52
+ const user = process.env.MEMORY_POSTGRES_USER;
53
+ const password = process.env.MEMORY_POSTGRES_PASSWORD;
54
+ const database = process.env.MEMORY_POSTGRES_DB;
55
+ if (!host || !user || !password || !database) {
56
+ return null;
57
+ }
58
+ return {
59
+ host,
60
+ port: Number(process.env.MEMORY_POSTGRES_PORT ?? 5432),
61
+ user,
62
+ password,
63
+ database,
64
+ ssl: (process.env.MEMORY_POSTGRES_SSL ?? 'disable') === 'require',
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Try to build a configured memory backend. Returns null when the required
70
+ * env vars are absent — caller should log a single "memory disabled" warning
71
+ * and continue. Never throws on missing config.
72
+ */
73
+ export async function buildMemoryBackendFromEnv(): Promise<BuildMemoryBackendResult | null> {
74
+ const cfg = readPgConfig();
75
+ if (!cfg) return null;
76
+
77
+ const pool = new Pool({
78
+ host: cfg.host,
79
+ port: cfg.port,
80
+ user: cfg.user,
81
+ password: cfg.password,
82
+ database: cfg.database,
83
+ ssl: cfg.ssl ? { rejectUnauthorized: false } : undefined,
84
+ max: 10,
85
+ });
86
+
87
+ const table = process.env.MEMORY_TABLE_NAME ?? DEFAULT_MEMORY_TABLE;
88
+
89
+ try {
90
+ await runMemoryMigration({ pool, table });
91
+ } catch (err) {
92
+ // Migration failures ARE fatal — they mean dimension mismatch or
93
+ // permissions wrong, and silently running past that would produce
94
+ // corrupt or missing memory reads.
95
+ await pool.end().catch(() => undefined);
96
+ throw err;
97
+ }
98
+
99
+ const backend = new PgvectorMemoryStore({ pool, table });
100
+
101
+ // Phase 2 — recall tracker migration is best-effort at boot. If it fails,
102
+ // tool wiring continues with a null tracker rather than crashing host.
103
+ const recallTracker = new PgvectorRecallTracker(pool);
104
+ try {
105
+ await recallTracker.migrate();
106
+ } catch {
107
+ // [phase2-recall-tracking] debug: migration skipped — non-fatal
108
+ }
109
+
110
+ return { backend, pool, recallTracker };
111
+ }
@@ -0,0 +1,46 @@
1
+ /** Public entry point for the memory package. */
2
+ export * from './types';
3
+ export * from './constants';
4
+ export * from './paths';
5
+ export { PgvectorMemoryStore } from './pgvectorStore';
6
+ export type { PgvectorStoreOptions } from './pgvectorStore';
7
+ export { CompositeMemoryBackend } from './compositeBackend';
8
+ export { getMemoryEmbedder, resetMemoryEmbedder } from './embeddings';
9
+ export type { EmbeddingProvider, EmbeddingProviderKind } from './embeddings';
10
+ export { runMemoryMigration } from './migrate';
11
+ export type { MigrationOptions } from './migrate';
12
+ export { buildMemoryBackendFromEnv } from './factory';
13
+ export type { BuildMemoryBackendResult } from './factory';
14
+
15
+ // Phase 2
16
+ export {
17
+ mmrRerank,
18
+ applyMMRToMemoryHits,
19
+ DEFAULT_MMR_CONFIG,
20
+ tokenize,
21
+ jaccardSimilarity,
22
+ textSimilarity,
23
+ computeMMRScore,
24
+ } from './mmr';
25
+ export type { MMRConfig, MMRItem } from './mmr';
26
+ export {
27
+ applyTemporalDecayToHits,
28
+ applyTemporalDecayToScore,
29
+ calculateTemporalDecayMultiplier,
30
+ parseMemoryDateFromPath,
31
+ isEvergreenMemoryPath,
32
+ DEFAULT_TEMPORAL_DECAY_CONFIG,
33
+ } from './temporalDecay';
34
+ export type { TemporalDecayConfig, DecayCandidate } from './temporalDecay';
35
+ export {
36
+ decorateCitations,
37
+ resolveMemoryCitationsMode,
38
+ shouldIncludeCitations,
39
+ } from './citations';
40
+ export type { MemoryCitationsMode, CitationCandidate } from './citations';
41
+ export {
42
+ PgvectorRecallTracker,
43
+ NullRecallTracker,
44
+ RECALL_TABLE,
45
+ } from './recallTracking';
46
+ export type { RecallTracker, RecallRecordParams } from './recallTracking';
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Idempotent schema install + vector-dimension safety check.
3
+ *
4
+ * Called once at agents-library startup by host's `memoryStore.js` singleton
5
+ * factory. Safe to call multiple times — every statement is `IF NOT EXISTS`.
6
+ */
7
+ import type { Pool } from 'pg';
8
+ import { readFileSync } from 'fs';
9
+ import { join, dirname } from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import { DEFAULT_MEMORY_DIMENSIONS, DEFAULT_MEMORY_TABLE } from './constants';
12
+ import { getMemoryEmbedder } from './embeddings';
13
+
14
+ export interface MigrationOptions {
15
+ pool: Pool;
16
+ table?: string;
17
+ /** If true, skip the live embedder probe (tests). */
18
+ skipEmbedderProbe?: boolean;
19
+ }
20
+
21
+ function resolveSchemaSqlPath(): string {
22
+ // Works under both ESM (import.meta.url) and compiled CJS (fall back to __dirname).
23
+ const meta = import.meta as unknown as { url?: string };
24
+ const metaUrl = typeof meta.url === 'string' ? meta.url : undefined;
25
+ if (metaUrl) {
26
+ return join(dirname(fileURLToPath(metaUrl)), 'schema.sql');
27
+ }
28
+ const globalDirname = (globalThis as unknown as { __dirname?: string })
29
+ .__dirname;
30
+ return join(globalDirname ?? process.cwd(), 'schema.sql');
31
+ }
32
+
33
+ export async function runMemoryMigration(
34
+ opts: MigrationOptions
35
+ ): Promise<void> {
36
+ const table = opts.table ?? DEFAULT_MEMORY_TABLE;
37
+ const schemaPath = resolveSchemaSqlPath();
38
+ const schemaSql = readFileSync(schemaPath, 'utf8').replace(
39
+ /agent_memories/g,
40
+ table
41
+ );
42
+
43
+ // Drop the legacy `(agent_id, path)` unique constraint BEFORE running
44
+ // the schema SQL. The schema creates the new
45
+ // `(agent_id, user_id, path)` NULLS-NOT-DISTINCT constraint, which
46
+ // must not collide with the old one. Dropping by the legacy name is a
47
+ // no-op on fresh installs where the constraint never existed, and on
48
+ // upgrades it cleanly frees the unique-index slot so the new
49
+ // constraint can be installed.
50
+ await opts.pool
51
+ .query(
52
+ `ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${table}_agent_path_uq`
53
+ )
54
+ .catch(() => undefined);
55
+
56
+ await opts.pool.query(schemaSql);
57
+
58
+ // Adopt legacy rows if the table pre-existed with an older shape.
59
+ // Idempotent — every statement is no-op-safe on a fresh schema.
60
+ await opts.pool
61
+ .query(`ALTER TABLE ${table} ALTER COLUMN user_id DROP NOT NULL`)
62
+ .catch(() => undefined);
63
+ await opts.pool.query(
64
+ `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`
65
+ );
66
+ await opts.pool.query(
67
+ `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS last_user_id TEXT`
68
+ );
69
+ // Belt-and-suspenders: if the schema CREATE was skipped because the
70
+ // table already existed AND the new constraint was absent, add it
71
+ // explicitly. Swallow duplicate-object errors on happy-path re-runs.
72
+ await opts.pool
73
+ .query(
74
+ `ALTER TABLE ${table}
75
+ ADD CONSTRAINT ${table}_agent_user_path_uq
76
+ UNIQUE NULLS NOT DISTINCT (agent_id, user_id, path)`
77
+ )
78
+ .catch(() => undefined);
79
+
80
+ if (opts.skipEmbedderProbe) return;
81
+
82
+ // Vector dimension sanity check — refuses to serve if the column width
83
+ // disagrees with the live embedder and the table has data. See plan §3.
84
+ const embedder = getMemoryEmbedder();
85
+ const liveDim = embedder.dimensions || DEFAULT_MEMORY_DIMENSIONS;
86
+
87
+ // pgvector stores the declared width directly in atttypmod (unlike varchar
88
+ // which uses atttypmod = N + VARHDRSZ). Reading it raw gives the true dim.
89
+ const dimResult = await opts.pool.query(
90
+ `
91
+ SELECT atttypmod AS dim
92
+ FROM pg_attribute
93
+ WHERE attrelid = to_regclass($1)
94
+ AND attname = 'embedding'
95
+ `,
96
+ [table]
97
+ );
98
+ const columnDim = dimResult.rows[0]?.dim;
99
+ if (columnDim && Number(columnDim) !== liveDim) {
100
+ const countRes = await opts.pool.query(
101
+ `SELECT COUNT(*)::int AS n FROM ${table}`
102
+ );
103
+ const rowCount = Number(countRes.rows[0]?.n ?? 0);
104
+ if (rowCount === 0) {
105
+ await opts.pool.query(
106
+ `ALTER TABLE ${table} ALTER COLUMN embedding TYPE VECTOR(${liveDim})`
107
+ );
108
+ } else {
109
+ throw new Error(
110
+ `Memory vector dimension mismatch: column is VECTOR(${columnDim}) but ` +
111
+ `embedder "${embedder.model}" produces ${liveDim}-d vectors, and ${rowCount} ` +
112
+ `rows exist. Refusing to serve memory. Admin must run a reindex job.`
113
+ );
114
+ }
115
+ }
116
+ }