From 1c1d09d85894a2f935ab8fab702c511c9516843c Mon Sep 17 00:00:00 2001 From: ismeth Date: Thu, 12 Feb 2026 16:46:23 +0100 Subject: [PATCH] =?UTF-8?q?fix(athena):=20prevent=20recursive=20council=20?= =?UTF-8?q?explosion=20=E2=80=94=20deny=20tool=20for=20bg=20tasks=20+=20de?= =?UTF-8?q?dup=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Council members launched as agent='athena' got Athena's system prompt saying 'ALWAYS call athena_council first', plus the tool wasn't denied for bg athena tasks. Each council member spawned 4 more → exponential explosion (47+ tasks). Three fixes: 1. Deny athena_council in ATHENA_RESTRICTIONS (agent-tool-restrictions.ts) - Only affects background athena tasks (task-starter.ts) - Primary Athena (user-selected) still has access via permission field 2. Session-level dedup guard prevents re-calling while council is running - If Athena retries during long wait, returns 'already running' 3. Increase wait timeout from 2min to 10min (council members need time for real code analysis with Read/Grep/LSP) --- src/shared/agent-tool-restrictions.ts | 2 +- src/tools/athena-council/tools.ts | 48 +++++++++++++++++---------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/shared/agent-tool-restrictions.ts b/src/shared/agent-tool-restrictions.ts index 7a3e1a14d..e26f418ad 100644 --- a/src/shared/agent-tool-restrictions.ts +++ b/src/shared/agent-tool-restrictions.ts @@ -14,7 +14,7 @@ const EXPLORATION_AGENT_DENYLIST: Record = { } const ATHENA_RESTRICTIONS = permissionToToolBooleans( - createAgentToolRestrictions(["write", "edit"]).permission + createAgentToolRestrictions(["write", "edit", "athena_council"]).permission ) const AGENT_RESTRICTIONS: Record> = { diff --git a/src/tools/athena-council/tools.ts b/src/tools/athena-council/tools.ts index 8dbebd986..6cb631f67 100644 --- a/src/tools/athena-council/tools.ts +++ b/src/tools/athena-council/tools.ts @@ -6,10 +6,13 @@ import { ATHENA_COUNCIL_TOOL_DESCRIPTION } from "./constants" import { createCouncilLauncher } from "./council-launcher" import type { AthenaCouncilToolArgs } from "./types" -const WAIT_INTERVAL_MS = 200 -const WAIT_TIMEOUT_MS = 120000 +const WAIT_INTERVAL_MS = 500 +const WAIT_TIMEOUT_MS = 600000 const TERMINAL_STATUSES: Set = new Set(["completed", "error", "cancelled", "interrupt"]) +/** Tracks active council executions per session to prevent duplicate launches. */ +const activeCouncilSessions = new Set() + function isCouncilConfigured(councilConfig: CouncilConfig | undefined): councilConfig is CouncilConfig { return Boolean(councilConfig && councilConfig.members.length > 0) } @@ -114,25 +117,34 @@ export function createAthenaCouncilTool(args: { return "Athena council not configured. Add agents.athena.council.members to your config." } - const execution = await executeCouncil({ - question: toolArgs.question, - council: councilConfig, - launcher: createCouncilLauncher(backgroundManager), - parentSessionID: toolContext.sessionID, - parentMessageID: toolContext.messageID, - parentAgent: toolContext.agent, - }) + if (activeCouncilSessions.has(toolContext.sessionID)) { + return "Council is already running for this session. Wait for the current council execution to complete." + } - const taskIds = execution.responses - .map((response) => response.taskId) - .filter((taskId) => taskId.length > 0) + activeCouncilSessions.add(toolContext.sessionID) + try { + const execution = await executeCouncil({ + question: toolArgs.question, + council: councilConfig, + launcher: createCouncilLauncher(backgroundManager), + parentSessionID: toolContext.sessionID, + parentMessageID: toolContext.messageID, + parentAgent: toolContext.agent, + }) - const latestTasks = await waitForTasksToSettle(taskIds, backgroundManager, toolContext.abort) - const refreshedResponses = execution.responses.map((response) => - response.taskId ? refreshResponse(response, latestTasks.get(response.taskId)) : response - ) + const taskIds = execution.responses + .map((response) => response.taskId) + .filter((taskId) => taskId.length > 0) - return formatCouncilOutput(refreshedResponses, execution.totalMembers) + const latestTasks = await waitForTasksToSettle(taskIds, backgroundManager, toolContext.abort) + const refreshedResponses = execution.responses.map((response) => + response.taskId ? refreshResponse(response, latestTasks.get(response.taskId)) : response + ) + + return formatCouncilOutput(refreshedResponses, execution.totalMembers) + } finally { + activeCouncilSessions.delete(toolContext.sessionID) + } }, }) }