From 7d0fb5445d92a24469f55488a56b629b353167ab Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 4 Jan 2026 20:40:14 -0500 Subject: [PATCH] Fix Addie performance metrics SQL and data storage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix PostgreSQL ROUND() function calls with FILTER clause by wrapping aggregate expressions in parentheses before casting to numeric - Add performance data (latency, tokens, timing) to reaction handler - Add error handling for reaction message logging - Add logging when streaming completes without done event - Change || to ?? (nullish coalescing) to preserve 0 values for tokens The main bug was SQL queries failing with "function round(double precision, integer) does not exist" because ROUND() requires numeric type, and the ::numeric cast wasn't being applied to the full aggregate expression. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .changeset/eleven-rabbits-enter.md | 2 ++ server/src/addie/bolt-app.ts | 55 +++++++++++++++++++--------- server/src/addie/thread-service.ts | 58 +++++++++++++++--------------- 3 files changed, 70 insertions(+), 45 deletions(-) create mode 100644 .changeset/eleven-rabbits-enter.md diff --git a/.changeset/eleven-rabbits-enter.md b/.changeset/eleven-rabbits-enter.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/eleven-rabbits-enter.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/server/src/addie/bolt-app.ts b/server/src/addie/bolt-app.ts index 94ebfc34c9..4fb1a2564e 100644 --- a/server/src/addie/bolt-app.ts +++ b/server/src/addie/bolt-app.ts @@ -942,8 +942,11 @@ async function handleUserMessage({ } } - // Build final response object if we used streaming + // Build final response object if we used streaming but didn't receive a 'done' event + // This shouldn't happen normally, but provides a fallback with logging if (!response) { + logger.warn({ fullTextLength: fullText.length, toolsUsedCount: toolsUsed.length }, + 'Addie Bolt: Streaming completed without done event - using fallback response'); response = { text: fullText, tools_used: toolsUsed, @@ -953,7 +956,8 @@ async function handleUserMessage({ duration_ms: 0, sequence: i + 1, })), - flagged: false, + flagged: true, + flag_reason: 'Streaming completed without done event', }; } @@ -2334,6 +2338,7 @@ async function handleReactionAdded({ 'Addie Bolt: Received reaction on Addie message' ); + const startTime = Date.now(); const threadService = getThreadService(); // Build external ID to find the thread @@ -2468,22 +2473,40 @@ async function handleReactionAdded({ logger.error({ error }, 'Addie Bolt: Failed to send reaction response'); } - // Log assistant response - await threadService.addMessage({ - thread_id: thread.thread_id, - role: 'assistant', - content: response.text, - tools_used: response.tools_used, - tool_calls: response.tool_executions?.map(exec => ({ - name: exec.tool_name, - input: exec.parameters, - result: exec.result, - })), - model: AddieModelConfig.chat, - }); + // Log assistant response with performance data + try { + await threadService.addMessage({ + thread_id: thread.thread_id, + role: 'assistant', + content: response.text, + tools_used: response.tools_used, + tool_calls: response.tool_executions?.map(exec => ({ + name: exec.tool_name, + input: exec.parameters, + result: exec.result, + duration_ms: exec.duration_ms, + is_error: exec.is_error, + })), + model: AddieModelConfig.chat, + latency_ms: Date.now() - startTime, + tokens_input: response.usage?.input_tokens, + tokens_output: response.usage?.output_tokens, + timing: response.timing ? { + system_prompt_ms: response.timing.system_prompt_ms, + total_llm_ms: response.timing.total_llm_ms, + total_tool_ms: response.timing.total_tool_execution_ms, + iterations: response.timing.iterations, + } : undefined, + tokens_cache_creation: response.usage?.cache_creation_input_tokens, + tokens_cache_read: response.usage?.cache_read_input_tokens, + active_rule_ids: response.active_rule_ids, + }); + } catch (error) { + logger.error({ error, threadId: thread.thread_id }, 'Addie Bolt: Failed to log reaction response'); + } logger.info( - { threadId: thread.thread_id, reaction, isConfirmation: isConfirmationRequest }, + { threadId: thread.thread_id, reaction, isConfirmation: isConfirmationRequest, latencyMs: Date.now() - startTime }, 'Addie Bolt: Processed reaction and responded' ); } diff --git a/server/src/addie/thread-service.ts b/server/src/addie/thread-service.ts index 574f7b4c3e..931bf9bc44 100644 --- a/server/src/addie/thread-service.ts +++ b/server/src/addie/thread-service.ts @@ -392,26 +392,26 @@ export class ThreadService { input.thread_id, input.role, input.content, - input.content_sanitized || null, - input.tools_used || null, + input.content_sanitized ?? null, + input.tools_used ?? null, input.tool_calls ? JSON.stringify(input.tool_calls) : null, - input.knowledge_ids || null, - input.model || null, - input.latency_ms || null, - input.tokens_input || null, - input.tokens_output || null, - input.flagged || false, - input.flag_reason || null, + input.knowledge_ids ?? null, + input.model ?? null, + input.latency_ms ?? null, + input.tokens_input ?? null, + input.tokens_output ?? null, + input.flagged ?? false, + input.flag_reason ?? null, sequenceNumber, - input.timing?.system_prompt_ms || null, - input.timing?.total_llm_ms || null, - input.timing?.total_tool_ms || null, - input.timing?.iterations || null, - input.tokens_cache_creation || null, - input.tokens_cache_read || null, - input.active_rule_ids || null, + input.timing?.system_prompt_ms ?? null, + input.timing?.total_llm_ms ?? null, + input.timing?.total_tool_ms ?? null, + input.timing?.iterations ?? null, + input.tokens_cache_creation ?? null, + input.tokens_cache_read ?? null, + input.active_rule_ids ?? null, input.router_decision ? JSON.stringify(input.router_decision) : null, - input.config_version_id || null, + input.config_version_id ?? null, ] ); @@ -909,14 +909,14 @@ export class ThreadService { `SELECT COUNT(*) as total_messages, COUNT(*) FILTER (WHERE role = 'assistant') as total_assistant_messages, - ROUND(AVG(latency_ms) FILTER (WHERE role = 'assistant')::numeric, 0) as avg_latency_ms, - ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY latency_ms) FILTER (WHERE role = 'assistant')::numeric, 0) as p50_latency_ms, - ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY latency_ms) FILTER (WHERE role = 'assistant')::numeric, 0) as p95_latency_ms, + ROUND((AVG(latency_ms) FILTER (WHERE role = 'assistant'))::numeric, 0) as avg_latency_ms, + ROUND((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY latency_ms) FILTER (WHERE role = 'assistant'))::numeric, 0) as p50_latency_ms, + ROUND((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY latency_ms) FILTER (WHERE role = 'assistant'))::numeric, 0) as p95_latency_ms, MAX(latency_ms) FILTER (WHERE role = 'assistant') as max_latency_ms, COALESCE(SUM(tokens_input), 0) as total_input_tokens, COALESCE(SUM(tokens_output), 0) as total_output_tokens, - ROUND(AVG(tokens_input) FILTER (WHERE tokens_input IS NOT NULL)::numeric, 0) as avg_input_tokens, - ROUND(AVG(tokens_output) FILTER (WHERE tokens_output IS NOT NULL)::numeric, 0) as avg_output_tokens + ROUND((AVG(tokens_input) FILTER (WHERE tokens_input IS NOT NULL))::numeric, 0) as avg_input_tokens, + ROUND((AVG(tokens_output) FILTER (WHERE tokens_output IS NOT NULL))::numeric, 0) as avg_output_tokens FROM addie_thread_messages WHERE created_at > NOW() - make_interval(days => $1)`, [days] @@ -955,8 +955,8 @@ export class ThreadService { `SELECT COALESCE(model, 'unknown') as model, COUNT(*) as count, - ROUND(AVG(latency_ms)::numeric, 0) as avg_latency_ms, - ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY latency_ms)::numeric, 0) as p50_latency_ms, + ROUND((AVG(latency_ms))::numeric, 0) as avg_latency_ms, + ROUND((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY latency_ms))::numeric, 0) as p50_latency_ms, COALESCE(SUM(tokens_input), 0) as total_input_tokens, COALESCE(SUM(tokens_output), 0) as total_output_tokens FROM addie_thread_messages @@ -987,9 +987,9 @@ export class ThreadService { SELECT tool->>'name' as tool_name, COUNT(*) as call_count, - ROUND(AVG((tool->>'duration_ms')::numeric), 0) as avg_duration_ms, - ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY (tool->>'duration_ms')::numeric), 0) as p50_duration_ms, - ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY (tool->>'duration_ms')::numeric), 0) as p95_duration_ms, + ROUND((AVG((tool->>'duration_ms')::numeric))::numeric, 0) as avg_duration_ms, + ROUND((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY (tool->>'duration_ms')::numeric))::numeric, 0) as p50_duration_ms, + ROUND((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY (tool->>'duration_ms')::numeric))::numeric, 0) as p95_duration_ms, COUNT(*) FILTER (WHERE tool->>'is_error' = 'true') as error_count FROM tool_calls GROUP BY tool->>'name' @@ -1006,7 +1006,7 @@ export class ThreadService { `SELECT t.channel, COUNT(m.message_id) as message_count, - ROUND(AVG(m.latency_ms) FILTER (WHERE m.role = 'assistant')::numeric, 0) as avg_latency_ms + ROUND((AVG(m.latency_ms) FILTER (WHERE m.role = 'assistant'))::numeric, 0) as avg_latency_ms FROM addie_threads t JOIN addie_thread_messages m ON t.thread_id = m.thread_id WHERE m.created_at > NOW() - make_interval(days => $1) @@ -1025,7 +1025,7 @@ export class ThreadService { `SELECT DATE_TRUNC('day', created_at)::date::text as date, COUNT(*) as message_count, - ROUND(AVG(latency_ms) FILTER (WHERE role = 'assistant')::numeric, 0) as avg_latency_ms, + ROUND((AVG(latency_ms) FILTER (WHERE role = 'assistant'))::numeric, 0) as avg_latency_ms, COALESCE(SUM(tokens_input), 0) + COALESCE(SUM(tokens_output), 0) as total_tokens FROM addie_thread_messages WHERE created_at > NOW() - make_interval(days => $1)