Commit Graph
13 Commits
Author SHA1 Message Date
Brennan BensonandOrca 36bdd06aa8 Prevent duplicate startup command echoes (#6023)
Co-authored-by: Orca <help@stably.ai>
2026-06-21 18:51:44 -07:00
NeilandOrca 4dfc3608b7 fix(daemon): reap dead terminal sessions and clear stale checkpoint flags (#5788)
* fix(daemon): reap dead terminal sessions and clear stale checkpoint flags

TerminalHost.sessions never removed exited sessions: sessionIds are minted
fresh per pane and never reused, so each dead Session pinned a @xterm/headless
emulator (~5000 rows of scrollback) for the lifetime of the long-lived daemon
process. Nothing reads a dead session's emulator (getSnapshot/takePendingOutput/
listSessions all skip !isAlive), so it was pure retained memory.

Wire a Session onExit hook to TerminalHost.reapSession, which disposes the
emulator and drops the entry from the map. Fires on natural exit and the
kill-timeout force-dispose path; the immediate-kill path reaps inline. This is
the 'TerminalHost dead-session cleanup' the handleSubprocessExit comment already
anticipated.

Also clear DaemonPtyAdapter.sessionsNeedingFullCheckpoint on session exit and
non-keepHistory shutdown — a cold-restored session that exited before its first
checkpoint stranded a permanent Set entry.

Regression tests assert the emulator is disposed / the flag cleared on exit
(both fail before the fix). Full daemon suite (557 tests) stays green.

Co-authored-by: Orca <help@stably.ai>

* test(daemon): cover forceDispose reaping; fix stale comments

Review follow-up:
- Update the 'already-exited session' test: with reaping, natural exit disposes
  + drops the session (never force-killed) at exit time via session.dispose, so
  host.dispose only sees live sessions. Comments now match; assert the exited
  session is gone from listSessions.
- Add a forceDispose (graceful-kill-timeout) test: a stubborn child that ignores
  kill is force-disposed after KILL_TIMEOUT_MS, disposing its emulator and reaping.
- Clarify the shutdown() comment that the unconditional checkpoint-flag delete is
  a harmless no-op under keepHistory.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-06-19 02:35:24 -07:00
slashdevcorpse f80704cc60 [perf] Speed up CLI launch through Orca
Speed up Windows CLI launch by delivering short startup commands through shell args, falling back to stdin for oversized commands, and moving Codex historical session bridging into an incremental background task. Includes review fixes for Windows symlink and WSL test reliability.
2026-06-18 22:21:48 -07:00
Brennan BensonandOrca 7f640ca904 Speed up Codex terminal startup (#5664)
* Speed up Codex terminal startup

Co-authored-by: Orca <help@stably.ai>

* Address Codex startup review comments

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-06-17 20:49:47 -07:00
Jinwoo HongandOrca 630bcce530 fix: replace per-5s full-buffer terminal checkpoints with an incremental log (#5292)
Co-authored-by: Orca <help@stably.ai>
2026-06-12 16:31:24 -07:00
Trevin Chow 519a120828 feat: show the running coding agent's icon in terminal tabs (#3410) 2026-05-30 02:14:22 -04:00
Jinwoo HongandOrca a638215729 fix(pty): release ptmx fd on natural exit + defuse SIGHUP-to-recycled-pid (#1327)
* fix(pty): release ptmx fd on natural exit + defuse SIGHUP-to-recycled-pid

Daemons accumulated ptmx fds over time because node-pty's UnixTerminal
only releases the master fd when destroy() runs. On the natural-exit
path (the common case — user closes a tab, shell runs `exit`) nothing
ever calls destroy(), so the fd leaks until GC. On macOS this
eventually hits kern.tty.ptmx_max=511 and all new terminals fail to
spawn.

Fix: release the fd synchronously on every teardown path (natural
exit, explicit kill, stale SSH spawn, daemon shutdown) and close the
concurrent SIGHUP-to-recycled-pid hazard inside node-pty's
UnixTerminal.destroy().

- src/main/daemon/pty-subprocess.ts: synchronous POSIX proc.kill
  neutralization inside proc.onExit; dead guards on forceKill/signal
  so they never target a reaped-and-possibly-recycled pid
- src/main/daemon/session.ts: new disposeSubprocess() for already-
  exited sessions (fd release only, no SIGKILL) — avoids sending
  SIGKILL to a recycled pid during daemon shutdown
- src/main/daemon/terminal-host.ts: dispose loop routes on isAlive —
  live sessions get forceKillAndDisposeSubprocess (SIGKILL + fd
  release), exited sessions get disposeSubprocess (fd release only)
- src/main/providers/local-pty-provider.ts: same POSIX kill
  neutralization at top of onExit for the legacy local path
- src/relay/pty-handler.ts: same neutralization in wireAndStore;
  disposed flag guards all public entry points; dispose() uses
  SIGKILL (not SIGTERM) before destroy since the relay is exiting;
  killTimer fallback + immediate-shutdown + stale-spawn cleanup all
  call disposeManagedPty + ptys.delete so wedged children (D-state,
  bad NFS) can't leak map entries against the 50-PTY cap

Windows is exempt everywhere — WindowsTerminal.destroy IS a kill()
call internally (closes the ConPTY agent), so neutralizing would
turn destroy into a no-op and leak the agent.

See docs/fix-pty-fd-leak.md for the full design.

Co-authored-by: Orca <help@stably.ai>

* fix(pty): patch node-pty native off-by-one leaking /dev/ptmx per spawn

node-pty 1.1.0's pty_posix_spawn on macOS walks low_fds[0..2] in an
allocation loop that breaks at the first fd >= STDERR_FILENO, then
cleans up via `for (; count > 0; count--) close(low_fds[count])`. In
the typical case (break at count=0) the cleanup body never runs and
low_fds[0] — a /dev/ptmx handle — leaks per spawn. Fixed upstream in
microsoft/node-pty af053f2 (PR #882), not in any 1.1.0 release.

Backport the 3-line cleanup-loop fix as a pnpm patch. E2E validated
against a dev daemon: 200 spawn/kill cycles kept the daemon's ptmx
fd count flat at baseline; prior runs reproduced linear 1-per-spawn
growth. Also documents the native root cause as a status addendum in
docs/fix-pty-fd-leak.md — the JS-side destroy() discipline previously
landed is still load-bearing for the SIGHUP-to-recycled-pid hazard and
for synchronous fd release on daemon shutdown.

Co-authored-by: Orca <help@stably.ai>

* fix(pty): capture stable kill spy ref in pty.test.ts

destroyPtyProcess reassigns proc.kill = () => {} on POSIX to defuse
the SIGHUP-to-recycled-pid hazard (see docs/fix-pty-fd-leak.md). After
that reassignment, proc.kill.mock is undefined and the assertions
crashed in CI. Capture a stable reference to the vi.fn() before it
gets reassigned.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-05-02 23:12:14 -07:00
Neil 1be13d58f3 fix(daemon): defer startup flush past shell raw-mode switch (#1060)
* fix(daemon): defer startup-command flush past shell raw-mode switch

When launching an agent (e.g. `claude`) through the quick-launch menu,
the command name appeared twice in the terminal — once from kernel
echo, once from readline's prompt redraw. The daemon session was
flushing its pre-ready stdin queue the moment the OSC 777 shell-ready
marker arrived, but that marker fires from precmd_functions /
PROMPT_COMMAND — before the shell draws its prompt and before
zle/readline flips the PTY into raw mode. Writing while ECHO was still
on produced the visible duplicate.

Mirror the gating already used by the non-daemon path
(local-pty-shell-ready.ts::writeStartupCommandWhenShellReady): wait
for the next data chunk after the marker (the prompt draw) plus a
short 30ms delay, with a 50ms wall-clock fallback for the case where
the prompt arrives in the same chunk as the marker.

This regression became visible after #1025 made the shell-ready
wrappers persist reliably — previously the marker was often missed
and the 15s fallback path fired long after the shell was already in
raw mode, masking the race.

* refactor(daemon): extract PostReadyFlushGate out of Session

Factor the post-ready flush gating out of Session into its own class in
post-ready-flush-gate.ts. Session's only responsibility is now to
arm() the gate on shell-ready, notifyData() on subsequent PTY data,
and clear() on teardown. Removes the max-lines oxlint-disable added in
the previous commit and puts the timing behavior behind focused unit
tests.

No behavior change — kept as a separate refactor commit from the
behavior fix for reviewability.

* fix(daemon): keep queueing writes while post-ready flush gate is pending

Codex review caught an ordering regression: once transitionToReady()
sets _shellState to 'ready', any Session.write() that arrives during
the 30–50ms gate window was being written directly to the subprocess,
bypassing the still-unflushed preReadyStdinQueue. That let late input
race ahead of the buffered startup command.

Expose PostReadyFlushGate.isPending and continue queuing while the gate
is armed so queued writes drain in their original order before any
fresh input reaches the subprocess.
2026-04-24 15:26:50 -07:00
Kelvin Amoaba 0a9187909d fix(term): stop daemon emulator from replying to xterm queries (#1024) 2026-04-24 14:05:42 -07:00
Jinwoo Hong a73b4a6c1f Fix daemon shell-ready wrapper persistence (#1025) 2026-04-24 13:01:27 -07:00
Jinwoo Hong 6feb93ab78 fix(terminal): guard PTY native calls against dead-process Napi::Error crash (#916) 2026-04-22 11:16:43 -07:00
Jinjing c2ed4fbd3b fix(terminal-host): avoid reattach to terminating session, force-kill on dispose (#801)
Reattaching to a session where kill() has been called but the subprocess hasn't
exited yet races the in-flight exit. Treat terminating sessions the same as
fully-exited ones in createOrAttach, and have Session.dispose() force-kill a
stuck subprocess and notify attached clients so we don't leak the process when
the killTimer is cleared mid-flight.
2026-04-18 09:24:27 -07:00
Jinwoo Hong fd4f986c59 feat: terminal persistence via out-of-process daemon (#729) 2026-04-17 01:42:41 -04:00