musistudio/claude-code-router · warning · Error

No active turn for thread + params.threadId

Error message

No active turn for thread  + params.threadId

What it means

The turn/steer middleware handler tries to forward user input into the stdin of the currently running turn for a thread. It looks up an active child process by threadId and throws when no active turn (or no live stdin stream) exists for that thread. Steering only works while a turn is mid-flight.

Source

Thrown at packages/core/src/agents/codex/cli-middleware-runtime.ts:2762

        const key = activeKey(params.threadId, params.turnId);
        const entry = this.active.get(key) || findActiveForThread(this.active, params.threadId);
        if (entry) {
          entry.child.kill("SIGTERM");
          this.active.delete(entry.key);
          const thread = this.threads.get(entry.threadId);
          const turn = thread && thread.turns.find((item) => item.id === entry.turnId);
          if (turn) {
            turn.status = "interrupted";
            turn.completedAt = nowSeconds();
            turn.durationMs = Math.max(0, (turn.completedAt - turn.startedAt) * 1000);
          }
        }
        writeResponse(id, {});
        return undefined;
      }
      case "turn/steer": {
        const entry = findActiveForThread(this.active, params.threadId);
        if (!entry || !entry.child.stdin) throw new Error("No active turn for thread " + params.threadId);
        entry.child.stdin.write(JSON.stringify(claudeInputMessage(params.input || params.message || params)) + "\n");
        writeResponse(id, {});
        return undefined;
      }
      case "model/list":
        writeResponse(id, modelList(params));
        return undefined;
      case "modelProvider/capabilities/read":
        writeResponse(id, { namespaceTools: false, imageGeneration: false, webSearch: false });
        return undefined;
      case "account/read":
        writeResponse(id, mockAccountRead());
        return undefined;
      case "getAuthStatus":
        writeResponse(id, mockAuthStatus(Boolean(params.includeToken)));
        return undefined;
      case "permissionProfile/list":
      case "skills/list":

View on GitHub (pinned to 99f24806c6)

Solutions

  1. Check that a turn is actually active for the thread before steering (poll thread/turn status)
  2. Start a new turn instead of steering once the previous turn completed
  3. Handle the race client-side: retry as a new prompt if steering fails
  4. If it happens consistently, inspect child process stderr — the turn may be dying immediately

Example fix

// before
await middleware.request("turn/steer", { threadId, input }); // throws if no active turn

// after
const active = await middleware.request("thread/activeTurn", { threadId }).catch(() => null);
if (active) {
  await middleware.request("turn/steer", { threadId, input });
} else {
  await middleware.request("turn/start", { threadId, input }); // start a fresh turn
}
Defensive patterns

Strategy: fallback

Validate before calling

const active = findActiveForThread(activeTurns, threadId);
if (!active) { /* start a new turn instead of steering */ }

Type guard

function hasActiveTurn(active, threadId) { const e = active.get(String(threadId)); return Boolean(e && e.child && e.child.stdin && !e.child.stdin.destroyed); }

Try / catch

try { await steer(threadId, input); } catch (e) { if (/No active turn for thread/.test(String(e))) { await startTurn(threadId, input); } else throw e; }

Prevention

When it happens

Trigger: Sending a turn/steer request for a threadId that has no entry in the active-turn map: the turn already completed, was cancelled/crashed, the child's stdin closed, or the threadId is wrong/stale from a reconnecting client.

Common situations: Client UI sends a follow-up message after the previous turn finished but before refreshing state; race between turn completion event and user hitting send; child process crashed silently; reused threadId from a previous runtime session.

Related errors


AI-assisted analysis of musistudio/claude-code-router@99f24806c6 (2026-08-27). Data as JSON: /api/errors/feafbdc6a7dd0e4e. Report an issue: GitHub.