From 11ab95531bdbcd8988e84c920b564cf9943aa208 Mon Sep 17 00:00:00 2001 From: yusufaytas Date: Sun, 15 Mar 2026 21:05:59 +0000 Subject: [PATCH] Fixed sqlite bug to store conversations. --- package.json | 4 +- scripts/runTests.ts | 33 +++++++++++++++ scripts/seedDatabase.ts | 5 +-- src/engine/conversationManager.ts | 19 +++++++-- src/engine/handlers/metric/followUpHandler.ts | 2 +- .../handlers/orchestration/intentHandler.ts | 10 ----- src/stores/sqliteConversationStore.ts | 32 ++++++++++++++- tests/sqliteConversationStore.test.ts | 41 +++++++++++++++++++ 8 files changed, 124 insertions(+), 22 deletions(-) create mode 100644 scripts/runTests.ts diff --git a/package.json b/package.json index 89ac562..f626e60 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "lint": "eslint .", "lint:fix": "eslint . --fix", "type-check": "tsc --noEmit", - "test": "NODE_ENV=test node --test --loader ts-node/esm tests/*.test.ts tests/**/*.test.ts", + "test": "node --loader ts-node/esm scripts/runTests.ts", "start": "node --loader ts-node/esm src/server.ts", "seed": "node --loader ts-node/esm scripts/seedDatabase.ts" }, @@ -53,4 +53,4 @@ "typescript": "^5.6.3", "typescript-eslint": "^8.48.1" } -} +} \ No newline at end of file diff --git a/scripts/runTests.ts b/scripts/runTests.ts new file mode 100644 index 0000000..6fb482d --- /dev/null +++ b/scripts/runTests.ts @@ -0,0 +1,33 @@ +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); +const testsDir = path.resolve(dirname, '../tests'); + +const testFiles = fs.readdirSync(testsDir, { recursive: true }) + .map(file => file.toString()) + .filter(file => file.endsWith('.test.ts')) + .map(file => path.join(testsDir, file)); + +const result = spawnSync('node', [ + '--loader', 'ts-node/esm', + '--test', + ...testFiles +], { + stdio: 'inherit', + env: { ...process.env, NODE_ENV: 'test' } +}); + +if (result.error) { + console.error('Failed to start test runner:', result.error); + process.exit(1); +} + +if (result.signal) { + process.kill(process.pid, result.signal); + process.exit(1); +} + +process.exit(result.status ?? 0); diff --git a/scripts/seedDatabase.ts b/scripts/seedDatabase.ts index 39dba10..4fa7d6b 100644 --- a/scripts/seedDatabase.ts +++ b/scripts/seedDatabase.ts @@ -5,7 +5,7 @@ */ import { SqliteConversationStore } from "../src/stores/sqliteConversationStore.js"; -import { Conversation, ConversationTurn, Entity } from "../src/types.js"; +import { Conversation, ConversationTurn } from "../src/types.js"; import { randomUUID } from "crypto"; const DB_PATH = "./data/conversations.db"; @@ -56,9 +56,6 @@ function generateIncidentId(): string { return `INC-${Math.floor(10000 + Math.random() * 90000)}`; } -function generateTicketId(): string { - return `TICK-${Math.floor(1000 + Math.random() * 9000)}`; -} interface ConversationTemplate { name: (service: string) => string; diff --git a/src/engine/conversationManager.ts b/src/engine/conversationManager.ts index 7c553df..da91e78 100644 --- a/src/engine/conversationManager.ts +++ b/src/engine/conversationManager.ts @@ -316,10 +316,23 @@ function safeStringify(value: unknown): string { if (typeof value === "string") { return value; } - + try { - return JSON.stringify(value); - } catch { + const errorCache = new Set(); + return JSON.stringify(value, (key, val) => { + if (val instanceof Error) { + if (errorCache.has(val)) { + return "[Circular]"; + } + errorCache.add(val); + return { name: val.name, message: val.message }; + } + if (typeof val === "bigint") { + return val.toString(); + } + return val; + }); + } catch (err) { return String(value); } } diff --git a/src/engine/handlers/metric/followUpHandler.ts b/src/engine/handlers/metric/followUpHandler.ts index 3078ccd..2683cf3 100644 --- a/src/engine/handlers/metric/followUpHandler.ts +++ b/src/engine/handlers/metric/followUpHandler.ts @@ -297,7 +297,7 @@ export const metricFollowUpHandler: FollowUpHandler = async ( // Check if we already added it to suggestions in this turn const alreadySuggested = suggestions.some(s => s.name === "describe-metrics" && - (s.arguments.scope as any)?.service === service + (s.arguments.scope as JsonObject)?.service === service ); if (!alreadySuggested) { diff --git a/src/engine/handlers/orchestration/intentHandler.ts b/src/engine/handlers/orchestration/intentHandler.ts index 42e3fab..a6c1d7b 100644 --- a/src/engine/handlers/orchestration/intentHandler.ts +++ b/src/engine/handlers/orchestration/intentHandler.ts @@ -41,13 +41,3 @@ export const orchestrationIntentHandler: IntentHandler = async (context): Promis reasoning: 'No orchestration keywords detected' }; }; -function extractQuery(text: string): string { - // Simple extraction: take words after key phrases - const connectives = ['for', 'about', 'finding', 'showing']; - const words = text.split(' '); - const idx = words.findIndex(w => connectives.includes(w)); - if (idx !== -1 && idx < words.length - 1) { - return words.slice(idx + 1).join(' '); - } - return text; -} diff --git a/src/stores/sqliteConversationStore.ts b/src/stores/sqliteConversationStore.ts index 5e5a2a7..4a79ddc 100644 --- a/src/stores/sqliteConversationStore.ts +++ b/src/stores/sqliteConversationStore.ts @@ -296,8 +296,36 @@ export class SqliteConversationStore implements ConversationStore { } } - // Serialize turns to JSON - const turnsJson = JSON.stringify(conversation.turns); + // Safely serialize turns to JSON considering Errors, BigInts, and circular references + let turnsJson: string; + try { + const errorCache = new Set(); + turnsJson = JSON.stringify(conversation.turns, (key, value) => { + if (value instanceof Error) { + if (errorCache.has(value)) { + return "[Circular]"; + } + errorCache.add(value); + return { + name: value.name, + message: value.message, + stack: value.stack, + ...((value as any).cause ? { cause: (value as any).cause } : {}) + }; + } + if (typeof value === "bigint") { + return value.toString(); + } + return value; + }); + } catch (stringifyError) { + console.warn(`[SqliteConversationStore] Failed to serialize turns for ${chatId}, saving a fallback format:`, stringifyError); + turnsJson = JSON.stringify([{ + userMessage: "[Error]", + assistantResponse: "Conversation could not be saved due to an unserializable object in the execution trace.", + timestamp: Date.now() + }]); + } // Insert or replace conversation this.setStmt.run( diff --git a/tests/sqliteConversationStore.test.ts b/tests/sqliteConversationStore.test.ts index 7ac6576..02e5124 100644 --- a/tests/sqliteConversationStore.test.ts +++ b/tests/sqliteConversationStore.test.ts @@ -577,6 +577,47 @@ describe('SqliteConversationStore', () => { cleanup(); } }); + + it('should safely stringify and store conversations with BigInt and Circular Errors', async () => { + const { path, cleanup } = createTempDb(); + + try { + const store = new SqliteConversationStore(defaultConfig, path); + const conversation = createTestConversation('chat2', 'Serialization Test'); + + // Construct a circular error specifically to mimic unhandled fetch exceptions + const fetchError = new Error("fetch failed"); + (fetchError as any).cause = fetchError; // Create circular reference + + conversation.turns.push({ + userMessage: 'Trigger complex log payload', + assistantResponse: 'Fetched with errors.', + timestamp: Date.now(), + toolResults: [{ + name: 'fetch-tool', + // BigInt and circular error mimic exact payloads that crash JSON.stringify + result: { count: BigInt(9007199254740991n), error: fetchError } as any + }] + }); + + await store.set('chat2', conversation); + const retrieved = await store.get('chat2'); + + assert.strictEqual(retrieved?.turns.length, 2); + + // Assert BigInt was safely stringified to string + const complexTurn = retrieved?.turns[1]; + const complexResult = complexTurn?.toolResults?.[0].result as any; + + assert.strictEqual(complexResult.count, "9007199254740991"); + assert.strictEqual(complexResult.error.message, "fetch failed"); + assert.strictEqual(complexResult.error.cause, "[Circular]"); + + await store.close(); + } finally { + cleanup(); + } + }); }); });