refactor: an arrival is delivered to the open waiting for it (#136 stage 3) - #139
Conversation
…adcast `stopped_on` was an engine-wide set of every `(engine id, system pid)` the engine had ever stopped on: every wait wrote into it and every guard polled it. Because it outlived the opens that read it, it needed a lifecycle of its own -- pruned at both openers for pid reuse, cleared where a session is replaced, and cleared again where one is ended. Each of those three arrived as a review finding on #133 rather than as a design, which is what #136 calls the lifecycle cluster. An opener now registers what it is waiting for, the pump routes a stop to the first open that wants it and has nothing yet, and the entry dies with its guard. None of the three is needed: nothing outlives its reader, so nothing can go stale. `prune_processes_that_left` is down to the attachment record, which is about the teardown decision rather than about an open. **Two launches pending at once are told apart.** A launch is identified by elimination, so the first arrival was new to both snapshots and ended both waits; `Arrival` documented that as accepted because the fix needed "new engine-wide state, cleared everywhere a session is replaced and pruned for pid reuse" -- which was the cost of the record it would have joined, and is the cost this shape does not have. An arrival is claimed by the open it goes to, so the second launch is still waiting when the next one comes. **The state is per client rather than per wrapper** (`ClientState`, held by `Arc`, keyed by client pointer in a `Weak` map). Two `DebugEngine`s can be live around one `IDebugClient6`, and a `wait_for_event` through one completed an open held by the other in its own copy of the record alone -- the other then read `Listed`, waited again, and spent its whole bound on an event that had already happened, which is `deferred_arrival` arm F undone by a wrapper boundary. That was written down as a known gap for two releases. Stage 2's break scope had the mirror of it and moves into the same `Arc`, because #136 says it is the same field and doing the halves apart would be two reviews of one seam. A `Weak` map needs no `reissue_identity` equivalent: a dead entry identifies itself, where a stale identity costs a re-read and a stale arrival would answer `Arrived` for a target that never stopped. The pending-attach exclusion is registry-local now as well as reading `attached_processes`: an attach's process is new to a launch's snapshot and looks like the launch's own, and the register knows about pending attaches where that per-wrapper set does not. **Two tests are gone rather than passing**, with the construction that makes each unreachable named where it was: `test_ending_a_session_forgets_which_processes_it_stopped_on` has no record to forget, and `test_a_process_that_left_takes_its_stop_with_it` became `test_reclaiming_an_engine_id_does_not_reclaim_its_arrival`, which asserts the property end to end instead of the guard that used to hold it. The four "records no stop" tests now ask a registered open whether anything was delivered to it, which is the same claim put to the thing that does the work. Four new tests, each mutation-verified against the mutation it is for, and the two pure-bookkeeping ones run under Miri. No public API changes. Verified: 173 tests, 4 doctests, the full Miri suite (140), the three `#[ignore]`d interrupt tests, `deferred_arrival` unmoved (arm A 0 short in 40 under 24 spinners, arm F 3.2us, arm H 3.6us, arm G 0/40), `session_fuzz` clean over seeds 1/2/7/13, and windbg-mcp against a local path dependency (644 unit + 99 smoke). Refs #136, #133 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyRDk1yX5UqRkdpGgodrMY
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Team Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3b0e75ac77
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| fn forget(&mut self, id: ArrivalId) { | ||
| self.pending.retain(|pending| pending.id != id); |
There was a problem hiding this comment.
Keep claimed processes excluded after their guard drops
When two launch guards overlap, consuming or dropping the first guard removes the only record of the process it claimed. If that first target produces another genuine stop before the second launch reaches its initial break, deliver no longer recognizes the process as claimed, and because it is absent from the second launch's snapshot, the stop is delivered to the second guard. Its wait() can then return Ok even though its own process has not stopped; transfer completed claims into the remaining registrations before forgetting this entry.
Useful? React with 👍 / 👎.
| let entry = held.into_iter().find(|(held, _)| *held == id)?; | ||
| self.stopped_on | ||
| let attached = self.attached_pids(); | ||
| self.state |
There was a problem hiding this comment.
Preserve attach provenance across wrapper pumps
When wrapper A starts an attach, drops that attach guard, and then holds a pending launch, an outside pump through wrapper B reads B's empty per-wrapper attached_processes set here. Since dropping the attach guard also removed its registry entry, the attach stop can be delivered to A's launch as a newly launched process, causing the launch guard to report success before its own target stops. The attach exclusion used during delivery must survive the guard and be visible regardless of which wrapper performs the pump.
Useful? React with 👍 / 👎.
…ession's Two P2s from review, both real, and the second is a regression this stage introduced. **A claim died with the guard that made it**, so the ambiguity stage 3 claims to close was closed only while both launch guards were held. Consume the first, and its target's *next* genuine stop is claimed by nobody -- and is absent from the second launch's snapshot, because it did not exist when that snapshot was taken -- so it is delivered to the second launch, whose `wait()` returns `Ok` for a process that never stopped. A claim is now inherited by the opens still waiting when its own entry goes. That is not the lifecycle creeping back: it is held by the opens that exist at that moment and goes when they do, where the record this replaced lived for the whole session, and the test asserts that half too. **A session's attachments were still per wrapper**, so delivery through a second wrapper read an empty set: wrapper A attaches, drops that guard, holds a pending launch, and an outside pump through wrapper B delivers the attached process to A's launch as a newly launched one. `attached_processes` moves into `ClientState` with the rest. That fixes a second thing nobody raised, and it is the sharper one. An `end_session` through a wrapper that did not perform the attach saw no attachment to detach, so its passive end **killed** somebody else's process -- the exact failure that record exists to prevent, reached through the wrapper boundary. `test_a_sessions_attachments_are_visible_through_every_wrapper` asserts both halves, and the mutation that stands in for the old placement fails it on the process being taken. The sentence that used to keep the record per wrapper argued that sharing would put "the crate's most consequential decision behind an eviction policy". That is true of `client_identities`, which is a cache with a cap, and not of `ClientState`, whose entry is `Weak` and dies with the last wrapper holding it. Rewritten where it stood rather than deleted, because the objection is right about the identity cache. Two new tests, both mutation-verified. Re-verified after: 175 tests, 4 doctests, the full Miri suite (141), `deferred_arrival` unmoved (arm A 0 short in 40, arm F 3.3us, arm H 3us, arm G 0/40), `session_fuzz` seeds 1 and 7. Refs #136 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyRDk1yX5UqRkdpGgodrMY
|
Both right, and the second is a regression this stage introduced. Claims dying with their guard. Correct, and it means the ambiguity this stage claims to close was closed only while both launch guards were held — exactly the case my test covered. Consume the first, its target's next genuine stop is claimed by nobody, and it is absent from the second launch's snapshot because it did not exist when that snapshot was taken. A claim is now inherited by the opens still waiting when its own entry goes, which is your "transfer completed claims into the remaining registrations". Worth saying why that is not the lifecycle creeping back: the claim is held by the opens that exist at that moment and goes when they do, where the record it replaced lived for the whole session and needed a prune and two clears. Attach provenance across wrapper pumps. Correct, and it was a regression: the register went per client while That turned out to fix a sharper thing nobody raised. An The sentence that kept the record per wrapper argued sharing would put "the crate's most consequential decision behind an eviction policy". That is true of Re-verified after: 175 tests, 4 doctests, full Miri (141), |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4101365d86
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if held | ||
| .iter() | ||
| .any(|entry| pending.wants(*entry, &others, attached)) | ||
| { |
There was a problem hiding this comment.
Exclude arrivals already claimed by another open
When two launch guards overlap and the first process has been delivered while its guard remains alive, this predicate still treats that process as evidence that the second launch is Listed: wants excludes inherited claims and pending attaches, but not others[*].arrived. If the second wait is interrupted before its own deferred process joins, wait_for_live_target consequently takes the non-Absent terminal branch and returns Ok(()) instead of LiveTargetInterrupted. Exclude entries currently claimed by another pending open when computing presence, just as deliver does.
Useful? React with 👍 / 👎.
| self.state | ||
| .attached_processes | ||
| .lock() | ||
| .unwrap_or_else(|e| e.into_inner()) | ||
| .retain(|pid| held.iter().any(|(_, held)| held == pid)); |
There was a problem hiding this comment.
Preserve deferred attachments while pruning
When attach_process_begin(pid) is dropped before its first wait and launch_process_begin is called next, the launch invokes this prune while session_processes() still omits the deferred attachment, so retain removes the PID that claim_attached just recorded. The fresh evidence that the existing review thread remains applicable is that moving attached_processes into shared state does not bypass this prune: after the attach registration is dropped, the next pump can still deliver that process to the launch, and teardown can then treat the externally owned process as launched and kill it. Pending attachment provenance must survive until the attachment joins the session or is canceled.
Useful? React with 👍 / 👎.
…eferred attach Two more P2s from review. The first is this stage's own claim surviving in the predicate's other caller; the second is pre-existing and is the more expensive of the two. **`presence` did not exclude another open's claim.** `wants` excluded inherited claims and pending attaches, and `deliver` excluded a claimed process with an early return of its own -- so the *listing* half never applied it. A second launch was therefore told `Listed` on the strength of a process the first had been given, and `Listed` is not `Absent`, so a wait interrupted before its own process joined answered `Ok(())` instead of `LiveTargetInterrupted`. The rule moves into `wants`, where both callers get it, and `deliver`'s early return goes because it now says the same thing twice. **A prune took an attachment that had not joined yet.** `AttachProcess` joins its process at the next `WaitForEvent` -- measured, and printed by `deferred_arrival` arm E -- so between `attach_process_begin` and that wait the pid is recorded and the session does not list it. An opener pruning in that window dropped the record, on a rule whose whole subject is a pid that no longer names anything. What that costs is not a missed exclusion: the teardown then treats somebody else's process as one this engine launched, and takes it. An attachment now carries whether it has been seen (`Attachment::Deferred` / `Joined`). A pid the session holds is promoted; a pid it does not hold has either left, and is dropped, or not yet joined, and is kept -- it cannot have left a session it was never in. **Promotion has to happen at the pump as well as at the prune**, which the existing `test_a_launch_after_a_lost_attach_is_still_a_launch` caught when it did not: an attach completes at its wait, and nothing lists the session again until the next opener prunes, by which time a `.detach` may have taken the process -- leaving a pid that joined and left still marked `Deferred` and kept for ever. Every stop lists the session, so `record_where_it_stopped` promotes there too. Two new tests, and three mutations checked: the prune's old rule, promotion removed, and the round-3 finding. The second is caught by the pre-existing test above rather than by anything new, which is the whole reason it was worth running. Re-verified: 176 tests, 4 doctests, the full Miri suite (141), the three `#[ignore]`d interrupt tests, `deferred_arrival` unmoved (arm A 0 short in 40, arm F 3.7us, arm H 3.9us, arm G 0/40), `session_fuzz` seeds 1 and 7, and windbg-mcp against a local path dependency (644 unit + 99 smoke). Refs #136 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyRDk1yX5UqRkdpGgodrMY
|
Both right again.
The prune taking a deferred attachment. Correct, pre-existing, and the more expensive of the two. An attachment now carries whether it has been seen ( One thing that fell out and is worth flagging, because it is the reason the mutation pass was worth running: promotion has to happen at the pump as well as at the prune. An attach completes at its wait, and nothing lists the session again until the next opener prunes — by which time a Two new tests; three mutations checked (the prune's old rule, promotion removed, and the Re-verified: 176 tests, 4 doctests, full Miri (141), the three |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 984049a957
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| .retain(|pid, state| { | ||
| *state == Attachment::Deferred || held.iter().any(|(_, held)| held == pid) |
There was a problem hiding this comment.
Retire deferred attachment records after failed joins
When AttachProcess is accepted but the target exits before the first WaitForEvent (so the deferred attach never joins and the wait fails), this predicate retains its PID forever because only a successful session listing can promote it to Joined. If Windows later reuses that PID for a process launched through this same engine, Pending::wants excludes the launch's stop as attached and end_session detaches the launched process instead of terminating it. Track when the deferred request has been processed unsuccessfully and remove its attachment record then, while continuing to preserve requests that genuinely have not been pumped yet.
Useful? React with 👍 / 👎.
…r ever Third round on this PR, and it is about the state the second round added. `Attachment::Deferred` is kept precisely because a deferred attach has not arrived and so cannot have left -- but only a successful listing promotes one, so an `AttachProcess` the engine accepted for a process that then exited before the first `WaitForEvent` left a pid recorded for the life of the session. If Windows handed that number to a process this engine went on to launch, the launch's own stop was excluded from delivery as somebody else's and its teardown *detached* from it instead of taking it: a launched process outliving its session. A widening of an existing residue rather than a new one -- before an attachment could be deferred, the next opener's prune dropped any pid the session did not hold -- and the bound is restored by the only party that can say the attach cannot join: a live open that pumped its whole `LIVE_WAIT_MS` and never saw its process retires the record. Deliberately not on the *interrupted* branch beside it, which says nothing about whether the attach is still coming, and only for `Deferred`, because an attachment that joined and has merely left is the prune's business. The residue left is an attach that never joins whose guard is abandoned without waiting: nothing then waits the bound, so nothing reaches the retirement. That is the documented hand-off, and the pid is held against a reuse this crate already lives with -- `prune_processes_that_left` names a retained handle as the real answer and declines it. Two tests. The rule is asserted directly, because reaching it through a real open costs thirty seconds; the end-to-end half is `#[ignore]`d and was run by hand (`30.55s`, passing), which is what says the timeout branch is actually wired to it. Both mutations caught -- the branch not retiring, and retirement taking a `Joined` record. Its first draft failed for a reason worth keeping: with no debuggee, the wait fails `E_UNEXPECTED` immediately rather than pumping, so the open never reached its bound. It launches a target first now. Re-verified: 177 tests, 4 doctests, the full Miri suite (141), `deferred_arrival` unmoved (arm A 0 short in 40, arm F 3.3us, arm H 5.2us, arm G 0/40), `session_fuzz` seeds 1 and 7. Refs #136 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyRDk1yX5UqRkdpGgodrMY
|
Right, and it is about the state the last round added.
The bound is restored by the only party that can say the attach cannot join: a live open that pumped its whole What is left is an attach that never joins whose guard is abandoned without waiting: nothing then waits the bound, so nothing reaches the retirement. That is the documented hand-off ( Two tests: the rule directly, because reaching it through a real open costs thirty seconds, and an Its first draft failed for a reason worth keeping: with no debuggee the wait fails Re-verified: 177 tests, 4 doctests, full Miri (141), |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9fcf9a306f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| #[derive(Debug, Default)] | ||
| struct ClientState { | ||
| /// The opens waiting for a target to join this session and stop. | ||
| arrivals: Mutex<Arrivals>, |
There was a problem hiding this comment.
Clear shared arrivals when the owner drops
When an owning wrapper is dropped while another wrapper around the same client still holds a pending guard, DebugEngine::drop ends the session but never calls forget_all, so this newly shared register survives through the other wrapper. If that wrapper reuses the client for a new launch, the canceled old registration remains first and can claim the new process's stop, leaving the new guard to time out even though its target stopped. Clear the shared arrivals after EndSession succeeds in the owning drop path, as end_session already does.
Useful? React with 👍 / 👎.
| if self.inherited.contains(&entry) | ||
| || others.iter().any(|other| other.arrived == Some(entry)) | ||
| { |
There was a problem hiding this comment.
Retire claims when their process leaves
When an outside pump completes a launch while its guard remains held, then that process is removed with raw .detach and the same PID is attached again, DbgEng can reuse the same engine ID, producing the identical pair here. The old guard's arrived value then makes the new attach reject its own stop as another open's claim, so its wait() eventually times out despite the reattached target having stopped. The previous stopped_on pruning explicitly handled this ordinary detach/reattach reuse; retained claims likewise need to stop excluding a later process instance once the claimed process leaves.
Useful? React with 👍 / 👎.
…process Fourth round, and both halves are the register being *shared* where it used to be a field on each wrapper -- the change this PR makes, meeting two places that had never had to care. **`Drop` never forgot the pending opens.** It tears the session down inline rather than calling `end_session`, so it did not inherit that line. With a per-wrapper register that cost nothing; with a shared one, a guard held by a second wrapper outlives the owner's drop, and an entry registered against a session that has ended is first in line for the next launch's stop through that wrapper -- leaving the new guard to time out on a target that stopped. Both teardowns now call one `forget_pending_opens`, gated on `EndSession`'s own outcome as `end_session` already was. **A claim outlived the process it named.** Engine ids are handed back immediately, so a `.detach` and a reattach of the same pid reproduce a `(engine id, pid)` pair exactly -- the reuse the record this replaced was pruned for, arriving from the opposite side. A stale entry there made a new open read `Arrived` for a target that had not stopped; a stale claim makes it read the opposite, refusing the reattach its own stop until its bound runs out. `Arrivals::forget_departed`, called from the same opener prune that handles the attachment record, clears both the claim and any inherited exclusion naming a process the session no longer holds. Clearing `arrived` and not merely the exclusion is what the old prune did, and is the honest answer: an open whose target has left has not arrived anywhere its caller can use. Three tests. Two pin the rules; the third is the **wiring**, and it earns its place -- with only the rule tests, removing the `forget_departed` call from the prune leaves everything green, which I measured before writing it. It asserts the pair actually comes back, or a run where DbgEng handed out a different engine id would pass without meeting the collision. All four mutations caught. Re-verified: 180 tests, 4 doctests, the full Miri suite (142), the `#[ignore]`d 30s retirement test (31.18s, passing), `deferred_arrival` unmoved (arm A 0 short in 40, arm F 3.5us, arm H 5.8us, arm G 0/40), `session_fuzz` seeds 1 and 7. Refs #136 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyRDk1yX5UqRkdpGgodrMY
|
Both right, and both are this PR's own doing: the register is shared where it used to be a field on each wrapper, and these are two places that never had to care.
A claim outlived the process it named. Three tests, and the third earns its place. Two pin the rules. With only those, removing the All four mutations caught. Re-verified: 180 tests, 4 doctests, full Miri (142), the |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c9486dc8da
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if waited { | ||
| self.retire_deferred_attachment(registered); | ||
| } |
There was a problem hiding this comment.
Retire deferred attachments when their wait fails
When AttachProcess is accepted but the target exits before its first WaitForEvent, pump returns an error through the ? at the bottom of this loop, so this new cleanup is never reached and the PID remains Deferred indefinitely. The fresh evidence that the existing review issue remains is that cleanup was added only to the timeout branch; a later launch reusing that PID is therefore still excluded from arrival delivery and detached rather than terminated during teardown. Retire the deferred record when a failed pump confirms that the attachment did not join, while preserving genuinely pending or interrupted attaches.
Useful? React with 👍 / 👎.
… until delivered Two things: a fifth review round, and a real failure of my own test on the coverage runner. **The retirement covered the bound and not the ending the scenario takes.** When the target exits before its first `WaitForEvent` the session holds nothing, so the pump *fails* rather than expiring (`test_a_wait_with_no_debuggee_fails_rather_than_expiring`) and the open returns through its `?` without ever reaching the bound -- leaving the pid `Deferred` for the life of the session, which is what the last round was supposed to close. The error ending retires too. **On a narrower condition, and the difference is evidence rather than taste.** At the bound an open has pumped for `LIVE_WAIT_MS`, so a pid still not listed is one that is not coming. On a failed pump it may have pumped nothing at all, and the same reading would retire an attach the engine had not yet had a chance to process -- after which the teardown takes somebody else's process, which is the direction that costs. So the error ending asks `holds_nothing_at_all`, which is the one reading that *proves* the attach produced no target. That predicate is named and pinned both ways, because the branch reading it can only be built one way: a pump that fails with a *live* session is not something this bench can arrange, and the doc comment says so rather than implying the half is covered. **And the wiring test I added last round failed on the coverage runner**, for the reason this whole seam exists: one `WaitForEvent` is one *event*. That session holds an attached process whose own events compete, so a single pump delivered the launch on this bench and did not on the instrumented runner -- dbgscope#128 in miniature, met while testing the fix for it. It pumps until delivered now, bounded, and says why. 182 tests, 4 doctests, the full Miri suite (142), `deferred_arrival` unmoved (arm A 0 short in 40), `session_fuzz` seeds 1 and 7. Refs #136, #128 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyRDk1yX5UqRkdpGgodrMY
|
Right — the retirement covered the bound and not the ending the scenario actually takes. When the target exits before its first On a narrower condition, and the difference is evidence rather than taste, which is your "while preserving genuinely pending or interrupted attaches": at the bound an open has pumped for That predicate is named and pinned both ways, because the branch reading it can only be built one way: a pump that fails with a live session is not something this bench can arrange, and the doc comment says so rather than implying the half is covered. If you can see a construction for it, I'll take it. Separately, and worth reporting because it is not from review: the wiring test I added last round failed on the coverage runner, for the reason this whole seam exists. One 182 tests, 4 doctests, full Miri (142), |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 74fa416b7e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if let Some(entry) = claimed { | ||
| for pending in &mut self.pending { | ||
| pending.inherited.push(entry); | ||
| } |
There was a problem hiding this comment.
Forward inherited claims when forgetting an open
With three overlapping launches, if the first receives process A and is dropped, the second and third inherit A; if the second then receives process B and is dropped, this loop forwards only B, while removing the sole copy of its inherited A claim. A subsequent stop from process A can therefore be delivered to the third launch, making its wait() succeed before its own process stops. Fresh evidence beyond the existing two-launch review thread is this non-transitive handoff: propagate both the completed entry's direct claim and its inherited claims.
Useful? React with 👍 / 👎.
| if pending.arrived.is_some_and(|entry| !still_here(&entry)) { | ||
| pending.arrived = None; |
There was a problem hiding this comment.
Retire completed registrations when their process departs
When an outside pump completes an attach while its guard remains held, and that process is detached before the same PID is attached again, this clears the old guard's arrived value but leaves its older Arrival::Attached(pid) registration eligible for delivery. The reattached process's stop is consequently assigned to the old guard first, and the new attach guard times out despite its target having stopped. The fresh regression after retiring the stale claim is that the completed registration itself is reactivated; tombstone or remove it instead of resetting it to a pending open.
Useful? React with 👍 / 👎.
…eaving `Arrivals::forget_departed` stops an open reporting a process that has left the session as arrived. Written as clearing the field it also made the entry *pending again* -- and nothing distinguishes a registration that has been given nothing from one whose process has gone. An `Arrival::Attached` open wants any stop bearing its pid, and the finished one is registered before the reattach's, so `deliver` -- which offers in registration order -- handed it the reattached process and the new guard waited out its whole bound on a target sitting in front of it. So what an open has been given is a `Claim` rather than an `Option`: `Waiting`, `Arrived(entry)`, or `Departed`. `Departed` is terminal -- `deliver` skips it, `presence` answers `Absent`, and `forget` hands it to nobody, since `forget_departed` has already taken that process out of every inheritance and putting it back is the stale claim it exists to remove. Terminal is right rather than convenient: a `.detach` and a reattach are a *new* debug attachment, so the reattach's initial break does not satisfy the postcondition the first guard's `wait()` promised. There is nothing left for that open to be waiting for. `test_a_departed_claim_does_not_reopen_the_open_that_held_it` is in two phases because the two rules fail differently. Alone, with the pair coming back through the raw hatch, is what pins `presence`: with a second open in the register the pair is spoken for and the answer is `Absent` either way, so `Listed` -- keep waiting, your target is here -- can only be asked where there is nobody else. Two attaches is what pins `deliver`, and is why this went unnoticed: `test_a_claim_stops_excluding_once_its_process_leaves` stages the same sequence with a *launch* held open, which an unrelated rule (a pending attach keeps its process from a pending launch) protects either way. Also pins the claim handoff, which review read as a chain and which is a broadcast: `forget` pushes onto every open still pending, so a third launch already holds its own copy and there is no sole copy to lose. `test_a_claim_is_inherited_by_every_open_still_waiting` puts it to three, because with two "every remaining open" and "the next one" are the same set and a chained handoff passes everything else here. Its last two steps assert the exclusion is targeted -- a genuinely new process still reaches the open -- or a register that refused everything would pass. Mutation-verified, one at a time: the tombstone back to a clear, `presence` falling through on a departed claim, and `forget` handing along a chain. Each fails exactly one test and nothing else. And rewraps 24 assertion messages whose `\` line-continuation had been eaten when they were written, leaving runs of interior spaces in the value. Text only, and text a human reads at the moment they can least guess what was meant. Four predate this branch. Raised in review on #139. Refs #136, #133 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyRDk1yX5UqRkdpGgodrMY
|
Round six. One of the two is real and is fixed; the other names a failure the code does not have, but it named a property nothing was pinning, so it is pinned now. Retire completed registrations when their process departs — correct, fixedRight about the fact and right about the remedy, which is the less common pairing. The state is a value now rather than an enum Claim {
Waiting,
Arrived((u32, u32)),
Departed,
}
Terminal is the right answer rather than a convenient one: a
Mutation-verified both ways — Forward inherited claims when forgetting an open — not reachable, and now pinnedThe scenario needs the handoff to be a chain. It is a broadcast: And an open registered after that point needs no inheritance at all: A is in the session by then, What the finding was right about is that nothing said so. With two opens "every remaining open" and Also in this commit24 assertion messages had runs of interior spaces where a Verification at this commit
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9073385c62
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| match pending.claim { | ||
| Claim::Arrived(_) => return Presence::Arrived, |
There was a problem hiding this comment.
Validate arrived claims against the live process list
When an outside pump delivers a target while its guard remains held, and that process then exits or is detached while another process keeps the session alive, this branch returns Arrived without checking whether the claimed pair is still in held. Calling the guard's wait() afterward therefore returns Ok(()) for a target that is no longer present; the previous implementation first located the target in the current session. Mark a missing claim as departed or otherwise return Absent during this presence check.
Useful? React with 👍 / 👎.
|
(retracted - posted in error, please ignore) |
…its own `presence` answered `Arrived` from the claim alone, so an open delivered a process that has since left reported it as arrived until something pruned. `Arrivals::forget_departed` is called from the openers, so a session that opens nothing more never calls it: the claim outlives the process by an unbounded amount, and the guard's `wait()` returns `Ok(())` for a target the caller cannot read. This branch's own regression rather than a pre-existing gap, and the old shape could not have it: `presence_of` located the process in `held` first and asked `stopped_on` second, so `Arrived` could not outlive the target. Replacing the set with a register dropped the ordering without noticing it had a second job. `Claim::Departed` is not made redundant by this and stays. The two rules answer different questions -- this one is what `presence` *reports*, the tombstone is what `deliver` *offers* -- and an open cleared back to waiting takes the next arrival bearing its pid however carefully `presence` reads. Backing either out alone was tried: each fails its own test and only its own. The answer is `Absent` rather than a fourth `Presence`, which costs the open the rest of its bound because the loop reads `Absent` as "keep pumping". That is what every release has done. "Arrived and then left" is a genuinely different state, but nothing above here has a use for the distinction, and inventing one to carry it would be wider than a regression fix. `test_an_arrived_claim_is_read_against_what_the_session_holds` keeps a second process in `held` deliberately: `presence_of` refuses an engine holding nothing at all before the register is asked, so a session kept alive by somebody else is the only way the question reaches here. Mutation-verified -- believing the claim again fails that test and nothing else in the suite, which is why this got through five rounds. Raised in review on #139. Refs #136, #133 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyRDk1yX5UqRkdpGgodrMY
|
Round seven. Correct, and it is this branch's regression rather than a pre-existing gap — fixed. Validate arrived claims against the live process listThe fact checks out against the old code, which is the part worth stating rather than taking on let ours = match arrival { Arrival::Attached(pid) => held.iter().find(|(_, held)| held == pid), … };
let Some(entry) = ours else { return Presence::Absent };
if self.stopped_on.lock()….contains(entry) { Presence::Arrived } else { Presence::Listed }So What makes it more than theoretical is when the prune runs: The claim is read against Claim::Arrived(entry) if held.contains(&entry) => return Presence::Arrived,
// Delivered, and gone since — whether or not a prune has caught up with it.
Claim::Arrived(_) | Claim::Departed => return Presence::Absent,Two notes on the shape, since the alternative was tempting.
The answer is
A correction to my last commentI wrote Verification at this commit
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c80d60f1c2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| .iter() | ||
| .find(|pending| pending.id == id) | ||
| .and_then(|pending| pending.claim.held()); | ||
| self.pending.retain(|pending| pending.id != id); |
There was a problem hiding this comment.
Preserve abandoned launch registrations until delivery
When a launch_process_begin guard is dropped before any pump, CreateProcessWide remains deferred as documented, but this removes its still-Waiting entry without leaving any exclusion. If another launch begins before the next WaitForEvent, its snapshot cannot contain the first process; the next pump can therefore materialize the first queued launch and deliver its stop to the second guard, making that guard return Ok before its own process reaches its initial break. Keep an abandoned launch registered until its deferred arrival is consumed or the session ends.
Useful? React with 👍 / 👎.
| && !others | ||
| .iter() | ||
| .any(|other| matches!(other.what, Arrival::Attached(pid) if pid == entry.1)) |
There was a problem hiding this comment.
Ignore departed attach registrations when filtering launches
When an attach guard remains alive after its delivered process exits or is detached, pruning changes its claim to Departed but leaves its Arrival::Attached(pid) registration here. If Windows then reuses that PID for a process launched by a later open, this predicate rejects the launch's stop solely because of the departed attach; no open claims the stop and the launch guard eventually times out despite its target being stopped. Restrict this exclusion to attach registrations that are still waiting or whose process is still present.
Useful? React with 👍 / 👎.
…m it The exclusion that keeps a pending attach's process from a pending launch read the *registration* -- which pid the open named -- and not whether it could still have it. So an attach whose process had left, sitting at `Claim::Departed` with its guard still held, went on excluding that pid; and Windows reuses pids, so a later launch that inherited the number was refused its own stop, nobody else claimed it, and that guard timed out with its target stopped in front of it. Restricted to attaches still `Waiting`, which is exactly the span over which the reason holds. One already delivered is covered by the `claim.held()` check above -- exact where this is by pid -- and one that has departed will never claim anything again. The clause is new in this branch, so the defect is too. Also narrows what `Arrival`'s doc claims. It said stage 3 "removes" the two-pending-launches ambiguity; it removes the two cases that have a signal to close them with -- both guards live, and delivered-then-dropped, where `forget` hands the claim to the opens still waiting. A launch abandoned *before* it is delivered anything has neither, and its deferred `CreateProcessWide` still produces a process that the next launch's elimination cannot tell from its own. That one is #141 rather than this commit. Keeping the entry is the obvious fix and is a trade: nothing retires an entry whose guard is gone, so an abandoned launch that never starts -- `deferred_arrival` arm C measures a missing image failing inside the wait -- leaves a registration that takes the next launch's process and times that guard out instead. A wrong `Ok` for a wrong timeout, in the shape stage 3 exists to remove. It is also pre-existing: `Arrival`'s doc on `main` records the same ambiguity as knowingly accepted. Raised in review on #139. Refs #136, #133, #141 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyRDk1yX5UqRkdpGgodrMY
|
Round eight. One is this branch's own bug and is fixed; the other is real, pre-existing, and its Ignore departed attach registrations when filtering launches — correct, fixedThe exclusion reads
The clause is new in this branch, so the defect is. Mutation-verified: reinstating the broad Preserve abandoned launch registrations until delivery — real, and not this PRThe mechanism is right. Dropping a launch guard before any pump takes a Two things about it, both checked rather than assumed. It is pre-existing.
That record is what this PR built, and it closed the two cases that have a signal to close them Keeping the entry is a trade, not a fix. A guard is what ends an entry's life and this one has So #141 has the analysis and three options that have not been weighed against each other, and none What does change here is the doc. Verification at this commit
On the red tick
|
…eaving `Arrivals::forget_departed` stops an open reporting a process that has left the session as arrived. Written as clearing the field it also made the entry *pending again* -- and nothing distinguishes a registration that has been given nothing from one whose process has gone. An `Arrival::Attached` open wants any stop bearing its pid, and the finished one is registered before the reattach's, so `deliver` -- which offers in registration order -- handed it the reattached process and the new guard waited out its whole bound on a target sitting in front of it. So what an open has been given is a `Claim` rather than an `Option`: `Waiting`, `Arrived(entry)`, or `Departed`. `Departed` is terminal -- `deliver` skips it, `presence` answers `Absent`, and `forget` hands it to nobody, since `forget_departed` has already taken that process out of every inheritance and putting it back is the stale claim it exists to remove. Terminal is right rather than convenient: a `.detach` and a reattach are a *new* debug attachment, so the reattach's initial break does not satisfy the postcondition the first guard's `wait()` promised. There is nothing left for that open to be waiting for. `test_a_departed_claim_does_not_reopen_the_open_that_held_it` is in two phases because the two rules fail differently. Alone, with the pair coming back through the raw hatch, is what pins `presence`: with a second open in the register the pair is spoken for and the answer is `Absent` either way, so `Listed` -- keep waiting, your target is here -- can only be asked where there is nobody else. Two attaches is what pins `deliver`, and is why this went unnoticed: `test_a_claim_stops_excluding_once_its_process_leaves` stages the same sequence with a *launch* held open, which an unrelated rule (a pending attach keeps its process from a pending launch) protects either way. Also pins the claim handoff, which review read as a chain and which is a broadcast: `forget` pushes onto every open still pending, so a third launch already holds its own copy and there is no sole copy to lose. `test_a_claim_is_inherited_by_every_open_still_waiting` puts it to three, because with two "every remaining open" and "the next one" are the same set and a chained handoff passes everything else here. Its last two steps assert the exclusion is targeted -- a genuinely new process still reaches the open -- or a register that refused everything would pass. Mutation-verified, one at a time: the tombstone back to a clear, `presence` falling through on a departed claim, and `forget` handing along a chain. Each fails exactly one test and nothing else. And rewraps 24 assertion messages whose `\` line-continuation had been eaten when they were written, leaving runs of interior spaces in the value. Text only, and text a human reads at the moment they can least guess what was meant. Four predate this branch. Raised in review on #139. Refs #136, #133 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyRDk1yX5UqRkdpGgodrMY
…its own `presence` answered `Arrived` from the claim alone, so an open delivered a process that has since left reported it as arrived until something pruned. `Arrivals::forget_departed` is called from the openers, so a session that opens nothing more never calls it: the claim outlives the process by an unbounded amount, and the guard's `wait()` returns `Ok(())` for a target the caller cannot read. This branch's own regression rather than a pre-existing gap, and the old shape could not have it: `presence_of` located the process in `held` first and asked `stopped_on` second, so `Arrived` could not outlive the target. Replacing the set with a register dropped the ordering without noticing it had a second job. `Claim::Departed` is not made redundant by this and stays. The two rules answer different questions -- this one is what `presence` *reports*, the tombstone is what `deliver` *offers* -- and an open cleared back to waiting takes the next arrival bearing its pid however carefully `presence` reads. Backing either out alone was tried: each fails its own test and only its own. The answer is `Absent` rather than a fourth `Presence`, which costs the open the rest of its bound because the loop reads `Absent` as "keep pumping". That is what every release has done. "Arrived and then left" is a genuinely different state, but nothing above here has a use for the distinction, and inventing one to carry it would be wider than a regression fix. `test_an_arrived_claim_is_read_against_what_the_session_holds` keeps a second process in `held` deliberately: `presence_of` refuses an engine holding nothing at all before the register is asked, so a session kept alive by somebody else is the only way the question reaches here. Mutation-verified -- believing the claim again fails that test and nothing else in the suite, which is why this got through five rounds. Raised in review on #139. Refs #136, #133 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyRDk1yX5UqRkdpGgodrMY
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 304fe116d7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if self.inherited.contains(&entry) | ||
| || others.iter().any(|other| other.claim.held() == Some(entry)) | ||
| { |
There was a problem hiding this comment.
Allow later attach stops for an already-claimed process
When a completed attach guard remains alive and DbgEng accepts another attach_process_begin for the same PID, both stops identify the same (engine id, PID). The first guard's claim makes this blanket check reject that pair for the second guard even when a subsequent stop was generated by the second attach, so the second wait() eventually reports LiveTargetTimeout despite its process having stopped. Apply the existing-claim exclusion to launches, while allowing a separate stop on the named PID to satisfy the next waiting attach.
Useful? React with 👍 / 👎.
dbgscope#141, partly. Dropping a launch guard before anything pumps does not un-queue its `CreateProcessWide`, and `forget` removed the entry with nothing left behind -- so that process still arrived, was new to the *next* launch's snapshot because it did not exist when that snapshot was taken, and was claimed by it. Measured before the fix: a launch of `ping.exe` behind an abandoned `cmd.exe` was delivered the `cmd.exe`. The entry now stays, marked `abandoned`. There is nothing to leave as an exclusion instead -- which process the queued create will produce is exactly what nobody knows yet -- and being first in registration order is what makes it work, since `deliver` offers in that order. Only a launch, and only one given nothing. An abandoned *attach* is already covered by the engine's `attached_processes`, which is what `Pending::wants` reads it for, and keeping its entry would leave `presence` answering `Listed` for an id nobody holds. An entry with a claim is removed and its claim inherited, as before. `Launched(None)` is removed too: it can never claim anything, so keeping it would leave an entry that does nothing. **What this does not fix, and cannot.** The next launch still does not reliably get its *own* process. `deliver` offers in registration order and the abandoned entry is first, so it takes whichever process arrives first -- and one `WaitForEvent` realises *both* queued creates (measured: session 0 -> 2 on a single pump), so which one the event names is a coin flip. Identifying a launch by arrival order is the residual ambiguity `Arrival` has always documented for two launches pending at once; #139 closed "both guards get the same arrival", not "each guard gets its own". What this does deliver is that the abandoned create is *accounted for*: one entry absorbs one arrival, so the next launch's wait() returns only once a second process has stopped, where before it could return with its own process not yet created at all. #141 stays open, narrowed to the identification. Two of this change's own tests passed for the wrong reason before landing, both worth knowing. Counting processes when wait() returns says nothing, because one pump realises both creates. Asking whether the second program is *listed* says nothing either -- membership is the weaker claim this module is built on not confusing with having stopped -- and it reported "0 short in 10" while the defect was live. The test reads the register instead, and asserts the property that holds: 8/8 with the fix, 4/4 failing without it. `examples/abandoned_launch.rs` is the measurement, kept as the record of what the public surface can and cannot see. Refs #141, #136, #133 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyRDk1yX5UqRkdpGgodrMY
Stage 3 of #136: deliver instead of broadcast. Only stage 4 is left after this.
What was wrong
stopped_onwas an engine-wide set of every(engine id, system pid)the engine had ever stoppedon. Every wait wrote into it and every guard polled it — and because it outlived the opens that read
it, it needed a lifecycle of its own:
.detachplus a reattachof the same pid brings a whole pair back;
Each of those three arrived as a review finding on #133 rather than as a design. That is the
lifecycle cluster #136 names.
What this does
An opener registers what it is waiting for (
Registered), the pump routes a stop to thefirst open that wants it and has nothing yet (
Arrivals), and the entry dies with its guard.None of the three lifecycle rules is needed any more: nothing outlives its reader, so nothing can go
stale.
prune_processes_that_leftis down to the attachment record, which is about the teardowndecision rather than about an open.
Two launches pending at once are told apart
Arrivaldocumented this as an accepted ambiguity — a launch is identified by elimination, so thefirst arrival was new to both snapshots and ended both waits. The fix was weighed and rejected at
the time because it needed "new engine-wide state, cleared everywhere a session is replaced and
pruned for pid reuse". That was the cost of the record it would have joined, and it is the cost
this shape does not have. An arrival is now claimed by the open it is delivered to, so the
second launch is still waiting when the next one comes.
The state is per client, not per wrapper
A new
ClientState, held byArcand keyed by client pointer in aWeakmap. TwoDebugEnginescan be live around one
IDebugClient6, and await_for_eventthrough one used to complete an openheld by the other in its own copy of the record alone — the other then read
Listed, waitedagain, and spent its whole bound on an event that had already happened. That is
examples/deferred_arrival.rsarm F (29.36s against 8.6 µs) undone by a wrapper boundary ratherthan by a missing record, and it was written down as a known gap for two releases.
Stage 2's break scope had the mirror of the same gap and moves into the same
Arc, because #136says it is the same field and doing the halves apart would be two reviews of one seam.
A
Weakmap needs no equivalent ofreissue_identity: a dead entry identifies itself, and theasymmetry is the point — a stale identity costs a re-read, where a stale arrival would answer
Arrivedfor a target that never stopped.One smaller thing
The pending-attach exclusion is registry-local now, as well as still reading
attached_processes.An attach's process is new to a launch's snapshot and so looks like the launch's own; the register
knows about pending attaches where that per-wrapper set does not. Both are kept — the set also
covers an attach whose guard was dropped, whose process joins with no registration left to name it.
A claim that has gone is not a claim that was never made
Arrivals::forget_departedstops an open reporting a process that has left the session as arrived.Written as clearing the field it also made the entry pending again — and an
Arrival::Attachedopen wants any stop bearing its pid, so the finished open, registered first, took the next attach of
that pid and the new guard waited out its bound on a target in front of it. So what an open has been
given is a
Claim—Waiting,Arrived(entry)orDeparted— andDepartedis terminal:deliverskips it,presenceanswersAbsent, andforgethands it to nobody.Terminal rather than convenient: a
.detachand a reattach are a new debug attachment, so thereattach's initial break does not satisfy the postcondition the first guard's
wait()promised.And a claim is read against what the session holds rather than believed on its own. That half
the old shape had for free —
presence_oflocated the process inheldfirst and askedstopped_onsecond, soArrivedcould not outlive the target — and replacing the set with aregister dropped the ordering without noticing it had a second job. Since
forget_departedrunsfrom the openers, a session that opens nothing more never prunes, so the claim outlived its process
by an unbounded amount and a guard's
wait()answeredOk(())for a target the caller could notread. The two rules are independent: one is what
presencereports, the other whatdeliveroffers, and backing either out alone fails its own test and only its own.
Tests
Eight new, each mutation-verified against the mutation it is for:
..._delivered_to_one_open_and_claimed,..._two_launches_......_delivered_to_one_open_and_claimed..._pending_attach_keeps_its_process_...forgetkeeps the entry..._delivered_to_one_open_and_claimedAbsent..._delivered_to_one_open_and_claimed..._pump_through_one_wrapper_completes_an_open_held_by_another..._departed_claim_does_not_reopen_the_open_that_held_itpresencefalls through on a departed claim..._departed_claim_does_not_reopen_the_open_that_held_itforgethands its claim along a chain..._claim_is_inherited_by_every_open_still_waitingheld..._arrived_claim_is_read_against_what_the_session_holds..._finished_attach_does_not_keep_a_launch_from_a_reused_pidThe last three are what review rounds six and seven asked about, and they are worth reading
together: two are defects this branch introduced and the chain one is not, but none was
distinguishable from the code that is right by any test here before — each new test fails under
its own mutation and nothing else in the suite does. That is the whole reason they got through five
rounds of review, and it is a property of the constructions rather than of the reviewers:
which an unrelated rule (a pending attach keeps its process from a pending launch) protects either
way;
one" are the same set;
heldread needs a second process in the session, becausepresence_ofrefuses an engineholding nothing at all before the register is asked;
guard takes the entry out of the register and the question with it.
The two pure-bookkeeping ones need no engine, so they run under Miri. Worth noting one thing I got
wrong on the way: my first mutation check aimed at "a claimed process is offered again" was caught
by neither end-to-end test — the two-launch test is pinned by a different rule
(
arrived.is_none()), and the claim guard is about a repeat stop on an already-delivered process.That is why the rules are asserted where they live rather than through an engine.
Two tests are gone rather than passing, with the construction that makes each unreachable named
where the test was:
test_ending_a_session_forgets_which_processes_it_stopped_on— there is no record to forget.test_a_process_that_left_takes_its_stop_with_itbecametest_reclaiming_an_engine_id_does_not_reclaim_its_arrival, which asserts the property end to endrather than the guard that used to hold it.
The four "records no stop" tests now ask a registered open whether anything was delivered to it,
which is the same claim put to the thing that now does the work, and put through the real predicate
rather than around it.
Verification
cargo nextest run: 186 passed.cargo test --doc: 4. fmt and clippy clean.Weakmap and anArcshared across wrappers.examples/deferred_arrival.rs, re-run per stage as Refactor: a wait outcome should be a value, not state three parties re-derive #136 asks: arm A 0 short in 40 under 24spinners, both orderings; arm F 1.1 µs, arm H 2.1 µs, arm G 0/40 — unmoved.
examples/session_fuzz.rs: clean over seeds 1, 2, 7, 13 (40 rounds, 14 steps).#[ignore]d interrupt and retirement tests: 14 passed, including the 30 s one.dump tier on. No public API changes, so its repoint is the pin alone.
What is left
One case of the ambiguity, and it is named rather than quietly left. A launch abandoned before
it is delivered anything takes its
Waitingentry out of the register with no exclusion behind it,so the process its deferred
CreateProcessWidestill produces is new to the next launch's snapshotand is claimed by it. That is pre-existing —
Arrival's doc onmainrecords the same ambiguity asaccepted — and the obvious remedy is a trade rather than a fix, since nothing retires an entry whose
guard is gone and a launch that never starts would then take the next launch's process. #141 has
the analysis; the doc comment here says which case is left and why.
Stage 4 — dropping
unsafe impl Sync for DebugEngine, or replacing it with a safety comment. It issemver-visible and independent of this.
Refs #136, #133, #141
🤖 Generated with Claude Code
https://claude.ai/code/session_01HyRDk1yX5UqRkdpGgodrMY