{"record":{"id":"4ff4bcc32683e48d","repo":"slopus/happy","slug":"no-active-thread-call-startthread-first","errorCode":null,"errorMessage":"No active thread. Call startThread first.","messagePattern":"No active thread\\. Call startThread first\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/happy-cli/src/codex/codexAppServerClient.ts","lineNumber":1091,"sourceCode":"        }\n        const resumedThread = await this.reconnectAndResumeThread();\n        return { hadActiveTurn: true, aborted: true, forcedRestart: true, resumedThread };\n    }\n\n    /**\n     * Send a user turn and wait for it to complete.\n     * Returns when task_complete or turn_aborted is received.\n     */\n    async sendTurn(prompt: string, opts?: {\n        model?: string;\n        cwd?: string;\n        approvalPolicy?: ApprovalPolicy;\n        sandbox?: SandboxMode;\n        effort?: ReasoningEffort;\n        extraInputItems?: InputItem[];\n    }): Promise<void> {\n        if (!this._threadId) {\n            throw new Error('No active thread. Call startThread first.');\n        }\n\n        const extraInputItems = opts?.extraInputItems ?? [];\n        const input: InputItem[] = [];\n        if (prompt.length > 0 || extraInputItems.length === 0) {\n            input.push({ type: 'text', text: prompt });\n        }\n        input.push(...extraInputItems);\n\n        // Build params — only include optional fields when set (server uses thread defaults otherwise)\n        const params: Record<string, unknown> = {\n            threadId: this._threadId,\n            input,\n        };\n        if (opts?.cwd) params.cwd = opts.cwd;\n        if (opts?.approvalPolicy) params.approvalPolicy = opts.approvalPolicy;\n        if (opts?.model) params.model = opts.model;\n        if (opts?.effort) params.effort = opts.effort;","sourceCodeStart":1073,"sourceCodeEnd":1109,"githubUrl":"https://github.com/slopus/happy/blob/b824cd0a4681d41af631a8e422a813873e4455b0/packages/happy-cli/src/codex/codexAppServerClient.ts#L1073-L1109","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait client.send(prompt); // throws if no thread\n\n// after\nif (!client.threadId) {\n  await client.startThread({ cwd, mcpServers });\n}\nawait client.send(prompt);","handlingStrategy":"validation","validationCode":"if (!client.threadId) {\n  await client.startThread({ cwd, mcpServers });\n}\nawait client.send(prompt);","typeGuard":"function hasActiveThread(client: { threadId?: string | null }): client is { threadId: string } {\n  return typeof client.threadId === 'string' && client.threadId.length > 0;\n}","tryCatchPattern":"try {\n  await client.send(prompt);\n} catch (error) {\n  if ((error as Error).message.includes('No active thread')) {\n    await client.startThread({ cwd, mcpServers });\n    await client.send(prompt);\n  } else { throw error; }\n}","preventionTips":["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"],"tags":["codex","thread","lifecycle","state"],"backgroundTag":"no-active-thread","analyzedSha":"b824cd0a4681d41af631a8e422a813873e4455b0","analyzedAt":"2026-08-31T23:12:36.205Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}