The timeout promise's executor runs synchronously while the race is
constructed, so the timer is always assigned; the undefined check was a
dead branch the per-file coverage gate rejected.
The PTY capture does continue past the terminal-takeover bytes with the
fatal diagnostic; only the reset never follows. State that precisely in
both notes.
Document on FailLoudProcess.exit that callers treat it as the end of the
run, matching how the release path already relies on it.
Review of the previous commit found two defects in the release path, both
reproduced against the implementation:
- The timeout guarding a never-settling release was unref'ed. An
unhandledRejection listener suppresses Node's default fatal exit, so with
nothing else referenced the process reached an empty event loop and exited
0 on the very failure it was reporting. Keep the timer referenced and clear
it once the race settles.
- The handler uninstalled itself before awaiting the release. A second
concurrent rejection then became uncaught and killed the process
mid-teardown, stranding exactly the terminal state this restores. Replace
the uninstall with a latch: the first rejection is the reported one, and
later rejections (teardown's own included) fall through to the pending exit.
Add the PTY regression the fake-process tests cannot express: boot the shipped
tree over a fixture whose llm-pi-ai providers value is list-shaped, expect exit
1, and assert the captured bytes carry both the diagnostic and ESC[?2004l.
Against the pre-fix source the stream ends at ESC[?2004h ESC[>7u ESC[?u ESC[c
with no reset and the case fails, so it pins the actual bug.
Split the two-shape formatting test into one install per case; a latched
handler reports once by design.
A dsh launch whose config failed validation returned the user to a broken
shell: typing was invisible and the next command was mangled by a stray
Device Attributes reply (1;2;4cecho ...).
The Loader mounts entries concurrently, so ui-tui can already hold the
terminal (raw mode, bracketed paste, keyboard protocol, plus an in-flight
DA query) when a sibling entry rejects on its own config. installFailLoud
wrote its diagnostic and exited immediately, so nothing disposed the tree
and ProcessTerminal.stop() never ran.
Give installFailLoud an optional release teardown, awaited between the
diagnostic and the exit and bounded by FAIL_LOUD_RELEASE_TIMEOUT_MS. The
TUI launcher passes one that disposes the root context, reaching the same
shutdown() the /exit path already uses (drainInput() + ui.stop()). The
context is captured in boot()'s prepare hook because the rejection arrives
while boot() is still in flight.
Bins that pass no release keep the previous behavior exactly.
dsh --dump-config and dsh web --dump-config compose the shipped base,
the surface overlay, and the --config or personal overlay — exactly the
layers that surface boots — and print the entry list as YAML without
booting; --dump-default-config stops at the surface overlay so the two
outputs diff to precisely the user layer's effect.
The dump shares the mounting code: the vendored include exports its
patch algorithm as applyEntryPatches() and its !!js dialect as
entryListSchema (logged in vendor/README.md), dsh-app-boot's
renderConfigDump() composes and renders through both (and now imports
the dialect instead of duplicating it), and the CLI adds a thin
dump-config mode. !!js expressions print verbatim; unmatched patches
warn on stderr; boot-only flags are rejected alongside the dump flags.
(cherry picked from commit 1fdbebfa8a5dc7df840d53666320064a7e3dae59)
Review follow-up: the llm/adapters-updated listener's disposer was
discarded, leaving it firing (harmlessly, behind isDisposed()) between
TUI shutdown and fiber disposal, asymmetric with the sibling channel
listeners. The controller now exposes detach(), and the channel's
detachListeners() calls it on both the dispose() and startup-failure
paths.
Loader activation is service-driven, so the TUI can mount before a
configured adapter plugin registers its provider routes; every fresh
session then printed 'Could not resolve model context: no adapter
registered for provider …' for a working configuration.
The model controller now treats a NO_ADAPTER rejection of the
context-window resolution as transient: it parks the resolution
silently and re-resolves on the next llm/adapters-updated commit. A
commit that still lacks the route parks the wait again; any target
change clears it; all other resolution errors still surface. A wrong
provider name keeps failing loudly at dispatch, where it is actionable.