-
Notifications
You must be signed in to change notification settings - Fork 5
fix: uncaughtException exit, N+1 queries, O(n²) feeds, path validation #136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -94,15 +94,19 @@ export async function terminateAgent(agentId) { | |||||||||||||||||||||||||||||||||||||||||||||
| // Kill the process | ||||||||||||||||||||||||||||||||||||||||||||||
| agent.process.kill('SIGTERM'); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Give it a moment, then force kill if needed | ||||||||||||||||||||||||||||||||||||||||||||||
| setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||
| // Give it a moment, then force kill if still running | ||||||||||||||||||||||||||||||||||||||||||||||
| const killTimer = setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (activeAgents.has(agentId)) { | ||||||||||||||||||||||||||||||||||||||||||||||
| agent.process.kill('SIGKILL'); | ||||||||||||||||||||||||||||||||||||||||||||||
| unregisterSpawnedAgent(agent.pid); | ||||||||||||||||||||||||||||||||||||||||||||||
| activeAgents.delete(agentId); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| }, 5000); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // 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; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
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; | |
| // 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); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,7 +12,8 @@ const COS_RUNNER_URL = process.env.COS_RUNNER_URL || 'http://localhost:5558'; | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Socket.IO client for real-time events | ||||||||||||||||||||||||||
| let socket = null; | ||||||||||||||||||||||||||
| let eventHandlers = new Map(); | ||||||||||||||||||||||||||
| // Map of event name -> array of handlers (supports multiple listeners per event) | ||||||||||||||||||||||||||
| const eventHandlers = new Map(); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Initialize connection to CoS Runner | ||||||||||||||||||||||||||
|
|
@@ -26,72 +27,46 @@ export function initCosRunnerConnection() { | |||||||||||||||||||||||||
| reconnectionDelay: 1000 | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const dispatch = (event, data) => { | ||||||||||||||||||||||||||
| const handlers = eventHandlers.get(event); | ||||||||||||||||||||||||||
| if (handlers) handlers.forEach(h => h(data)); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| 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
AI
Apr 10, 2026
There was a problem hiding this comment.
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.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import { readdir, stat, rm, mkdir, writeFile as fsWriteFile } from 'fs/promises'; | ||
| import { join, relative } from 'path'; | ||
| import { join, relative, resolve } from 'path'; | ||
| import { existsSync } from 'fs'; | ||
| import { execFile } from 'child_process'; | ||
| import { promisify } from 'util'; | ||
|
|
@@ -169,9 +169,9 @@ export async function purgeCategory(categoryKey, options = {}) { | |
| if (!existsSync(dirPath)) throw new Error(`Category directory not found: ${categoryKey}`); | ||
|
|
||
| if (options.subPath) { | ||
| if (!SAFE_NAME.test(options.subPath.replace(/\//g, ''))) throw new Error('Invalid subpath'); | ||
| const targetPath = join(dirPath, options.subPath); | ||
| if (!targetPath.startsWith(dirPath)) throw new Error('Path traversal not allowed'); | ||
| const resolvedTarget = resolve(join(dirPath, options.subPath)); | ||
| if (!resolvedTarget.startsWith(resolve(dirPath))) throw new Error('Invalid subPath'); | ||
| const targetPath = resolvedTarget; | ||
|
Comment on lines
+172
to
+174
|
||
| await rm(targetPath, { recursive: true, force: true }); | ||
| } else { | ||
| const entries = await readdir(dirPath).catch(() => []); | ||
|
|
||
There was a problem hiding this comment.
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, butscheduleEventshas other legitimate listeners (e.g.server/services/socket.jslistens 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-localinitializedflag, or track/remove only this module’s specific handler (e.g. keep a reference and checklisteners('execute').includes(handler)).