Skip to content

fix: 2.11.0 test-flight findings — probe hits read at exit, probes wait for their class, clearer replies, hint-free flights - #52

Merged
novoj merged 6 commits into
mainfrom
fix/2.11.0-flight-findings
Oct 31, 2026
Merged

novoj merged 6 commits into
mainfrom
fix/2.11.0-flight-findings

Conversation

@novoj

@novoj novoj commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Fixes what the 2.11.0 test-flight run and the PR #46 review found.

Closes #48
Closes #49
Closes #50
Closes #51
Part of #47: § 1–3 are fixed; § 4 has three of its live cases (see "Not in this PR").

Server (#48, #50, #47)

  • Probes wait for their class (B1). jdwp_set_fast_logpoint / jdwp_set_fast_breakpoint on a class that is not loaded yet register a waiting probe instead of refusing. It arms on the class's ClassPrepareEvent, keeps its ID, and shows as WAITING_FOR_CLASS in the overview.
  • Hits are read at the target's exit (B2). While a probe is armed, a SUSPEND_ALL VMDeathRequest holds the target at VM death until each ring buffer is drained (bounded at 5 s). This also works under System.exit, which is how surefire's forked JVM ends. The [VM_DEATH] reply lists what the run recorded and each field watchpoint's hit count, with a note when a watch saw nothing.
  • Reset counts manual holds (B3). jdwp_reset reports the jdwp_suspend_thread holds it released. jdwp_reset and jdwp_disconnect say when the target has already exited.
  • Stops say what stopped.
  • jdwp_set_exception_breakpoint takes suspendPolicy (thread | all).
  • Evaluation.
    • Checked exceptions are allowed (throws Throwable).
    • Unqualified calls to the frame's methods work.
    • The thread defaults to the current stop.
    • Compile errors are listed once, with the offending code.
  • Frames and locks.
    • JDK frames inside the test harness are collapsed.
    • A monitor-blocked frame shows the synchronized line: HotSpot reports the line after it.
    • jdwp_dump_locks lists Object.wait() threads separately from the lock graph.
  • Watchdog (Follow-ups from the PR #46 code review: per-stop reasons, per-invocation watchdog timing, late ring writes, race tests #47 § 2). It times each invocation, not the whole lease. Two blocked samples must name the same monitor.
  • Late ring writes (Follow-ups from the PR #46 code review: per-stop reasons, per-invocation watchdog timing, late ring writes, race tests #47 § 3). A probe write counts its writers, and a clear waits for in-flight writes before the last drain.

Plugin install (#51)

hooks/ensure-server-jar.sh builds the server JAR at session start. It tries ./mvnw, then mvn -o, then mvn, and says when all three fail. The jdwp-inspector entry moved into .claude-plugin/plugin.json, and the root .mcp.json is gone. After a hook-triggered build, the server needs a reconnect in /mcp, and the hook says so.

Test flights (#49)

Tests

  • Unit: 1379 tests, 0 failures. The 43 skipped are the opt-in live tests.
  • The listener, session and watchdog tests pass on a single core (taskset -c 0).
  • Live suites against forked JVMs pass, including:
    • the new FastProbeLifecycleLiveTest: waiting probes, and hits read at exit after main returns and under System.exit; it passed 6 runs in a row;
    • three new SuspensionOwnershipLiveTest cases: reset during an evaluation, co-delivered arming, and disconnect during an evaluation that never returns.

Not in this PR

novoj added 6 commits October 30, 2026 19:03
…itself up (#51)

The hook built the server JAR with ./mvnw only. Where ~/.m2 is read-only
(a sandboxed Claude Code) the wrapper cannot install Maven, the build failed
silently, and the plugin's MCP server started without a JAR.

- hooks/ensure-server-jar.sh replaces the one-line hook: ./mvnw, then
  mvn -o, then mvn, with the old JAR moved aside so a failed or killed build
  never leaves a partial one, and a lock against parallel sessions.
- A failed build is visible: a systemMessage names the build log and the
  exact command to build by hand; Claude gets the same as context.
- The MCP server entry moves from the root .mcp.json into
  .claude-plugin/plugin.json: a checkout of this repository no longer gets
  a broken project-level jdwp-inspector next to the plugin's.
- README and docs describe the hook, the fallback and the reconnect.
- #5 The Field That Lies: field watchpoints never see a write through
  Field.set or Unsafe, which the hint, the debug path and the sandbox house
  rules claimed they do. The flight keeps the reflective write and now teaches
  the blind spot: the hint describes the tell (a watchpoint that fires for the
  constructor and then stays silent while the value flips), the debug path
  bisects before/after the suspect call; par 5.
- #8 The Magic Patch: the confirming resume counts, par 4.
- #10 The Oversold House: solved when one specific double sale is named
  (seat, first and second window), which a count after the fact cannot
  give. The debug path uses the probes waiting for BoxOffice to load and the
  hits read at the target's exit; par 4 stays. It no longer promises every
  double sale and says the probe sits inside the race window. The class
  Javadoc no longer points at check-then-act.
- The scoring rule is written into both READMEs and the house rules: every
  jdwp_* call from the first after attaching until the root cause is
  evident; attach and clean-up are free.
- Sandbox nits: package-info and test Javadocs carry the README's flight
  numbers and names and no longer state the bug; the spoiler comment in
  TransferService is gone.
…t, probes wait for their class, clearer stops and replies (#48, #50, #47)

#48
- B2: while fast probes exist, a SUSPEND_ALL VMDeathRequest holds the target
  at its exit until the listener has drained every probe ring and resumed
  it; FAST_*_READ_AT_EXIT says what was read. Hits nobody read before a test
  JVM exited were lost.
- B1: jdwp_set_fast_logpoint / jdwp_set_fast_breakpoint wait for a class that
  is not loaded yet (own ClassPrepareRequest, same ID, FAST_*_CLASS_LOADED or
  _ARM_FAILED) instead of refusing; forceLoad cannot load anything at VM
  start, and a failed force-load says so and waits too.
- B3: jdwp_reset counts the jdwp_suspend_thread holds it releases.

#50
- Exception stops name type and message (Throwable.detailMessage, read as a
  field); jdwp_set_exception_breakpoint takes suspendPolicy ('thread' default,
  'all').
- [VM_DEATH] lists what the run recorded and every field breakpoint with its
  hit count (a silent one points at the reflection/Unsafe blind spot);
  jdwp_reset and jdwp_disconnect after the exit say the target exited and
  what went with it; reset warns that released threads run on.
- Field events carry the access site and values; a field-breakpoint stop is
  field_breakpoint=N; a stop names the logpoint hits recorded since the last
  jdwp_get_events; get_events marks events of earlier attachments.
- Evaluation: generated methods declare throws Throwable (checked exceptions
  in blocks), unqualified calls of this's methods are rewritten, compile
  errors are listed once with the offending line instead of the temp path,
  long expressions are echoed shortened, jdwp_evaluate_expression defaults
  threadId to the current stop.
- JDK frames inside the test harness collapse with it and gaps are named;
  a thread blocked entering synchronized shows the monitorenter's line
  (HotSpot reports the next bytecode); jdwp_dump_locks lists Object.wait()
  threads apart from the lock graph; no class suggestion from JDK classes at
  VM start; 'Released before waiting:' instead of 'Resumed: VM start'.
- A deferred class restore names the stop's own thread, and jdwp_overview
  lists pending restores.

#47
- § 1: a fast breakpoint stop keeps its own reason.
- § 2: the watchdog ages each invocation from when its thread is seen
  running (suspended between invocations = host work) and needs two blocked
  samples on the same monitor.
- § 3: the probe recorder counts its writers and re-checks the kill switch;
  a clear waits for them before the final drain, so no entry is missed or
  written after it.
- § 4: live cases for jdwp_reset during an invocation, co-delivered arming
  hits on one line, and disconnect during an invocation; live tests for B1
  and B2.

The java-debug skill, README and docs follow.
…le .mcp.json mentions (#48, #49, #51)

- Live test: hits nobody read are read when the target ends with
  System.exit, as surefire's forked JVM does (flight #10's path), not only
  when main returns. Both exit tests wait until the probe has written before
  the target exits: a field watch set after arming could slow the workers
  enough that no hit came in the two seconds, which made the test flaky.
- README's feature list and docs/breakpoints.md no longer claim that field
  watchpoints see writes through reflection or Unsafe; the sandbox house
  rules no longer list the bugs of two flights; docs/testing.md drops a
  stale line number.
- jdwp_connect's description, a Javadoc and application.properties no
  longer point at the removed root .mcp.json.
- JdiEventListener: the new helpers sat between formatLogpointResult and its
  Javadoc.
The failing tests are the players' problem statement, yet their comments
named each root cause; the production Javadocs did too. Both now state
only what a reader of real code would see. The READMEs drop the Hint
lines, several of which were the root cause, and flight #10's symptom no
longer points at the check-then-sell gap.

Flight #5 takes three calls: a field watch that fires once in the
constructor and never again, while the value still flips, shows the
write bypasses field stores. The sandbox zip needs the plugin release
after 2.11.0, which reads probe and watch hits at the target's exit.
@novoj
novoj merged commit 70414b4 into main Oct 31, 2026
1 check passed
@novoj
novoj deleted the fix/2.11.0-flight-findings branch October 31, 2026 09:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment