fix(studio): auto-launch seam contract holes — throwing probe rejects past NEVER-THROWS, falsy launch reads as started, poll-observed handle leaves stale memo - #519
Merged
KnockOutEZ merged 2 commits intoAug 28, 2026
Conversation
…o-invalidation contracts Six arms, all red on tip: a throwing failed/sleep/readHandleFn rejects out of ensureStudioRunning; a falsy non-false launcher answer reads as started and pays the full poll budget; a handle observed inside the poll leaves the negative memo standing.
…memo-invalidation contracts The handle poll calls failed(), the handle reader and sleep() a tick past the try that guarded the launcher, and the top-of-call read sat outside every try, so a throw from any of them rejected out of ensureStudioRunning into a caller that awaits with no catch — and skipped the memo write, making the fan-out re-pay the poll budget per URL. Both are guarded now: the top read falls through to the launch path, and a poll fault resolves as an attempt that published nothing. normalizeLaunch ended in "anything else means started", so null/0/''/NaN bought a 30s poll for a process nobody launched plus a 60s lockout. The legal answers are now enumerated and anything else throws at the seam, which the launcher catch already turns into a zero-tick decline. A handle observed inside the poll now clears the negative memo, which only the top-of-call read did — so a substrate that came up on the second attempt left a stale window that declined the next caller after its session ended.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
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.
Closes #191 (PX0 exit-7).
Three contract holes in
ensureStudioRunning's exported deps surface, all on shapesnormalizeLaunchalready accepts.1 — a poll-loop fault escaped as a rejection.
failed(), the handle reader andsleep()are all called a tick past the try that guards the launcher, the.thenmemo mapper had no rejection branch, and the body istry { return await inFlight } finallywith no catch. Solaunch: () => ({ started: true, failed: () => { throw } })rejected out intostudioBridgeFetch, which awaits with no catch — a launch problem becoming the user's fetch error, which the module docstring promises never happens. It also skipped the memo write, so the fan-out re-paid the poll budget per caller. The poll loop is now wrapped and resolves{handle: null, attempted: true}(logged, memoed) — an attempt that published nothing, which is what the negative memo is for, and distinct from the launcher catch's un-memoizedNOT_ATTEMPTED.Same class, one line up: the top-of-call handle read sat outside every try too, so the plainest fake — a reader that always throws — rejected straight out of the entry point before the poll was ever reached. Guarded; a handle that cannot be read is not evidence the substrate is absent, so it falls through to the launch path (A-191-2).
2 — falsy non-
falselaunch answers read as "started".normalizeLaunchended in "anything else means started", sonull/0/''/NaN— one token from a correct decline, and() => nullis what a launcher written againstreadSubstrateRecord's own return shape produces — bought the full 30s handle poll for a process nobody launched and then armed the 60s negative memo. The legal answers are now enumerated (false,true,void,LaunchOutcome) and anything else throws the same TypeError class the seam already gives a thenable or a non-callablefailed. Chosen over a quiet decline because the throw is synchronous, so the launcher catch turns it into exactly the zero-tick un-memoized decline the alternative gives — plus a logged reason. Allowlist rather than a falsy check, because the same branch also read1and'yes'as started. Recorded as A-191-1.3 — a handle observed IN the poll never cleared the memo. Only the top-of-call read did, so a recovery that ran through the launch path — a memo-bypassing caller whose substrate came up on the second attempt — left the stale window standing: when that session ended, a default caller still inside the original 60s was declined with no spawn attempt, contradicting the memo's own "a handle appearing invalidates it immediately". The clear now sits beside the memo write on the shared promise, so single-flight participants get it too.
Demo — the three shapes red on tip
11b58f3a, green on the fixOver-catching is guarded by a paired positive arm (
still returns a handle the poll finds, and still honours a probe that reports failure); mutatingif (h) return { handle: h, … }to return null reds it along with 8 pre-existing arms.Local verification (CI deferred — quota)
Non-goals untouched: memo backoff/escalation, keying the memo by dataDir,
defaultLaunch. Territory:src/studio/**+tests/unit/studio/**only.