slopus/happy · error
No active thread. Call startThread first.
Error message
No active thread. Call startThread first.
What it means
sendUserTurn (the method that forwards a user prompt to Codex) needs an active thread; if this._threadId is null it throws, telling you to call startThread first. Unlike resumeThread, it does not accept an explicit thread id — the client must already hold one.
Source
Thrown at packages/happy-cli/src/codex/codexAppServerClient.ts:1091
}
const resumedThread = await this.reconnectAndResumeThread();
return { hadActiveTurn: true, aborted: true, forcedRestart: true, resumedThread };
}
/**
* Send a user turn and wait for it to complete.
* Returns when task_complete or turn_aborted is received.
*/
async sendTurn(prompt: string, opts?: {
model?: string;
cwd?: string;
approvalPolicy?: ApprovalPolicy;
sandbox?: SandboxMode;
effort?: ReasoningEffort;
extraInputItems?: InputItem[];
}): Promise<void> {
if (!this._threadId) {
throw new Error('No active thread. Call startThread first.');
}
const extraInputItems = opts?.extraInputItems ?? [];
const input: InputItem[] = [];
if (prompt.length > 0 || extraInputItems.length === 0) {
input.push({ type: 'text', text: prompt });
}
input.push(...extraInputItems);
// Build params — only include optional fields when set (server uses thread defaults otherwise)
const params: Record<string, unknown> = {
threadId: this._threadId,
input,
};
if (opts?.cwd) params.cwd = opts.cwd;
if (opts?.approvalPolicy) params.approvalPolicy = opts.approvalPolicy;
if (opts?.model) params.model = opts.model;
if (opts?.effort) params.effort = opts.effort;View on GitHub (pinned to b824cd0a46)
Solutions
- Call await client.startThread({...}) before sending any prompt
- Ensure the resume/startThread await completed (don't fire prompts before initialization resolves)
- Gate message sending on a check like `if (!client.threadId) await ensureThread()`
- On resume failure, surface the error instead of continuing to send prompts
Example fix
// before
await client.send(prompt); // throws if no thread
// after
if (!client.threadId) {
await client.startThread({ cwd, mcpServers });
}
await client.send(prompt); Defensive patterns
Strategy: validation
Validate before calling
if (!client.threadId) {
await client.startThread({ cwd, mcpServers });
}
await client.send(prompt); Type guard
function hasActiveThread(client: { threadId?: string | null }): client is { threadId: string } {
return typeof client.threadId === 'string' && client.threadId.length > 0;
} Try / catch
try {
await client.send(prompt);
} catch (error) {
if ((error as Error).message.includes('No active thread')) {
await client.startThread({ cwd, mcpServers });
await client.send(prompt);
} else { throw error; }
} Prevention
- Serialize message sending behind thread initialization (await startThread before queuing prompts)
- Expose an isReady/ensureReady helper instead of checking threadId ad hoc
- Abort pending sends when a resume fails instead of continuing
When it happens
Trigger: Calling the send/prompt method on a CodexAppServerClient that was constructed but never had startThread() or resumeThread() succeed; sending a second prompt after a failed resume; using a client whose thread was cleared during reconnection.
Common situations: Race where a remote/mobile message arrives before thread initialization completes; resume failed earlier (e.g. error 61) and code kept sending prompts; instantiating the client directly in tests without starting a thread.
Related errors
- No thread available to resume.
- No active Codex thread
- Backend has been disposed
- Failed to resume Codex thread ${opts.threadId}: ${reason}
- Session not started
AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31).
Data as JSON: /api/errors/4ff4bcc32683e48d.
Report an issue: GitHub.