All features

A useless process must not hold the lock

Try the real interface

Explore this workspace.

Real Torbie UI · mock sessions
Tab bar
Loading Torbie…
Technical background

Exactly one process answers for the app, so once that process cannot show a window, every later launch is handed to it and silently swallowed — no window, no error, no crash, indefinitely. Six hours of it, measured. A process in which no window has ever reported itself ready has run nothing and holds nothing to lose, and that one rule is what makes quitting safe.

The problem

Exactly one process answers for the app. Once that process cannot show a window, every later launch is handed to it and silently swallowed — no window, no error, no crash, indefinitely. Six hours of it, measured. Nothing anywhere checked that the lock holder had ever produced a working window.

How it works

app:ready is the only line that matters. It is sent once the Angular root exists in a renderer, and everything that can open a tab or spawn a PTY lives at or after that point — so a process in which no window has ever emitted it has never run a session and holds nothing to lose. The first one disarms the watchdog permanently for the life of the process. That single rule is what makes code that can call app.exit() safe to ship.

Settings
  • TABBY_WATCHDOG: 1

    Environment variable. 0 disables it.

  • TABBY_WATCHDOG_BOOT_MS: 60000

    How long a window gets to reach app:ready.

  • TABBY_WATCHDOG_NO_WINDOW_MS: 5000

    How long the process may have no window at all.

Details
  • Two failure shapes, needing different tests. No window at all — a creation that threw, or a handoff that produced nothing, since the paths that call newWindow() have no catch and the failure simply vanishes. And a window that never booted — the one that actually bit, and the half a zero-window check cannot see, because the window is pushed onto the list before its readiness is awaited, so from outside nothing looks wrong.
  • The boot budget is spent in ticks of a live event loop, not wall clock. A main process blocked for 19.7s during startup is measured on every cold launch here — it has not given the renderer that time, and burning the budget on it would quit a build that was only slow. Measured margin: a healthy dev build reaches app:ready 1.3–3.8s after the watchdog arms.
  • app.exit(), never app.quit(). The window's own close handler asks the renderer to confirm; a renderer that never booted never answers, so quitting politely would hang in exactly the place we are escaping.
  • It writes to both logs before exiting. diagnostics.log batches behind a one-second timer and app.exit() runs no timers, so the flush is synchronous — otherwise the one record explaining the exit is the one record guaranteed to be lost.
  • A window-ready record is now written on every successful boot, with how long it took. "How long does this build take to start?" was previously unanswerable from outside the process.
What this does not claim
  • The original reproduction no longer reproduces. A poisoned NODE_PATH was the fault this was written for, and the module-lookup fix landed first — a dev build launched with the installed app's plugin directories on its path now boots in under two seconds. The test blacklists a required builtin instead, which lands in the same state. Like the module-lookup test it also runs the fault with the watchdog disabled and asserts it still hangs, because a fixture that quietly stopped reproducing would turn the whole run green.
  • A wedged main process is beyond this by construction — the watchdog runs on that loop.
Source commits
RobustnessAdded 2026-09-066 files+597 / −3 lines