KaimonGate: raw-mode TUI apps wedge the host REPL on exit #67
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
kahliburke/Kaimon.jl#67
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Hello, and first of all thank you very much for all the work you're doing on
Kaimon, Tachikoma (and KaimonSlate, which I tried a bit a few days ago) — it's
incredible how many valuable contributions you're stacking up in such a short
time!
I've been building an interactive variable explorer with Tachikoma: a TUI app
meant to be called on demand from a working REPL, rather than run as a
standalone app (like Kaimon). I also have Kaimon basically auto-loaded in all my
REPL sessions for easier agent integration, and ran into the odd issue described
below.
I think this is a genuine bug that deserves a fix, though I'm not sure the fix
location I suggest below is the appropriate one. All of the analysis and
debugging below was done together with AI.
Summary
When an interactive raw-mode TUI (any Tachikoma
app(...)) is run from aKaimonGate-hosted REPL, the REPL becomes unusable after the TUI exits:
typed input is echoed but never evaluated — no new prompt, no results. The
Julia process is alive (the Kaimon gate still evaluates over its socket), but
the REPL frontend can no longer read stdin, so the only recovery is killing and
restarting the REPL.
The same TUI run from a plain
juliaREPL (no Kaimon) exits cleanly. And in aKaimon session it also exits cleanly until the first gate eval — the wedge
starts only once the stdout-capture mux is installed, which is the tell-tale
fingerprint of the cause below.
MWE (two registered packages, no MCP server / no VS Code)
The wedge needs neither a running Kaimon server nor the MCP transport — the
single call
KaimonGate._ensure_capture_installed!()(what the first gate evaldoes under the hood) is the entire trigger. So it reproduces in a plain REPL
with only Tachikoma + KaimonGate installed:
Without the
_ensure_capture_installed!()line the same demo quits cleanly —that one call is the whole difference.
Confirmed recovery — restoring the real streams around the app makes it behave
exactly like a plain REPL:
(Verified on Julia 1.12.6, Tachikoma 2.3.6, KaimonGate 1.0.1.)
Platform note (Linux vs Windows)
The wedge occurs on TUI exit on both Linux and Windows, but recovery differs:
Ctrl+Cdoes not un-wedge it — the onlyrecovery is killing and restarting the process.
Ctrl+Cafter the TUI exits un-wedges the REPL andreturns a usable prompt.
This asymmetry is consistent with the platform split in the stdin/console-input
handling (libuv
wait(stdin)on Unix vs the polling console-input backend onWindows): on Windows the interrupt appears to kick the input loop back into
reading; on Unix it does not. Either way the prevention (restore the real
streams around the app) fixes both platforms.
Root cause
KaimonGate._ensure_capture_installed!(lazily, on the first gate eval) rebindsthe process output streams to non-TTY wrappers for REPL mirroring:
stdinstays the real TTY; the accompanying comment notes the interactive REPLuses its own terminal handle for line-editing, so mirroring is transparent to
the line editor. It is not transparent to a raw-mode TUI framework.
Tachikoma's
with_terminal/_start_capturegates its own terminalsave/restore on the stream being a literal
Base.TTY:With
_CaptureIOinstalled,stdout isa Base.TTYisfalse, so Tachikomaskips the capture/restore cycle it performs in a plain REPL. Running its
raw-mode enter/leave without that cycle leaves the host REPL's stdin reading
dead on exit. (Tachikoma renders to
/dev/ttydirectly, so the TUI stilldisplays correctly — the damage is only visible after quitting.)
Proposed fix (KaimonGate side)
The mux only needs to be suspended while a TUI owns the terminal. KaimonGate
already integrates with Tachikoma via the
_TACHIKOMA[]host hook and alreadyrestores stream/terminal state for the restart path (
prepare_for_exec!); themissing piece is the symmetric handling for interactive app runs. Options,
cheapest first:
Suspend the mux around
Tachikoma.app/with_terminal. Whenset_tachikoma!(T)is called, wrap (or have Tachikoma call a host hookaround)
with_terminalso it runs_restore_capture!()on enter and_ensure_capture_installed!()on leave. Fixes every Tachikoma app with noper-app code — this is the real fix.
Make the capture gate visible to frameworks (Tachikoma side, complementary).
Have Tachikoma test "terminal-backed" rather than
stdout isa Base.TTY(follow a wrapper down to its underlying tty). Helps any stdout wrapper, not
just Kaimon's; worth an upstream Tachikoma issue regardless.
Expose a public helper —
KaimonGate.with_real_streams(f)/with_tui(f)— so app authors can opt in explicitly. Lowest effort for KaimonGate, but
pushes the burden onto every TUI author, and requires them to reach for the
currently-private
_restore_capture!/_ensure_capture_installed!.Recommendation: (1) as the transparent fix, optionally with (2) filed upstream
so the gate is robust regardless of who wraps stdout. A public helper (3) is a
useful addition either way, since it lets app authors that must self-wrap avoid
the private-name coupling.
@disberd Thanks for the report and kind words, I will take a look soon.
Fixed. Root cause: KaimonGate's output-capture mux rebinds
Base.stdout/stderrto non-TTY wrappers, so Tachikoma's
with_terminalsawstdout isa Base.TTYasfalse and skipped its terminal capture/restore cycle — the raw-mode enter/leave
then left the host REPL's stdin read wedged on exit.
The fix is a stream-guard handshake between the two packages: Tachikoma 2.4 adds a
set_stream_guard!host hook, and KaimonGate now registers its_with_uncaptured_streamssuspender as that guard, so a TUI launched from a gateREPL runs with the real process streams restored and exits cleanly. It's a no-op
for standalone Tachikoma and self-heals on the first eval, so no user action is
needed beyond upgrading. A
disable_wedge_guard!opt-out exists for a host thatruns its own persistent full-screen TUI alongside an active mux.
Ships in KaimonGate 1.1.0 (merged to General) and Kaimon 2.2.0 (registering
now), which requires Tachikoma 2.4. Once your registries update,
]upwill pullthem in.
Thanks for the clear report and MWE, @disberd — it made this one much easier to
pin down.