feat(backend): suspend_thread for LLDB-backed targets (#21 LLDB completion)#17
Merged
Conversation
zachgenius
added a commit
that referenced
this pull request
May 12, 2026
…ments (#21 reviewer pass) Reviewer-pass on PR #17 (v1.6 #21 LLDB suspend_thread landing). Three findings, all addressed: 1. CRITICAL: dispatcher mapped backend errors to -32001 vs -32004 by substring-matching `e.what()` for "not implemented". Any backend that threw a descriptive error containing those words for a legitimate runtime failure would have been silently promoted to kNotImplemented. Fix: typed `backend::NotImplementedError` subclass of `backend::Error`; dispatcher catches the subclass first (→ -32001), generic Error second (→ -32004). GdbMiBackend's suspend_thread throws the new type. Catch order matters and is pinned in a comment — the compiler does not warn on reversed order. 2. Stale comment blocks describing thread.suspend as "still returns -32001 kNotImplemented for LLDB-backed targets" — that's the pre-#21 contract. Replaced both: the describe.endpoints text and the function-level comment in handle_thread_suspend. The function comment now enumerates the three dispatch branches in order (RSP channel → vCont;t, LLDB-backed → backend.suspend_thread, NotImplemented-throwing backend → -32001). 3. MINOR: stale `[legacy]` Catch2 tag on a test that was updated to assert the new non-RSP forwarding path. Renamed to `[non-rsp]`. New tests (test_dispatcher_nonstop.cpp) lock in the typed-error contract: - `NotImplementedError → -32001 by exception type` — message deliberately does NOT contain "not implemented" to prove the dispatcher uses the exception type, not its message. - `generic Error with 'not implemented' substring stays -32004` — inverse case; this is the exact regression the pre-fix code was vulnerable to. Also updated the gdbmi `todo()` helper (currently `[[maybe_unused]]`) to throw NotImplementedError, so the contract is uniform for future stubs. 70/70 ctest green. +2 unit cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zachgenius
added a commit
that referenced
this pull request
May 12, 2026
…etion) Drops the -32001 kNotImplemented stub that thread.suspend returned for LLDB-backed targets and lands a real per-thread suspend. Why this works without SetAsync(true): SBThread::Suspend() is a flag flip — the suspend bit gates the NEXT SBProcess::Continue, not the suspend call itself. The async-mode flip is only required for the *delivery* of stop events (the listener path), not for marking a thread as parked. RSP-backed targets keep their existing vCont;t route (#17 phase-2); LLDB-backed targets now flow through the new backend virtual. - DebuggerBackend grows a `suspend_thread(target_id, tid)` virtual. LldbBackend resolves SBThread by id and calls Suspend(); throws backend::Error on bad target/tid or no live process. GdbMiBackend throws "not implemented" — MI's per-thread semantics differ enough to warrant a separate item. - Dispatcher::handle_thread_suspend forwards to backend_->suspend_thread for non-RSP targets. The "not implemented" branch surfaces -32001 so agents still see kNotImplemented when the backend genuinely lacks support; everything else is -32004 kBackendError. - New test_backend_suspend_thread.cpp (4 cases) exercises live LLDB- launched suspend against the sleeper fixture + error paths. - test_dispatcher_nonstop.cpp's "-32001 kNotImplemented in phase-1" case is replaced with two cases: forwards-to-backend (happy path) and unknown-target → -32004. test_dispatcher_vcont_rsp.cpp's "non- RSP returns -32001" case is updated to assert the new "no process" -32004 against an empty target. - All 7 stub-backend test files grow a one-line suspend_thread override. Stretch (SetAsync(true) flip + pump_until_stopped wrappers around continue/step) intentionally deferred — docs/27 §7 already calls out that cascade as its own commit. Ships minimum scope clean: 70/70 ctest green, +5 unit cases, no other behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zachgenius
added a commit
that referenced
this pull request
May 12, 2026
…ments (#21 reviewer pass) Reviewer-pass on PR #17 (v1.6 #21 LLDB suspend_thread landing). Three findings, all addressed: 1. CRITICAL: dispatcher mapped backend errors to -32001 vs -32004 by substring-matching `e.what()` for "not implemented". Any backend that threw a descriptive error containing those words for a legitimate runtime failure would have been silently promoted to kNotImplemented. Fix: typed `backend::NotImplementedError` subclass of `backend::Error`; dispatcher catches the subclass first (→ -32001), generic Error second (→ -32004). GdbMiBackend's suspend_thread throws the new type. Catch order matters and is pinned in a comment — the compiler does not warn on reversed order. 2. Stale comment blocks describing thread.suspend as "still returns -32001 kNotImplemented for LLDB-backed targets" — that's the pre-#21 contract. Replaced both: the describe.endpoints text and the function-level comment in handle_thread_suspend. The function comment now enumerates the three dispatch branches in order (RSP channel → vCont;t, LLDB-backed → backend.suspend_thread, NotImplemented-throwing backend → -32001). 3. MINOR: stale `[legacy]` Catch2 tag on a test that was updated to assert the new non-RSP forwarding path. Renamed to `[non-rsp]`. New tests (test_dispatcher_nonstop.cpp) lock in the typed-error contract: - `NotImplementedError → -32001 by exception type` — message deliberately does NOT contain "not implemented" to prove the dispatcher uses the exception type, not its message. - `generic Error with 'not implemented' substring stays -32004` — inverse case; this is the exact regression the pre-fix code was vulnerable to. Also updated the gdbmi `todo()` helper (currently `[[maybe_unused]]`) to throw NotImplementedError, so the contract is uniform for future stubs. 70/70 ctest green. +2 unit cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3b5586b to
22aa0f6
Compare
…etion) Drops the -32001 kNotImplemented stub that thread.suspend returned for LLDB-backed targets and lands a real per-thread suspend. Why this works without SetAsync(true): SBThread::Suspend() is a flag flip — the suspend bit gates the NEXT SBProcess::Continue, not the suspend call itself. The async-mode flip is only required for the *delivery* of stop events (the listener path), not for marking a thread as parked. RSP-backed targets keep their existing vCont;t route (#17 phase-2); LLDB-backed targets now flow through the new backend virtual. - DebuggerBackend grows a `suspend_thread(target_id, tid)` virtual. LldbBackend resolves SBThread by id and calls Suspend(); throws backend::Error on bad target/tid or no live process. GdbMiBackend throws "not implemented" — MI's per-thread semantics differ enough to warrant a separate item. - Dispatcher::handle_thread_suspend forwards to backend_->suspend_thread for non-RSP targets. The "not implemented" branch surfaces -32001 so agents still see kNotImplemented when the backend genuinely lacks support; everything else is -32004 kBackendError. - New test_backend_suspend_thread.cpp (4 cases) exercises live LLDB- launched suspend against the sleeper fixture + error paths. - test_dispatcher_nonstop.cpp's "-32001 kNotImplemented in phase-1" case is replaced with two cases: forwards-to-backend (happy path) and unknown-target → -32004. test_dispatcher_vcont_rsp.cpp's "non- RSP returns -32001" case is updated to assert the new "no process" -32004 against an empty target. - All 7 stub-backend test files grow a one-line suspend_thread override. Stretch (SetAsync(true) flip + pump_until_stopped wrappers around continue/step) intentionally deferred — docs/27 §7 already calls out that cascade as its own commit. Ships minimum scope clean: 70/70 ctest green, +5 unit cases, no other behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rgets Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ments (#21 reviewer pass) Reviewer-pass on PR #17 (v1.6 #21 LLDB suspend_thread landing). Three findings, all addressed: 1. CRITICAL: dispatcher mapped backend errors to -32001 vs -32004 by substring-matching `e.what()` for "not implemented". Any backend that threw a descriptive error containing those words for a legitimate runtime failure would have been silently promoted to kNotImplemented. Fix: typed `backend::NotImplementedError` subclass of `backend::Error`; dispatcher catches the subclass first (→ -32001), generic Error second (→ -32004). GdbMiBackend's suspend_thread throws the new type. Catch order matters and is pinned in a comment — the compiler does not warn on reversed order. 2. Stale comment blocks describing thread.suspend as "still returns -32001 kNotImplemented for LLDB-backed targets" — that's the pre-#21 contract. Replaced both: the describe.endpoints text and the function-level comment in handle_thread_suspend. The function comment now enumerates the three dispatch branches in order (RSP channel → vCont;t, LLDB-backed → backend.suspend_thread, NotImplemented-throwing backend → -32001). 3. MINOR: stale `[legacy]` Catch2 tag on a test that was updated to assert the new non-RSP forwarding path. Renamed to `[non-rsp]`. New tests (test_dispatcher_nonstop.cpp) lock in the typed-error contract: - `NotImplementedError → -32001 by exception type` — message deliberately does NOT contain "not implemented" to prove the dispatcher uses the exception type, not its message. - `generic Error with 'not implemented' substring stays -32004` — inverse case; this is the exact regression the pre-fix code was vulnerable to. Also updated the gdbmi `todo()` helper (currently `[[maybe_unused]]`) to throw NotImplementedError, so the contract is uniform for future stubs. 70/70 ctest green. +2 unit cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
22aa0f6 to
9e89061
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
-32001 kNotImplementedstub thatthread.suspendreturned for LLDB-backed targets. Lands a real per-thread suspend viaSBThread::Suspend()on the resolved thread.DebuggerBackend::suspend_thread(TargetId, ThreadId)virtual; LldbBackend implements it, GdbMiBackend throws "not implemented" (kept on -32001 in the dispatcher).vCont;troute from feat(backend): suspend_thread for LLDB-backed targets (#21 LLDB completion) #17 phase-2 — only the non-RSP path changes.Why this works without
SetAsync(true)SBThread::Suspend()is a flag flip. The suspend bit gates the nextSBProcess::Continue, not the suspend call itself. We don't need the full async-mode cascade to express a per-thread suspend intent; we only need it for delivery of stop events (the listener path). docs/26 §1 originally called the bit-flip a "phase-1 limitation gated on SetAsync(true)" — that turns out to have been conservative.The
SetAsync(true)flip +pump_until_stoppedwrappers around continue/step (the stretch scope) remain deferred. docs/27 §7 already documents that flip as its own commit — every existing endpoint that depends on "Continue() returns after the next stop" cascades when the mode flips, and ironing out the cascade is a multi-day audit. Shipping minimum scope clean here is the cheap correct slice.Wire change
thread.suspendon RSP-backed targetvCont;t:<tid>thread.suspendon LLDB-backed live process-32001 kNotImplementedthread.suspendon bad target / no process / bad tid-32001 kNotImplemented-32004 kBackendErrorwith typed messagethread.suspendon GdbMi-backed target-32001 kNotImplemented-32001 kNotImplemented(unchanged)Test plan
tests/unit/test_backend_suspend_thread.cpp(new, 4 cases) — live LLDB-launched suspend against the sleeper fixture + the three error paths (bad target_id, no process, unknown tid). All pass.test_dispatcher_nonstop.cpp— "-32001 in phase-1" case replaced with forwards-to-backend + unknown-target → -32004. Both new cases pass.test_dispatcher_vcont_rsp.cpp— "non-RSP target still returns -32001" case updated to assert the new "no process" -32004 against an empty target. RSP path test unchanged and still passing.suspend_threadoverride.ctest --test-dir build --output-on-failure: 70/70 passes (unchanged from baseline; +5 unit cases under theunit_testsumbrella).Punted
SetAsync(true)flip +pump_until_stoppedwrappers aroundcontinue_process/continue_thread/step_thread— stretch scope. docs/27 §7 already calls out the cascade as its own commit. Flagged in worklogNext:.continue_thread(target, tid)doing actual per-thread keep-running — needs the SetAsync flip first. Today it's still a sync passthrough.NotImplementedErrorsubclass ofbackend::Error. Dispatcher currently string-matches "not implemented" on the message to pick -32001 vs -32004. Acceptable for one call site; should be cleaned up when the GDB/MI backend grows its own suspend implementation.suspend_thread. MI's per-thread semantics requireset non-stop onat session init, which is off today. Its own item.🤖 Generated with Claude Code