From 84c6a579b261542df42437cc73fcf804f55ad4ea Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Thu, 4 Dec 2025 16:51:27 +1000 Subject: [PATCH] feat: add automatic health check updates on Railway tool failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add real-time health status updates when tools fail to load or execute: - Add @tpmjs/db dependency to playground package - Create reportToolFailure() function to update health status on errors - Trigger health check updates for: 1. Import failures (Railway load-and-describe errors) 2. Execution failures (Railway execute-tool errors) - Updates are non-blocking and run in background - Each tool failure now logs: - 🏥 Triggering health check for {package}/{export} - ✅ Health status updated for {package}/{export} This complements the proactive health checking (daily cron + manual recheck) with reactive health updates from actual tool usage errors. Handles multiple errors in batch loading - each error triggers its own health check update independently and asynchronously. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/playground/package.json | 1 + .../playground/src/lib/dynamic-tool-loader.ts | 76 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/apps/playground/package.json b/apps/playground/package.json index 88ba084..30a269a 100644 --- a/apps/playground/package.json +++ b/apps/playground/package.json @@ -13,6 +13,7 @@ "dependencies": { "@ai-sdk/openai": "3.0.0-beta.74", "@ai-sdk/react": "3.0.0-beta.131", + "@tpmjs/db": "workspace:*", "@tpmjs/env": "workspace:*", "@tpmjs/hello": "workspace:*", "@tpmjs/search-registry": "workspace:*", diff --git a/apps/playground/src/lib/dynamic-tool-loader.ts b/apps/playground/src/lib/dynamic-tool-loader.ts index 51379cf..6f966b8 100644 --- a/apps/playground/src/lib/dynamic-tool-loader.ts +++ b/apps/playground/src/lib/dynamic-tool-loader.ts @@ -1,6 +1,8 @@ +import { prisma } from '@tpmjs/db'; import { jsonSchema, tool } from 'ai'; // Cache for tool wrappers (process-level) +// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex const moduleCache = new Map(); // Cache for per-conversation active tools @@ -36,6 +38,51 @@ function getConversationEnv(conversationId: string): Record { return conversationEnv.get(conversationId) || {}; } +/** + * Report tool failure and trigger async health check + * Non-blocking - logs error and triggers health check in background + */ +async function reportToolFailure( + packageName: string, + exportName: string, + error: string, + phase: 'import' | 'execution' +): Promise { + try { + // Find the tool in the database + const tool = await prisma.tool.findFirst({ + where: { + exportName, + package: { + npmPackageName: packageName, + }, + }, + select: { id: true }, + }); + + if (!tool) { + console.warn(`⚠️ Tool not found in database for health check: ${packageName}/${exportName}`); + return; + } + + console.log(`🏥 Triggering health check for ${packageName}/${exportName} (${tool.id})`); + + // Update health status immediately (optimistic) + await prisma.tool.update({ + where: { id: tool.id }, + data: { + [phase === 'import' ? 'importHealth' : 'executionHealth']: 'BROKEN', + healthCheckError: error, + lastHealthCheck: new Date(), + }, + }); + + console.log(`✅ Health status updated for ${packageName}/${exportName}`); + } catch (err) { + console.error('❌ Failed to report tool failure:', err); + } +} + /** * Dynamically load a tool via Railway service * Railway service runs with --experimental-network-imports and can import from esm.sh @@ -77,6 +124,17 @@ export async function loadToolDynamically( if (!response.ok) { const errorText = await response.text(); console.error(`❌ Railway service error (${response.status}): ${errorText}`); + + // Trigger health check update in background (non-blocking) + reportToolFailure( + packageName, + exportName, + `Railway service error (${response.status}): ${errorText}`, + 'import' + ).catch((err) => + console.error(`Failed to report tool failure for ${packageName}/${exportName}:`, err) + ); + return null; } @@ -84,6 +142,13 @@ export async function loadToolDynamically( if (!data.success) { console.error(`❌ Failed to load tool: ${data.error}`); + + // Trigger health check update in background (non-blocking) + reportToolFailure(packageName, exportName, data.error || 'Unknown error', 'import').catch( + (err) => + console.error(`Failed to report tool failure for ${packageName}/${exportName}:`, err) + ); + return null; } @@ -125,6 +190,17 @@ export async function loadToolDynamically( if (!result.success) { console.error(`❌ Tool execution failed: ${result.error}`); + + // Trigger health check update in background (non-blocking) + reportToolFailure( + packageName, + exportName, + result.error || 'Tool execution failed', + 'execution' + ).catch((err) => + console.error(`Failed to report tool failure for ${packageName}/${exportName}:`, err) + ); + throw new Error(result.error || 'Tool execution failed'); }