From 05604e405725dd288524c4e36be4d284832a69a6 Mon Sep 17 00:00:00 2001 From: Grant Eubanks Date: Sat, 10 Jan 2026 16:27:44 -0500 Subject: [PATCH 1/2] lots of stuff --- ARTIFACT_REGISTRY_FIX.md | 2 + STIFF_LAYOUT_SUMMARY.md | 2 + agent-server.js | 15 +- bridge-daemon-legacy.js | 5 +- electron-builder.json | 2 + scripts/fix-artifact-registry.sh | 2 + src/NodeCanvas.jsx | 5 +- src/components/panel/views/LeftAIView.jsx | 5 +- src/services/agentRuntime/Executor.js | 2 + src/services/agentRuntime/README.md | 2 + src/services/agentRuntime/StateMirror.js | 2 + src/services/bridgeConfig.js | 122 +++++++++- src/services/orchestrator/roleRunners.js | 9 +- src/services/persistentAuth.js | 9 +- src/services/save.worker.js | 2 + src/services/toolValidator.js | 5 +- src/store/graphStore.jsx | 11 +- src/utils/colorUtils.js | 2 + src/utils/debugLogger.js | 275 ++++++++++++++++++++++ src/utils/electronStorage.js | 2 + src/utils/oauthAdapter.js | 2 + src/wizard/LLMClient.js | 9 +- src/wizard/tools/createEdge.js | 5 +- src/wizard/tools/createGroup.js | 3 +- src/wizard/tools/getNodeContext.test.js | 2 + src/wizard/tools/searchNodes.test.js | 2 + wizard-server.js | 13 +- 27 files changed, 473 insertions(+), 44 deletions(-) create mode 100644 src/utils/debugLogger.js diff --git a/ARTIFACT_REGISTRY_FIX.md b/ARTIFACT_REGISTRY_FIX.md index b122ee5..ba24c13 100644 --- a/ARTIFACT_REGISTRY_FIX.md +++ b/ARTIFACT_REGISTRY_FIX.md @@ -149,3 +149,5 @@ gcloud artifacts repositories add-iam-policy-binding gcr.io --location=us --memb + + diff --git a/STIFF_LAYOUT_SUMMARY.md b/STIFF_LAYOUT_SUMMARY.md index c416f79..2338a2c 100644 --- a/STIFF_LAYOUT_SUMMARY.md +++ b/STIFF_LAYOUT_SUMMARY.md @@ -24,3 +24,5 @@ In unweighted graphs, geometric patterns (hexagons, triangles, grids) emerge whe + + diff --git a/agent-server.js b/agent-server.js index e8a07e0..5116d07 100644 --- a/agent-server.js +++ b/agent-server.js @@ -9,27 +9,28 @@ process.env.AGENT_SERVER_MODE = 'true'; -// #region agent log -fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'agent-server.js:TOP',message:'agent-server.js executing',data:{pid:process.pid},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'A'})}).catch(()=>{}); -// #endregion - // Import and start the clean wizard server import { startWizardServer } from './wizard-server.js'; +import { debugLogSync } from './src/utils/debugLogger.js'; + +// #region agent log +debugLogSync('agent-server.js:TOP', 'agent-server.js executing', { pid: process.pid }, 'debug-session', 'A'); +// #endregion // #region agent log -fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'agent-server.js:IMPORT_OK',message:'wizard-server imported successfully',data:{},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'B'})}).catch(()=>{}); +debugLogSync('agent-server.js:IMPORT_OK', 'wizard-server imported successfully', {}, 'debug-session', 'B'); // #endregion startWizardServer() .then(({ port }) => { // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'agent-server.js:START_OK',message:'Wizard started successfully',data:{port},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'C'})}).catch(()=>{}); + debugLogSync('agent-server.js:START_OK', 'Wizard started successfully', { port }, 'debug-session', 'C'); // #endregion console.log(`[AgentServer] Wizard service running on port ${port}`); }) .catch(e => { // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'agent-server.js:START_FAIL',message:'Wizard failed to start',data:{error:e.message,stack:e.stack},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'C'})}).catch(()=>{}); + debugLogSync('agent-server.js:START_FAIL', 'Wizard failed to start', { error: e.message, stack: e.stack }, 'debug-session', 'C'); // #endregion console.error('[AgentServer] Failed to start wizard service:', e); process.exit(1); diff --git a/bridge-daemon-legacy.js b/bridge-daemon-legacy.js index 62a3d54..6a18a8d 100644 --- a/bridge-daemon-legacy.js +++ b/bridge-daemon-legacy.js @@ -6,6 +6,7 @@ import cors from 'cors'; import { exec } from 'node:child_process'; import fetch from 'node-fetch'; import queueManager from './src/services/queue/Queue.js'; +import { debugLogSync } from './src/utils/debugLogger.js'; import eventLog from './src/services/EventLog.js'; import committer from './src/services/Committer.js'; import { setBridgeStoreRef } from './src/services/bridgeStoreAccessor.js'; @@ -75,7 +76,7 @@ app.use(express.json({ limit: '2mb' })); // Serve static files from public directory (for debug viewer) app.use((req, res, next) => { - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'bridge-daemon-legacy.js:middleware',message:'Incoming request',data:{url: req.url, method: req.method},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'B'})}).catch(()=>{}); + debugLogSync('bridge-daemon-legacy.js:middleware', 'Incoming request', { url: req.url, method: req.method }, 'debug-session', 'B'); next(); }); app.use(express.static('public')); @@ -4558,7 +4559,7 @@ const startBridgeListener = () => { const { server: netServer, protocol } = createBridgeServer(); serverProtocol = protocol; netServer.listen(PORT, () => { - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'bridge-daemon-legacy.js:startBridgeListener',message:'Server started',data:{port: PORT, protocol},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'A'})}).catch(()=>{}); + debugLogSync('bridge-daemon-legacy.js:startBridgeListener', 'Server started', { port: PORT, protocol }, 'debug-session', 'A'); console.log(`✅ Bridge daemon listening on ${protocol}://localhost:${PORT}`); committer.start(); import('./src/services/orchestrator/Scheduler.js').then(mod => { scheduler = mod.default; }).catch(() => { }); diff --git a/electron-builder.json b/electron-builder.json index e6791d0..9aa5cb5 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -68,3 +68,5 @@ + + diff --git a/scripts/fix-artifact-registry.sh b/scripts/fix-artifact-registry.sh index f0761b5..75305e5 100755 --- a/scripts/fix-artifact-registry.sh +++ b/scripts/fix-artifact-registry.sh @@ -83,3 +83,5 @@ echo -e "${BLUE}You can now run your deployment scripts.${NC}" + + diff --git a/src/NodeCanvas.jsx b/src/NodeCanvas.jsx index 6fe7f2d..d8ae217 100644 --- a/src/NodeCanvas.jsx +++ b/src/NodeCanvas.jsx @@ -33,6 +33,7 @@ import ForceSimulationModal from './components/ForceSimulationModal'; import { parseInputData, generateGraph } from './services/autoGraphGenerator'; import { applyLayout, getClusterGeometries, FORCE_LAYOUT_DEFAULTS } from './services/graphLayoutService.js'; import { NavigationMode, calculateNavigationParams } from './services/canvasNavigationService.js'; +import { debugLogSync } from './utils/debugLogger.js'; // Import Zustand store and selectors/actions import useGraphStore, { @@ -5308,7 +5309,7 @@ function NodeCanvas() { const handleWheel = async (e) => { // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'NodeCanvas.jsx:5282', message: 'handleWheel START', data: { deltaY: e.deltaY?.toFixed?.(2), ctrlKey: e.ctrlKey }, timestamp: Date.now(), sessionId: 'debug-session', hypothesisId: 'C' }) }).catch(() => { }); + debugLogSync('NodeCanvas.jsx:handleWheel', 'handleWheel START', { deltaY: e.deltaY?.toFixed?.(2), ctrlKey: e.ctrlKey }, 'debug-session', 'C'); // #endregion // If a gesture/pinch is active, ignore wheel to prevent double-handling on Safari if (pinchRef.current.active) { @@ -9944,7 +9945,7 @@ function NodeCanvas() { // #region agent log const multiEdgePairs = Array.from(edgePairGroups.entries()).filter(([k, v]) => v.length > 1); if (multiEdgePairs.length > 0) { - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'NodeCanvas.jsx:edgeRender', message: 'Edge rendering info', data: { totalEdges: visibleEdges.length, multiEdgePairs: multiEdgePairs.map(([k, v]) => ({ pair: k, edgeCount: v.length, edgeIds: v })), enableAutoRouting, routingStyle, willUseCurves: !(enableAutoRouting && (routingStyle === 'manhattan' || routingStyle === 'clean')) }, timestamp: Date.now(), sessionId: 'debug-session', hypothesisId: 'D-E' }) }).catch(() => { }); + debugLogSync('NodeCanvas.jsx:edgeRender', 'Edge rendering info', { totalEdges: visibleEdges.length, multiEdgePairs: multiEdgePairs.map(([k, v]) => ({ pair: k, edgeCount: v.length, edgeIds: v })), enableAutoRouting, routingStyle, willUseCurves: !(enableAutoRouting && (routingStyle === 'manhattan' || routingStyle === 'clean')) }, 'debug-session', 'D-E'); } // #endregion diff --git a/src/components/panel/views/LeftAIView.jsx b/src/components/panel/views/LeftAIView.jsx index d7d1a42..10f789e 100644 --- a/src/components/panel/views/LeftAIView.jsx +++ b/src/components/panel/views/LeftAIView.jsx @@ -170,8 +170,9 @@ const LeftAIView = ({ compact = false, activeGraphId, graphsMap, edgesMap }) => } }); - eventSource.onerror = (err) => { - console.warn('[AI Collaboration] SSE error:', err); + eventSource.onerror = () => { + // Silently handle SSE errors - server may not be available + // Connection errors are expected when bridge server isn't running }; } catch (err) { console.warn('[AI Collaboration] Failed to establish SSE:', err); diff --git a/src/services/agentRuntime/Executor.js b/src/services/agentRuntime/Executor.js index 78d280b..a55d368 100644 --- a/src/services/agentRuntime/Executor.js +++ b/src/services/agentRuntime/Executor.js @@ -420,3 +420,5 @@ export function execute(plan, context, cid, ensureSchedulerStarted) { + + diff --git a/src/services/agentRuntime/README.md b/src/services/agentRuntime/README.md index 67371a4..08d6f7d 100644 --- a/src/services/agentRuntime/README.md +++ b/src/services/agentRuntime/README.md @@ -72,3 +72,5 @@ const result = await coordinator.handle({ + + diff --git a/src/services/agentRuntime/StateMirror.js b/src/services/agentRuntime/StateMirror.js index 5ab567f..3bcd5cc 100644 --- a/src/services/agentRuntime/StateMirror.js +++ b/src/services/agentRuntime/StateMirror.js @@ -192,3 +192,5 @@ export function createInitialState() { + + diff --git a/src/services/bridgeConfig.js b/src/services/bridgeConfig.js index 7dd0cd2..20fdf44 100644 --- a/src/services/bridgeConfig.js +++ b/src/services/bridgeConfig.js @@ -153,11 +153,127 @@ export function bridgeFetch(path, options) { export function bridgeEventSource(path) { // Consumers should pass a path like '/events/stream' - return new EventSource(bridgeUrl(path)); + const eventSource = new EventSource(bridgeUrl(path)); + + // Add error handler to suppress console errors when server is not available + eventSource.addEventListener('error', (event) => { + // Silently handle connection errors - don't log to console + // The error event will still fire, but we prevent console spam + if (eventSource.readyState === EventSource.CLOSED) { + // Connection closed - server likely not available + // Silently close and don't log + try { + eventSource.close(); + } catch (e) { + // Ignore errors during close + } + } + }, { once: false }); + + return eventSource; +} + +// OAuth server availability cache +// Try to restore from sessionStorage to persist across page reloads +function loadOAuthHealth() { + try { + if (typeof sessionStorage !== 'undefined') { + const stored = sessionStorage.getItem('redstring_oauth_health'); + if (stored) { + const parsed = JSON.parse(stored); + const now = Date.now(); + // Only use stored data if cooldown is still active + if (parsed.cooldownUntil > now) { + return { + consecutiveFailures: parsed.consecutiveFailures || 0, + cooldownUntil: parsed.cooldownUntil, + isAvailable: false, + lastChecked: parsed.lastChecked || 0, + firstFailureTime: parsed.firstFailureTime || 0 + }; + } + } + } + } catch (e) { + // Ignore storage errors + } + return { + consecutiveFailures: 0, + cooldownUntil: 0, + isAvailable: true, + lastChecked: 0, + firstFailureTime: 0 + }; +} + +function saveOAuthHealth(health) { + try { + if (typeof sessionStorage !== 'undefined') { + sessionStorage.setItem('redstring_oauth_health', JSON.stringify({ + consecutiveFailures: health.consecutiveFailures, + cooldownUntil: health.cooldownUntil, + lastChecked: health.lastChecked, + firstFailureTime: health.firstFailureTime + })); + } + } catch (e) { + // Ignore storage errors + } } -// OAuth-specific fetch function (separate server, no circuit breaker needed) +const __oauthHealth = loadOAuthHealth(); + +// OAuth-specific fetch function with aggressive error suppression export function oauthFetch(path, options) { - return fetch(oauthUrl(path), options); + const now = Date.now(); + + // If in cooldown period, don't make the request at all (prevents browser console errors) + if (__oauthHealth.cooldownUntil > now) { + // Return a rejected promise that won't trigger network request + return Promise.reject(new Error('OAuth server unavailable (cooldown)')); + } + + // Make the request with a timeout to fail fast + const controller = typeof AbortController !== 'undefined' ? new AbortController() : null; + const timeoutId = controller ? setTimeout(() => controller.abort(), 2000) : null; // 2 second timeout + + const fetchOptions = { + ...options, + ...(controller ? { signal: controller.signal } : {}) + }; + + return fetch(oauthUrl(path), fetchOptions) + .then((res) => { + if (timeoutId) clearTimeout(timeoutId); + // Reset failures on any response (even errors) + __oauthHealth.consecutiveFailures = 0; + __oauthHealth.isAvailable = true; + __oauthHealth.lastChecked = now; + __oauthHealth.firstFailureTime = 0; + __oauthHealth.cooldownUntil = 0; + saveOAuthHealth(__oauthHealth); + return res; + }) + .catch((err) => { + if (timeoutId) clearTimeout(timeoutId); + __oauthHealth.lastChecked = now; + + if (isLikelyNetworkRefusal(err) || err.name === 'AbortError') { + __oauthHealth.consecutiveFailures += 1; + __oauthHealth.isAvailable = false; + + // Set cooldown immediately after first failure (prevents spam) + if (__oauthHealth.consecutiveFailures === 1) { + __oauthHealth.firstFailureTime = now; + __oauthHealth.cooldownUntil = now + 300000; // 5 minute cooldown after first failure + } else if (__oauthHealth.consecutiveFailures > 1) { + // Extend cooldown with each failure + __oauthHealth.cooldownUntil = now + Math.min(300000, 60000 * __oauthHealth.consecutiveFailures); + } + saveOAuthHealth(__oauthHealth); + } + // Re-throw but the caller should handle it gracefully + throw err; + }); } diff --git a/src/services/orchestrator/roleRunners.js b/src/services/orchestrator/roleRunners.js index ac5d413..df818dc 100644 --- a/src/services/orchestrator/roleRunners.js +++ b/src/services/orchestrator/roleRunners.js @@ -5,6 +5,7 @@ import { RolePrompts, ToolAllowlists } from '../roles.js'; import { getBridgeStore, getGraphById, getActiveGraph } from '../bridgeStoreAccessor.js'; import { getGraphSemanticStructure } from '../graphQueries.js'; import executionTracer from '../ExecutionTracer.js'; +import { debugLogSync } from '../../utils/debugLogger.js'; // Helper to normalize string to Title Case function toTitleCase(str) { @@ -196,13 +197,13 @@ export async function runExecutorOnce() { if (!allow.has(task.toolName)) throw new Error(`Tool not allowed for executor: ${task.toolName}`); // #region agent log if (task.toolName === 'create_group') { - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'roleRunners.js:pre-validation', message: 'About to validate create_group', data: { toolName: task.toolName, argsKeys: Object.keys(task.args || {}) }, timestamp: Date.now(), sessionId: 'debug-session', hypothesisId: 'H3' }) }).catch(() => { }); + debugLogSync('roleRunners.js:pre-validation', 'About to validate create_group', { toolName: task.toolName, argsKeys: Object.keys(task.args || {}) }, 'debug-session', 'H3'); } // #endregion const validation = toolValidator.validateToolArgs(task.toolName, task.args || {}); // #region agent log if (task.toolName === 'create_group') { - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'roleRunners.js:post-validation', message: 'Validation result for create_group', data: { valid: validation.valid, error: validation.error, sanitizedKeys: validation.sanitized ? Object.keys(validation.sanitized) : null }, timestamp: Date.now(), sessionId: 'debug-session', hypothesisId: 'H3' }) }).catch(() => { }); + debugLogSync('roleRunners.js:post-validation', 'Validation result for create_group', { valid: validation.valid, error: validation.error, sanitizedKeys: validation.sanitized ? Object.keys(validation.sanitized) : null }, 'debug-session', 'H3'); } // #endregion if (!validation.valid) { @@ -708,7 +709,7 @@ export async function runExecutorOnce() { const isExpandMode = providedGraphId && !name; const graphId = providedGraphId || `graph-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'roleRunners.js:create_populated_graph', message: 'Mode determined', data: { isExpandMode, hasName: !!name, hasGraphId: !!providedGraphId, graphId }, timestamp: Date.now(), sessionId: 'debug-session', hypothesisId: 'fix-verify', runId: 'post-fix' }) }).catch(() => { }); + debugLogSync('roleRunners.js:create_populated_graph', 'Mode determined', { isExpandMode, hasName: !!name, hasGraphId: !!providedGraphId, graphId }, 'debug-session', 'fix-verify'); // #endregion // 1. Create the graph (only if not in expand mode) @@ -1545,7 +1546,7 @@ export async function runExecutorOnce() { } // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'roleRunners.js:create_edge', message: 'Executor creating edge op', data: { edgeId, sourceInstanceId, targetInstanceId, graphId, name, typeNodeId }, timestamp: Date.now(), sessionId: 'debug-session', hypothesisId: 'A-C' }) }).catch(() => { }); + debugLogSync('roleRunners.js:create_edge', 'Executor creating edge op', { edgeId, sourceInstanceId, targetInstanceId, graphId, name, typeNodeId }, 'debug-session', 'A-C'); // #endregion ops.push({ type: 'addEdge', diff --git a/src/services/persistentAuth.js b/src/services/persistentAuth.js index 7912191..fc477b6 100644 --- a/src/services/persistentAuth.js +++ b/src/services/persistentAuth.js @@ -156,8 +156,8 @@ export class PersistentAuth { // OPTIONAL: Sync from server as backup (stateless server doesn't persist) // This is only useful for initial server-side OAuth completion try { - const response = await oauthFetch('/api/github/auth/state?includeTokens=true'); - if (response.ok) { + const response = await oauthFetch('/api/github/auth/state?includeTokens=true').catch(() => null); + if (response && response.ok) { const state = await response.json(); // Only apply server state if browser doesn't have tokens if (!this.oauthCache?.accessToken && state?.oauth?.accessToken) { @@ -167,7 +167,8 @@ export class PersistentAuth { } } } catch (serverError) { - console.log('[PersistentAuth] Server sync skipped (expected for stateless server):', serverError.message); + // Silently handle - OAuth server may not be running + // Browser will log network errors, but we don't add additional logging } this.authStateLoaded = true; @@ -463,7 +464,7 @@ export class PersistentAuth { console.log('[PersistentAuth] No stored GitHub App installation found; attempting discovery...'); try { // Ask backend for installations associated with this app - const listResp = await oauthFetch('/api/github/app/installations'); + const listResp = await oauthFetch('/api/github/app/installations').catch(() => null); if (listResp && listResp.ok) { const installations = await listResp.json(); if (Array.isArray(installations) && installations.length > 0) { diff --git a/src/services/save.worker.js b/src/services/save.worker.js index 260ff56..72eda09 100644 --- a/src/services/save.worker.js +++ b/src/services/save.worker.js @@ -81,3 +81,5 @@ self.onmessage = (e) => { + + diff --git a/src/services/toolValidator.js b/src/services/toolValidator.js index 8ff50ed..8e2cb77 100644 --- a/src/services/toolValidator.js +++ b/src/services/toolValidator.js @@ -4,6 +4,7 @@ */ import { v4 as uuidv4 } from 'uuid'; +import { debugLogSync } from '../utils/debugLogger.js'; class ToolValidator { constructor() { @@ -679,7 +680,7 @@ class ToolValidator { // Group tools // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'toolValidator.js:registerSchema-create_group',message:'Registering create_group schema with graph_id',data:{required:['graph_id'],properties_keys:['graph_id','name','color','memberInstanceIds']},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'H1'})}).catch(()=>{}); + debugLogSync('toolValidator.js:registerSchema-create_group', 'Registering create_group schema with graph_id', { required: ['graph_id'], properties_keys: ['graph_id', 'name', 'color', 'memberInstanceIds'] }, 'debug-session', 'H1'); // #endregion this.registerSchema('create_group', { type: 'object', @@ -855,7 +856,7 @@ class ToolValidator { const schema = this.schemas.get(toolName); // #region agent log if (toolName === 'create_group') { - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'toolValidator.js:validateToolArgs',message:'Validating create_group',data:{toolName,argsKeys:Object.keys(args||{}),schemaRequired:schema?.required,schemaPropsKeys:schema?.properties?Object.keys(schema.properties):null},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'H1-H3'})}).catch(()=>{}); + debugLogSync('toolValidator.js:validateToolArgs', 'Validating create_group', { toolName, argsKeys: Object.keys(args || {}), schemaRequired: schema?.required, schemaPropsKeys: schema?.properties ? Object.keys(schema.properties) : null }, 'debug-session', 'H1-H3'); } // #endregion if (!schema) { diff --git a/src/store/graphStore.jsx b/src/store/graphStore.jsx index 372ba1e..ba8913a 100644 --- a/src/store/graphStore.jsx +++ b/src/store/graphStore.jsx @@ -5,6 +5,7 @@ import { NODE_WIDTH, NODE_HEIGHT, NODE_DEFAULT_COLOR } from '../constants.js'; import { getFileStatus, restoreLastSession, clearSession, notifyChanges } from './fileStorage.js'; import { importFromRedstring } from '../formats/redstringFormat.js'; import { MAX_LAYOUT_SCALE_MULTIPLIER } from '../services/graphLayoutService.js'; +import { debugLogSync } from '../utils/debugLogger.js'; // Enable Immer Map/Set plugin support enableMapSet(); @@ -1453,7 +1454,7 @@ const useGraphStore = create(saveCoordinatorMiddleware((set, get, api) => { // Adds a NEW edge connecting two instances. addEdge: (graphId, newEdgeData, contextOptions = {}) => { // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'graphStore.jsx:addEdge',message:'addEdge called',data:{graphId,edgeId:newEdgeData?.id,sourceId:newEdgeData?.sourceId,destId:newEdgeData?.destinationId,stack:new Error().stack?.split('\n').slice(1,5)},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'A-B'})}).catch(()=>{}); + debugLogSync('graphStore.jsx:addEdge', 'addEdge called', { graphId, edgeId: newEdgeData?.id, sourceId: newEdgeData?.sourceId, destId: newEdgeData?.destinationId, stack: new Error().stack?.split('\n').slice(1, 5) }, 'debug-session', 'A-B'); // #endregion api.setChangeContext({ type: 'edge_create', target: 'edge', finalize: true, ...contextOptions }); return set(produce((draft) => { @@ -1481,7 +1482,7 @@ const useGraphStore = create(saveCoordinatorMiddleware((set, get, api) => { (e.sourceId === sourceInstanceId && e.destinationId === destInstanceId) || (e.sourceId === destInstanceId && e.destinationId === sourceInstanceId) ); - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'graphStore.jsx:addEdge:check',message:'Checking existing edges',data:{edgeId,sourceId:sourceInstanceId,destId:destInstanceId,existingEdgeCount:existingEdges.length,existingEdgeIds:existingEdges.map(e=>e.id)},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'B'})}).catch(()=>{}); + debugLogSync('graphStore.jsx:addEdge:check', 'Checking existing edges', { edgeId, sourceId: sourceInstanceId, destId: destInstanceId, existingEdgeCount: existingEdges.length, existingEdgeIds: existingEdges.map(e => e.id) }, 'debug-session', 'B'); // #endregion if (!draft.edges.has(edgeId)) { @@ -1497,11 +1498,11 @@ const useGraphStore = create(saveCoordinatorMiddleware((set, get, api) => { } graph.edgeIds.push(edgeId); // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'graphStore.jsx:addEdge:created',message:'Edge created',data:{edgeId,totalEdgesNow:graph.edgeIds.length},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'A-B'})}).catch(()=>{}); + debugLogSync('graphStore.jsx:addEdge:created', 'Edge created', { edgeId, totalEdgesNow: graph.edgeIds.length }, 'debug-session', 'A-B'); // #endregion } else { // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'graphStore.jsx:addEdge:skip',message:'Edge already exists - skipped',data:{edgeId},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'A'})}).catch(()=>{}); + debugLogSync('graphStore.jsx:addEdge:skip', 'Edge already exists - skipped', { edgeId }, 'debug-session', 'A'); // #endregion } })); @@ -3150,7 +3151,7 @@ const useGraphStore = create(saveCoordinatorMiddleware((set, get, api) => { updateGraphView: (graphId, panOffset, zoomLevel) => { // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'graphStore.jsx:2917',message:'updateGraphView called',data:{graphId,zoomLevel:zoomLevel?.toFixed?.(3)},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'C'})}).catch(()=>{}); + debugLogSync('graphStore.jsx:updateGraphView', 'updateGraphView called', { graphId, zoomLevel: zoomLevel?.toFixed?.(3) }, 'debug-session', 'C'); // #endregion api.setChangeContext({ type: 'viewport', target: 'graph' }); set(produce((draft) => { diff --git a/src/utils/colorUtils.js b/src/utils/colorUtils.js index 324c141..725ee2d 100644 --- a/src/utils/colorUtils.js +++ b/src/utils/colorUtils.js @@ -179,3 +179,5 @@ export const generateProgressiveColor = (baseColor, level) => { + + diff --git a/src/utils/debugLogger.js b/src/utils/debugLogger.js new file mode 100644 index 0000000..3bd6eda --- /dev/null +++ b/src/utils/debugLogger.js @@ -0,0 +1,275 @@ +/** + * Debug Logger Utility + * Provides a centralized way to log debug events to the MCP server + * with automatic error suppression and availability checking + * Works in both browser and Node.js environments + */ + +// Check if we're in Node.js environment +const isNode = typeof process !== 'undefined' && process.versions && process.versions.node; + +// Cache for server availability to avoid repeated checks +// Try to restore from sessionStorage to persist across page reloads +function loadDebugServerHealth() { + try { + if (typeof sessionStorage !== 'undefined') { + const stored = sessionStorage.getItem('redstring_debug_server_health'); + if (stored) { + const parsed = JSON.parse(stored); + const now = Date.now(); + // Only use stored data if cooldown is still active + if (parsed.cooldownUntil > now) { + return { + isAvailable: false, + lastChecked: parsed.lastChecked || 0, + checkInterval: 30000, + cooldownUntil: parsed.cooldownUntil, + consecutiveFailures: parsed.consecutiveFailures || 0 + }; + } + } + } + } catch (e) { + // Ignore storage errors + } + return { + isAvailable: false, + lastChecked: 0, + checkInterval: 30000, + cooldownUntil: 0, + consecutiveFailures: 0 + }; +} + +function saveDebugServerHealth(health) { + try { + if (typeof sessionStorage !== 'undefined') { + sessionStorage.setItem('redstring_debug_server_health', JSON.stringify({ + lastChecked: health.lastChecked, + cooldownUntil: health.cooldownUntil, + consecutiveFailures: health.consecutiveFailures + })); + } + } catch (e) { + // Ignore storage errors + } +} + +let serverAvailabilityCache = loadDebugServerHealth(); + +// On module load, do a quick async check to see if server is available +// This prevents the first request from being made if server is known to be down +if (typeof window !== 'undefined' && typeof fetch !== 'undefined') { + // Check server availability in the background + (async () => { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 500); + const response = await fetch(`${DEBUG_SERVER_URL}/health`, { + method: 'GET', + signal: controller.signal + }); + clearTimeout(timeoutId); + if (response.ok) { + serverAvailabilityCache.isAvailable = true; + serverAvailabilityCache.consecutiveFailures = 0; + serverAvailabilityCache.cooldownUntil = 0; + } else { + // Server responded but with error - mark as unavailable + serverAvailabilityCache.isAvailable = false; + serverAvailabilityCache.cooldownUntil = Date.now() + 300000; // 5 min cooldown + saveDebugServerHealth(serverAvailabilityCache); + } + } catch (error) { + // Server not available - set cooldown immediately + serverAvailabilityCache.isAvailable = false; + serverAvailabilityCache.consecutiveFailures = 1; + serverAvailabilityCache.cooldownUntil = Date.now() + 300000; // 5 min cooldown + serverAvailabilityCache.lastChecked = Date.now(); + saveDebugServerHealth(serverAvailabilityCache); + } + })(); +} + +// Universe ID for debug logging (can be configured) +const DEFAULT_UNIVERSE_ID = '52d0fe28-158e-49a4-b331-f013fcb14181'; +const DEBUG_SERVER_URL = 'http://127.0.0.1:7242'; + +/** + * Check if the debug logging server is available + * Uses a cache to avoid excessive checks + * Works in both browser and Node.js + */ +async function checkServerAvailability() { + const now = Date.now(); + + // Use cached result if recent + if (now - serverAvailabilityCache.lastChecked < serverAvailabilityCache.checkInterval) { + return serverAvailabilityCache.isAvailable; + } + + // Check server availability + try { + // AbortController is available in Node.js 15+ and all modern browsers + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 500); // 500ms timeout + + const response = await fetch(`${DEBUG_SERVER_URL}/health`, { + method: 'GET', + signal: controller.signal + }); + + clearTimeout(timeoutId); + + serverAvailabilityCache.isAvailable = response.ok; + serverAvailabilityCache.lastChecked = now; + return response.ok; + } catch (error) { + // Server not available - silently fail + serverAvailabilityCache.isAvailable = false; + serverAvailabilityCache.lastChecked = now; + return false; + } +} + +/** + * Log a debug event to the MCP server + * Silently fails if server is not available + * + * @param {string} location - Location identifier (e.g., 'NodeCanvas.jsx:handleWheel') + * @param {string} message - Log message + * @param {object} data - Additional data to log + * @param {string} sessionId - Session identifier (optional) + * @param {string} hypothesisId - Hypothesis identifier (optional) + */ +export async function debugLog(location, message, data = {}, sessionId = 'debug-session', hypothesisId = null) { + // Only attempt logging if server might be available + // Check availability asynchronously without blocking + checkServerAvailability().then(isAvailable => { + if (!isAvailable) { + return; // Silently skip if server not available + } + + // Attempt to send log (with silent error handling) + fetch(`${DEBUG_SERVER_URL}/ingest/${DEFAULT_UNIVERSE_ID}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + location, + message, + data, + timestamp: Date.now(), + sessionId, + hypothesisId + }) + }).catch(() => { + // Silently handle errors - don't log to console + // Update cache to mark server as unavailable + serverAvailabilityCache.isAvailable = false; + serverAvailabilityCache.lastChecked = Date.now(); + }); + }).catch(() => { + // Silently handle promise rejection + }); +} + +/** + * Synchronous version that doesn't check availability first + * Use this for high-frequency events where we want minimal overhead + * Still silently handles errors + * Works in both browser and Node.js + */ +export function debugLogSync(location, message, data = {}, sessionId = 'debug-session', hypothesisId = null) { + // Silently skip if fetch is not available (shouldn't happen in modern environments) + if (typeof fetch === 'undefined') { + return; + } + + const now = Date.now(); + + // If we're in cooldown, don't make the request at all (prevents browser console errors) + if (serverAvailabilityCache.cooldownUntil > now) { + return; // Silently skip - no network request = no console error + } + + // If we've had failures recently, skip the request (even if cooldown expired, wait a bit) + if (serverAvailabilityCache.consecutiveFailures > 0) { + // If we've had failures, only retry after a longer interval + const timeSinceLastCheck = now - serverAvailabilityCache.lastChecked; + if (timeSinceLastCheck < 10000) { // Wait 10 seconds between retries after failures + return; // Silently skip + } + } + + // Use AbortController for timeout (available in Node.js 15+ and all modern browsers) + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 100); // 100ms timeout + + fetch(`${DEBUG_SERVER_URL}/ingest/${DEFAULT_UNIVERSE_ID}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + location, + message, + data, + timestamp: now, + sessionId, + hypothesisId + }), + signal: controller.signal + }) + .then(() => { + clearTimeout(timeoutId); + // Mark server as available on success + serverAvailabilityCache.isAvailable = true; + serverAvailabilityCache.lastChecked = now; + serverAvailabilityCache.consecutiveFailures = 0; + serverAvailabilityCache.cooldownUntil = 0; + saveDebugServerHealth(serverAvailabilityCache); + }) + .catch(() => { + clearTimeout(timeoutId); + // Silently handle errors - don't log to console + serverAvailabilityCache.isAvailable = false; + serverAvailabilityCache.lastChecked = now; + serverAvailabilityCache.consecutiveFailures += 1; + + // Set cooldown after first failure (prevents spam) + if (serverAvailabilityCache.consecutiveFailures === 1) { + serverAvailabilityCache.cooldownUntil = now + 300000; // 5 minute cooldown + } else if (serverAvailabilityCache.consecutiveFailures > 1) { + // Extend cooldown with each failure + serverAvailabilityCache.cooldownUntil = now + Math.min(300000, 60000 * serverAvailabilityCache.consecutiveFailures); + } + saveDebugServerHealth(serverAvailabilityCache); + }); +} + +/** + * Reset the server availability cache + * Useful for testing or when server status changes + */ +export function resetServerAvailabilityCache() { + serverAvailabilityCache = { + isAvailable: false, + lastChecked: 0, + checkInterval: 30000, + cooldownUntil: 0, + consecutiveFailures: 0 + }; + try { + if (typeof sessionStorage !== 'undefined') { + sessionStorage.removeItem('redstring_debug_server_health'); + } + } catch (e) { + // Ignore storage errors + } +} + +/** + * Get current server availability status + */ +export async function getServerAvailability() { + return await checkServerAvailability(); +} + diff --git a/src/utils/electronStorage.js b/src/utils/electronStorage.js index 30405fb..e5f6a20 100644 --- a/src/utils/electronStorage.js +++ b/src/utils/electronStorage.js @@ -196,3 +196,5 @@ export default { + + diff --git a/src/utils/oauthAdapter.js b/src/utils/oauthAdapter.js index 3a3c5f4..f09c7aa 100644 --- a/src/utils/oauthAdapter.js +++ b/src/utils/oauthAdapter.js @@ -64,3 +64,5 @@ export const hasOAuthCallback = () => { + + diff --git a/src/wizard/LLMClient.js b/src/wizard/LLMClient.js index 8d58713..2664dbe 100644 --- a/src/wizard/LLMClient.js +++ b/src/wizard/LLMClient.js @@ -4,6 +4,7 @@ */ import apiKeyManager from '../services/apiKeyManager.js'; +import { debugLogSync } from '../utils/debugLogger.js'; /** * Get default config if not provided @@ -77,7 +78,7 @@ async function* streamOpenRouter(messages, tools, { endpoint, model, apiKey, tem }; // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'LLMClient.js:streamOpenRouter:REQUEST',message:'Sending request to OpenRouter',data:{model,toolCount:tools?.length||0,hasTools:!!tools,messageCount:messages?.length},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'F'})}).catch(()=>{}); + debugLogSync('LLMClient.js:streamOpenRouter:REQUEST', 'Sending request to OpenRouter', { model, toolCount: tools?.length || 0, hasTools: !!tools, messageCount: messages?.length }, 'debug-session', 'F'); // #endregion const response = await fetch(endpoint, { @@ -125,7 +126,7 @@ async function* streamOpenRouter(messages, tools, { endpoint, model, apiKey, tem // #region agent log if (delta?.tool_calls || delta?.content) { - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'LLMClient.js:streamOpenRouter:DELTA',message:'Received delta',data:{hasToolCalls:!!delta?.tool_calls,hasContent:!!delta?.content,contentPreview:delta?.content?.substring?.(0,100),finishReason:choice.finish_reason},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'G'})}).catch(()=>{}); + debugLogSync('LLMClient.js:streamOpenRouter:DELTA', 'Received delta', { hasToolCalls: !!delta?.tool_calls, hasContent: !!delta?.content, contentPreview: delta?.content?.substring?.(0, 100), finishReason: choice.finish_reason }, 'debug-session', 'G'); } // #endregion @@ -186,7 +187,7 @@ async function* streamOpenRouter(messages, tools, { endpoint, model, apiKey, tem if (currentToolCall) { console.log('[LLMClient:OpenRouter] Yielding tool_call (flush):', currentToolCall.function?.name); // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'LLMClient.js:streamOpenRouter:TOOL_CALL_FLUSH',message:'Flushing tool call',data:{name:currentToolCall.function?.name,hasArgs:!!currentToolCall.function?.arguments},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'H'})}).catch(()=>{}); + debugLogSync('LLMClient.js:streamOpenRouter:TOOL_CALL_FLUSH', 'Flushing tool call', { name: currentToolCall.function?.name, hasArgs: !!currentToolCall.function?.arguments }, 'debug-session', 'H'); // #endregion yield { type: 'tool_call', @@ -201,7 +202,7 @@ async function* streamOpenRouter(messages, tools, { endpoint, model, apiKey, tem } // #region agent log - end of streamOpenRouter -fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'LLMClient.js:MODULE_LOADED',message:'LLMClient module loaded',data:{},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'F'})}).catch(()=>{}); +debugLogSync('LLMClient.js:MODULE_LOADED', 'LLMClient module loaded', {}, 'debug-session', 'F'); // #endregion /** diff --git a/src/wizard/tools/createEdge.js b/src/wizard/tools/createEdge.js index afeb45e..c84246b 100644 --- a/src/wizard/tools/createEdge.js +++ b/src/wizard/tools/createEdge.js @@ -3,11 +3,12 @@ */ import queueManager from '../../services/queue/Queue.js'; +import { debugLogSync } from '../../utils/debugLogger.js'; export async function createEdge(args, graphState, cid, ensureSchedulerStarted) { const { sourceId, targetId, type } = args; // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'createEdge.js:entry',message:'createEdge tool called',data:{sourceId,targetId,type,cid},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'A-C'})}).catch(()=>{}); + debugLogSync('createEdge.js:entry', 'createEdge tool called', { sourceId, targetId, type, cid }, 'debug-session', 'A-C'); // #endregion if (!sourceId || !targetId) { throw new Error('sourceId and targetId are required'); @@ -39,7 +40,7 @@ export async function createEdge(args, graphState, cid, ensureSchedulerStarted) }; // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'createEdge.js:enqueue',message:'Enqueuing edge creation goal',data:{activeGraphId,dagTaskCount:dag.tasks.length},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'A-C'})}).catch(()=>{}); + debugLogSync('createEdge.js:enqueue', 'Enqueuing edge creation goal', { activeGraphId, dagTaskCount: dag.tasks.length }, 'debug-session', 'A-C'); // #endregion const goalId = queueManager.enqueue('goalQueue', { type: 'goal', diff --git a/src/wizard/tools/createGroup.js b/src/wizard/tools/createGroup.js index 24d98f5..3747e62 100644 --- a/src/wizard/tools/createGroup.js +++ b/src/wizard/tools/createGroup.js @@ -3,6 +3,7 @@ */ import queueManager from '../../services/queue/Queue.js'; +import { debugLogSync } from '../../utils/debugLogger.js'; /** * Find instance IDs by node names in the active graph @@ -73,7 +74,7 @@ export async function createGroup(args, graphState, cid, ensureSchedulerStarted) memberInstanceIds: resolvedMemberIds }; // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'createGroup.js:enqueue',message:'Enqueuing create_group task',data:{argsKeys:Object.keys(taskArgs),graphId:activeGraphId},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'H1-H4'})}).catch(()=>{}); + debugLogSync('createGroup.js:enqueue', 'Enqueuing create_group task', { argsKeys: Object.keys(taskArgs), graphId: activeGraphId }, 'debug-session', 'H1-H4'); // #endregion const dag = { tasks: [{ diff --git a/src/wizard/tools/getNodeContext.test.js b/src/wizard/tools/getNodeContext.test.js index 1749091..c1e906d 100644 --- a/src/wizard/tools/getNodeContext.test.js +++ b/src/wizard/tools/getNodeContext.test.js @@ -197,3 +197,5 @@ describe('getNodeContext', () => { + + diff --git a/src/wizard/tools/searchNodes.test.js b/src/wizard/tools/searchNodes.test.js index a8f8e85..51176ba 100644 --- a/src/wizard/tools/searchNodes.test.js +++ b/src/wizard/tools/searchNodes.test.js @@ -130,3 +130,5 @@ describe('searchNodes', () => { + + diff --git a/wizard-server.js b/wizard-server.js index a55997e..a1718ef 100644 --- a/wizard-server.js +++ b/wizard-server.js @@ -12,6 +12,7 @@ import express from 'express'; import cors from 'cors'; import net from 'net'; import { runAgent } from './src/wizard/AgentLoop.js'; +import { debugLogSync } from './src/utils/debugLogger.js'; const app = express(); @@ -69,16 +70,16 @@ async function ensureSchedulerStarted() { if (!scheduler) { try { // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'wizard-server.js:scheduler:IMPORT_START',message:'Importing scheduler',data:{},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'E'})}).catch(()=>{}); + debugLogSync('wizard-server.js:scheduler:IMPORT_START', 'Importing scheduler', {}, 'debug-session', 'E'); // #endregion const mod = await import('./src/services/orchestrator/Scheduler.js'); scheduler = mod.default; // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'wizard-server.js:scheduler:IMPORT_OK',message:'Scheduler imported',data:{hasDefault:!!mod.default},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'E'})}).catch(()=>{}); + debugLogSync('wizard-server.js:scheduler:IMPORT_OK', 'Scheduler imported', { hasDefault: !!mod.default }, 'debug-session', 'E'); // #endregion } catch (e) { // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'wizard-server.js:scheduler:IMPORT_FAIL',message:'Scheduler import failed',data:{error:e.message},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'E'})}).catch(()=>{}); + debugLogSync('wizard-server.js:scheduler:IMPORT_FAIL', 'Scheduler import failed', { error: e.message }, 'debug-session', 'E'); // #endregion console.warn('[Wizard] Failed to load scheduler:', e.message); return; @@ -270,15 +271,15 @@ app.all('/api/*', (req, res) => { // Start server function export async function startWizardServer() { // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'wizard-server.js:startWizardServer:ENTRY',message:'startWizardServer called',data:{},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'C'})}).catch(()=>{}); + debugLogSync('wizard-server.js:startWizardServer:ENTRY', 'startWizardServer called', {}, 'debug-session', 'C'); // #endregion try { // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'wizard-server.js:getPort:BEFORE',message:'About to call getPort',data:{},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'D'})}).catch(()=>{}); + debugLogSync('wizard-server.js:getPort:BEFORE', 'About to call getPort', {}, 'debug-session', 'D'); // #endregion const PORT = await getPort(); // #region agent log - fetch('http://127.0.0.1:7242/ingest/52d0fe28-158e-49a4-b331-f013fcb14181',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'wizard-server.js:getPort:AFTER',message:'getPort returned',data:{port:PORT},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'D'})}).catch(()=>{}); + debugLogSync('wizard-server.js:getPort:AFTER', 'getPort returned', { port: PORT }, 'debug-session', 'D'); // #endregion return new Promise((resolve, reject) => { From 9628e0e047ce18b455859f0b9fa801a11300f564 Mon Sep 17 00:00:00 2001 From: Grant Eubanks Date: Sat, 10 Jan 2026 17:01:35 -0500 Subject: [PATCH 2/2] stuff --- wizard-server.js | 172 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 155 insertions(+), 17 deletions(-) diff --git a/wizard-server.js b/wizard-server.js index a1718ef..3fc1872 100644 --- a/wizard-server.js +++ b/wizard-server.js @@ -36,13 +36,13 @@ async function getPort() { if (await isPortAvailable(preferred)) { return preferred; } - + // If 3001 is in use (e.g., by Electron's embedded bridge), that's fine // The UI will connect to whatever is on 3001 console.log(`[Wizard] Port ${preferred} already in use.`); console.log(`[Wizard] If running alongside Electron, stop the Electron app first`); console.log(`[Wizard] or set WIZARD_PORT=3002 to run on a different port.`); - + throw new Error(`Port ${preferred} in use. Set WIZARD_PORT env var to use a different port.`); } @@ -85,7 +85,7 @@ async function ensureSchedulerStarted() { return; } } - + if (scheduler && typeof scheduler.start === 'function') { const status = scheduler.status(); if (!status.enabled) { @@ -119,14 +119,14 @@ app.get('/health', (req, res) => { app.post('/api/wizard', async (req, res) => { try { const { message, graphState, conversationHistory, config } = req.body || {}; - + if (!message) { return res.status(400).json({ error: 'Message is required' }); } const apiKey = req.headers.authorization?.replace(/^Bearer\s+/i, '') || ''; const apiConfig = config?.apiConfig || {}; - + if (!apiKey) { return res.status(401).json({ error: 'API key required in Authorization header' }); } @@ -136,7 +136,7 @@ app.post('/api/wizard', async (req, res) => { res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering - + const llmConfig = { apiKey, provider: apiConfig.provider || 'openrouter', @@ -164,7 +164,7 @@ app.post('/api/wizard', async (req, res) => { console.error('[Wizard] Agent error:', error); res.write(`data: ${JSON.stringify({ type: 'error', message: error.message })}\n\n`); } - + res.end(); } catch (error) { console.error('[Wizard] Request error:', error); @@ -203,7 +203,7 @@ app.post('/api/bridge/register-store', (req, res) => { // State endpoint - UI polls this app.get('/api/bridge/state', (req, res) => { - res.json({ + res.json({ graphs: [], pendingActions: [], source: 'wizard-server' @@ -223,12 +223,12 @@ app.get('/events/stream', (req, res) => { res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Accel-Buffering', 'no'); - + // Send initial ping res.write(`data: ${JSON.stringify({ type: 'connected', source: 'wizard-server' })}\n\n`); - + sseClients.add(res); - + req.on('close', () => { sseClients.delete(res); }); @@ -246,9 +246,147 @@ function broadcastEvent(event) { } } +// ───────────────────────────────────────────────────────────── +// Pending Actions State (for UI ↔ Server communication) +// ───────────────────────────────────────────────────────────── + +let pendingActions = []; +const inflightActionIds = new Set(); +const inflightMeta = new Map(); // id -> { ts, action, params } +let telemetry = []; +let chatLog = []; + // Telemetry endpoint app.get('/api/bridge/telemetry', (req, res) => { - res.json({ chat: [], tools: [] }); + res.json({ telemetry, chat: chatLog.slice(-200) }); +}); + +// ───────────────────────────────────────────────────────────── +// Pending Actions Endpoints (for Committer and UI) +// ───────────────────────────────────────────────────────────── + +// GET pending actions - UI polls this to receive mutations +app.get('/api/bridge/pending-actions', (req, res) => { + try { + const available = pendingActions.filter(a => !inflightActionIds.has(a.id)); + available.forEach(a => { + inflightActionIds.add(a.id); + inflightMeta.set(a.id, { ts: Date.now(), action: a.action, params: a.params }); + telemetry.push({ ts: Date.now(), type: 'tool_call', name: a.action, args: a.params, leased: true, id: a.id }); + }); + res.json({ pendingActions: available }); + } catch (err) { + console.error('[Wizard] Pending actions error:', err); + res.status(500).json({ error: String(err?.message || err) }); + } +}); + +// POST enqueue actions - Committer uses this to push mutations +app.post('/api/bridge/pending-actions/enqueue', (req, res) => { + try { + const { actions } = req.body || {}; + if (!Array.isArray(actions) || actions.length === 0) { + return res.status(400).json({ ok: false, error: 'actions[] required' }); + } + // Prepend openGraph actions inferred from any applyMutations ops to avoid UI timing races + const expanded = []; + for (const a of actions) { + if (a && a.action === 'applyMutations' && Array.isArray(a.params?.[0])) { + const ops = a.params[0]; + const graphIds = new Set(); + for (const op of ops) { + if (op && typeof op.graphId === 'string' && op.graphId) graphIds.add(op.graphId); + } + for (const gid of graphIds) expanded.push({ action: 'openGraph', params: [gid] }); + } + expanded.push(a); + } + const id = (suffix) => `pa-${Date.now()}-${Math.random().toString(36).slice(2, 8)}-${suffix}`; + for (const a of expanded) { + pendingActions.push({ id: id(a.action || 'act'), action: a.action, params: a.params, timestamp: Date.now() }); + telemetry.push({ ts: Date.now(), type: 'tool_call', name: a.action, args: a.params, status: 'queued' }); + } + res.json({ ok: true, enqueued: actions.length }); + } catch (err) { + console.error('[Wizard] Enqueue error:', err); + res.status(500).json({ error: String(err?.message || err) }); + } +}); + +// POST action completed - UI calls this when an action is done +app.post('/api/bridge/action-completed', (req, res) => { + try { + const { actionId } = req.body || {}; + if (actionId) { + pendingActions = pendingActions.filter(a => a.id !== actionId); + inflightActionIds.delete(actionId); + const meta = inflightMeta.get(actionId); + if (meta) { + telemetry.push({ ts: Date.now(), type: 'tool_call', name: meta.action, args: meta.params, status: 'completed', id: actionId }); + inflightMeta.delete(actionId); + } + } + res.json({ success: true }); + } catch (err) { + console.error('[Wizard] Action completed error:', err); + res.status(500).json({ error: String(err?.message || err) }); + } +}); + +// POST action feedback - for warnings and errors +app.post('/api/bridge/action-feedback', (req, res) => { + try { + const { action, status, error, params } = req.body || {}; + telemetry.push({ ts: Date.now(), type: 'action_feedback', action, status, error, params }); + console.log(`[Wizard] Action feedback: ${action} - ${status}`); + res.json({ acknowledged: true }); + } catch (err) { + console.error('[Wizard] Action feedback error:', err); + res.status(500).json({ error: String(err?.message || err) }); + } +}); + +// POST tool status - track tool execution +app.post('/api/bridge/tool-status', (req, res) => { + try { + const { cid, toolCalls } = req.body || {}; + if (!Array.isArray(toolCalls) || toolCalls.length === 0) { + return res.status(400).json({ error: 'toolCalls array required' }); + } + for (const tool of toolCalls) { + telemetry.push({ + ts: tool.timestamp || Date.now(), + type: 'tool_call', + name: tool.name, + args: tool.args || {}, + status: tool.status || 'completed', + result: tool.result, + error: tool.error, + executionTime: tool.executionTime, + cid + }); + } + res.json({ ok: true, updated: toolCalls.length }); + } catch (err) { + console.error('[Wizard] Tool status error:', err); + res.status(500).json({ error: String(err?.message || err) }); + } +}); + +// POST chat append - add messages to chat log +app.post('/api/bridge/chat/append', (req, res) => { + try { + const { role, text, cid, channel } = req.body || {}; + if (!text) return res.status(400).json({ error: 'text required' }); + const entry = { ts: Date.now(), role: role || 'system', text: String(text), cid, channel: channel || 'agent' }; + chatLog.push(entry); + if (chatLog.length > 1000) chatLog = chatLog.slice(-800); + telemetry.push({ ts: entry.ts, type: 'chat', role: entry.role, text: entry.text, cid, channel }); + res.json({ ok: true }); + } catch (err) { + console.error('[Wizard] Chat append error:', err); + res.status(500).json({ error: String(err?.message || err) }); + } }); // ───────────────────────────────────────────────────────────── @@ -260,7 +398,7 @@ app.all('/api/*', (req, res) => { res.status(404).json({ error: 'Endpoint not available in wizard-server', path: req.path, - available: ['/api/wizard', '/api/bridge/health', '/api/bridge/state', '/events/stream'] + available: ['/api/wizard', '/api/bridge/health', '/api/bridge/state', '/api/bridge/pending-actions', '/events/stream'] }); }); @@ -281,7 +419,7 @@ export async function startWizardServer() { // #region agent log debugLogSync('wizard-server.js:getPort:AFTER', 'getPort returned', { port: PORT }, 'debug-session', 'D'); // #endregion - + return new Promise((resolve, reject) => { const server = app.listen(PORT, () => { console.log(` @@ -296,15 +434,15 @@ export async function startWizardServer() { ║ GET /api/scheduler/status - Queue processor status ║ ╚═══════════════════════════════════════════════════════════╝ `); - + // Start scheduler on boot ensureSchedulerStarted().catch(e => { console.warn('[Wizard] Failed to start scheduler on boot:', e.message); }); - + resolve({ server, port: PORT }); }); - + server.on('error', reject); }); } catch (e) {