Skip to content

[P2][BUG] No agent execution timeout — infinite tool-calling loops run indefinitely and drain API credits #11

Description

@pratik-saptarshi

Summary

Probus spawns Claude Code agents via runClaudeAgent() and runChatAgent() with no wall-clock timeout. There is no deadline on how long an agent may run, how many tool calls it may make, or how many tokens it may consume. In pathological cases — adversarial repositories, model misalignment, or complex codebases — an agent can enter an infinite read-edit loop, running indefinitely until the user manually clicks Stop (or the API rate limit kicks in). This drains API credits silently.

Panel verdict: P2, VERIFIED, EXISTING_DEFECT (Completeness Audit finding G-06 / VAL-21).


Affected Files

File Lines Issue
src/claude-agent.ts L51–L61 No timeout option in Options object
src/server/chat-agent.ts L50–L59 No timeout option in chat agent Options
src/scanner.ts L156–L188 runAgent() passes signal? but never creates a timeout signal

Root Cause — Code Evidence

No timeout in scanner agent options (src/claude-agent.ts, lines 51–61):

const options: Options = {
  cwd,
  model,
  env: { ...process.env, ...env } as Record<string, string | undefined>,
  abortController,
  includePartialMessages: true,
  permissionMode: 'bypassPermissions',
  settingSources: [],
  // ↑ No maxTokens, no timeout, no maxTurns — agent can run forever
};

runAgent() accepts a signal but never creates a timeout one (src/scanner.ts, lines 156–188):

async function* runAgent(
  prompt: string,
  cwd: string,
  model: string,
  signal?: AbortSignal,   // ← caller provides signal, but callers never add a timeout
  logFile?: string,
  stageLabel?: string,
): AsyncGenerator<...> {
  // ...
  for await (const ev of runClaudeAgent({ prompt, cwd, model, env, signal, logFile, stageLabel })) {
    yield ev;
  }
}

Impact

  • Credit drain: A single stuck agent on a complex file can consume $10–$100+ in API credits before the user notices.
  • UI freeze: The scan/chat interface shows a perpetual loading state with no timeout indicator.
  • Cascading scans: In high-parallelism mode (parallel=16), 16 concurrent agents can all get stuck simultaneously.

Steps to Reproduce

  1. Scan a repository containing a deliberately complex or adversarially crafted source file (e.g. a 10,000-line minified JS bundle).
  2. Observe: the agent enters a read-loop trying to process the file, never completing.
  3. Wait 10 minutes — the scan is still running with no timeout.

Remediation

Step 1 — Add per-agent wall-clock timeout using AbortSignal.timeout()

// src/claude-agent.ts — in runClaudeAgent()
const MAX_AGENT_DURATION_MS = 5 * 60 * 1000; // 5 minutes per file scan

// Compose the caller's signal with a timeout signal:
const timeoutSignal = AbortSignal.timeout(MAX_AGENT_DURATION_MS);
const combined = AbortSignal.any([timeoutSignal, ...(signal ? [signal] : [])]);

const options: Options = {
  // ...
  abortController, // use abortController driven by combined signal
};

// Drive abortController from combined signal:
combined.addEventListener('abort', () => abortController.abort(combined.reason), { once: true });

Step 2 — Add a max token budget per agent

If the Claude Code SDK supports a token cap:

const options: Options = {
  // ...
  maxTokens: 50_000,  // cap per-agent invocation
};

Step 3 — Add configurable timeout to scan options

Expose agentTimeoutSeconds in the POST /api/scans body (defaulting to 300s) and thread it through to runAgent():

const agentTimeout = Math.min(body.agentTimeoutSeconds ?? 300, 600) * 1000;
// Pass to scanner as AbortSignal.timeout(agentTimeout)

Step 4 — Show elapsed time in the UI

Display per-file elapsed time in the scan progress view so users can see when an agent is running unusually long and manually abort it.


References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions