@falai/agent 2.6.1 → 2.7.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 (320) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/MemoryAdapter.js +29 -33
  3. package/dist/adapters/MemoryAdapter.js.map +1 -1
  4. package/dist/adapters/PostgreSQLAdapter.d.ts.map +1 -1
  5. package/dist/adapters/PostgreSQLAdapter.js +16 -11
  6. package/dist/adapters/PostgreSQLAdapter.js.map +1 -1
  7. package/dist/adapters/RedisAdapter.d.ts +1 -0
  8. package/dist/adapters/RedisAdapter.d.ts.map +1 -1
  9. package/dist/adapters/RedisAdapter.js +75 -25
  10. package/dist/adapters/RedisAdapter.js.map +1 -1
  11. package/dist/adapters/SQLiteAdapter.d.ts.map +1 -1
  12. package/dist/adapters/SQLiteAdapter.js +7 -29
  13. package/dist/adapters/SQLiteAdapter.js.map +1 -1
  14. package/dist/adapters/sessionRow.d.ts +22 -0
  15. package/dist/adapters/sessionRow.d.ts.map +1 -0
  16. package/dist/adapters/sessionRow.js +48 -0
  17. package/dist/adapters/sessionRow.js.map +1 -0
  18. package/dist/cjs/adapters/MemoryAdapter.js +29 -33
  19. package/dist/cjs/adapters/MemoryAdapter.js.map +1 -1
  20. package/dist/cjs/adapters/PostgreSQLAdapter.d.ts.map +1 -1
  21. package/dist/cjs/adapters/PostgreSQLAdapter.js +16 -11
  22. package/dist/cjs/adapters/PostgreSQLAdapter.js.map +1 -1
  23. package/dist/cjs/adapters/RedisAdapter.d.ts +1 -0
  24. package/dist/cjs/adapters/RedisAdapter.d.ts.map +1 -1
  25. package/dist/cjs/adapters/RedisAdapter.js +75 -25
  26. package/dist/cjs/adapters/RedisAdapter.js.map +1 -1
  27. package/dist/cjs/adapters/SQLiteAdapter.d.ts.map +1 -1
  28. package/dist/cjs/adapters/SQLiteAdapter.js +7 -29
  29. package/dist/cjs/adapters/SQLiteAdapter.js.map +1 -1
  30. package/dist/cjs/adapters/sessionRow.d.ts +22 -0
  31. package/dist/cjs/adapters/sessionRow.d.ts.map +1 -0
  32. package/dist/cjs/adapters/sessionRow.js +52 -0
  33. package/dist/cjs/adapters/sessionRow.js.map +1 -0
  34. package/dist/cjs/core/Agent.d.ts +8 -0
  35. package/dist/cjs/core/Agent.d.ts.map +1 -1
  36. package/dist/cjs/core/Agent.js +40 -2
  37. package/dist/cjs/core/Agent.js.map +1 -1
  38. package/dist/cjs/core/AutoChainExecutor.d.ts +8 -18
  39. package/dist/cjs/core/AutoChainExecutor.d.ts.map +1 -1
  40. package/dist/cjs/core/AutoChainExecutor.js +23 -26
  41. package/dist/cjs/core/AutoChainExecutor.js.map +1 -1
  42. package/dist/cjs/core/CompactionEngine.d.ts +14 -1
  43. package/dist/cjs/core/CompactionEngine.d.ts.map +1 -1
  44. package/dist/cjs/core/CompactionEngine.js +30 -6
  45. package/dist/cjs/core/CompactionEngine.js.map +1 -1
  46. package/dist/cjs/core/FlowRouter.d.ts.map +1 -1
  47. package/dist/cjs/core/FlowRouter.js +32 -10
  48. package/dist/cjs/core/FlowRouter.js.map +1 -1
  49. package/dist/cjs/core/ResponseGenerationError.d.ts.map +1 -1
  50. package/dist/cjs/core/ResponseGenerationError.js +3 -5
  51. package/dist/cjs/core/ResponseGenerationError.js.map +1 -1
  52. package/dist/cjs/core/ResponseModal.d.ts +29 -0
  53. package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
  54. package/dist/cjs/core/ResponseModal.js +141 -18
  55. package/dist/cjs/core/ResponseModal.js.map +1 -1
  56. package/dist/cjs/core/ResponsePipeline.d.ts +36 -6
  57. package/dist/cjs/core/ResponsePipeline.d.ts.map +1 -1
  58. package/dist/cjs/core/ResponsePipeline.js +208 -73
  59. package/dist/cjs/core/ResponsePipeline.js.map +1 -1
  60. package/dist/cjs/core/SessionFinalizer.d.ts.map +1 -1
  61. package/dist/cjs/core/SessionFinalizer.js +30 -3
  62. package/dist/cjs/core/SessionFinalizer.js.map +1 -1
  63. package/dist/cjs/core/SessionManager.d.ts +10 -1
  64. package/dist/cjs/core/SessionManager.d.ts.map +1 -1
  65. package/dist/cjs/core/SessionManager.js +43 -16
  66. package/dist/cjs/core/SessionManager.js.map +1 -1
  67. package/dist/cjs/core/SignalProcessor.d.ts.map +1 -1
  68. package/dist/cjs/core/SignalProcessor.js +5 -77
  69. package/dist/cjs/core/SignalProcessor.js.map +1 -1
  70. package/dist/cjs/core/Step.d.ts.map +1 -1
  71. package/dist/cjs/core/Step.js +50 -2
  72. package/dist/cjs/core/Step.js.map +1 -1
  73. package/dist/cjs/core/StepLifecycle.d.ts +16 -6
  74. package/dist/cjs/core/StepLifecycle.d.ts.map +1 -1
  75. package/dist/cjs/core/StepLifecycle.js +97 -14
  76. package/dist/cjs/core/StepLifecycle.js.map +1 -1
  77. package/dist/cjs/core/StreamingToolExecutor.d.ts.map +1 -1
  78. package/dist/cjs/core/StreamingToolExecutor.js +28 -4
  79. package/dist/cjs/core/StreamingToolExecutor.js.map +1 -1
  80. package/dist/cjs/core/ToolLoopExecutor.d.ts +5 -1
  81. package/dist/cjs/core/ToolLoopExecutor.d.ts.map +1 -1
  82. package/dist/cjs/core/ToolLoopExecutor.js +155 -63
  83. package/dist/cjs/core/ToolLoopExecutor.js.map +1 -1
  84. package/dist/cjs/core/ToolManager.d.ts +1 -1
  85. package/dist/cjs/core/ToolManager.d.ts.map +1 -1
  86. package/dist/cjs/core/ToolManager.js +40 -17
  87. package/dist/cjs/core/ToolManager.js.map +1 -1
  88. package/dist/cjs/core/flow-namespace.d.ts +15 -0
  89. package/dist/cjs/core/flow-namespace.d.ts.map +1 -1
  90. package/dist/cjs/core/flow-namespace.js +22 -0
  91. package/dist/cjs/core/flow-namespace.js.map +1 -1
  92. package/dist/cjs/index.d.ts +4 -1
  93. package/dist/cjs/index.d.ts.map +1 -1
  94. package/dist/cjs/index.js +5 -2
  95. package/dist/cjs/index.js.map +1 -1
  96. package/dist/cjs/providers/AnthropicProvider.d.ts +10 -3
  97. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
  98. package/dist/cjs/providers/AnthropicProvider.js +54 -73
  99. package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
  100. package/dist/cjs/providers/GeminiProvider.d.ts +9 -3
  101. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  102. package/dist/cjs/providers/GeminiProvider.js +24 -68
  103. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  104. package/dist/cjs/providers/OpenAICompatibleProvider.d.ts +9 -0
  105. package/dist/cjs/providers/OpenAICompatibleProvider.d.ts.map +1 -1
  106. package/dist/cjs/providers/OpenAICompatibleProvider.js +101 -74
  107. package/dist/cjs/providers/OpenAICompatibleProvider.js.map +1 -1
  108. package/dist/cjs/providers/OpenAIProvider.d.ts +1 -1
  109. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
  110. package/dist/cjs/providers/OpenAIProvider.js +1 -1
  111. package/dist/cjs/providers/errorClassification.d.ts +7 -0
  112. package/dist/cjs/providers/errorClassification.d.ts.map +1 -1
  113. package/dist/cjs/providers/errorClassification.js +27 -0
  114. package/dist/cjs/providers/errorClassification.js.map +1 -1
  115. package/dist/cjs/types/agent.d.ts +26 -0
  116. package/dist/cjs/types/agent.d.ts.map +1 -1
  117. package/dist/cjs/types/flow.d.ts +49 -32
  118. package/dist/cjs/types/flow.d.ts.map +1 -1
  119. package/dist/cjs/types/index.d.ts +1 -1
  120. package/dist/cjs/types/index.d.ts.map +1 -1
  121. package/dist/cjs/types/index.js.map +1 -1
  122. package/dist/cjs/types/tool.d.ts +2 -0
  123. package/dist/cjs/types/tool.d.ts.map +1 -1
  124. package/dist/cjs/types/tool.js.map +1 -1
  125. package/dist/cjs/utils/index.d.ts +4 -4
  126. package/dist/cjs/utils/index.d.ts.map +1 -1
  127. package/dist/cjs/utils/index.js +10 -1
  128. package/dist/cjs/utils/index.js.map +1 -1
  129. package/dist/cjs/utils/retry.d.ts +99 -1
  130. package/dist/cjs/utils/retry.d.ts.map +1 -1
  131. package/dist/cjs/utils/retry.js +140 -3
  132. package/dist/cjs/utils/retry.js.map +1 -1
  133. package/dist/cjs/utils/serialize.d.ts +17 -0
  134. package/dist/cjs/utils/serialize.d.ts.map +1 -1
  135. package/dist/cjs/utils/serialize.js +33 -0
  136. package/dist/cjs/utils/serialize.js.map +1 -1
  137. package/dist/cjs/utils/session.d.ts +21 -1
  138. package/dist/cjs/utils/session.d.ts.map +1 -1
  139. package/dist/cjs/utils/session.js +36 -4
  140. package/dist/cjs/utils/session.js.map +1 -1
  141. package/dist/core/Agent.d.ts +8 -0
  142. package/dist/core/Agent.d.ts.map +1 -1
  143. package/dist/core/Agent.js +40 -2
  144. package/dist/core/Agent.js.map +1 -1
  145. package/dist/core/AutoChainExecutor.d.ts +8 -18
  146. package/dist/core/AutoChainExecutor.d.ts.map +1 -1
  147. package/dist/core/AutoChainExecutor.js +23 -26
  148. package/dist/core/AutoChainExecutor.js.map +1 -1
  149. package/dist/core/CompactionEngine.d.ts +14 -1
  150. package/dist/core/CompactionEngine.d.ts.map +1 -1
  151. package/dist/core/CompactionEngine.js +30 -6
  152. package/dist/core/CompactionEngine.js.map +1 -1
  153. package/dist/core/FlowRouter.d.ts.map +1 -1
  154. package/dist/core/FlowRouter.js +32 -10
  155. package/dist/core/FlowRouter.js.map +1 -1
  156. package/dist/core/ResponseGenerationError.d.ts.map +1 -1
  157. package/dist/core/ResponseGenerationError.js +3 -5
  158. package/dist/core/ResponseGenerationError.js.map +1 -1
  159. package/dist/core/ResponseModal.d.ts +29 -0
  160. package/dist/core/ResponseModal.d.ts.map +1 -1
  161. package/dist/core/ResponseModal.js +142 -19
  162. package/dist/core/ResponseModal.js.map +1 -1
  163. package/dist/core/ResponsePipeline.d.ts +36 -6
  164. package/dist/core/ResponsePipeline.d.ts.map +1 -1
  165. package/dist/core/ResponsePipeline.js +208 -73
  166. package/dist/core/ResponsePipeline.js.map +1 -1
  167. package/dist/core/SessionFinalizer.d.ts.map +1 -1
  168. package/dist/core/SessionFinalizer.js +31 -4
  169. package/dist/core/SessionFinalizer.js.map +1 -1
  170. package/dist/core/SessionManager.d.ts +10 -1
  171. package/dist/core/SessionManager.d.ts.map +1 -1
  172. package/dist/core/SessionManager.js +44 -17
  173. package/dist/core/SessionManager.js.map +1 -1
  174. package/dist/core/SignalProcessor.d.ts.map +1 -1
  175. package/dist/core/SignalProcessor.js +5 -77
  176. package/dist/core/SignalProcessor.js.map +1 -1
  177. package/dist/core/Step.d.ts.map +1 -1
  178. package/dist/core/Step.js +50 -2
  179. package/dist/core/Step.js.map +1 -1
  180. package/dist/core/StepLifecycle.d.ts +16 -6
  181. package/dist/core/StepLifecycle.d.ts.map +1 -1
  182. package/dist/core/StepLifecycle.js +97 -14
  183. package/dist/core/StepLifecycle.js.map +1 -1
  184. package/dist/core/StreamingToolExecutor.d.ts.map +1 -1
  185. package/dist/core/StreamingToolExecutor.js +28 -4
  186. package/dist/core/StreamingToolExecutor.js.map +1 -1
  187. package/dist/core/ToolLoopExecutor.d.ts +5 -1
  188. package/dist/core/ToolLoopExecutor.d.ts.map +1 -1
  189. package/dist/core/ToolLoopExecutor.js +155 -63
  190. package/dist/core/ToolLoopExecutor.js.map +1 -1
  191. package/dist/core/ToolManager.d.ts +1 -1
  192. package/dist/core/ToolManager.d.ts.map +1 -1
  193. package/dist/core/ToolManager.js +41 -18
  194. package/dist/core/ToolManager.js.map +1 -1
  195. package/dist/core/flow-namespace.d.ts +15 -0
  196. package/dist/core/flow-namespace.d.ts.map +1 -1
  197. package/dist/core/flow-namespace.js +22 -0
  198. package/dist/core/flow-namespace.js.map +1 -1
  199. package/dist/index.d.ts +4 -1
  200. package/dist/index.d.ts.map +1 -1
  201. package/dist/index.js +2 -1
  202. package/dist/index.js.map +1 -1
  203. package/dist/providers/AnthropicProvider.d.ts +10 -3
  204. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  205. package/dist/providers/AnthropicProvider.js +56 -75
  206. package/dist/providers/AnthropicProvider.js.map +1 -1
  207. package/dist/providers/GeminiProvider.d.ts +9 -3
  208. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  209. package/dist/providers/GeminiProvider.js +26 -70
  210. package/dist/providers/GeminiProvider.js.map +1 -1
  211. package/dist/providers/OpenAICompatibleProvider.d.ts +9 -0
  212. package/dist/providers/OpenAICompatibleProvider.d.ts.map +1 -1
  213. package/dist/providers/OpenAICompatibleProvider.js +103 -76
  214. package/dist/providers/OpenAICompatibleProvider.js.map +1 -1
  215. package/dist/providers/OpenAIProvider.d.ts +1 -1
  216. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  217. package/dist/providers/OpenAIProvider.js +1 -1
  218. package/dist/providers/errorClassification.d.ts +7 -0
  219. package/dist/providers/errorClassification.d.ts.map +1 -1
  220. package/dist/providers/errorClassification.js +26 -0
  221. package/dist/providers/errorClassification.js.map +1 -1
  222. package/dist/types/agent.d.ts +26 -0
  223. package/dist/types/agent.d.ts.map +1 -1
  224. package/dist/types/flow.d.ts +49 -32
  225. package/dist/types/flow.d.ts.map +1 -1
  226. package/dist/types/index.d.ts +1 -1
  227. package/dist/types/index.d.ts.map +1 -1
  228. package/dist/types/index.js.map +1 -1
  229. package/dist/types/tool.d.ts +2 -0
  230. package/dist/types/tool.d.ts.map +1 -1
  231. package/dist/types/tool.js.map +1 -1
  232. package/dist/utils/index.d.ts +4 -4
  233. package/dist/utils/index.d.ts.map +1 -1
  234. package/dist/utils/index.js +3 -3
  235. package/dist/utils/index.js.map +1 -1
  236. package/dist/utils/retry.d.ts +99 -1
  237. package/dist/utils/retry.d.ts.map +1 -1
  238. package/dist/utils/retry.js +137 -3
  239. package/dist/utils/retry.js.map +1 -1
  240. package/dist/utils/serialize.d.ts +17 -0
  241. package/dist/utils/serialize.d.ts.map +1 -1
  242. package/dist/utils/serialize.js +31 -0
  243. package/dist/utils/serialize.js.map +1 -1
  244. package/dist/utils/session.d.ts +21 -1
  245. package/dist/utils/session.d.ts.map +1 -1
  246. package/dist/utils/session.js +33 -4
  247. package/dist/utils/session.js.map +1 -1
  248. package/docs/concepts/architecture.md +3 -3
  249. package/docs/concepts/directives.md +1 -1
  250. package/docs/guides/error-handling.md +46 -45
  251. package/docs/guides/flow-control.md +8 -1
  252. package/docs/guides/instructions.md +15 -6
  253. package/docs/guides/persistence.md +12 -5
  254. package/docs/guides/streaming.md +10 -0
  255. package/docs/migration/README.md +4 -0
  256. package/docs/migration/v2-3-to-v2-4.md +4 -0
  257. package/docs/migration/v2-6-to-v2-7.md +246 -0
  258. package/docs/reference/adapters.md +15 -1
  259. package/docs/reference/create-agent.md +28 -0
  260. package/docs/reference/directive.md +1 -1
  261. package/docs/reference/errors.md +29 -31
  262. package/docs/reference/providers.md +23 -19
  263. package/docs/reference/step.md +28 -21
  264. package/docs/reference/tool.md +14 -5
  265. package/docs/start/02-first-agent.md +8 -4
  266. package/docs/start/03-collect-data.md +19 -10
  267. package/examples/01-quickstart.ts +1 -1
  268. package/examples/02-data-extraction.ts +1 -1
  269. package/examples/03-tools.ts +1 -1
  270. package/examples/04-instructions.ts +1 -1
  271. package/examples/05-branching.ts +1 -1
  272. package/examples/06-flow-control.ts +3 -3
  273. package/examples/07-streaming.ts +1 -1
  274. package/examples/08-persistence.ts +1 -1
  275. package/examples/09-signals.ts +1 -1
  276. package/package.json +2 -2
  277. package/src/adapters/MemoryAdapter.ts +29 -33
  278. package/src/adapters/PostgreSQLAdapter.ts +23 -18
  279. package/src/adapters/RedisAdapter.ts +81 -34
  280. package/src/adapters/SQLiteAdapter.ts +10 -31
  281. package/src/adapters/sessionRow.ts +57 -0
  282. package/src/core/Agent.ts +48 -2
  283. package/src/core/AutoChainExecutor.ts +37 -50
  284. package/src/core/CompactionEngine.ts +40 -6
  285. package/src/core/FlowRouter.ts +38 -12
  286. package/src/core/ResponseGenerationError.ts +3 -6
  287. package/src/core/ResponseModal.ts +185 -19
  288. package/src/core/ResponsePipeline.ts +259 -85
  289. package/src/core/SessionFinalizer.ts +35 -5
  290. package/src/core/SessionManager.ts +55 -21
  291. package/src/core/SignalProcessor.ts +5 -85
  292. package/src/core/Step.ts +79 -2
  293. package/src/core/StepLifecycle.ts +125 -22
  294. package/src/core/StreamingToolExecutor.ts +31 -6
  295. package/src/core/ToolLoopExecutor.ts +200 -74
  296. package/src/core/ToolManager.ts +45 -18
  297. package/src/core/flow-namespace.ts +29 -0
  298. package/src/index.ts +4 -1
  299. package/src/providers/AnthropicProvider.ts +83 -131
  300. package/src/providers/GeminiProvider.ts +42 -116
  301. package/src/providers/OpenAICompatibleProvider.ts +132 -133
  302. package/src/providers/OpenAIProvider.ts +2 -2
  303. package/src/providers/errorClassification.ts +34 -0
  304. package/src/types/agent.ts +25 -0
  305. package/src/types/flow.ts +37 -39
  306. package/src/types/index.ts +1 -0
  307. package/src/types/tool.ts +2 -0
  308. package/src/utils/index.ts +22 -3
  309. package/src/utils/retry.ts +251 -2
  310. package/src/utils/serialize.ts +38 -0
  311. package/src/utils/session.ts +41 -4
  312. package/dist/cjs/core/DirectiveBus.d.ts +0 -88
  313. package/dist/cjs/core/DirectiveBus.d.ts.map +0 -1
  314. package/dist/cjs/core/DirectiveBus.js +0 -196
  315. package/dist/cjs/core/DirectiveBus.js.map +0 -1
  316. package/dist/core/DirectiveBus.d.ts +0 -88
  317. package/dist/core/DirectiveBus.d.ts.map +0 -1
  318. package/dist/core/DirectiveBus.js +0 -192
  319. package/dist/core/DirectiveBus.js.map +0 -1
  320. package/src/core/DirectiveBus.ts +0 -248
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falai/agent",
3
- "version": "2.6.1",
3
+ "version": "2.7.0",
4
4
  "description": "Conversational state engine for TypeScript where the AI understands, but the code is in control",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -43,7 +43,7 @@
