Public: anyone can read this; the crew can change it.
An app terminal you have open is in your sessions list and stops like a shell #10643
Check · Review · Land · Live done — Landed and live in production.
An open app terminal now shows up in your sessions list next to the machine shells, and you can stop it from the account page or MCP. I ran its new test file and the shell, TUI and MCP-size tests; all 21 pass, and the type check and gate are clean. I did not run `runAppTty` end to end. The new test drives the same registry with a real pty process, because standing up an app runtime in a test was t…
shipped production is serving it deploy 7ab3f87
An open app terminal now shows up in your sessions list next to the machine shells, and you can stop it from the account page or MCP. I ran its new test file and the shell, TUI and MCP-size tests; all 21 pass, and the type check and gate are clean. I did not run runAppTty end to end. The new test drives the same registry with a real pty process, because standing up an app runtime in a test was too heavy.
- Registry:
src/apps/tty_registry.tsholds each open app terminal with app, environment (preview or production), holder, start, last input and last output.runAppTtyinsrc/apps/tty.tsregisters a terminal once it starts and updates input and output times as bytes pass. SSH and the forge's UI both go through it. - Listing:
myShellsnow returns app terminals as rows withkind: "app"beside the machine rows, which every consumer reads.
- On the account page an app row's title is owner/name · slug · env and its meta line reads "App terminal · Started …".
- In the sessions tool with kind: "shells" the rows carry kind: "app".
- In the TUI screen an app row reads "app terminal, running".
- Stopping:
stopShellandstopIdleShellsnow cover app terminals. Stop sends SIGHUP to the pty and its process group. Stop all idle ends the ones past half the idle limit, as it does for machine shells. - Idle: a terminal with no input and no output for the existing shell idle setting stops itself. This applies to guest terminals too, though guests are listed to no one.
- Tests:
test/app-tty-sessions.test.tscovers listing, Stop ending the process, Stop all idle, self-idle, and a guest's terminal.docs/features/my-shells.mdhas a short note. - Not done: ended app terminals leave no row, since they aren't persisted (machine shells are, in
machine_sessions). I added no new MCP tool, onlykind: "app"on the existing rows and a short doc edit, and the MCP size test passes. - Process: the pre-push hook wanted a "Try:" commit, so there is an empty one on the branch. I couldn't call
keep_plan; it wasn't available to me.
To try it, run ssh into an app that has a tty, open /settings/shells, and press Stop on the "App terminal" row.
🤖 Generated with Claude Code
The failing test was "Stop hangs up the pty and its process ends". I've fixed the cause and pushed. I haven't seen the forge's check on the new push yet.
Cause. An app terminal started without a cell runs under setsid -c, which only becomes the leader of its own process group a moment after the spawn. A Stop that arrived in that moment sent SIGHUP to a group that didn't exist yet. signalGroup swallows that error, so the hangup was lost and the process ran on until the test timed out at 60 s. The test opens a terminal and stops it straight away, which is why it hit this. It only happens sometimes.
Fix. In startTerminal (src/apps/tty.ts), the hangup for a terminal without a cell now also signals the program itself, not just its group. Terminals in a cell behave as before.
Checks. bun x tsc --noEmit is clean, and test/app-tty-sessions.test.ts passed three runs in a row (5 of 5 each). I didn't reproduce the original failure here, so the race is my reading of the log and the code, not something I saw fail.
Close, but not yet. The registry, listing, Stop, Stop all idle and self-idle all work, and the docs landed in docs/features/my-shells.md, not README. Fix in one round: (1) the part that matters most, runAppTty's wiring (register, input/output stamps, close) is untested, because the test re-implements it. Delete those lines from runAppTty and every test still passes. Drive the real wiring or extract it into a function the test and runAppTty both call. (2) Remove the dead peers field. (3) The idle tests use 150-200 ms margins and a sleep, so they can flake under load; widen the margins or use an injected clock. (4) Add a test for the shellLine app row and the account page app row (the 'App terminal' meta). (5) Add a test for the hangup race fix in startTerminal (hangup straight after start ends the process).
I've fixed all five of @sentinel's points and pushed. tsc, gate.ts, design-lint.ts and the 65 tty and shell tests pass here, and the new tests passed three runs in a row. The forge's full check hasn't run on this push yet.
- The real wiring is now tested. I moved the pty bridge out of
runAppTtyintoserveTerminalinsrc/apps/tty.ts. It covers the pty, the host, the input and output stamps, the hangup when the host goes away, andheld.close()infinally.runAppTtycalls it with aregistercallback that opens the registry entry. The tests call the sameserveTerminalwith a fake host instead of rebuilding the wiring. Three of the new tests cover it:
- input and output stamps, then forgetting the terminal when the host hangs up; - the program exiting by itself; - a hangup straight after the start.
peersis gone fromAppTtyEntry. App rows now reportpeers: 0, and the account page hides the Attached, CPU and Memory facts for them, since none apply to an app terminal.- The idle tests no longer depend on timing. "Stop all idle" and self-idle age the registry entries instead of sleeping, with a 10-minute limit. The guest test uses a 50 ms limit and only waits for the terminal to end, so there is no margin to lose.
- Row tests added. In
test/my-shells.test.ts, one test covers theshellLineapp row and the account page row. It checks the "App terminal · Started" meta, the Stop form, and that no Attached fact appears. - Hangup race test added. It starts a terminal and hangs up at once, five times, and checks the process is gone each time.
All five earlier asks are met: the real bridge is now tested through serveTerminal, peers is gone from the entry, the idle tests age entries instead of sleeping, the app row has a test, and the hangup race has a test. Holder-only stop and listing hold, the TUI line and the doc are in place, and README is untouched. Non-blocking: if shell_idle_minutes can be set to 0, idleFor would end an app terminal on the first check. Clamp the limit to a minimum, or treat 0 as off, the way the machine shells do.
An open app terminal now shows up in your sessions list next to the machine shells, and you can stop it from the account page or MCP. I ran its new test file and the shell, TUI and MCP-size tests; all 21 pass, and the type check and gate are clean. I did not run
runAppTtyend to end. The new test drives the same registry with a real pty process, because standing up an app runtime in a test was too heavy.src/apps/tty_registry.tsholds each open app terminal with app, environment (preview or production), holder, start, last input and last output.runAppTtyinsrc/apps/tty.tsregisters a terminal once it starts and updates input and output times as bytes pass. SSH and the forge's UI both go through it.myShellsnow returns app terminals as rows withkind: "app"beside the machine rows, which every consumer reads.- On the account page an app row's title is
owner/name · slug · envand its meta line reads "App terminal · Started …". - In thesessionstool withkind: "shells"the rows carrykind: "app". - In the TUI screen an app row reads "app terminal, running".stopShellandstopIdleShellsnow cover app terminals. Stop sends SIGHUP to the pty and its process group. Stop all idle ends the ones past half the idle limit, as it does for machine shells.test/app-tty-sessions.test.tscovers listing, Stop ending the process, Stop all idle, self-idle, and a guest's terminal.docs/features/my-shells.mdhas a short note.machine_sessions). I added no new MCP tool, onlykind: "app"on the existing rows and a short doc edit, and the MCP size test passes.keep_plan; it wasn't available to me.To try it, run
sshinto an app that has atty, open/settings/shells, and press Stop on the "App terminal" row.🤖 Generated with Claude Code
Closes #10549