An app's terminal that runs without a cell ends with everything it started, on a hangup or its own exit #66

Merged
grokbot merged 1 commit from grokbot/forge:fix/tty-stop-whole-tree into main 2026-10-18 23:58:13 +00:00
Owner

Where the host cannot hold a cell (CI, a pod without bwrap), an app's
terminal (ssh <slug>@forge, or t in the forge's terminal UI) ran as
a plain /bin/sh -c child in the forge's own process group, and a
hangup sent SIGHUP to the program's pid alone. What the program had
started (a server it left running, a job sent to the background) was
re-parented and kept running in the deployed checkout, with the
environment's data dir, forge token and secrets, after the terminal had
closed; nothing ever stopped it.

The terminal now leads a process group of its own (detached, without
a cell only), a hangup signals the group, and once the program has
exited, what is left of the group is killed: the pattern #63 and #64
gave apps and steps. The spawn moves into startTerminal, so it is
tested without a deploy. A cell is unchanged: its tree dies with
bubblewrap. signalGroup is exported from apps/process.ts for it.

Validation

pipeline/validate on a97b8e6b: baseline in 594s — bun 1.3.14, CI-shaped env (scrubbed vars, runner PATH), exact check.yml shard + forge.json commands.

step result time
bun install --frozen-lockfile ok (exit 0) 0s
forge.json check.fast BASELINE (exit 1) 118s
check.yml gate (1) pull_request BASELINE (exit 1) 14s
check.yml gate (2) pull_request ok (exit 0) 268s
check.yml gate (3) pull_request ok (exit 0) 194s
  • ⚠️ node v20.19.2 here, v22 on CI (tsc runs under node)
  • ⚠️ check.yml gate (1) pull_request stopped early at a BASELINE failure (fail-fast): its later files were not run in this step
test output (failures, totals)
▶ bun install --frozen-lockfile
  ok  exit 0  0s
▶ forge.json check.fast
(fail) egress, reported: an undeclared public host goes through and is counted; a private one never does > an undeclared public host is let through and counted [0.98ms] [shard 10]
Ran 2571 tests across 260 files in 98.8s; 1 failed.
classify: reran test/contain.test.ts test/subscriptions.test.ts test/watches.test.ts  alone on origin/main 5881c081 and on this branch
  BASELINE  exit 1  118s
▶ check.yml gate (1) pull_request
(fail) egress, reported: an undeclared public host goes through and is counted; a private one never does > an undeclared public host is let through and counted [1.40ms] [shard 3]
Ran 113 tests across 3 files in 14.2s; 1 failed.
classify: reran test/contain.test.ts  alone on origin/main 5881c081 and on this branch
  BASELINE  exit 1  14s
▶ check.yml gate (2) pull_request
Ran 1339 tests across 137 files in 267.7s (137 shards).
  ok  exit 0  268s
▶ check.yml gate (3) pull_request
Ran 1502 tests across 137 files in 193.2s (137 shards).
  ok  exit 0  194s
Where the host cannot hold a cell (CI, a pod without bwrap), an app's terminal (`ssh <slug>@forge`, or `t` in the forge's terminal UI) ran as a plain `/bin/sh -c` child in the forge's own process group, and a hangup sent SIGHUP to the program's pid alone. What the program had started (a server it left running, a job sent to the background) was re-parented and kept running in the deployed checkout, with the environment's data dir, forge token and secrets, after the terminal had closed; nothing ever stopped it. The terminal now leads a process group of its own (`detached`, without a cell only), a hangup signals the group, and once the program has exited, what is left of the group is killed: the pattern #63 and #64 gave apps and steps. The spawn moves into `startTerminal`, so it is tested without a deploy. A cell is unchanged: its tree dies with bubblewrap. `signalGroup` is exported from `apps/process.ts` for it. <!-- pipeline:validation --> ### Validation `pipeline/validate` on `a97b8e6b`: **baseline** in 594s — bun 1.3.14, CI-shaped env (scrubbed vars, runner PATH), exact check.yml shard + forge.json commands. | step | result | time | |---|---|---| | `bun install --frozen-lockfile` | ok (exit 0) | 0s | | `forge.json check.fast` | BASELINE (exit 1) | 118s | | `check.yml gate (1) pull_request` | BASELINE (exit 1) | 14s | | `check.yml gate (2) pull_request` | ok (exit 0) | 268s | | `check.yml gate (3) pull_request` | ok (exit 0) | 194s | - ⚠️ node v20.19.2 here, v22 on CI (tsc runs under node) - ⚠️ check.yml gate (1) pull_request stopped early at a BASELINE failure (fail-fast): its later files were not run in this step <details><summary>test output (failures, totals)</summary> ``` ▶ bun install --frozen-lockfile ok exit 0 0s ▶ forge.json check.fast (fail) egress, reported: an undeclared public host goes through and is counted; a private one never does > an undeclared public host is let through and counted [0.98ms] [shard 10] Ran 2571 tests across 260 files in 98.8s; 1 failed. classify: reran test/contain.test.ts test/subscriptions.test.ts test/watches.test.ts alone on origin/main 5881c081 and on this branch BASELINE exit 1 118s ▶ check.yml gate (1) pull_request (fail) egress, reported: an undeclared public host goes through and is counted; a private one never does > an undeclared public host is let through and counted [1.40ms] [shard 3] Ran 113 tests across 3 files in 14.2s; 1 failed. classify: reran test/contain.test.ts alone on origin/main 5881c081 and on this branch BASELINE exit 1 14s ▶ check.yml gate (2) pull_request Ran 1339 tests across 137 files in 267.7s (137 shards). ok exit 0 268s ▶ check.yml gate (3) pull_request Ran 1502 tests across 137 files in 193.2s (137 shards). ok exit 0 194s ``` </details>
An app's terminal that runs without a cell ends with everything it started, on a hangup or its own exit
All checks were successful
check / gate (3) (pull_request) Successful in 4m6s
check / gate (2) (pull_request) Successful in 5m27s
check / gate (1) (pull_request) Successful in 22m9s
check / check (pull_request) Successful in 0s
preview / preview (pull_request) Has been skipped
a97b8e6bb2
Where the host cannot hold a cell (CI, a pod without bwrap), an app's
terminal (`ssh <slug>@forge`, or `t` in the forge's terminal UI) ran as
a plain `/bin/sh -c` child in the forge's own process group, and a
hangup sent SIGHUP to the program's pid alone. What the program had
started (a server it left running, a job sent to the background) was
re-parented and kept running in the deployed checkout, with the
environment's data dir, forge token and secrets, after the terminal had
closed; nothing ever stopped it.

The terminal now leads a process group of its own (`detached`, without
a cell only), a hangup signals the group, and once the program has
exited, what is left of the group is killed: the pattern #63 and #64
gave apps and steps. The spawn moves into `startTerminal`, so it is
tested without a deploy. A cell is unchanged: its tree dies with
bubblewrap. `signalGroup` is exported from `apps/process.ts` for it.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
trevato-apps/forge!66
No description provided.