43
43
  "build:cjs": "tsc --project tsconfig.cjs.json",
44
44
  "fix:cjs": "echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
45
45
  "dev": "tsc --watch",
46
- "typecheck": "tsc --noEmit",
46
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
47
47
  "typecheck:examples": "tsc --noEmit --project examples/tsconfig.json",
48
48
  "lint": "eslint src/**/*.ts",
49
49
  "lint:fix": "eslint src/**/*.ts --fix",
@@ -155,34 +155,25 @@ class MemorySessionRepository<TData = Record<string, unknown>>
155
155
  return Promise.resolve(cloneDeep(updated));
156
156
  }
157
157
 
158
+ // Derived updaters route through update() so every state write shares one
159
+ // semantic across adapters: version bump + updatedAt refresh (the SQL
160
+ // adapters delegate to their update() the same way).
161
+
158
162
  async updateStatus(
159
163
  id: string,
160
164
  status: SessionStatus,
161
165
  completedAt?: Date
162
166
  ): Promise<SessionData<TData> | null> {
163
- const session = this.sessions.get(id);
164
- if (session) {
165
- session.status = status;
166
- if (completedAt) {
167
- session.completedAt = completedAt;
168
- }
169
- this.sessions.set(id, cloneDeep(session));
170
- return Promise.resolve(cloneDeep(session));
171
- }
172
- return Promise.resolve(null);
167
+ // An undefined completedAt must be omitted, not spread — update() writes
168
+ // whatever keys it receives, and SQL skips undefined columns.
169
+ return this.update(id, completedAt ? { status, completedAt } : { status });
173
170
  }
