Public: anyone can read this; the crew can change it.

A hosted app with children still working is never put to sleep by the sweep #350

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

A sleeping environment is now first checked for work in progress: if the app's processes are still running children or the proxy has a stream open to it, the sweep leaves it awake. It stays awake for at most one crew run's length past its idle window, so an app that always looks busy still goes to sleep in the end. `bun x tsc --noEmit` is clean, and the new suite plus `test/studio.test.ts` and `te…

shipped production is serving it deploy 2e641a3

opened this

A sleeping environment is now first checked for work in progress: if the app's processes are still running children or the proxy has a stream open to it, the sweep leaves it awake. It stays awake for at most one crew run's length past its idle window, so an app that always looks busy still goes to sleep in the end. bun x tsc --noEmit is clean, and the new suite plus test/studio.test.ts and test/web.test.ts pass. I didn't run the whole forge.json check locally.

What changed

  • src/apps/busy.ts (new): busyReason(pid, openStreams) returns "stream", "children" or nothing. Children are read from /proc/<pid>/task/*/children. On a machine without /proc the answer is "no children", so the app sleeps on time alone, as it did before.
  • src/apps/runtime.ts:

- Each environment keeps a count of open streams. holdStream() adds one and hands back a release function that only counts once. - Past the idle window, the sweep asks busyReason. A busy environment is spared until busyGraceMs beyond the window; after that it sleeps anyway. - The deploy log records "kept awake: …" when a busy app is spared and "asleep: still busy …" when the limit runs out. - EnvStatus.busy gives the reason while the app is being spared, and a request clears it.

  • src/apps/proxy.ts: a proxied websocket holds the environment from connect until its upstream closes. A text/event-stream response is passed through a small wrapper that holds it until the stream ends, errors, or the browser goes away. Other responses are untouched.
  • src/main.ts: FORGE_CREW_TIMEOUT_MIN (default 45 minutes) is computed once and used as both the crew run timeout and the busy limit.
  • Pages: envFacts in src/web/ui.ts adds a line like "busy — its child processes are still working; kept awake past its idle window". It feeds the app page and the studio's app header. The Now card (src/web/studio/now.ts) adds a busy row for each environment being spared.
  • Tests (test/apps-busy.test.ts, added to the forge.json check):

- an idle app still sleeps; - an app with a working child survives the sweep, the app page and Now card say busy, and it sleeps once the child exits; - an open event stream keeps the app awake until it closes; - an app that stays busy sleeps once the limit is spent.

The two child-process tests skip on machines without /proc. docs/APPS.md ("Asleep") explains busy and its limit.

What I chose not to do

  • The tests live in test/, where this repository keeps them, rather than the src/apps/*.test.ts the issue named.
  • The sweep doesn't ask the app over HTTP. Looking at the process tree and the proxy's own connections needs nothing from the app, so an app can't stay awake just by saying it's busy.
  • The Now card's live region only refreshes on run, pull and issue events, so the busy row appears on page load and on the next live refresh, not the moment the sweep spares the app.

How to try it: run bun test test/apps-busy.test.ts. Or deploy an app whose request handler starts sleep 120, set a short idle window, and watch the app page say busy until the child exits.

🤖 Generated with Claude Code

Closes #332

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs