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

  1. Call await client.startThread({...}) before sending any prompt
  2. Ensure the resume/startThread await completed (don't fire prompts before initialization resolves)
  3. Gate message sending on a check like `if (!client.threadId) await ensureThread()`
  4. 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

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


AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31). Data as JSON: /api/errors/4ff4bcc32683e48d. Report an issue: GitHub.