{"record":{"id":"457a87a3f69b865f","repo":"can1357/oh-my-pi","slug":"debugger-reported-no-threads","errorCode":null,"errorMessage":"Debugger reported no threads.","messagePattern":"Debugger reported no threads\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/dap/session.ts","lineNumber":1666,"sourceCode":"\t\t\t\tsnapshot: buildSummary(resultSession),\n\t\t\t\tstate: \"running\",\n\t\t\t\ttimedOut: resultSession.status === \"running\",\n\t\t\t};\n\t\t}\n\t}\n\n\tasync #resolveThreadId(session: DapSession, signal?: AbortSignal, timeoutMs: number = 30_000): Promise<number> {\n\t\tif (session.stop.threadId !== undefined) {\n\t\t\treturn session.stop.threadId;\n\t\t}\n\t\tif (session.threads.length > 0) {\n\t\t\treturn session.threads[0].id;\n\t\t}\n\t\tconst response = await session.client.sendRequest<DapThreadsResponse>(\"threads\", undefined, signal, timeoutMs);\n\t\tsession.threads = response?.threads ?? [];\n\t\tconst threadId = session.threads[0]?.id;\n\t\tif (threadId === undefined) {\n\t\t\tthrow new Error(\"Debugger reported no threads.\");\n\t\t}\n\t\treturn threadId;\n\t}\n\n\tasync #sendRequestWithConfig<TBody>(\n\t\tsession: DapSession,\n\t\tcommand: string,\n\t\targs: unknown,\n\t\tsignal?: AbortSignal,\n\t\ttimeoutMs: number = 30_000,\n\t): Promise<TBody> {\n\t\tawait this.#ensureConfigurationDone(session, signal, timeoutMs);\n\t\tconst body = await session.client.sendRequest<TBody>(command, args, signal, timeoutMs);\n\t\tthis.#touchSessionAndAncestors(session);\n\t\treturn body;\n\t}\n\n\tasync #ensureConfigurationDone(","sourceCodeStart":1648,"sourceCodeEnd":1684,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/dap/session.ts#L1648-L1684","documentation":"DapSessionManager resolves a thread id after requesting the DAP `threads` response; when the debugger returns an empty thread list (or none at all), it throws because every follow-up request (stack traces, breakpoints, stepping) needs a thread id. This indicates the debug adapter is alive but reported no debuggable threads.","triggerScenarios":"Calling any public DapSessionManager method that resolves the first thread (`#resolveThreadId`) while `response.threads` is empty or undefined — e.g. evaluating, setting breakpoints, or resuming right after launch/attach before the debuggee started, or after the debuggee exited but the adapter session is still alive.","commonSituations":"Attaching to a process that already exited; launching a program that crashed immediately; adapters (e.g. node/js-debug, python debugpy) that return zero threads during startup races; querying too early before a 'stopped' event produced any threads.","solutions":["Wait for a `stopped` event (a breakpoint hit or pause) before issuing thread-dependent requests; the manager already gates some ops on status === 'stopped'","Verify the debuggee actually started: check the program path/args in the launch config and inspect adapter console output for early-exit errors","Re-launch or re-attach the session; the debuggee may have terminated","Catch the error and retry after a short delay if you are racing adapter startup"],"exampleFix":"// before\nconst threadId = await manager.getThreadId(); // throws if called right after launch\n// after\nawait manager.waitForStoppedEvent(); // or pause() the debuggee first\nconst threadId = await manager.getThreadId();","handlingStrategy":"retry","validationCode":"const threads = await session.client.sendRequest('threads');\nif (!threads?.threads?.length) {\n\tthrow new SkipOperation('debugger has no threads yet');\n}","typeGuard":"function hasThreads(r) { return Array.isArray(r?.threads) && r.threads.length > 0; }","tryCatchPattern":"try {\n\tconst threadId = await manager.getThreadId();\n} catch (err) {\n\tif (err.message === 'Debugger reported no threads.') {\n\t\tawait Bun.sleep(250); // adapter startup race\n\t\t// retry once, else surface 'debuggee not running'\n\t}\n}","preventionTips":["Only issue thread-dependent DAP requests after a stopped event","Verify the debuggee program launches and stays alive","Treat zero threads as 'debuggee exited' in your UI"],"tags":["dap","debugger","threads","race-condition"],"backgroundTag":"debugger-no-threads","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}