From 14a6c21fbe359201cab2a5cfb698ec9a93bfe839 Mon Sep 17 00:00:00 2001 From: dimakis Date: Sat, 4 Jul 2026 15:20:59 +0100 Subject: [PATCH] fix(orchestrator): prevent runaway session spawning with multi-layer safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: ID namespace mismatch in orphan detection — getActiveSessionIds() returned SDK sessionIds (UUIDs) but tasks stored clientIds (headless:xxx), causing every spawned task to look orphaned and triggering infinite respawns. Safety layers: - Fix ID namespace to use clientIds consistently - Grace period (60s) before orphan detection can reclaim recently-spawned tasks - Spawn depth limit (5 per tick chain) prevents runaway in single tick - Global rate limiter (5 per 60s window) caps cross-chain spawning - Kill switch (spawnEnabled flag, default OFF) with UI toggle - Clear spawn tracking state on start()/stop() to prevent cross-goal leakage Client propagation: - spawnEnabled added to LoopStatus type, protocol parser, store, and API client - Optimistic UI update for instant toggle feedback - CSS classes in global scope for mobile visibility Tests: 82 passing (orchestrator + route endpoint tests) Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/TaskBoardSection.tsx | 18 +- frontend/src/components/TokenBar.tsx | 8 - .../components/__tests__/TokenBar.test.tsx | 11 - .../src/hooks/__tests__/useTaskBoard.test.ts | 1 + frontend/src/hooks/useTaskBoard.ts | 3 + .../pages/__tests__/DesktopChatView.test.tsx | 3 +- .../src/pages/__tests__/TaskBoard.test.tsx | 1 + frontend/src/styles/global.css | 46 +- frontend/src/types/task.ts | 1 + frontend/src/types/ws-messages.ts | 7 +- .../client/__tests__/protocol-parser.test.ts | 21 +- packages/client/src/api-client.ts | 12 + packages/client/src/protocol-parser.ts | 5 +- packages/client/src/slices/tasks.ts | 2 + packages/client/src/slices/tokens.ts | 2 - packages/client/src/store.ts | 13 + packages/harness/src/session-registry.ts | 4 - server/__tests__/session-overview.test.ts | 1 + server/__tests__/task-orchestrator.test.ts | 574 ++++++++++++++---- server/__tests__/task-routes.test.ts | 68 ++- server/__tests__/token-update.test.ts | 71 --- server/app.ts | 15 + server/chat.ts | 5 - server/index.ts | 58 +- server/query-loop.ts | 22 - server/task-orchestrator.ts | 114 +++- 26 files changed, 713 insertions(+), 373 deletions(-) diff --git a/frontend/src/components/TaskBoardSection.tsx b/frontend/src/components/TaskBoardSection.tsx index 376aecdb..bcbbf7f5 100644 --- a/frontend/src/components/TaskBoardSection.tsx +++ b/frontend/src/components/TaskBoardSection.tsx @@ -26,6 +26,7 @@ export function TaskBoardSection() { pauseLoop, resumeLoop, stopLoop, + setSpawnEnabled, approveTask, rejectTask, approveSpec, @@ -47,7 +48,7 @@ export function TaskBoardSection() { [deleteTask], ); - const { state, progress, awaitingApproval } = loopStatus; + const { state, progress, awaitingApproval, spawnEnabled } = loopStatus; // Count items needing attention const needsAttention = tasks.filter( @@ -60,9 +61,18 @@ export function TaskBoardSection() { badge={needsAttention || undefined} storageKey="cc-taskboard" actions={ - + <> + + + } > {/* Compact loop bar */} diff --git a/frontend/src/components/TokenBar.tsx b/frontend/src/components/TokenBar.tsx index b3b6068c..b18353fa 100644 --- a/frontend/src/components/TokenBar.tsx +++ b/frontend/src/components/TokenBar.tsx @@ -29,14 +29,6 @@ export function TokenBar({ tokenState }: Props) { // Show only session total in that case — the agent context bar is meaningless. const isCompleted = agentContext === 0 && sessionTotal > 0; - if (tokenState.compacting) { - return ( -
- COMPACTING -
- ); - } - return ( <>