Skip to content

fix: uncaughtException exit, N+1 queries, O(n²) feeds, path validation#136

Merged
atomantic merged 2 commits intomainfrom
better/bugs-perf
Apr 10, 2026
Merged

fix: uncaughtException exit, N+1 queries, O(n²) feeds, path validation#136
atomantic merged 2 commits intomainfrom
better/bugs-perf

Conversation

@atomantic
Copy link
Copy Markdown
Owner

Better Audit — Bugs, Performance & Error Handling

Summary

10 findings addressed across 9 files.

Changes

  • [CRITICAL] Add process.exit(1) to uncaughtException handler in errorHandler.js
  • [HIGH] Fix unhandled async EventEmitter listener in agentActionExecutor.js + duplicate listener guard
  • [HIGH] Replace sequential await loops (N+1) with Promise.all in memoryRetriever.js
  • [HIGH] Fix O(F×I) unreadCount computation in feeds.js — pre-compute Map in one pass
  • [HIGH] Fix path validation in dataManager.js — use resolve-based containment
  • [MEDIUM] Fix cosRunnerClient.js event handler Map to support multiple handlers per event
  • [MEDIUM] Replace Math.max spread with reduce in dataSync.js (prevent V8 arg limit)
  • [MEDIUM] Use AbortSignal.timeout in memoryEmbeddings.js checkAvailability
  • [MEDIUM] Clear SIGKILL fallback timeout on clean process exit in agentManagement.js

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

Copilot AI review requested due to automatic review settings April 10, 2026 04:47
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +572 to +576
// Prevent duplicate listeners; safe to re-register after listeners are removed
if (scheduleEvents.listenerCount('execute') > 0) return;

scheduleEvents.on('execute', ({ scheduleId, schedule, timestamp }) => {
(async () => {
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)).

Copilot uses AI. Check for mistakes.
Comment on lines +172 to +174
const resolvedTarget = resolve(join(dirPath, options.subPath));
if (!resolvedTarget.startsWith(resolve(dirPath))) throw new Error('Invalid subPath');
const targetPath = resolvedTarget;
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +109
// 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;

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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);

Copilot uses AI. Check for mistakes.

const dispatch = (event, data) => {
const handlers = eventHandlers.get(event);
if (handlers) handlers.forEach(h => h(data));
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);
}
});

Copilot uses AI. Check for mistakes.
Comment on lines +65 to 70
* 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);
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@atomantic atomantic merged commit 639ae47 into main Apr 10, 2026
2 checks passed
@atomantic atomantic deleted the better/bugs-perf branch April 10, 2026 13:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants