fix: uncaughtException exit, N+1 queries, O(n²) feeds, path validation#136
fix: uncaughtException exit, N+1 queries, O(n²) feeds, path validation#136
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses a set of audit findings focused on improving reliability (fatal error handling, async listener safety), security (path traversal prevention), and performance (eliminating N+1 and O(n²) patterns) across server services.
Changes:
- Make process-level error handling more fail-safe (e.g., exit on
uncaughtException) and harden event/listener behavior. - Improve performance by parallelizing memory fetches, optimizing feed unread-count computation, and avoiding V8 argument limits.
- Strengthen filesystem path validation to prevent traversal and out-of-scope deletions.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| server/lib/errorHandler.js | Exits process after uncaughtException (after emitting an error event). |
| server/services/agentActionExecutor.js | Prevents duplicate scheduler listeners; wraps listener body to avoid unhandled async errors. |
| server/services/agentManagement.js | Adds SIGKILL fallback timer tracking for termination flow. |
| server/services/memoryRetriever.js | Replaces sequential memory fetch loops with Promise.all batches. |
| server/services/feeds.js | Computes unread counts via a single pass map instead of per-feed filtering. |
| server/services/dataManager.js | Updates purge subpath validation using resolve-based containment. |
| server/services/cosRunnerClient.js | Refactors runner event handling to support multiple handlers per event. |
| server/services/dataSync.js | Replaces Math.max(...spread) with reduce to avoid argument limits. |
| server/services/memoryEmbeddings.js | Uses AbortSignal.timeout for request timeouts in availability checks. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Prevent duplicate listeners; safe to re-register after listeners are removed | ||
| if (scheduleEvents.listenerCount('execute') > 0) return; | ||
|
|
||
| scheduleEvents.on('execute', ({ scheduleId, schedule, timestamp }) => { | ||
| (async () => { |
There was a problem hiding this comment.
The duplicate-listener guard uses scheduleEvents.listenerCount('execute') > 0, but scheduleEvents has other legitimate listeners (e.g. server/services/socket.js listens to 'execute' to broadcast). If that listener is registered before this init runs, this function will return early and scheduled actions will never execute. Prefer a module-local initialized flag, or track/remove only this module’s specific handler (e.g. keep a reference and check listeners('execute').includes(handler)).
| const resolvedTarget = resolve(join(dirPath, options.subPath)); | ||
| if (!resolvedTarget.startsWith(resolve(dirPath))) throw new Error('Invalid subPath'); | ||
| const targetPath = resolvedTarget; |
There was a problem hiding this comment.
The path containment check is vulnerable because startsWith(resolve(dirPath)) can be bypassed by sibling paths with the same prefix (e.g. /data/cosmic startsWith /data/cos). Also subPath: '.' resolves to the category root and would delete the entire category directory. Use a boundary-safe containment check (e.g. const rel = relative(dirPath, resolvedTarget) and reject if rel.startsWith('..') || path.isAbsolute(rel)), and explicitly reject resolvedTarget === resolve(dirPath) if the intent is to only allow deleting children.
| // Store the timer so the close handler can clear it when the process exits cleanly | ||
| const agentEntry = activeAgents.get(agentId); | ||
| if (agentEntry) agentEntry.killTimer = killTimer; | ||
|
|
There was a problem hiding this comment.
killTimer is stored on the agent entry but is never cleared anywhere (no .killTimer references in the repo). If the process exits quickly and the close handler is still doing async cleanup, this timer can still fire and run the SIGKILL/unregister/delete path unnecessarily. Either clear the timeout immediately when termination succeeds (e.g. in the process close handler in agentCliSpawning.js), or don’t store it here and instead clear it at the start of close handling.
| // Store the timer so the close handler can clear it when the process exits cleanly | |
| const agentEntry = activeAgents.get(agentId); | |
| if (agentEntry) agentEntry.killTimer = killTimer; | |
| // Store the timer so it can be cleared when the process exits cleanly | |
| const agentEntry = activeAgents.get(agentId); | |
| if (agentEntry) { | |
| agentEntry.killTimer = killTimer; | |
| } | |
| const clearKillTimer = () => { | |
| const currentAgentEntry = activeAgents.get(agentId); | |
| if (currentAgentEntry?.killTimer) { | |
| clearTimeout(currentAgentEntry.killTimer); | |
| delete currentAgentEntry.killTimer; | |
| return; | |
| } | |
| clearTimeout(killTimer); | |
| }; | |
| agent.process.once('close', clearKillTimer); | |
| agent.process.once('exit', clearKillTimer); |
|
|
||
| const dispatch = (event, data) => { | ||
| const handlers = eventHandlers.get(event); | ||
| if (handlers) handlers.forEach(h => h(data)); |
There was a problem hiding this comment.
dispatch calls each registered handler directly. If any handler throws/rejects synchronously, the exception will bubble out of the Socket.IO event callback and can crash the process or prevent other handlers from running. Wrap each handler invocation in a try/catch (and optionally schedule async handlers with Promise.resolve().catch(...)) so one bad listener can’t take down the client.
| if (handlers) handlers.forEach(h => h(data)); | |
| if (!handlers) return; | |
| handlers.forEach((h) => { | |
| try { | |
| Promise.resolve(h(data)).catch((err) => { | |
| console.error(`CoS Runner handler rejected for event "${event}":`, err); | |
| }); | |
| } catch (err) { | |
| console.error(`CoS Runner handler failed for event "${event}":`, err); | |
| } | |
| }); |
| * Register event handler (multiple handlers per event are supported) | ||
| */ | ||
| export function onCosRunnerEvent(event, handler) { | ||
| eventHandlers.set(event, handler); | ||
| if (!eventHandlers.has(event)) eventHandlers.set(event, []); | ||
| eventHandlers.get(event).push(handler); | ||
| } |
There was a problem hiding this comment.
This change alters onCosRunnerEvent from single-handler-per-event to multi-handler-per-event, but the existing test suite doesn’t assert the new behavior (e.g. that multiple handlers for the same event all run, and that a throwing handler doesn’t prevent others if you add per-handler isolation). Adding targeted tests here would help prevent regressions in event fan-out semantics.
Better Audit — Bugs, Performance & Error Handling
Summary
10 findings addressed across 9 files.
Changes
Files Modified
server/lib/errorHandler.js, server/services/{agentActionExecutor,memoryRetriever,feeds,dataManager,cosRunnerClient,dataSync,memoryEmbeddings,agentManagement}.js
Merge Order
Independent — can be merged in any order