fix(hmr,include): settle a failing boot instead of a silent exit 13

Master's transactional loader made the invalid-provider PTY case regress:
the HMR main watcher's initial scan refreshed the include mid-initial-apply,
the concurrent group updates stranded the include fiber, and once serialized
the failing apply's rollback deadlocked on HMR's refresh drain — dsh exited
13 with no diagnostic and the terminal stranded, the exact symptom this
branch fixes. Serialize every include child-tree mutation through one queue
and pass ignoreInitial to the HMR main watcher; the failing boot now settles
through boot()'s labelled rejection with the tree disposed and exit 1. The
PTY case asserts the settled diagnostic; the fail-loud release remains the
guard for rejections boot cannot see.
This commit is contained in:
Turtle
2026-08-03 14:14:20 +08:00
parent b37c22bd0a
commit ad4aeacd19
14 files changed
+156 -28

No files matched your search

+9 -7
View File
@@ -142,13 +142,15 @@ export async function runTui(
const execve = process.execve?.bind(process)
const app: { current?: Context } = {}
// The Loader mounts entries concurrently, so `ui-tui` can already hold the
// terminal (raw mode, bracketed paste, keyboard protocol) when a sibling
// entry rejects — and that rejection arrives while `boot` is still in
// flight. Disposing the tree runs the TUI's own shutdown, which stops the
// terminal and hands the shell back; without it a failed boot returns to a
// corrupted prompt. `app.current` is captured from boot's `prepare` hook, so
// it holds the root context for the whole mounting window rather than only
// after boot resolves.
// terminal (raw mode, bracketed paste, keyboard protocol) when something
// else fails. A config-tree failure settles through `boot`, which disposes
// the tree itself; this release covers the rejections `boot` cannot see — a
// plugin's detached async work rejecting while mounting is still in flight
// or after the tree settled. Disposing the tree runs the TUI's own shutdown,
// which stops the terminal and hands the shell back; without it such a
// failure returns to a corrupted prompt. `app.current` is captured from
// boot's `prepare` hook, so it holds the root context for the whole mounting
// window rather than only after boot resolves.
installFailLoud(NAME, process, async () => {
await app.current?.fiber.dispose()
})