Skip to content

RFC: Native non-PTY process protocol - #173

Open
pcarrier wants to merge 3 commits into
mainfrom
codex/native-process-rfc
Open

RFC: Native non-PTY process protocol#173
pcarrier wants to merge 3 commits into
mainfrom
codex/native-process-rfc

Conversation

@pcarrier

@pcarrier pcarrier commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Extract the non-PTY process design from #167 into a standalone, all-client protocol RFC.

  • feature bit 13 and direction-local 0xC0 through 0xC6
  • direct argv execution with bounded arguments and environment
  • flow-controlled binary stdin, stdout, and stderr with lifetime offsets
  • ordinary endpoint-scoped children plus opt-in detachable children
  • client-generated 128-bit adoption tokens and atomic snapshot-to-live reattachment
  • bounded output gaps and repeatable retained exit snapshots across reconnects
  • correlated spawn/control outcomes, stdin state, explicit exit reasons, and kill causes
  • Unix process-group and Windows job cleanup semantics
  • count, stream-window, pending-request, spawn-concurrency, and retained-result limits
  • feature gate and security posture independent of Wasmi

Network clients use their existing transport. In-process extensions use the same packets through the ordinary host ABI, but neither implementation depends on the other.

Review follow-up

The revised RFC makes detach/adopt ownership, outbox cutoffs, inherited stdin, output-credit reset, terminal expiry, and concurrent adoption linearization explicit. It also serializes Unix spawn registration against Blit's existing waitpid(-1) backstop, documents expected supervisor BUDGET handling, and gives supervisors an explicit Unix per-process grace-period recipe.

Detached state is intentionally server-memory-only: it does not survive a Blit restart, and output produced while unbound is drained and represented as an offset gap rather than replayed.

Stack

This is intentionally based on #167 so the extension RFC can link to it while keeping the process family out of the parent PR. It should be reviewed and merged separately after the base lands.

Verification

  • ./bin/lint passes
  • cargo fmt --check, repository-wide Prettier, and git diff --check pass
  • independent consistency and flow-control reviews found no remaining merge blocker

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Coverage

Crate Lines Functions Regions
alacritty-driver 73.5% (837/1139) 76.4% (68/89) 77.0% (1360/1767)
browser 0.0% (0/822) 0.0% (0/68) 0.0% (0/1401)
cli 24.6% (2436/9889) 36.0% (300/833) 27.2% (4128/15172)
compositor 33.4% (5530/16578) 47.1% (451/958) 33.2% (7587/22872)
fonts 81.4% (721/886) 88.6% (70/79) 83.0% (1427/1719)
fssync 92.5% (5516/5961) 94.4% (501/531) 92.7% (10182/10981)
gateway 30.7% (492/1603) 34.5% (50/145) 26.1% (692/2655)
git 87.6% (4432/5060) 89.3% (359/402) 87.4% (7061/8082)
lsp 77.4% (2688/3471) 79.4% (262/330) 75.2% (4210/5595)
proxy 19.2% (172/898) 20.5% (26/127) 21.0% (293/1392)
remote 91.0% (10177/11183) 93.6% (743/794) 89.0% (17081/19182)
sd-notify 73.9% (68/92) 100.0% (6/6) 83.2% (109/131)
server 47.6% (11823/24829) 61.2% (1132/1850) 49.6% (19359/39001)
ssh 32.2% (165/512) 48.2% (27/56) 31.4% (261/830)
upsidedown 31.4% (391/1247) 27.8% (55/198) 34.8% (797/2287)
webrtc-forwarder 6.9% (189/2756) 10.3% (21/204) 5.3% (243/4549)
webserver 62.1% (1133/1825) 65.9% (164/249) 64.5% (1912/2964)
Total 52.7% (46770/88751) 61.2% (4235/6919) 54.6% (76702/140580)

@pcarrier
pcarrier force-pushed the codex/native-process-rfc branch from 9a56e1f to 2efd517 Compare August 5, 2026 18:31
@pcarrier
pcarrier marked this pull request as ready for review August 5, 2026 18:31
@indent

indent Bot commented Aug 5, 2026

Copy link
Copy Markdown
PR Summary