174
171
 
175
172
  async updateCollectedData(
176
173
  id: string,
177
174
  collectedData: CollectedStateData<TData>
178
175
  ): Promise<SessionData<TData> | null> {
179
- const session = this.sessions.get(id);
180
- if (session) {
181
- session.collectedData = collectedData;
182
- this.sessions.set(id, cloneDeep(session));
183
- return Promise.resolve(cloneDeep(session));
184
- }
185
- return Promise.resolve(null);
176
+ return this.update(id, { collectedData });
186
177
  }
187
178
 
188
179
  async updateFlowStep(
@@ -190,24 +181,27 @@ class MemorySessionRepository<TData = Record<string, unknown>>
190
181
  flow?: string,
191
182
  step?: string
192
183
  ): Promise<SessionData<TData> | null> {
193
- const session = this.sessions.get(id);
194
- if (session) {
195
- session.currentFlow = flow;
196
- session.currentStep = step;
197
- this.sessions.set(id, cloneDeep(session));
198
- return Promise.resolve(cloneDeep(session));
199
- }
200
- return Promise.resolve(null);
184
+ // Mirror SQL's conditional column assembly: only defined values are written.
185
+ return this.update(id, {
186
+ ...(flow !== undefined && { currentFlow: flow }),
187
+ ...(step !== undefined && { currentStep: step }),
188
+ });
201
189
  }
202
190
 
203
191
  async incrementMessageCount(id: string): Promise<SessionData<TData> | null> {
192
+ // Bookkeeping write, deliberately NOT routed through update(): count bumps
193
+ // run alongside saveSessionState's version CAS every turn, so they must not
194
+ // move `version` (matches the SQL adapters' dedicated statement).
204
195
  const session = this.sessions.get(id);
205
- if (session) {
206
- session.messageCount = (session.messageCount || 0) + 1;
207
- this.sessions.set(id, cloneDeep(session));
208
- return Promise.resolve(cloneDeep(session));
209
- }
210
- return Promise.resolve(null);
196
+ if (!session) return Promise.resolve(null);
197
+ const updated: SessionData<TData> = {
198
+ ...session,
199
+ messageCount: (session.messageCount || 0) + 1,
200
+ lastMessageAt: new Date(),
201
+ updatedAt: new Date(),
202
+ };
203
+ this.sessions.set(id, cloneDeep(updated));
204
+ return Promise.resolve(cloneDeep(updated));
211
205
  }
212
206
 
213
207
  async delete(id: string): Promise<boolean> {
@@ -232,8 +226,10 @@ class MemoryMessageRepository implements MessageRepository {
232
226
  createdAt: new Date(),
233
227
  };
234
228
 
235
- this.messages.set(id, message);
236
- return Promise.resolve(message);
229
+ // Clone on store and on return so callers can't mutate the stored row —
230
+ // same isolation the session repository provides.
231
+ this.messages.set(id, cloneDeep(message));
232
+ return Promise.resolve(cloneDeep(message));
237
233
  }
238
234
 
239
235
  async findById(id: string): Promise<MessageData | null> {
@@ -29,6 +29,7 @@ import type {
29
29
  } from "../types";
30
30
  import { SessionConflictError } from "../types/errors";
31
31
  import { createSessionId } from "../utils";
32
+ import { deserializeSessionRow } from "./sessionRow";
32
33
 
33
34
  /**
34
35
  * PostgreSQL query result interface
@@ -195,61 +196,65 @@ class PostgreSQLSessionRepository<TData = Record<string, unknown>>
195
196
  data.id || createSessionId();
196
197
  const now = new Date();
197
198
 
198
- const result = await this.client.query<SessionData<TData>>(
199
+ const result = await this.client.query<Record<string, unknown>>(
199
200
  `INSERT INTO ${this.tableName}
200
- (id, user_id, agent_name, status, collected_data, message_count, version, created_at, updated_at)
201
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
201
+ (id, user_id, agent_name, status, current_flow, current_step, collected_data, message_count, last_message_at, completed_at, version, created_at, updated_at)
202
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
202
203
  RETURNING *`,
203
204
  [
204
205
  id,
205
206
  data.userId || null,
206
207
  data.agentName || null,
207
208
  data.status || "active",
209
+ data.currentFlow || null,
210
+ data.currentStep || null,
208
211
  JSON.stringify(data.collectedData || {}),
209
212
  data.messageCount || 0,
213
+ data.lastMessageAt ?? null,
214
+ data.completedAt ?? null,
210
215
  data.version ?? 1,
211
216
  now,
212
217
  now,
213
218
  ]
214
219
  );
215
220
 
216
- return result.rows[0];
221
+ return deserializeSessionRow(result.rows[0]);
217
222
  }
218
223
 
219
224
  async findById(id: string): Promise<SessionData<TData> | null> {
220
- const result = await this.client.query<SessionData<TData>>(
225
+ const result = await this.client.query<Record<string, unknown>>(
221
226
  `SELECT * FROM ${this.tableName} WHERE id = $1`,
222
227
  [id]
223
228
  );
224
229
 
225
- return result.rows[0] || null;
230
+ return result.rows[0] ? deserializeSessionRow(result.rows[0]) : null;
226
231
  }
227
232
 
228
233
  async findActiveByUserId(userId: string): Promise<SessionData<TData> | null> {
229
- const result = await this.client.query<SessionData<TData>>(
230
- `SELECT * FROM ${this.tableName}
234
+ const result = await this.client.query<Record<string, unknown>>(
235
+ `SELECT * FROM ${this.tableName}
231
236
  WHERE user_id = $1 AND status = 'active'
232
237
  ORDER BY created_at DESC
233
238
  LIMIT 1`,
234
239
  [userId]
235
240
  );
236
241
 
237
- return result.rows[0] || null;
242
+ return result.rows[0] ? deserializeSessionRow(result.rows[0]) : null;
238
243
  }
239
244
 
240
245
  async findByUserId(
241
246
  userId: string,
242
247
  limit = 100
243
248
  ): Promise<SessionData<TData>[]> {
244
- const result = await this.client.query<SessionData<TData>>(
245
- `SELECT * FROM ${this.tableName}
249
+ const result = await this.client.query<Record<string, unknown>>(
250
+ `SELECT * FROM ${this.tableName}
246
251
  WHERE user_id = $1
247
252
  ORDER BY created_at DESC
248
253
  LIMIT $2`,
249
254
  [userId, limit]
250
255
  );
251
256
 
252
- return result.rows;
257
+ return result.rows.map((row) => deserializeSessionRow(row));
253
258
  }
254
259
 
255
260
  async update(
@@ -306,7 +311,7 @@ class PostgreSQLSessionRepository<TData = Record<string, unknown>>
306
311
  const versionParam = paramIndex++;
307
312
  values.push(expectedVersion);
308
313
 
309
- const result = await this.client.query<SessionData<TData>>(
314
+ const result = await this.client.query<Record<string, unknown>>(
310
315
  `UPDATE ${this.tableName}
311
316
  SET ${fields.join(", ")}
312
317
  WHERE id = $${idParam} AND (version IS NULL OR version = $${versionParam})
@@ -314,7 +319,7 @@ class PostgreSQLSessionRepository<TData = Record<string, unknown>>
314
319
  values
315
320
  );
316
321
 
317
- if (result.rows[0]) return result.rows[0];
322
+ if (result.rows[0]) return deserializeSessionRow(result.rows[0]);
318
323
 
319
324
  const existing = await this.findById(id);
320
325
  if (!existing) return null;
@@ -324,7 +329,7 @@ class PostgreSQLSessionRepository<TData = Record<string, unknown>>
324
329
  fields.push(`version = COALESCE(version, 0) + 1`);
325
330
  values.push(id);
326
331
 
327
- const result = await this.client.query<SessionData<TData>>(
332
+ const result = await this.client.query<Record<string, unknown>>(
328
333
  `UPDATE ${this.tableName}
329
334
  SET ${fields.join(", ")}
330
335
  WHERE id = $${paramIndex}
@@ -332,7 +337,7 @@ class PostgreSQLSessionRepository<TData = Record<string, unknown>>
332
337
  values
333
338
  );
334
339
 
335
- return result.rows[0] || null;
340
+ return result.rows[0] ? deserializeSessionRow(result.rows[0]) : null;
336
341
  }
337
342
 
338
343
  async updateStatus(
@@ -362,7 +367,7 @@ class PostgreSQLSessionRepository<TData = Record<string, unknown>>
362
367
  }
363
368
 
364
369
  async incrementMessageCount(id: string): Promise<SessionData<TData> | null> {
365
- const result = await this.client.query<SessionData<TData>>(
370
+ const result = await this.client.query<Record<string, unknown>>(
366
371
  `UPDATE ${this.tableName}
367
372
  SET message_count = message_count + 1,
368
373
  last_message_at = NOW(),
@@ -372,7 +377,7 @@ class PostgreSQLSessionRepository<TData = Record<string, unknown>>
372
377
  [id]
373
378
  );
374
379
 
375
- return result.rows[0] || null;
380
+ return result.rows[0] ? deserializeSessionRow(result.rows[0]) : null;
376
381
  }
377
382
 
378
383
  async delete(id: string): Promise<boolean> {
@@ -15,6 +15,7 @@ import type {
15
15
  } from "../types";
16
16
  import { SessionConflictError } from "../types/errors";
17
17
  import { createSessionId, logger } from "../utils";
18
+ import { coerceDate } from "./sessionRow";
18
19
 
19
20
  /**
20
21
  * Redis client interface - matches ioredis/redis clients
@@ -27,6 +28,7 @@ export interface RedisClient {
27
28
  keys(pattern: string): Promise<string[]>;
28
29
  hgetall(key: string): Promise<Record<string, string>>;
29
30
  hset(key: string, field: string, value: string): Promise<number>;
31
+ hdel(key: string, field: string): Promise<number>;
30
32
  expire(key: string, seconds: number): Promise<number>;
31
33
  quit(): Promise<string>;
32
34
  }
@@ -162,7 +164,16 @@ class RedisSessionRepository<TData = Record<string, unknown>>
162
164
  const data = await this.redis.get(this.getKey(id));
163
165
  if (!data) return null;
164
166
  try {
165
- return JSON.parse(data) as SessionData<TData>;
167
+ const parsed = JSON.parse(data) as SessionData<TData>;
168
+ // JSON round-trips turn Dates into ISO strings — coerce on read so
169
+ // callers always get Date instances (parity with the SQL adapters).
170
+ return {
171
+ ...parsed,
172
+ lastMessageAt: coerceDate(parsed.lastMessageAt),
173
+ completedAt: coerceDate(parsed.completedAt),
174
+ createdAt: coerceDate(parsed.createdAt) ?? new Date(),
175
+ updatedAt: coerceDate(parsed.updatedAt) ?? new Date(),
176
+ };
166
177
  } catch (error) {
167
178
  logger.error(`Error parsing session data for id ${id}:`, error);
168
179
  return null;
@@ -172,7 +183,13 @@ class RedisSessionRepository<TData = Record<string, unknown>>
172
183
  async findActiveByUserId(userId: string): Promise<SessionData<TData> | null> {
173
184
  const sessionIds = await this.redis.hgetall(this.getUserKey(userId));
174
185
 
175
- for (const sessionId of Object.keys(sessionIds)) {
186
+ // Newest-first: the hash values ARE the sessions' createdAt ISO strings,
187
+ // which sort lexicographically. Iterating hash order instead would return
188
+ // an arbitrary active session, not the most recent one.
189
+ const newestFirst = Object.keys(sessionIds).sort((a, b) =>
190
+ sessionIds[b].localeCompare(sessionIds[a])
191
+ );
192
+ for (const sessionId of newestFirst) {
176
193
  const session = await this.findById(sessionId);
177
194
  if (session && session.status === "active") {
178
195
  return session;
@@ -187,19 +204,21 @@ class RedisSessionRepository<TData = Record<string, unknown>>
187
204
  limit = 100
188
205
  ): Promise<SessionData<TData>[]> {
189
206
  const sessionIds = await this.redis.hgetall(this.getUserKey(userId));
190
- const sessions: SessionData<TData>[] = [];
191
207
 
192
- for (const sessionId of Object.keys(sessionIds).slice(0, limit)) {
208
+ // Sort BEFORE limiting (hash values are the createdAt ISO strings)
209
+ // limiting first would keep an arbitrary hash-order subset.
210
+ const newestFirst = Object.keys(sessionIds)
211
+ .sort((a, b) => sessionIds[b].localeCompare(sessionIds[a]))
212
+ .slice(0, limit);
213
+ const sessions: SessionData<TData>[] = [];
214
+ for (const sessionId of newestFirst) {
193
215
  const session = await this.findById(sessionId);
194
216
  if (session) {
195
217
  sessions.push(session);
196
218
  }
197
219
  }
198
220
 
199
- return sessions.sort(
200
- (a, b) =>
201
- new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
202
- );
221
+ return sessions;
203
222
  }
204
223
 
205
224
  async update(
@@ -240,7 +259,9 @@ class RedisSessionRepository<TData = Record<string, unknown>>
240
259
  status: SessionStatus,
241
260
  completedAt?: Date
242
261
  ): Promise<SessionData<TData> | null> {
243
- return this.update(id, { status, completedAt });
262
+ // An undefined completedAt must be omitted, not spread — update() writes
263
+ // whatever keys it receives, and SQL skips undefined columns.
264
+ return this.update(id, completedAt ? { status, completedAt } : { status });
244
265
  }
245
266
 
246
267
  async updateCollectedData(
@@ -255,21 +276,40 @@ class RedisSessionRepository<TData = Record<string, unknown>>
255
276
  flow?: string,
256
277
  step?: string
257
278
  ): Promise<SessionData<TData> | null> {
258
- return this.update(id, { currentFlow: flow, currentStep: step });
279
+ // Mirror SQL's conditional column assembly: only defined values are written.
280
+ return this.update(id, {
281
+ ...(flow !== undefined && { currentFlow: flow }),
282
+ ...(step !== undefined && { currentStep: step }),
283
+ });
259
284
  }
260
285
 
261
286
  async incrementMessageCount(id: string): Promise<SessionData<TData> | null> {
262
287
  const session = await this.findById(id);
263
288
  if (!session) return null;
264
289
 
265
- return this.update(id, {
290
+ // Bookkeeping write, deliberately NOT routed through update(): count bumps
291
+ // run alongside saveSessionState's version CAS every turn, so they must
292
+ // not move `version` (matches the SQL adapters' dedicated statement).
293
+ const updated: SessionData<TData> = {
294
+ ...session,
266
295
  messageCount: (session.messageCount || 0) + 1,
267
296
  lastMessageAt: new Date(),
268
- });
297
+ updatedAt: new Date(),
298
+ };
299
+ await this.redis.setex(this.getKey(id), this.ttl, JSON.stringify(updated));
300
+ return updated;
269
301
  }
270
302
 
271
303
  async delete(id: string): Promise<boolean> {
304
+ // Look up first so the user index can be cleaned too — otherwise
305
+ // findActiveByUserId/findByUserId chase dead ids forever.
306
+ const existing = await this.findById(id);
307
+ if (!existing) return false;
308
+
272
309
  const result = await this.redis.del(this.getKey(id));
310
+ if (existing.userId) {
311
+ await this.redis.hdel(this.getUserKey(existing.userId), id);
312
+ }
273
313
  return result > 0;
274
314
  }
275
315
  }
@@ -316,7 +356,8 @@ class RedisMessageRepository implements MessageRepository {
316
356
  const data = await this.redis.get(this.getKey(id));
317
357
  if (!data) return null;
318
358
  try {
319
- return JSON.parse(data) as MessageData;
359
+ const message = JSON.parse(data) as MessageData;
360
+ return { ...message, createdAt: coerceDate(message.createdAt) ?? new Date() };
320
361
  } catch (error) {
321
362
  logger.error(`Error parsing message data for id ${id}:`, error);
322
363
  return null;
@@ -328,18 +369,21 @@ class RedisMessageRepository implements MessageRepository {
328
369
  limit = 1000
329
370
  ): Promise<MessageData[]> {
330
371
  const messageIds = await this.redis.hgetall(this.getSessionKey(sessionId));
331
- const messages: MessageData[] = [];
332
-
333
- for (const messageId of Object.keys(messageIds).slice(0, limit)) {
334
- const message = await this.findById(messageId);
335
- if (message) {
336
- messages.push(message);
337
- }
338
- }
339
372
 
340
- return messages.sort(
341
- (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
373
+ // Fetch ALL candidates first — limiting before sorting would keep an
374
+ // arbitrary hash-order subset instead of the oldest messages. The fetches
375
+ // are independent, so run them concurrently instead of N serialized
376
+ // round trips.
377
+ const fetched = await Promise.all(
378
+ Object.keys(messageIds).map((messageId) => this.findById(messageId))
342
379
  );
380
+ const messages = fetched.filter((m): m is MessageData => m !== null);
381
+
382
+ // Chronological order (matches PostgreSQL/Mongo adapters); limit applies
383
+ // only after sorting is meaningful.
384
+ return messages
385
+ .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
386
+ .slice(0, limit);
343
387
  }
344
388
 
345
389
  async findByUserId(userId: string, limit = 100): Promise<MessageData[]> {
@@ -347,21 +391,24 @@ class RedisMessageRepository implements MessageRepository {
347
391
  // This would require additional indexing
348
392
  const pattern = `${this.keyPrefix}message:*`;
349
393
  const keys = await this.redis.keys(pattern);
350
- const messages: MessageData[] = [];
351
394
 
352
- for (const key of keys.slice(0, limit)) {
353
- const data = await this.redis.get(key);
354
- if (data) {
355
- const message: MessageData = JSON.parse(data) as MessageData;
356
- if (message.userId === userId) {
357
- messages.push(message);
358
- }
395
+ // Filter by userId BEFORE limiting — slicing keys first would drop
396
+ // matching messages that happen to sort after unrelated users' keys.
397
+ // ponytail: full keyspace scan per user query (pre-existing ceiling of
398
+ // this adapter); the upgrade path is a per-user message index.
399
+ const fetched = await Promise.all(keys.map((key) => this.redis.get(key)));
400
+ const messages: MessageData[] = [];
401
+ for (const data of fetched) {
402
+ if (!data) continue;
403
+ const parsed: MessageData = JSON.parse(data);
404
+ if (parsed.userId === userId) {
405
+ messages.push({ ...parsed, createdAt: coerceDate(parsed.createdAt) ?? new Date() });
359
406
  }
360
407
  }
361
408
 
362
- return messages.sort(
363
- (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
364
- );
409
+ return messages
410
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
411
+ .slice(0, limit);
365
412
  }
366
413
 
367
414
  async delete(id: string): Promise<boolean> {
@@ -33,6 +33,7 @@ import type {
33
33
  } from "../types";
34
34
  import { SessionConflictError } from "../types/errors";
35
35
  import { createSessionId } from "../utils";
36
+ import { deserializeSessionRow } from "./sessionRow";
36
37
 
37
38
  /**
38
39
  * SQLite database interface - matches better-sqlite3
@@ -202,8 +203,8 @@ class SQLiteSessionRepository<TData = Record<string, unknown>>
202
203
 
203
204
  const stmt = this.db.prepare(`
204
205
  INSERT INTO ${this.tableName}
205
- (id, user_id, agent_name, status, collected_data, message_count, version, created_at, updated_at)
206
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
206
+ (id, user_id, agent_name, status, current_flow, current_step, collected_data, message_count, last_message_at, completed_at, version, created_at, updated_at)
207
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
207
208
  `);
208
209
 
209
210
  stmt.run(
@@ -211,8 +212,12 @@ class SQLiteSessionRepository<TData = Record<string, unknown>>
211
212
  data.userId || null,
212
213
  data.agentName || null,
213
214
  session.status,
215
+ data.currentFlow || null,
216
+ data.currentStep || null,
214
217
  JSON.stringify(data.collectedData || {}),
215
218
  session.messageCount,
219
+ data.lastMessageAt ? data.lastMessageAt.toISOString() : null,
220
+ data.completedAt ? data.completedAt.toISOString() : null,
216
221
  session.version,
217
222
  now.toISOString(),
218
223
  now.toISOString()
@@ -226,7 +231,7 @@ class SQLiteSessionRepository<TData = Record<string, unknown>>
226
231
  `SELECT * FROM ${this.tableName} WHERE id = ?`
227
232
  );
228
233
  const row = stmt.get(id);
229
- return Promise.resolve(row ? this.deserializeSession(row) : null);
234
+ return Promise.resolve(row ? deserializeSessionRow(row) : null);
230
235
  }
231
236
 
232
237
  findActiveByUserId(userId: string): Promise<SessionData<TData> | null> {
@@ -237,7 +242,7 @@ class SQLiteSessionRepository<TData = Record<string, unknown>>
237
242
  LIMIT 1`
238
243
  );
239
244
  const row = stmt.get(userId);
240
- return Promise.resolve(row ? this.deserializeSession(row) : null);
245
+ return Promise.resolve(row ? deserializeSessionRow(row) : null);
241
246
  }
242
247
 
243
248
  findByUserId(userId: string, limit = 100): Promise<SessionData<TData>[]> {
@@ -248,7 +253,7 @@ class SQLiteSessionRepository<TData = Record<string, unknown>>
248
253
  LIMIT ?`
249
254
  );
250
255
  const rows = stmt.all(userId, limit);
251
- return Promise.resolve(rows.map((row) => this.deserializeSession(row)));
256
+ return Promise.resolve(rows.map((row) => deserializeSessionRow(row)));
252
257
  }
253
258
 
254
259
  async update(
@@ -378,32 +383,6 @@ class SQLiteSessionRepository<TData = Record<string, unknown>>
378
383
  const result = stmt.run(id);
379
384
  return Promise.resolve(result.changes > 0);
380
385
  }
381
-
382
- private deserializeSession(row: Record<string, unknown>): SessionData<TData> {
383
- return {
384
- id: row.id as string,
385
- userId: (row.user_id as string) || undefined,
386
- agentName: (row.agent_name as string) || undefined,
387
- status: row.status as SessionStatus,
388
- currentFlow: (row.current_flow as string) || undefined,
389
- currentStep: (row.current_step as string) || undefined,
390
- collectedData: row.collected_data
391
- ? (JSON.parse(
392
- row.collected_data as string
393
- ) as CollectedStateData<TData>)
394
- : undefined,
395
- messageCount: (row.message_count as number) || 0,
396
- lastMessageAt: row.last_message_at
397
- ? new Date(row.last_message_at as string)
398
- : undefined,
399
- completedAt: row.completed_at
400
- ? new Date(row.completed_at as string)
401
- : undefined,
402
- version: (row.version as number | null) ?? undefined,
403
- createdAt: new Date(row.created_at as string),
404
- updatedAt: new Date(row.updated_at as string),
405
- };
406
- }
407
386
  }
408
387
 
409
388
  /**
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Shared row-mapping for the SQL-shaped session adapters (PostgreSQL, SQLite)
3
+ * and date coercion for JSON-round-tripped values (Redis).
4
+ *
5
+ * One implementation so the adapters can't drift on what a stored session row
6
+ * means. PostgreSQL's driver returns JSONB pre-parsed and timestamptz as Date
7
+ * instances; SQLite returns strings — both shapes are accepted here.
8
+ */
9
+
10
+ import type {
11
+ CollectedStateData,
12
+ SessionData,
13
+ SessionStatus,
14
+ } from "../types";
15
+
16
+ /**
17
+ * Coerce a raw stored value into a Date. Drivers variously hand back Date
18
+ * instances (node-postgres timestamptz) or ISO strings (SQLite, JSON blobs).
19
+ * `null`/`undefined` maps to `undefined`.
20
+ */
21
+ export function coerceDate(value: unknown): Date | undefined {
22
+ if (value == null) return undefined;
23
+ return value instanceof Date ? value : new Date(value as string);
24
+ }
25
+
26
+ /**
27
+ * Map a raw snake_case row onto the camelCase SessionData interface.
28
+ * `collected_data` arrives JSONB-parsed on PostgreSQL and as a raw string on
29
+ * SQLite — both are handled. Exported for the adapter mapping tests.
30
+ */
31
+ export function deserializeSessionRow<TData = Record<string, unknown>>(
32
+ row: Record<string, unknown>
33
+ ): SessionData<TData> {
34
+ const collected = row.collected_data;
35
+ return {
36
+ id: row.id as string,
37
+ userId: (row.user_id as string) || undefined,
38
+ agentName: (row.agent_name as string) || undefined,
39
+ status: row.status as SessionStatus,
40
+ currentFlow: (row.current_flow as string) || undefined,
41
+ currentStep: (row.current_step as string) || undefined,
42
+ collectedData:
43
+ collected == null
44
+ ? undefined
45
+ : typeof collected === "string"
46
+ ? (JSON.parse(collected) as CollectedStateData<TData>)
47
+ : (collected as CollectedStateData<TData>),
48
+ messageCount: (row.message_count as number) || 0,
49
+ lastMessageAt: coerceDate(row.last_message_at),
50
+ completedAt: coerceDate(row.completed_at),
51
+ version: (row.version as number | null) ?? undefined,
52
+ // created_at/updated_at are NOT NULL in every adapter's schema; the
53
+ // fallback only covers hand-crafted rows.
54
+ createdAt: coerceDate(row.created_at) ?? new Date(),
55
+ updatedAt: coerceDate(row.updated_at) ?? new Date(),
56
+ };
57
+ }