Skip to content

fix(studio): auto-launch honours its own seam contracts — validate LaunchOutcome.failed, strip HIDDEN case-insensitively - #516

Merged
KnockOutEZ merged 2 commits into
studio-handoff-corefrom
sd-187-fix-studio-auto-launch-honours-i
Aug 28, 2026
Merged

fix(studio): auto-launch honours its own seam contracts — validate LaunchOutcome.failed, strip HIDDEN case-insensitively#516
KnockOutEZ merged 2 commits into
studio-handoff-corefrom
sd-187-fix-studio-auto-launch-honours-i

Conversation

@KnockOutEZ

Copy link
Copy Markdown
Owner

Closes KnockOutEZ/wigolo-studio-run#187.

Two contract gaps in src/studio/auto-launch.ts, both introduced by this phase's auto-launch work, both confirmed at the tip before the fix.

Gap 1 — normalizeLaunch validated started but not failed. The merge was result.failed ?? NEVER_FAILED, which only replaces a nullish probe, so a truthy non-function survived normalization and was called by the handle poll. That call sits one tick past the try/catch that guards the launcher itself, so { started: true, failed: someError } — the plausible misreading of storing the error rather than a probe of it — rejected the shared promise, the .then memo mapper had no rejection branch, and await inFlight handed the TypeError to studioBridgeFetch, which awaits with no catch. The documented never-throws contract broke at the one seam that exists to be supplied, and the rejection also bypassed the memo write while finally cleared inFlight, so every fan-out URL re-attempted. Validated at the seam instead, it is a synchronous throw the try/catch already turns into a clean decline — and, per A-185-2, an unattempted launch is still not memoized.

Gap 2 — defaultLaunch set WIGOLO_STUDIO_HIDDEN without the case-insensitive strip its sibling has. { ...process.env, WIGOLO_STUDIO_HIDDEN: '1' } collapses win32's case-insensitive env proxy into a plain object: an inherited wigolo_studio_hidden=0 left two keys and the child resolved whichever the OS handed back first, so a window could surface for a session that exists only to serve the agent — a consent surface in this design language, not cosmetics. Now filtered by uppercased key before the canonical one goes back, mirroring runStudio's mechanism at src/cli/studio.ts:1207.

What auto-launch SETS is unchanged: hidden stays '1', pinned by sd-164/165.

Red-then-green

The tests were written and committed against the unfixed tip, so the tip itself was the reverted state — no separate revert was needed. All three arms red there, each for its own diagnosed mechanism:

× throws on an object whose `failed` is not callable
    AssertionError: expected [Function] to throw an error
× a non-callable `failed` resolves null through the fetch path rather than rejecting, and is not remembered
    AssertionError: promise rejected "TypeError: failed is not a function" instead of resolving
× leaves exactly one key case-insensitively equal to WIGOLO_STUDIO_HIDDEN, set to 1
    AssertionError: expected [ 'wigolo_studio_hidden', …(1) ] to deeply equal [ 'WIGOLO_STUDIO_HIDDEN' ]
Tests  3 failed | 34 passed (37)

The middle arm is the load-bearing one: it reproduces the escaping rejection end-to-end rather than at normalizeLaunch, because the rejection does not happen there.

After:

✓ normalizeLaunch rejects a shape it cannot honestly read > throws on an object whose `failed` is not callable
✓ normalizeLaunch rejects a shape it cannot honestly read > a non-callable `failed` resolves null through the fetch path rather than rejecting, and is not remembered
✓ defaultLaunch sets the hidden flag once, whatever casing the parent carried > leaves exactly one key case-insensitively equal to WIGOLO_STUDIO_HIDDEN, set to 1
Tests  37 passed (37)

Verification

npm run build 0 · npx tsc --noEmit 0 · npm run gate:studio 0 (debt ratchet holds at baseline 363) · full npm test — see the closing comment on the issue.

CI deferred — quota.

Territory

src/studio/** + tests/unit/studio/** only, 2 files. No widening, no lane-extra:. The WIGOLO_STUDIO_HIDDEN spelling stays duplicated rather than shared with src/cli/studio.ts, because unifying it means editing a file in lane core:cli.

…tracts

Three arms, all red on this tip: normalizeLaunch keeps a truthy non-function
`failed` (the `??` merge only replaces nullish), the poll's first `failed()`
call sits outside the try/catch so that shape rejects out of a path documented
never to throw, and defaultLaunch's env spread leaves two hidden-flag keys when
the parent carried another casing.
normalizeLaunch now rejects a defined non-function `failed` the same way it
rejects a non-boolean `started`. The `??` merge only replaced a nullish probe,
so a truthy non-function reached the handle poll, and the poll's first
`failed()` call sits a tick past the try/catch that guards the launcher: the
TypeError rejected the shared promise, the memo mapper had no rejection branch,
and `await inFlight` handed it to studioBridgeFetch, which awaits with no catch.
Thrown at the seam instead, it is the synchronous throw the try/catch already
turns into a clean decline — so the never-throws contract holds and the
non-memo rule for an unattempted launch still applies.

defaultLaunch strips the inherited hidden flag in any casing before re-adding
the canonical one, mirroring runStudio's mechanism. The env spread collapses
win32's case-insensitive proxy into a plain object, so an inherited
`wigolo_studio_hidden=0` left two keys and the child resolved whichever the OS
handed back first — a window could surface for a session that exists only to
serve the agent.
@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f7705f80-0add-4540-ba5d-a2523b6361f1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@KnockOutEZ
KnockOutEZ merged commit 62e6699 into studio-handoff-core Aug 28, 2026
20 checks passed
@KnockOutEZ
KnockOutEZ deleted the sd-187-fix-studio-auto-launch-honours-i branch August 28, 2026 08:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant