@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.
- package/README.md +1 -1
- package/dist/adapters/MemoryAdapter.js +29 -33
- package/dist/adapters/MemoryAdapter.js.map +1 -1
- package/dist/adapters/PostgreSQLAdapter.d.ts.map +1 -1
- package/dist/adapters/PostgreSQLAdapter.js +16 -11
- package/dist/adapters/PostgreSQLAdapter.js.map +1 -1
- package/dist/adapters/RedisAdapter.d.ts +1 -0
- package/dist/adapters/RedisAdapter.d.ts.map +1 -1
- package/dist/adapters/RedisAdapter.js +75 -25
- package/dist/adapters/RedisAdapter.js.map +1 -1
- package/dist/adapters/SQLiteAdapter.d.ts.map +1 -1
- package/dist/adapters/SQLiteAdapter.js +7 -29
- package/dist/adapters/SQLiteAdapter.js.map +1 -1
- package/dist/adapters/sessionRow.d.ts +22 -0
- package/dist/adapters/sessionRow.d.ts.map +1 -0
- package/dist/adapters/sessionRow.js +48 -0
- package/dist/adapters/sessionRow.js.map +1 -0
- package/dist/cjs/adapters/MemoryAdapter.js +29 -33
- package/dist/cjs/adapters/MemoryAdapter.js.map +1 -1
- package/dist/cjs/adapters/PostgreSQLAdapter.d.ts.map +1 -1
- package/dist/cjs/adapters/PostgreSQLAdapter.js +16 -11
- package/dist/cjs/adapters/PostgreSQLAdapter.js.map +1 -1
- package/dist/cjs/adapters/RedisAdapter.d.ts +1 -0
- package/dist/cjs/adapters/RedisAdapter.d.ts.map +1 -1
- package/dist/cjs/adapters/RedisAdapter.js +75 -25
- package/dist/cjs/adapters/RedisAdapter.js.map +1 -1
- package/dist/cjs/adapters/SQLiteAdapter.d.ts.map +1 -1
- package/dist/cjs/adapters/SQLiteAdapter.js +7 -29
- package/dist/cjs/adapters/SQLiteAdapter.js.map +1 -1
- package/dist/cjs/adapters/sessionRow.d.ts +22 -0
- package/dist/cjs/adapters/sessionRow.d.ts.map +1 -0
- package/dist/cjs/adapters/sessionRow.js +52 -0
- package/dist/cjs/adapters/sessionRow.js.map +1 -0
- package/dist/cjs/core/Agent.d.ts +8 -0
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +40 -2
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/AutoChainExecutor.d.ts +8 -18
- package/dist/cjs/core/AutoChainExecutor.d.ts.map +1 -1
- package/dist/cjs/core/AutoChainExecutor.js +23 -26
- package/dist/cjs/core/AutoChainExecutor.js.map +1 -1
- package/dist/cjs/core/CompactionEngine.d.ts +14 -1
- package/dist/cjs/core/CompactionEngine.d.ts.map +1 -1
- package/dist/cjs/core/CompactionEngine.js +30 -6
- package/dist/cjs/core/CompactionEngine.js.map +1 -1
- package/dist/cjs/core/FlowRouter.d.ts.map +1 -1
- package/dist/cjs/core/FlowRouter.js +32 -10
- package/dist/cjs/core/FlowRouter.js.map +1 -1
- package/dist/cjs/core/ResponseGenerationError.d.ts.map +1 -1
- package/dist/cjs/core/ResponseGenerationError.js +3 -5
- package/dist/cjs/core/ResponseGenerationError.js.map +1 -1
- package/dist/cjs/core/ResponseModal.d.ts +29 -0
- package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
- package/dist/cjs/core/ResponseModal.js +141 -18
- package/dist/cjs/core/ResponseModal.js.map +1 -1
- package/dist/cjs/core/ResponsePipeline.d.ts +36 -6
- package/dist/cjs/core/ResponsePipeline.d.ts.map +1 -1
- package/dist/cjs/core/ResponsePipeline.js +208 -73
- package/dist/cjs/core/ResponsePipeline.js.map +1 -1
- package/dist/cjs/core/SessionFinalizer.d.ts.map +1 -1
- package/dist/cjs/core/SessionFinalizer.js +30 -3
- package/dist/cjs/core/SessionFinalizer.js.map +1 -1
- package/dist/cjs/core/SessionManager.d.ts +10 -1
- package/dist/cjs/core/SessionManager.d.ts.map +1 -1
- package/dist/cjs/core/SessionManager.js +43 -16
- package/dist/cjs/core/SessionManager.js.map +1 -1
- package/dist/cjs/core/SignalProcessor.d.ts.map +1 -1
- package/dist/cjs/core/SignalProcessor.js +5 -77
- package/dist/cjs/core/SignalProcessor.js.map +1 -1
- package/dist/cjs/core/Step.d.ts.map +1 -1
- package/dist/cjs/core/Step.js +50 -2
- package/dist/cjs/core/Step.js.map +1 -1
- package/dist/cjs/core/StepLifecycle.d.ts +16 -6
- package/dist/cjs/core/StepLifecycle.d.ts.map +1 -1
- package/dist/cjs/core/StepLifecycle.js +97 -14
- package/dist/cjs/core/StepLifecycle.js.map +1 -1
- package/dist/cjs/core/StreamingToolExecutor.d.ts.map +1 -1
- package/dist/cjs/core/StreamingToolExecutor.js +28 -4
- package/dist/cjs/core/StreamingToolExecutor.js.map +1 -1
- package/dist/cjs/core/ToolLoopExecutor.d.ts +5 -1
- package/dist/cjs/core/ToolLoopExecutor.d.ts.map +1 -1
- package/dist/cjs/core/ToolLoopExecutor.js +155 -63
- package/dist/cjs/core/ToolLoopExecutor.js.map +1 -1
- package/dist/cjs/core/ToolManager.d.ts +1 -1
- package/dist/cjs/core/ToolManager.d.ts.map +1 -1
- package/dist/cjs/core/ToolManager.js +40 -17
- package/dist/cjs/core/ToolManager.js.map +1 -1
- package/dist/cjs/core/flow-namespace.d.ts +15 -0
- package/dist/cjs/core/flow-namespace.d.ts.map +1 -1
- package/dist/cjs/core/flow-namespace.js +22 -0
- package/dist/cjs/core/flow-namespace.js.map +1 -1
- package/dist/cjs/index.d.ts +4 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +5 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.d.ts +10 -3
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.js +54 -73
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
- package/dist/cjs/providers/GeminiProvider.d.ts +9 -3
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +24 -68
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/OpenAICompatibleProvider.d.ts +9 -0
- package/dist/cjs/providers/OpenAICompatibleProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAICompatibleProvider.js +101 -74
- package/dist/cjs/providers/OpenAICompatibleProvider.js.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.d.ts +1 -1
- package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.js +1 -1
- package/dist/cjs/providers/errorClassification.d.ts +7 -0
- package/dist/cjs/providers/errorClassification.d.ts.map +1 -1
- package/dist/cjs/providers/errorClassification.js +27 -0
- package/dist/cjs/providers/errorClassification.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +26 -0
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/flow.d.ts +49 -32
- package/dist/cjs/types/flow.d.ts.map +1 -1
- package/dist/cjs/types/index.d.ts +1 -1
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/index.js.map +1 -1
- package/dist/cjs/types/tool.d.ts +2 -0
- package/dist/cjs/types/tool.d.ts.map +1 -1
- package/dist/cjs/types/tool.js.map +1 -1
- package/dist/cjs/utils/index.d.ts +4 -4
- package/dist/cjs/utils/index.d.ts.map +1 -1
- package/dist/cjs/utils/index.js +10 -1
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/utils/retry.d.ts +99 -1
- package/dist/cjs/utils/retry.d.ts.map +1 -1
- package/dist/cjs/utils/retry.js +140 -3
- package/dist/cjs/utils/retry.js.map +1 -1
- package/dist/cjs/utils/serialize.d.ts +17 -0
- package/dist/cjs/utils/serialize.d.ts.map +1 -1
- package/dist/cjs/utils/serialize.js +33 -0
- package/dist/cjs/utils/serialize.js.map +1 -1
- package/dist/cjs/utils/session.d.ts +21 -1
- package/dist/cjs/utils/session.d.ts.map +1 -1
- package/dist/cjs/utils/session.js +36 -4
- package/dist/cjs/utils/session.js.map +1 -1
- package/dist/core/Agent.d.ts +8 -0
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +40 -2
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/AutoChainExecutor.d.ts +8 -18
- package/dist/core/AutoChainExecutor.d.ts.map +1 -1
- package/dist/core/AutoChainExecutor.js +23 -26
- package/dist/core/AutoChainExecutor.js.map +1 -1
- package/dist/core/CompactionEngine.d.ts +14 -1
- package/dist/core/CompactionEngine.d.ts.map +1 -1
- package/dist/core/CompactionEngine.js +30 -6
- package/dist/core/CompactionEngine.js.map +1 -1
- package/dist/core/FlowRouter.d.ts.map +1 -1
- package/dist/core/FlowRouter.js +32 -10
- package/dist/core/FlowRouter.js.map +1 -1
- package/dist/core/ResponseGenerationError.d.ts.map +1 -1
- package/dist/core/ResponseGenerationError.js +3 -5
- package/dist/core/ResponseGenerationError.js.map +1 -1
- package/dist/core/ResponseModal.d.ts +29 -0
- package/dist/core/ResponseModal.d.ts.map +1 -1
- package/dist/core/ResponseModal.js +142 -19
- package/dist/core/ResponseModal.js.map +1 -1
- package/dist/core/ResponsePipeline.d.ts +36 -6
- package/dist/core/ResponsePipeline.d.ts.map +1 -1
- package/dist/core/ResponsePipeline.js +208 -73
- package/dist/core/ResponsePipeline.js.map +1 -1
- package/dist/core/SessionFinalizer.d.ts.map +1 -1
- package/dist/core/SessionFinalizer.js +31 -4
- package/dist/core/SessionFinalizer.js.map +1 -1
- package/dist/core/SessionManager.d.ts +10 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +44 -17
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/SignalProcessor.d.ts.map +1 -1
- package/dist/core/SignalProcessor.js +5 -77
- package/dist/core/SignalProcessor.js.map +1 -1
- package/dist/core/Step.d.ts.map +1 -1
- package/dist/core/Step.js +50 -2
- package/dist/core/Step.js.map +1 -1
- package/dist/core/StepLifecycle.d.ts +16 -6
- package/dist/core/StepLifecycle.d.ts.map +1 -1
- package/dist/core/StepLifecycle.js +97 -14
- package/dist/core/StepLifecycle.js.map +1 -1
- package/dist/core/StreamingToolExecutor.d.ts.map +1 -1
- package/dist/core/StreamingToolExecutor.js +28 -4
- package/dist/core/StreamingToolExecutor.js.map +1 -1
- package/dist/core/ToolLoopExecutor.d.ts +5 -1
- package/dist/core/ToolLoopExecutor.d.ts.map +1 -1
- package/dist/core/ToolLoopExecutor.js +155 -63
- package/dist/core/ToolLoopExecutor.js.map +1 -1
- package/dist/core/ToolManager.d.ts +1 -1
- package/dist/core/ToolManager.d.ts.map +1 -1
- package/dist/core/ToolManager.js +41 -18
- package/dist/core/ToolManager.js.map +1 -1
- package/dist/core/flow-namespace.d.ts +15 -0
- package/dist/core/flow-namespace.d.ts.map +1 -1
- package/dist/core/flow-namespace.js +22 -0
- package/dist/core/flow-namespace.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/AnthropicProvider.d.ts +10 -3
- package/dist/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.js +56 -75
- package/dist/providers/AnthropicProvider.js.map +1 -1
- package/dist/providers/GeminiProvider.d.ts +9 -3
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +26 -70
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/OpenAICompatibleProvider.d.ts +9 -0
- package/dist/providers/OpenAICompatibleProvider.d.ts.map +1 -1
- package/dist/providers/OpenAICompatibleProvider.js +103 -76
- package/dist/providers/OpenAICompatibleProvider.js.map +1 -1
- package/dist/providers/OpenAIProvider.d.ts +1 -1
- package/dist/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/providers/OpenAIProvider.js +1 -1
- package/dist/providers/errorClassification.d.ts +7 -0
- package/dist/providers/errorClassification.d.ts.map +1 -1
- package/dist/providers/errorClassification.js +26 -0
- package/dist/providers/errorClassification.js.map +1 -1
- package/dist/types/agent.d.ts +26 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/flow.d.ts +49 -32
- package/dist/types/flow.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/tool.d.ts +2 -0
- package/dist/types/tool.d.ts.map +1 -1
- package/dist/types/tool.js.map +1 -1
- package/dist/utils/index.d.ts +4 -4
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -3
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/retry.d.ts +99 -1
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +137 -3
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/serialize.d.ts +17 -0
- package/dist/utils/serialize.d.ts.map +1 -1
- package/dist/utils/serialize.js +31 -0
- package/dist/utils/serialize.js.map +1 -1
- package/dist/utils/session.d.ts +21 -1
- package/dist/utils/session.d.ts.map +1 -1
- package/dist/utils/session.js +33 -4
- package/dist/utils/session.js.map +1 -1
- package/docs/concepts/architecture.md +3 -3
- package/docs/concepts/directives.md +1 -1
- package/docs/guides/error-handling.md +46 -45
- package/docs/guides/flow-control.md +8 -1
- package/docs/guides/instructions.md +15 -6
- package/docs/guides/persistence.md +12 -5
- package/docs/guides/streaming.md +10 -0
- package/docs/migration/README.md +4 -0
- package/docs/migration/v2-3-to-v2-4.md +4 -0
- package/docs/migration/v2-6-to-v2-7.md +246 -0
- package/docs/reference/adapters.md +15 -1
- package/docs/reference/create-agent.md +28 -0
- package/docs/reference/directive.md +1 -1
- package/docs/reference/errors.md +29 -31
- package/docs/reference/providers.md +23 -19
- package/docs/reference/step.md +28 -21
- package/docs/reference/tool.md +14 -5
- package/docs/start/02-first-agent.md +8 -4
- package/docs/start/03-collect-data.md +19 -10
- package/examples/01-quickstart.ts +1 -1
- package/examples/02-data-extraction.ts +1 -1
- package/examples/03-tools.ts +1 -1
- package/examples/04-instructions.ts +1 -1
- package/examples/05-branching.ts +1 -1
- package/examples/06-flow-control.ts +3 -3
- package/examples/07-streaming.ts +1 -1
- package/examples/08-persistence.ts +1 -1
- package/examples/09-signals.ts +1 -1
- package/package.json +2 -2
- package/src/adapters/MemoryAdapter.ts +29 -33
- package/src/adapters/PostgreSQLAdapter.ts +23 -18
- package/src/adapters/RedisAdapter.ts +81 -34
- package/src/adapters/SQLiteAdapter.ts +10 -31
- package/src/adapters/sessionRow.ts +57 -0
- package/src/core/Agent.ts +48 -2
- package/src/core/AutoChainExecutor.ts +37 -50
- package/src/core/CompactionEngine.ts +40 -6
- package/src/core/FlowRouter.ts +38 -12
- package/src/core/ResponseGenerationError.ts +3 -6
- package/src/core/ResponseModal.ts +185 -19
- package/src/core/ResponsePipeline.ts +259 -85
- package/src/core/SessionFinalizer.ts +35 -5
- package/src/core/SessionManager.ts +55 -21
- package/src/core/SignalProcessor.ts +5 -85
- package/src/core/Step.ts +79 -2
- package/src/core/StepLifecycle.ts +125 -22
- package/src/core/StreamingToolExecutor.ts +31 -6
- package/src/core/ToolLoopExecutor.ts +200 -74
- package/src/core/ToolManager.ts +45 -18
- package/src/core/flow-namespace.ts +29 -0
- package/src/index.ts +4 -1
- package/src/providers/AnthropicProvider.ts +83 -131
- package/src/providers/GeminiProvider.ts +42 -116
- package/src/providers/OpenAICompatibleProvider.ts +132 -133
- package/src/providers/OpenAIProvider.ts +2 -2
- package/src/providers/errorClassification.ts +34 -0
- package/src/types/agent.ts +25 -0
- package/src/types/flow.ts +37 -39
- package/src/types/index.ts +1 -0
- package/src/types/tool.ts +2 -0
- package/src/utils/index.ts +22 -3
- package/src/utils/retry.ts +251 -2
- package/src/utils/serialize.ts +38 -0
- package/src/utils/session.ts +41 -4
- package/dist/cjs/core/DirectiveBus.d.ts +0 -88
- package/dist/cjs/core/DirectiveBus.d.ts.map +0 -1
- package/dist/cjs/core/DirectiveBus.js +0 -196
- package/dist/cjs/core/DirectiveBus.js.map +0 -1
- package/dist/core/DirectiveBus.d.ts +0 -88
- package/dist/core/DirectiveBus.d.ts.map +0 -1
- package/dist/core/DirectiveBus.js +0 -192
- package/dist/core/DirectiveBus.js.map +0 -1
- 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.
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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<
|
|
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<
|
|
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]
|
|
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<
|
|
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]
|
|
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<
|
|
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<
|
|
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<
|
|
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]
|
|
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<
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
|
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 ?
|
|
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 ?
|
|
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) =>
|
|
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
|
+
}
|