Documentation-first RFC that extracts the non-PTY process protocol from the extension RFC (#167, now merged) into a standalone, all-client design doc, plus a small fd-channel example fix. The PR targets main. Feature bit 13 moves from RESERVED to PROCESS, and the family occupies direction-local opcodes 0xC0-0xC6.

  • Adds docs/design/processes.md: feature bit 13, PROCESS_* opcodes in both directions, direct argv execution (no shell), flow-controlled binary stdin/stdout/stderr with lifetime offsets, endpoint-scoped process IDs, and correlated spawn/control/exit outcomes with explicit exit reasons and kill_cause.
  • Specifies opt-in detachable children: a server-wide detached registry, client-generated 128-bit adopt_token bearer secrets, PROCESS_ADOPT/PROCESS_ADOPTED with an atomic snapshot-to-live reattachment, a DETACH control action, bounded output gaps (drained, not replayed, while unbound), and repeatable retained exit snapshots with a 5-minute TTL. Detached state is server-memory-only and does not survive a restart.
  • Documents Unix process-group / Windows job cleanup, waitpid(-1) backstop serialization, admission limits (count/window/request-bytes/spawn-concurrency/retained-result), expected supervisor BUDGET backpressure, and a BLIT_PROCESS=0 gate with authority parity to CREATE2(HAS_COMMAND) (explicitly not a sandbox).
  • Updates docs/design/extensions.md to point its process-execution alternatives at the new RFC, and docs/protocol.md to rename bit 13, cite RFC: Native non-PTY process protocol #173 in the proposal note, and add the processes.md#security-and-deployment gate link.
  • Updates the examples/fd-channel-bun.ts and examples/fd-channel-python.py demos to accept the server's initial state burst: they now read frames until S2C_READY, capturing S2C_LIST and tolerating intervening compositor/terminal state, instead of expecting exactly LIST then READY.

Reviewed for internal consistency (opcode block 0xC0-0xC6 clear of Git's 0xA0-0xBF; opcode/field-width/enum tables; adopt-token rules; status-registry membership; ID/token lifetime; stdin accounting; capacity table) and the example loops for EOF-safety and correct handshake handling. No issues found.

Issues

No issues found.

CI Checks

The e2e check failed on Playwright browser-UI tests unrelated to this PR. This PR changes only three Markdown docs and two examples/fd-channel-* demo scripts; none of the failing tests exercise those files, so the failure is not caused by this change.

Failing e2e (github-actions)
  • The failures are Playwright browser-UI e2e specs that time out waiting on UI elements: e2e/tests/font-size-sync.spec.ts:11 and e2e/tests/touch-draggables.spec.ts:205 failed (expect.poll/toBeVisible 10s timeouts), with pane-tools.spec.ts:193 and terminal.spec.ts:137 flaky (element-not-found / 'New terminal' button intercepted, 30s timeout). All exercise the js/ui web app (terminal panes, dock cards, font-size sync, switcher canvases). This PR touches only docs/design/processes.md, docs/design/extensions.md, docs/protocol.md, and the examples/fd-channel-{bun.ts,python.py} scripts — none of which are loaded by these UI tests — so the failure is unrelated to the change and not fixable within this PR. It matches the suite's known shared-server session-leakage flakiness; a re-run or a fix on main is the appropriate path.

View session

@indent

indent Bot commented Aug 5, 2026

Copy link
Copy Markdown

Reviewing this against #94 (units — declarative process supervision: unit files, Restart=, readiness and health checks, dependencies). This family is a better Backing=pipe than the one I specified — exact offsets, cumulative-byte windows, MERGE_STDERR, and "raw bytes, not UTF-8 and not line-framed" is exactly what regex readiness matching needs — and the group/job kill semantics are the ones I'd argued for. I'd like to drop my version and build on this.

One thing prevents that today.

Please add a detach/adopt path

No detached children. Every child belongs to one logical endpoint and is terminated when that endpoint closes.

A restarted extension attempt gets no handles to the previous attempt's children.

For a client running a build or a test, that's the correct default and I wouldn't change it. For a supervisor it inverts the property that makes a supervisor worth having: the supervisor is supposed to be more reliable than the things it supervises. Here, updating the supervising extension, cancelling its attempt, or tripping a guest trap takes down every process it manages. A units extension could not ship a new version without killing production.

It's also inconsistent with the neighbouring family, in a way that would surface as two backings with opposite crash semantics. #167 says PTYs "remain server-session objects, exactly as they do when an ordinary network client disconnects. Attempt cleanup does not invent extension-only PTY ownership." So a PTY-backed unit survives its supervisor restarting and a pipe-backed one does not — same declarative config, same restart, different outcome, for a reason nobody writing a unit file would predict.

What I need is the weakest version of this that works: an opt-in spawn flag that makes a child outlive its endpoint, plus a way for a later endpoint to re-attach to it by a stable identity. (extension_id, process_id) would do, since #167 already gives extensions a stable ID across attempts. I don't need detached children to be discoverable server-wide, or adoptable by an unrelated client, or to buffer output while unattached — dropping output during the gap is fine, and a supervisor that reconnects to a live process and a known exit status has everything it needs.

Worth saying that this is useful beyond units. Any client that wants a long-running job to survive a reconnect wants it, and today the only way to get it is CREATE2(HAS_COMMAND) — i.e. taking a PTY you don't want purely for its lifetime semantics. That's a bad reason to allocate a terminal.

Please make the per-endpoint cap reachable for a supervisor

BLIT_PROCESS_MAX_PER_CLIENT defaults to 16, and a supervisor is a single endpoint. Combined with generations staying reserved through drain, a handful of crash-looping units can exhaust it, and the failure lands on unrelated healthy units as BUDGET at the worst moment. Server-wide 64 has the same shape.

I'm not asking for a higher default — 16 is right for an interactive client. I'd like either the per-endpoint cap to be raisable for a specific endpoint, or an acknowledgement in the RFC that a supervising client should expect to hit it, so the units design sizes against it deliberately rather than discovering it in a crash loop.

Two smaller notes

KILLED doesn't say who killed it. A client-requested KILL, an endpoint teardown, and a post-grace escalation after TERMINATE all arrive as reason = KILLED, code = 0. Restart=on-failure works fine off RETURNED plus code, but "was this my own intentional stop, or did something else kill my child?" has to be reconstructed client-side by correlating against the PROCESS_CONTROLLED you got back. A sub-reason byte, or reserving a couple of the free 5..255 values, would make the common supervisor case unambiguous. Not a blocker.

BLIT_PROCESS_KILL_GRACE is server-fixed at 2 s and "clients cannot tune resources per spawn," so a unit's TimeoutStopSec= can't be expressed through TERMINATE. The workaround is fine and I'll take it — SIGNAL(SIGTERM), run my own timer, then KILL — but it does mean TERMINATE is unusable for anything with a per-unit stop timeout, which is most of what I'd want it for. Worth a sentence noting that supervisors are expected to do their own escalation.

Everything else here I'd take as-is.

@pcarrier

pcarrier commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in fe67fb5 and rebased onto current #167. The RFC now specifies opt-in DETACHABLE children with client-persisted 128-bit adoption tokens; atomic PROCESS_ADOPT / PROCESS_ADOPTED snapshot-to-live binding; bounded output gaps, stdin inheritance, detach cutoffs, terminal-result retention, and endpoint-loss races; explicit KILLED causes; supervisor BUDGET guidance; and the Unix custom-grace workaround / Windows limitation. It also serializes child registration against the existing waitpid(-1) backstop and makes process-lifetime tasks server-owned so an extension replacement can adopt a surviving child.

@pcarrier
pcarrier force-pushed the codex/native-process-rfc branch from fe67fb5 to 5493f9b Compare August 6, 2026 01:21
pcarrier added a commit that referenced this pull request Aug 6, 2026
…dlines, retention (#204)

Closes #181.

Implements all three gaps in #181. They share one cause — nothing in the server owned a PTY's lifetime — and one implementation: a supervisor loop that runs when the delivery tick does not.

Five commits, each building standalone and separately verified:

| Commit | #181 item | What it fixes |
| --- | --- | --- |
| `answer a refused create instead of dropping it` | 3 (half) | all four create arms refuse with a bare `continue`, so a nonce-bearing client waits forever |
| `kill a terminal's process group, not just its leader` | 2 | `kill(pid)` / `kill(pid, SIGHUP)` reached the session leader alone — kill a shell, keep its children |
| `detect a terminal's exit from the child, not from EOF` | 1, 2 | exit detection was EOF-on-master, which means "the slave closed", not "the child exited" |
| `enforce opt-in terminal deadlines, and say when one fired` | 1 | every timeout was client-side, so none survived the client that set it |
| `bound retained terminals, and count the cap against live ones` | 3 (rest) | nothing but an explicit `CLOSE` ever removed an exited entry |

## Relationship to #188

#188 made `BLIT_MAX_PTYS` reachable and argued — correctly — that unlimited is the right default. It also documented the gap this PR closes, in `allocate_pty_id`: *"the protocol has no 'create refused' message"*, settling for an `eprintln` so the cap at least leaves a trace in the server log.

So the cap could be set but not safely used: turning it on traded an unbounded terminal count for a client that hangs. This adds the missing message and **leaves the default at 0**. `--max-ptys` is added alongside the env var for symmetry with the other server knobs; the `eprintln` stays, because the older create opcodes still drop the request silently by design.

The last commit does change the cap's *counting* to live terminals only, so a client running short commands under `--max-ptys N` is not refused after N of them with nothing running. Exited terminals get their own bound instead.

## Verification

Each commit message records its own check. The load-bearing ones, all re-run after the rebase onto main:

- **Refusal**: server with `--max-ptys 1` refuses the second create in milliseconds with `budget exhausted (terminal cap reached (1); raise --max-ptys or close a terminal)` and exit 1. Previously: a 10s hang, then a generic socket timeout.
- **Exit detection**: A/B'd against a pre-change server with the same command, `bash -c '(trap "" HUP; sleep N) & exit 7'` — a grandchild that ignores the hangup and keeps the slave open. Before: the terminal sits at `running` indefinitely. After: `exited(7)`.
- **Deadlines**: a terminal created with `--deadline 5` and abandoned dies at ~5s with no client attached. `blit terminal wait` prints `signal(15) — killed by deadline` where a hand-rolled `kill 9` prints a bare `signal(9)`. Refreshed every 2s against a 4s deadline it survived 12s, then died 8s after the refreshes stopped.
- **Retention**: with `--max-ptys 2 BLIT_MAX_EXITED=3`, six consecutive short commands all succeed and the list settles at the newest three.
- **Group kill**: two tests pin both halves — one asserts a child survives a leader-only kill, the other that a group kill reaches it. Mutation-checked by flipping the second to leader-only and confirming it fails.

Workspace clippy clean, `cargo fmt` clean, 556 Rust tests and 812 JS tests passing, JS typecheck clean.

## Review notes

**Two things not verified here.** The Windows job-object half has no toolchain in this checkout (Nix, no rustup) and rests on CI's windows build. And the third commit removes `reap_zombies`' global `waitpid(-1)` drain — a strict improvement, since it was reaping other subsystems' children and discarding their statuses out from under the audio pipeline's own `try_wait`, but it is a change outside the PTY family.

**Group kill's limit, stated rather than papered over.** It reaches the leader's process group and, via `TIOCGPGRP`, the terminal's foreground group. A job backgrounded by an interactive shell is in neither and survives. Bounding that needs a cgroup, not a signal.

**Feature bits 11–13 are left unallocated** for the extension, channel, and process families under review in #167 and #173. This takes 14 (`CREATE_STATUS`), 15 (`KILL_MODE`), and 16 (`PTY_DEADLINE`), matching the allocation #167's `protocol.md` already proposes for 14. The common status registry this introduces is #167's design; landing it here means #167 can drop that section rather than restate it.

**Five pre-existing test failures in `crates/git`** are unrelated — identical 55-passed/5-failed on a tree with none of these changes (a local git config makes `git tag v1` demand a message).

## Follow-ups, deliberately not in here

- `docs/design/units.md` (#94) needs reconciling before it merges: it allocates `S2C_LEASE = 0x10`, which this PR now uses for `CREATE_FAILED`; it gives `CREATE_FAILED` a different opcode *and* payload; and its "the `C2S_KILL` flags arm is `data.len() >= 7`" is off by one, since 7 is the existing message length.
- The timed `C2S_CLOSE` escalation from units.md needs `CLOSE` to hold the entry in a "closing" state, which tangles with the retention path, and is not part of what #181 asks for.
- Bounding the *aggregate* `S2C_LIST` size needs a logical-message ceiling that does not exist yet. The per-field `TOO_LARGE` check is in.
@pcarrier pcarrier mentioned this pull request Aug 11, 2026
@pcarrier
pcarrier force-pushed the codex/native-process-rfc branch from 5493f9b to 6888864 Compare August 13, 2026 02:09
@blacksmith-sh

blacksmith-sh Bot commented Aug 13, 2026

Copy link
Copy Markdown

Found 6 test failures on Blacksmith runners:

Failures

Test View Logs
[chromium] › tests/
font-size-sync.spec.ts:11:5 › font size stays local while previewing and reaches peers
on Apply
View Logs
[chromium] › tests/
pane-tools.spec.ts:193:7 › Parked terminal does not resurrect › closing the session tha
t displaced a parked one shows it, not an empty pane
View Logs
[chromium] › tests/
terminal.spec.ts:137:7 › Terminal › Switcher preview canvases render with non-zero dime
nsions
View Logs
[chromium] › tests/
touch-draggables.spec.ts:205:7 › Touch drag on list rows › a swipe across a dock card s
till dismisses it
View Logs
tests/font-size-sync.spec.ts/
font size stays local while previewing and reaches peers on Apply
View Logs
Touch drag on list rows/a swipe across a dock card still dismisses it View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

Base automatically changed from codex/wasmi-plugin-rfc to main August 13, 2026 02:27
pcarrier added a commit that referenced this pull request Aug 13, 2026
## Summary

- define Wasmi-hosted Rust extensions as in-process logical Blit clients
using the ordinary packet dispatcher
- expose a four-call host ABI: packet `send`, blocking packet `recv`,
direct realtime/monotonic `clock`, and OS-backed `random`
- use BLAKE3 content-addressed upload so `blit run --on ... FILE
ARGS...` sends module bytes only on a cache miss
- supervise attached, detached, autorestarting, and restart-persistent
extensions, with one named OS thread per running attempt
- add reliable bidirectional named channels as an ordinary packet family
- let live named persistent extensions advertise discoverable `@name`
CLI command trees, with invocation I/O carried over channels

## Lifecycle and resource model

- a module hash identifies exact immutable Wasm bytes; an `extension_id`
identifies one supervised installation; `(extension_id, attempt)`
identifies one Wasmi instance
- the same hash can back multiple isolated extensions, and separate
durable names permit concurrent versions such as `builder` and
`builder-canary`
- failures restart with backoff under `on-failure`; update,
cancellation, shutdown, and persistence have explicit supervisor
transitions
- the raw Wasm CAS uses automatic unpinned LRU under a 2 GiB default
budget; persistent definitions pin their objects
- server-wide defaults bound running threads, supervisors, storage,
validation, guest memory/tables/stacks, and transport queues

## CLI and command extensibility

- `blit ext run --on ... FILE ARGS...` installs/runs an extension; `blit
run` is an alias and positional tokens after `FILE` are guest arguments
- `blit ext update NAME FILE ARGS...` atomically switches a persistent
definition after ID/revision checks
- a live named persistent extension registers a bounded `blit.cli.v1`
descriptor and channel listener through `EXT_COMMAND`
- `blit ext commands`, `@name --help`, and completion use immutable
directory snapshots; invocation is never retried across attempts

## Key decisions

- Wasmi only; no QuickJS, WASI, manifest, capability/grant model,
per-run limits, state, or topic family
- one guest target, `wasm32-unknown-unknown`, and one host import
module, `blit_v1`
- direct host services are packet I/O, clocks, and entropy; every Blit
operation remains a packet
- pipe-oriented non-PTY process execution is independent and moved to
draft #173

## Protocol allocation

- feature bit 11: `EXTENSION`, direction-local `0x90` through `0x94`
- feature bit 12: `CHANNEL`, bidirectional `0x95`; `0x96` remains free
- feature bit 13 remains unallocated in this PR
- feature bit 14: proposed correlated `CREATE2(WANT_STATUS)` failures

## Split follow-ups

- #173: standalone all-client non-PTY process RFC
- #174: existing-family common-status implementation and diagnostics
cleanup

Both are stacked on this branch so #167 remains focused and reviewable.
They can merge separately after the base.

## Scope and verification

This PR is a design RFC plus protocol documentation corrections. It does
not implement the runtime, SDK, CLI, packet families, or non-PTY process
protocol.

- `git diff --check` passes
- the focused net diff is `docs/design/extensions.md`, selected
`docs/protocol.md` changes, and the stale Git opcode comment correction
@pcarrier
pcarrier force-pushed the codex/native-process-rfc branch from 6888864 to 850902e Compare August 13, 2026 02:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant