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
- Check that a turn is actually active for the thread before steering (poll thread/turn status)
- Start a new turn instead of steering once the previous turn completed
- Handle the race client-side: retry as a new prompt if steering fails
- 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
- Confirm turn state before sending follow-up input
- Refresh thread state after turn completion events
- Treat steer failure as a signal to fall back to a new prompt
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.