fix: prevent agents from killing production dashboard on port 4040

An AI review agent (FN-1506) killed the running dashboard by finding
the process on port 4040 via lsof and running kill -9, causing exit
code 137 (SIGKILL) with no logs. This adds multi-layer guardrails:

- AGENTS.md: project-level rule reserving port 4040
- Executor/reviewer system prompts: explicit prohibition on killing
  port 4040 processes, with instruction to use --port 0 instead
- Core agent-prompts.ts: same guardrails in all prompt variants
- Reviewer told to issue REVISE if executor violates the rule
- SIGHUP handlers in dashboard.ts and serve.ts for resilience
- Background engine reconciliation in dashboard/serve startup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-13 18:00:49 -07:00
parent 9a028b6573
commit 564659fcbe
6 changed files with 64 additions and 2 deletions

View File

@@ -497,6 +497,14 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
// Start engines for all registered projects eagerly
await engineManager.startAll();
// Start background reconciliation to detect and start engines for projects
// registered after startup (without requiring dashboard UI access).
// This ensures project task execution starts from backend runtime alone.
// The onProjectFirstAccessed callback in createServer remains as a fast-path
// fallback for immediate engine startup on project access, but it is NOT
// required for correctness — reconciliation handles all cases.
engineManager.startReconciliation();
// Resolve the cwd project's engine for the dashboard's HTTP layer defaults.
// The engine for the cwd project provides onMerge, automationStore, etc.
// for requests that arrive without ?projectId=. This is transitional —
@@ -572,6 +580,14 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
};
registerHandler(process, "SIGINT", () => void shutdown("SIGINT"));
registerHandler(process, "SIGTERM", () => void shutdown("SIGTERM"));
// Ignore SIGHUP so the dashboard survives SSH session disconnects.
// Without this, SIGHUP (sent when the controlling terminal closes) kills
// the process silently — the exit handler tries to log to the now-dead
// PTY and the write is lost.
registerHandler(process, "SIGHUP", () => {
console.log("[dashboard] Received SIGHUP (terminal disconnected) — ignoring");
});
} else {
// Dev mode: create HeartbeatMonitor + TriggerScheduler inline (engine not started)
try {
@@ -698,6 +714,11 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
};
registerHandler(process, "SIGINT", () => void devShutdown("SIGINT"));
registerHandler(process, "SIGTERM", () => void devShutdown("SIGTERM"));
// Ignore SIGHUP so the dashboard survives SSH session disconnects
registerHandler(process, "SIGHUP", () => {
console.log("[dashboard] Received SIGHUP (terminal disconnected) — ignoring");
});
}
const server = app.listen(selectedPort);

View File

@@ -302,6 +302,14 @@ export async function runServe(
// Start engines for all registered projects eagerly
await engineManager.startAll();
// Start background reconciliation to detect and start engines for projects
// registered after startup (without requiring headless node API access).
// This ensures project task execution starts from backend runtime alone.
// The onProjectFirstAccessed callback in createServer remains as a fast-path
// fallback for immediate engine startup on project access, but it is NOT
// required for correctness — reconciliation handles all cases.
engineManager.startReconciliation();
// Get the cwd project's engine and store for the HTTP layer.
// serve.ts needs a store for plugin setup, diagnostics, and the server.
const cwdEngine = ntfyProjectId ? engineManager.getEngine(ntfyProjectId) : undefined;
@@ -618,4 +626,12 @@ export async function runServe(
process.on("SIGTERM", () => {
void shutdown();
});
// Ignore SIGHUP so the server survives SSH session disconnects.
// Without this, SIGHUP (sent when the controlling terminal closes) kills
// the process silently — the exit handler tries to log to the now-dead
// PTY and the write is lost.
process.on("SIGHUP", () => {
console.log("[serve] Received SIGHUP (terminal disconnected) — ignoring");
});
}