✨ Open terminal grids with tmux in foreground runs (#732) - #747

Draft
taras wants to merge 15 commits into
agent/issue-731-pane-native-launchfrom
agent/issue-732-tmux-grid
Draft

✨ Open terminal grids with tmux in foreground runs (#732)#747
taras wants to merge 15 commits into
agent/issue-731-pane-native-launchfrom
agent/issue-732-tmux-grid

Conversation

@taras

@tarastaras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes#732. Fifth and final implementation Story under Quest #717, stacked on #741.

Base is agent/issue-731-pane-native-launch, not main, as the Story's verification stack directs. Branched from 8b8936c25c5b1c934f7ee1a4b003b8324c2e3452, the accepted head of #741.

Draft: checkpoint 3 of 3 is outstanding. Host installation, Node/Bun refusal, SIGHUP → structured cancellation, the CLI regressions, and the architecture inventory update are not in this branch yet. Everything below is delivered and verified.

Why

#729 froze the language, #730 made a grid execute through a replaceable provider, and #731 let native Agent sessions live in its panes — all against a controlled provider that presents nothing. This Story is the first production presentation: an authored grid opening as one foreground tmux workspace, with no tmux command or identifier anywhere in the document.

What changes

Before: xmd run on a real terminal had the whole grid contract and nothing to present it with.

After: the Deno and compiled foreground hosts open the grid through one invocation-private tmux server — panes in the authored row-major order, each running a worker that owns its pane's terminal.

How it works

prerequisites → private dir (0700) + per-pane socket + 0600 token
→ private tmux server → pane per ordinal, each on its worker
→ explicit layout + swaps into authored order
→ workers authenticate → readiness → attach
→ … → detach asked → workers quiesce → server proved gone → dir removed

packages/runtime/terminal-processes.ts is the host's answer to "is this pane free": the process table, terminal holders, signal delivery and reachability, behind one seam whose default refuses every question. Refusing is the point — "nobody is there" and "I cannot see" are the two answers a quiescence proof must never confuse. paneOccupants() snapshots before the first signal, because a killed child's children reparent to init.

packages/cli/src/terminal/layout.ts writes the layout string tmux prints and accepts, sized row-major from the authored column count. select-layout tiled cannot implement an authored columns — the same four panes become 2×2 in one terminal and 4×1 in another. tmux also fills leaves in window-list order and ignores the pane ids in them, so authored order is imposed afterwards by swapsInto().

packages/cli/src/terminal/pane-channel.ts + pane-protocol.ts + pane-worker.ts are the private channel and the worker. Exact argv, cwd and environment cross the socket as bytes; tmux's parser is told a directory and an ordinal and nothing else. The worker is xmd terminal-worker <ordinal> <dir> — this executable, so it works in the compiled distribution — dispatched at the entrypoint before main() and run under run(), because main() binds SIGINT to its own shutdown and would eat the first ^C typed into a pane.

packages/cli/src/terminal/tmux-grid.ts is the hidden composite. Three clients kept apart: the reader's visible one, a control-mode client attached -f no-output so pane bytes never reach this process, and the workers, which are not clients at all. An attach client's exit code cannot classify a close — it is 0 after detach-client, 0 after kill-session and 1 after kill-server — so the control channel does it.

packages/cli/src/terminal/attach-client.ts owns the visible client and nothing else. A pane child is settled by sweeping its group and its terminal; the reader's terminal is held by XMD, whatever started XMD, and the rest of its foreground group, so a settlement of that shape aimed at this client is aimed at the document.

Review guide

Start with:packages/cli/src/terminal/attach-client.ts — the narrowest boundary in the change, and the one with the most to lose.

Then review:

  1. packages/runtime/terminal-processes.ts — what may be claimed about a pane, and the refusing default.
  2. packages/cli/src/terminal/pane-worker.ts and pane-channel.ts — the handshake and what crosses it.
  3. packages/cli/src/terminal/tmux-grid.ts — the three clients, and stop().
  4. packages/cli/src/terminal/layout.ts — the string, and why the swaps exist.

Look carefully at:

  • escalate() in pane-child.ts excluding the worker's ancestors. A pane worker is tmux's session leader, so its group holds nothing above it — but a settlement one signal from killing the run that started the grid should not rest on the topology being what it should be. TW9 found this by sweeping the test runner.
  • stop() in tmux-grid.ts: a successful kill-server is not proof. It refuses unless the recorded pid is unreachable and the server refuses its session, and the socket file is not part of it because it outlives the server.

What must stay true

  • A pane is free only when it is proved free — enforced by the refusing seam default, checked by TP1 and TP5–TP9.
  • Nothing private in a diagnostic — enforced by TmuxCommandFailed carrying the step name alone, checked by TG10 and TG13 against planted markers in the socket, session, pane/client identifiers, worker directory, argv, environment and stderr.
  • The visible client's escalation names one pid — enforced by attach-client.ts never reading a group, descendants or holders, checked by TG11 and TG13 with bystanders in the table.
  • Authored order survives a server that ignores the layout's ids — enforced by swapsInto(), checked by TG2 against a fake that reproduces that behaviour.
  • The private directory outlives its sockets — enforced by finalizer ordering plus awaited closures, checked by TG12 counting real close events at the moment of removal.

How to verify it

deno task test packages/runtime/tests/terminal-processes.test.ts # 1 passed (9 steps)
deno task test packages/cli/tests/terminal-grid-tmux.test.ts # 3 passed (34 steps)
deno task test packages/cli/tests/session-launch-cli.test.ts # 1 passed (5 steps)

deno task check exit 0; deno task lint 0 errors.

Tier TW starts a real worker process over a real socket — no tmux at all — and covers the modes, the token's single use, three ways of failing the handshake, awkward argv (spaces, ;, both quote kinds, $HOME) arriving intact, a child that never starts, one-live-child exclusivity, display written and never read, and shutdown's final sweep.

Tier TG runs against a fake server that reproduces the two behaviours this code exists for: a split inserts its pane into the window list after the one it split, and layout leaves are filled in window-list order with the ids ignored. Not faked: the layout string, the swap decisions, and real control-mode bytes from a fixture process through the same splitter and classifier.

Every substantive claim was broken on purpose and re-run: removing the swaps fails TG2; a fake that honours the leaf ids also fails TG2; settling the visible client like a pane child fails TG11; restoring raw arguments to a failure fails TG10; not awaiting the socket closures fails TG12; discarding the final wait after SIGKILL fails TG13.

Scope

Included

  • The host process/terminal observer and its quiescence shapes.
  • Layout, swaps, the private channel, the worker, the hidden composite, the visible client.

Intentionally unchanged

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • The description matches the final diff and test results — checkpoint 3 still to come.

A terminal grid may not report a pane settled, admit the next launch into it, or
let the document continue while something a launch started can still act. A PID,
a delivered signal, an attach client going away and an elapsed timeout each
establish none of that.
`packages/runtime/terminal-processes.ts` is what does: the process table,
terminal holders, signal delivery and reachability, behind one host seam whose
own default refuses every question. Refusing is the point — "nobody is there"
and "I cannot see" are the two answers a quiescence proof must never confuse, so
a host that installs no observer stops the document rather than reporting a pane
quiet it never looked at. The POSIX handler answers with `ps` and `lsof`; the
`lsof` sweep is the expensive half and grows with the process count, which is
why it is behind the seam rather than inlined.
Two shapes carry the rule. `paneOccupants()` takes the snapshot — the child, its
descendants, its process group — and must be taken *before* the first signal,
because a killed child's children reparent to init and a later reading names
fewer processes than the launch actually started. `establishQuiescence()` asks
about every one of them and about the terminal, and reports everything still
true rather than the first thing it found.
Nothing here decides policy. It reports; the pane worker finishing a launch and
the provider tearing a grid down decide what the report means.
Tier TP proves the difference between establishing and assuming: a host with no
observer refuses, the POSIX reader finds this process in the real table, a
snapshot read after a kill names nobody, and a pane whose child is gone is still
not free while a descendant runs or anything else holds the terminal.
`select-layout tiled` picks its own column count from the window's dimensions,
so the same four panes are 2×2 in one terminal and 4×1 in another. An authored
`columns` has to be told to tmux rather than asked of it.
`packages/cli/src/terminal/layout.ts` writes the description tmux prints in
`#{window_layout}` and accepts back: a checksum, then a tree of cells sized
row-major from the pane count and the authored column count. A final row with
fewer panes than columns spans the row, because tmux has no empty cells and the
author wrote panes rather than a rectangle.
One thing the string cannot do is place a particular pane — tmux fills the
leaves in window-list order and ignores the pane ids they name — so authored
order is imposed afterwards by swaps. `swapsInto()` says which, produces none
for an order that is already right, and refuses a window that does not hold a
pane the author wrote instead of putting some other pane there.
Tier TX checks the geometry at four terminal sizes, that the cells tile exactly
with one separator between them, that the checksum tracks the tree, and all
three swap cases.
A pane's initial process is a worker that owns the pane's terminal for the
pane's whole life, and everything it does is asked of it over a socket only this
invocation can reach.
**The channel.** One directory per grid, mode 0700, directly under `$TMPDIR`
because a Unix socket path is capped at 104 bytes and a directory named after a
repository path spends most of that first. Inside it, one socket and one
mode-0600 token per pane, both written before any pane exists, so a worker that
starts finds its socket listening rather than racing it. Admission is the whole
boundary: a connection is admitted when its first frame is a `hello` naming this
pane's ordinal and carrying this pane's token, and a connection that says
anything else, says it late, names another ordinal, or arrives after that pane is
admitted is closed without being answered. The token is single-use because the
worker removes the file as it reads it.
**What crosses it.** The exact argv vector, working directory and environment.
tmux has a command parser, and a command parser is a place where an argument can
become two arguments, or a quote, or a `;`. tmux is told a directory and an
ordinal, and that is all its parser ever sees.
**The worker.** `xmd terminal-worker <ordinal> <dir>` — reusing this executable
rather than shipping a second script, which is what makes it work in the
compiled distribution. It is in no command table, so it is in no help output and
no catalog, and naming it grants nothing: without a pane's single-use token
nobody answers. It is dispatched at the entrypoint, before `main()`, and runs
under `run()`, because `main()` binds SIGINT to its own shutdown and would exit
130 on the first `^C` typed into the pane — the keystroke the foreground child is
supposed to receive. It ignores SIGINT, SIGQUIT and SIGTSTP itself so the child,
which gets default dispositions across `exec`, is the one interrupted.
**Readiness and settlement, kept apart.** Readiness is the runtime's `spawn`
event and nothing earlier; a missing executable delivers `error` instead of it,
never after it. Settlement is the escalation and sweep that follow — a child that
exited on its own may have left descendants in its group or an orphan holding the
terminal, and the pane is not free until neither is true. `exited` is reported
only after that, so the next launch is refused while a sweep that would reach it
is still running.
One hazard the evidence found: the settlement sweeps the process group it is in,
and a worker that was not a session leader would be sweeping whatever started it.
In a pane tmux makes it one — but a settlement one signal away from killing the
run that started the grid is not something to leave to the topology being what it
should be, so the sweep now never reaches an ancestor of the worker.
Tier TW proves it with a real worker process over a real socket and no tmux at
all: the modes, the removal, the handshake, three ways of failing it, awkward
argv crossing intact, a child that never starts, one-live-child exclusivity,
display written and never read, and shutdown's final sweep.
One invocation-private server per grid, on its own socket, started with
`-f /dev/null` so a reader's `.tmux.conf` cannot redecide an authored layout. A
pane per authored ordinal, each running that pane's worker — tmux's parser sees
an ordinal and a directory and never a launch's argv. Nothing is visible until
`attach()`, which core calls only after every pane has reported a start.
Three clients, kept apart because they answer different questions. The visible
one is the reader's. The control one attaches `-f no-output`, so pane bytes
never travel through this process, and what it reports is how reader detach,
server stop and control loss are told apart — an attach client's exit code
cannot tell them apart, being 0 after `detach-client`, 0 after `kill-session`
and 1 after `kill-server`. The workers are not clients at all; they are the
panes.
Teardown is registered before the first command, so a composite that fails
half-built still takes its server down. A detach is *asked for* before anything
is signalled, because a client that leaves restores the terminal and one that is
killed cannot. `stop()` establishes the server pid is unreachable and the server
refuses its session — never the socket file's absence, which outlives it.
`probeTmux()` answers the prerequisites before a server exists: a terminal to
divide, and a tmux new enough to divide it as an authored layout needs.
Tier TG runs against a fake server that reproduces the behaviours this code
exists to work around — a split inserts its pane into the window list after the
one it split, and a layout string's leaves are filled in window-list order with
the ids in them ignored. What is not faked is the composite: the same layout
string, the same swap decisions, and real control-mode lines from a fixture
process through the same splitter and classifier.
Both halves of the ordering claim were broken on purpose: removing the swaps
fails TG2, and a fake that honours the leaf ids fails TG2 as well — so the row
is passing because the composite imposes the order, not because the two happened
to coincide.
Stated plainly, and not claimed here: a fixture client inherits a pipe, so it
cannot restore a terminal it never had. That a real `tmux attach` gives the
reader's terminal back when asked to detach is #726's evidence on real tmux.
**The visible client is not a pane child.** A pane child is settled by sweeping
its process group and its terminal, because a pane's terminal belongs to the
grid. The reader's terminal belongs to the run: everything holding it is XMD,
whatever started XMD, and the rest of XMD's foreground group. A settlement of
that shape aimed at the attach client is a settlement aimed at the document.
`attach-client.ts` owns exactly one process instead — asked to detach first,
through tmux, and only then insisted on by pid, with no group, no descendants
and no terminal sweep anywhere in it.
**A successful `kill-server` is not proof.** Teardown now succeeds only once the
recorded server pid is unreachable and the server refuses its own session, and
throws a provider-neutral `TerminalTeardownFailed` when either is still unproved
at the bound. The rule is in the resource finalizer too, so a preparation that
failed halfway is held to it as well.
**Nothing private in a diagnostic.** `TmuxCommandFailed` carries the step's name
and nothing else — not the arguments, which hold the socket path, session name,
pane and client identifiers and the worker's private directory, and not stderr,
which tmux writes paths into. A provider's topology stays private on the paths
taken when something goes wrong, which are the paths a diagnostic is read on.
**Closures before removal.** The private directory is removed only after every
accepted socket and every listening server has actually closed — counted from
their own `close` events rather than from having been asked.
Three regressions, each broken on purpose and re-run:
- TG11 gives the process table company — XMD, its parent, two more in the same
group, and four holders of the reader's terminal — and proves the escalation
reaches the client's pid alone. Settling it like a pane child fails it.
- TG10 plants markers in the socket, session, pane and client identifiers, the
worker directory, the arguments and stderr, and proves none reaches the
surfaced error. Restoring raw arguments fails it.
- TG12 counts real closures at the moment of removal. Not awaiting them fails it.
Also conformed to the repository's rules: `@effectionx/fs` for stat, rm,
readTextFile and writeTextFile, with `node:fs/promises` kept only for `chmod`
and `appendFile`, both adapted through `until`; the client fixture is an
Effection operation; and the newly introduced `as const` assertions are gone in
favour of typed values.
`end()` sent SIGKILL and then discarded what the wait after it established, so
a client still holding the reader's terminal was reported as torn down. The
shared `stop()` resolved successfully on top of that, and the document carried
on.
It now establishes the client is gone, and raises a provider-neutral teardown
failure when it is not — so `stop()` rejects and the document stops instead.
`leftWithin()` also looks once more at the boundary itself rather than falling
back on the cached exit event: a client that left during the final interval is
gone, and reporting it as still there would be reporting a stale reading.
The boundary is unchanged and still narrow: detach is asked for through tmux
first, and every signal after that names the exact client pid. Nothing inspects
or signals its process group, its descendants, or the holders of the reader's
terminal — on this terminal, each of those is the run itself. The refusal
carries none of the socket, session, client name, argv, environment, terminal
or host message.
TG13 models a client that survives the ask, SIGTERM and SIGKILL: teardown
refuses, the signals delivered are exactly SIGTERM and SIGKILL to the client's
pid, three same-group bystanders and three holders of the reader's terminal are
untouched, and no planted marker reaches the refusal. TG11's successful
escalation is unchanged.
Reinstating the discarded result fails TG13 and leaves TG11 green, which is the
discrimination the two rows are for.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 7 redundant comments. Inline suggestions to remove them below.

child = spawnChild(command, args, {
cwd: options.cwd,
env: options.env,
// The reader's terminal, handed straight through.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The reader's terminal, handed straight through.

env: request.env,
// The whole point of a pane: the child reads this terminal and draws on
// it directly, so nothing between it and the reader can buffer, reorder
// or capture what passes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// or capture what passes.

found.set(id, {
ordinal: paneIds.indexOf(id),
id,
// The worker reports `ttys003`; tmux reports `/dev/ttys003`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The worker reports `ttys003`; tmux reports `/dev/ttys003`.

while (Date.now() < deadline) {
const names = yield* clientNames(tmux);
// The control client attaches with no tty of its own, so a named client is
// the visible one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// the visible one.

switch (command) {
case "new-session": {
alive = true;
// `... -c <cwd> <command...>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// `... -c <cwd> <command...>`

case "set":
return "";
case "split-window": {
// `... -t <target> -c <cwd> -P -F #{pane_id} <command...>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// `... -t <target> -c <cwd> -P -F #{pane_id} <command...>`

*reachable([pid]): Operation<boolean> {
try {
// Signal 0 delivers nothing: it asks the kernel whether the pid is
// reachable, which is the whole question here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// reachable, which is the whole question here.

@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown

PR #747: ✨ Open terminal grids with tmux in foreground runs (#732)

32 files, +7136 / -50

Scope

🔴 PR has 7186 lines changed. Split into focused PRs.

🟡 7186 lines changed. PRs under 400 receive more thorough review.

🟡 32 files changed. Are all changes related?

🟡 Changes span 8 directories.

🟡 New abstraction files: packages/cli/src/terminal/provider.ts. Verify 3+ consumers.

Structural

Oxlint structural signals:

  • no-unused-vars ×10: packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
  • no-redundant-type-constituents ×4: packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/pane-child.ts, packages/cli/src/terminal/pane-channel.ts
  • no-empty-function ×2: packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts
  • no-unnecessary-type-arguments ×2: packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/tmux-grid.ts
  • no-unnecessary-type-assertion ×2: packages/core/src/expand.ts

Slop

  • packages/runtime/terminal-processes.ts:292 (removed)
  • packages/cli/src/compiled.ts:58// is supposed to receive.
  • packages/cli/src/deno.ts:74// is supposed to receive.
  • packages/cli/src/deno.ts:122// re-invoke itself for one pane.
  • packages/cli/src/terminal/pane-channel.ts:160// again.

Oxlint slop signals:

  • no-console ×3: packages/cli/src/cli.ts
  • no-inferrable-types ×2: packages/core/src/expand.ts

Static Analysis

Oxlint: 57 diagnostics across 12 files (19 rules)
Density: 0.008 violations/added-line

no-unused-vars (10): packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
no-unsafe-type-assertion (5): packages/cli/src/deno.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
no-floating-promises (5): packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts (+1)
no-redundant-type-constituents (4): packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/pane-child.ts, packages/cli/src/terminal/pane-channel.ts
consistent-return (4): packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/provider.ts, packages/cli/src/terminal/pane-channel.ts (+1)
no-base-to-string (4): packages/core/src/expand.ts
no-shadow (3): packages/cli/src/terminal/provider.ts, packages/runtime/terminal.ts, packages/core/src/expand.ts
no-console (3): packages/cli/src/cli.ts
unbound-method (3): packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts
consistent-function-scoping (2): packages/cli/src/terminal/pane-worker.ts, packages/cli/tests/fixtures/fake-tmux.ts
no-empty-function (2): packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts
no-array-sort (2): packages/cli/src/terminal/tmux-grid.ts, packages/cli/src/terminal/layout.ts
no-inferrable-types (2): packages/core/src/expand.ts
no-unnecessary-type-arguments (2): packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/tmux-grid.ts
no-unnecessary-type-assertion (2): packages/core/src/expand.ts
no-useless-spread (1): packages/cli/src/terminal/pane-channel.ts
no-useless-fallback-in-spread (1): packages/core/src/expand.ts
restrict-template-expressions (1): packages/core/src/expand.ts
no-implied-eval (1): packages/core/src/expand.ts

Correctness

No extraneous code patterns detected.

`provider.ts` is where #730's provider-neutral request meets tmux: it prepares
the private channels, the hidden server and the panes, resolves each pane's
worker command before a server exists, and hands core a composite it drives
through its own lifecycle. Nothing tmux-shaped crosses in either direction.
The reader leaving and the host's terminal going away settle the same
`closed()`. That is deliberate: a hangup is not a second teardown path to keep
honest separately, it is the ordinary structured close every other stop uses.
The SIGHUP listener is a resource, so it is removed with the run rather than
answering for a terminal the next one is using.
`host.ts` states which hosts present grids. The Deno entrypoint and the compiled
binary supply `foregroundTerminalGrid()`; every other caller gets
`unsupportedTerminalGrid`, which still opens the installation so a grid is
validated and refused by core rather than being silently absent. Node and Bun
therefore catalog and validate the same grids and open none — threaded through
`AgentStack` beside the machine-session assembly, which is the same shape this
repository already uses for "Deno supplies the live one, Node and Bun supply the
one that installs nothing".
architecture.md's terminal-grid inventory row said "implementation unbuilt",
which four layers had made untrue. It now says what each Story built, that the
controlled provider remains the authority for core lifecycle semantics, that
this Story's evidence uses a fake tmux with real tmux behaviour remaining
#726's, and that Node and Bun install no operational provider.
Checkpoint 3's evidence is not in this commit: the Node/Bun refusal row, the
SIGHUP-through-host-installation row, and the CLI regressions are still to come.
Implements architecture commit 802b07d.
`TerminalComposite.launch(ordinal, request, spawned)` is required of every
composite. Core closes the pane-scoped launcher over it and the pane's authored
ordinal, so after claim admission and the pane flush a `<Session.Launch>`
written in a paired pane reaches *that pane's* terminal. The ordinal lives in
core's closure and enters no native request, Agent request, session key,
construction route, durable phase, result or diagnostic.
The pane launcher is now the end of the chain. Middleware written nearer the
authored launch still composes in front and may observe, wrap, refuse or
short-circuit; what it can no longer do is reach past, because past it is the
root foreground launcher and the root terminal is the one thing a pane exists
to avoid. A composite that cannot run a pane's launch refuses — there is no
fallback, because the only thing to fall back to is the wrong terminal. Root
`<Session.Launch>` is untouched.
The tmux composite sends the exact command vector, working directory and
environment over the pane's authenticated channel; tmux's parser sees a
directory and an ordinal. `shell()` stays separate and keeps deriving the
executable from live host policy. `spawned` is invoked only for the
worker-observed runtime spawn event.
Also fails closed on settlement: `requireQuiescent()` is the rule everything
downstream is conditional on, and a settlement that could not prove the pane
free no longer clears the pane, reports success, or admits another launch.
TG20 proves the endpoint against real workers on real sockets with a fake tmux
that now starts the pane commands it is given, and with no `<TestAgent>` or
nearer launcher in front: exact argv, cwd and environment arrive at the pane's
authenticated worker; a root-foreground-launcher sentinel is never entered while
a root launch still reaches it; distinct panes launch concurrently; and a pane
the composite cannot serve refuses.
Tier GN is rewired through the endpoint, which is what exposed the gap: before
this, its pane launches reached `<TestAgent>`'s launcher and nothing could tell
that from reaching the pane. GN now separates the two — `launches` at the pane
endpoint, `agentLaunches` at the root route — and GN3 and GN8 assert both.
Carries the three evidence gaps from 7511e77 and the six repairs.
**TW13 now proves the worker.** The rejected environment switch is replaced by an
injected child seam: `runPaneWorker` takes what it starts, so a suite can run the
real worker in-process against a real channel with the one thing it cannot
arrange in another process — a child whose settlement cannot say the pane is
free. After `quiet:false` the worker clears no live entry, reports no
settlement, starts no second child, and refuses.
**TG20c is a discriminator.** Each child announces itself and blocks until both
have; a serial pair would wait for a start that had not happened.
**TG20e proves cancellation.** `runInPane()` owns it: registered before the
launch is asked for, a cancellation sends the worker's cancel and waits for a
settlement that proves the pane free. A cancelled launch does not return while
its child is live — TG20e reads the child's own pid and finds it gone.
**Process observation fails closed**, in `deno-terminal-processes.ts` behind the
runtime-named boundary Deno, the compiled binary and their pane workers install.
`kill(pid, 0)` establishes absence only for ESRCH; EPERM is a process that
exists and this user may not signal, so it raises rather than reading "I may not
ask" as "nothing is there". A `ps` that would not run is not an empty table. Only
`lsof -t`'s documented exit-1-with-no-output is read as "nobody".
**Every listener is scope-owned.** No `.once()` and no `{ once: true }` in the
touched production code: named handlers, removed by the scope that installed
them, and kept installed through any wait they resolve. The worker's SIGINT,
SIGQUIT and SIGTSTP handlers are its run scope's. TW14 counts them across event
delivery, no delivery, startup failure and cancellation.
**One ordered teardown.** `tearDown()` is idempotent and covers both core's
`destroy()` and a preparation that failed halfway: detach and prove the visible
client stopped, ask every worker to shut down, await each settlement, terminal
sweep and goodbye, refuse on anything unproved, and only then stop the server and
prove it gone. Sockets, their servers and the private directory come down after
it, in the scopes that own them.
**SIGHUP is cancellation, not a reader close.** A reader who detaches selects a
close outcome; a terminal that is gone cancels the document through the ordinary
structured path, runs the whole teardown, and lets no following sibling run.
**Hosts state what they are.** Deno and compiled install the provider and the
observer together; everyone else installs neither and still validates. TH1–TH3
cover a missing terminal, an unusable tmux, and a host with no provider.
The inventory now says what was built and what the evidence is: fake tmux with
real workers and real sockets, with real tmux behaviour on macOS remaining #726's.
…ce (#732)
**Observation carries stderr, and reads only what it understands.** `lsof -t`
exits 1 saying nothing when a file has no holders, and exits 1 *with a
diagnostic* when it could not look; without stderr those are the same status,
and one means "nobody" while the other means "I do not know". Only the exact
empty shape is accepted. A successful run whose lines are not all readable, and
a `ps` reading with lines it cannot parse, now refuse rather than answering with
the subset they happened to recognise — a sweep satisfied by that is a sweep
that never saw what was there. TP2f and TP2g cover both.
**Teardown is one retry-safe lifecycle.** It is marked complete only after it
succeeds, so a repeat caller observes the same teardown rather than skipping
unfinished work, and a teardown that failed is retried rather than remembered as
done. Per worker, in order: shutdown asked, settlement required, a goodbye that
names no surviving holder, then the channel closing — a worker that was gone,
disconnected, or stopped part-way is a failure, not a success. Channels close
before the server is stopped, and the server's absence is proved before the
private paths go. Every acquired resource is still attempted after an earlier
failure, and the first failure is what surfaces.
**Every listener is scope-owned, including the frame reader.** `readFrames()` is
a resource whose named data, close and error handlers come off on delivery, on a
frame that does not parse, on cancellation and on ordinary exit. Startup
listeners are removed once startup resolves; the ones a settlement still needs
stay until the scope ends. TW14 now counts on the emitters themselves — the
child process, and the channel's sockets and servers — across delivery, no
delivery, startup failure and cancellation, with the cancellation coordinated by
the child's own start rather than a sleep.
**Host evidence.** TH4 drives the hangup through the operation the foreground
installer wraps `Execution.document` with: it stays structured cancellation,
runs the complete teardown, and lets no following sibling run. TH5 exercises the
assembly the runtime-named entrypoints call — provider and observer together, or
neither. CL6 and CL7 add the CLI grid regressions.
One thing CL6 found and records rather than hides: a grid under a pipe is
refused at the run's foreground lease, before any provider is contacted — and
the wording it gets is the foreground launcher's, which names `<Session.Launch>`
though the document writes none. The refusal is correct and early; the sentence
is aimed at the wrong feature.
The inventory no longer says the required pane endpoint remains to be
implemented, and claims the completed teardown now that it is there.
**Every registration is named and owned.** `net.createServer(cb)` and
`server.listen(cb)` both register anonymous listeners nothing can take off
again; both are now named handlers, with `connection` removed by the channel's
scope and `listening`/`error` removed synchronously once the listen resolves,
however it resolved. `readFrames()` takes all three protocol handlers off the
moment that reader terminates — a close, an error, or a frame that is not the
protocol — and tells its consumers, because a reader that detached silently
would leave them waiting on a conversation that ended. The resource cleanup
stays for the paths that terminate nothing: a cancelled scope, and a socket that
never says anything.
`spawn` and `error` are the two answers to one question, so whichever arrives
takes both off; `exit` stays, because the settlement is still waiting on it. The
same rule for the visible attach client.
**TW14 is discriminating.** It holds references to the child processes, the
accepted socket and the servers, and asserts their listener counts after each
scope ends — delivery, no delivery, startup failure and cancellation, with the
cancellation coordinated by the child's own start signal. It caught two real
misses while being written: the server's `connection` handler was still
anonymous, and the frame reader's early detach had stopped closing its queue.
**`PaneChannels.close()` publishes before it closes.** The in-flight settlement
is created and stored first, so a concurrent caller shares this close rather
than starting a second one or being told a close that has not happened had
finished. A close that fails clears it, so the next caller retries.
**CL7 asserts the concrete refusal** — the named prop and the source location —
rather than the absence of a provider message, which an unrelated failure would
also satisfy.
**The deadlock was mine, not the provider's.** A bounded reproduction — one
self-closing pane through `foregroundTerminalGrid()`, fake tmux, a real worker
on a real socket, and a shell fixture that signals its start and then stays —
traced the whole teardown in order the moment a hangup was actually delivered:
detach, worker settlement, holder-free goodbye, channels closed, server stopped.
The earlier row never delivered one. It passed `hangup` as a provider
dependency, which an earlier repair had removed, so the override was inert and
the run waited on a real SIGHUP that never came. No production change was needed
for it, and the instrumentation is gone.
**One real defect it did expose.** `useHangupCancellation` discarded what
`next(request)` returned, so every ordinary run through the installer was
refused for having "returned before the document produced a result". The result
is returned now, and `underHangup` is typed to carry it.
**TH4 is the host boundary, driven by the installed listener.** A real document
with a live grid, through everything `foregroundTerminalGrid()` installs, with
`process.kill(process.pid, "SIGHUP")` rather than a stand-in. It proves
cancellation rather than reader close, that the sibling after the grid never
ran, that the pane's child and every worker are gone, that the server is gone,
that the private directory — removed last, after its sockets close — is gone,
and that the SIGHUP listener went with the run that installed it. Every wait is
on an event: the pane child's own start file, and each worker's own exit.
Both halves of the installer are load-bearing: removing
`useHangupCancellation()` leaves TH4 hanging on a grid nothing ends, and
removing the provider registration fails it outright.
One thing recorded rather than asserted around: cancelling the document from
inside its own middleware surfaces as core's "middleware returned before the
document produced a result" rather than as `TerminalLost`, because the guard
fires on the cancelled canonical execution first. The observable contract holds
— the run fails, teardown completes, no sibling runs — so the row asserts those
and not the wording.
… handling (#732)
The teardown was the least-covered part of this provider, and covering it
found two defects.
A close *request* could fail outside the boundary that handled the waits.
`socket.destroy()` and `server.close()` were called after their closure
watch had been attached and queued, so a request that threw left a wait
nothing would ever settle — the whole close hung rather than failing. The
requests are now inside the same boundary, a watch whose request threw is
abandoned rather than awaited, and the handle stays out of the closed set so
a later call asks it again while leaving the ones that closed alone.
A retried teardown restarted rather than resumed. Every phase was re-asked,
so a worker that had already said goodbye and gone answered the second ask
as "a worker that was gone" — and that answer replaced the reason the first
attempt could not finish. The composite's own finalizer retries after a
failed `destroy()`, so this was the ordinary path: a document was told its
pane had vanished when what had actually happened was that the server would
not stop. Phases that succeeded are now remembered, and a retry resumes at
the one that failed.
The teardown itself moves out of the composite closure into
`createGridTeardown()`, which is what lets a row drive it with scripted
workers over real private sockets.
Rows: TH5 freezes the ordinary foreground-host branch — the same live grid
as TH4, ended by a reader detach through the fake control channel instead of
a hangup, asserting the exact result handed back through
`useHangupCancellation()`. It fails if either the tmux provider or the POSIX
observer is removed from `foregroundTerminalGrid()`. TH6 freezes entrypoint
selection. Tier TD covers the combined teardown: shared in-flight teardown
under concurrent destroys, the three protocol refusals, one pane's failure
stranding neither the next pane nor the channels nor the server, first-
failure preservation, the frozen order through to path removal, the retried
close request, the resumed retry, and the document-level refusal.
Real terminal restoration remains #726's real-tmux evidence.
…732)
The suite hung forever under Node and Bun. Not failed — hung, which leaves a
runtime shard running until the job's own timeout with nothing to read.
A pane's worker is this executable re-invoked under the hidden
`terminal-worker` subcommand, and only the hosts that present grids register
it: the Deno entrypoint and the compiled binary. On Node and Bun the same
argument vector names a *document* called `terminal-worker`, so the worker
exits with ENOENT before it connects and the parent waits for a pane that
will never say hello. It stalls entering TW3, the first row that spawns a
real worker.
$ tsx packages/cli/src/node.ts terminal-worker 0 <dir>
ENOENT: no such file or directory, open 'terminal-worker'
That Node and Bun install no grid provider is the design, so the fix is the
exclusion this repository already has a mechanism for rather than a portable
worker. Every other test file in this stack runs under Node unchanged; this
is the only one that cannot.
What the exclusion does and does not preserve, stated precisely because the
rationale is the reason a later reader would trust it: provider absence is
covered portably by TG9 in packages/core/tests/terminal-grid.test.ts, which
runs on all three runtimes. TH6's entrypoint-selection freeze is textual, so
proving it once under Deno proves it everywhere. TH3 makes the same claim as
TG9 but is excluded with the rest of the file and proves nothing here. What
is genuinely Deno-only is the worker, socket and fake-tmux integration.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 4 redundant comments. Inline suggestions to remove them below.

if (paneWorker !== undefined) {
// Not `main()`: it would bind SIGINT to its own shutdown and exit 130 on the
// first `^C` typed into the pane, which is the keystroke the foreground child
// is supposed to receive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is supposed to receive.

if (paneWorker !== undefined) {
// Not `main()`: it would bind SIGINT to its own shutdown and exit 130 on the
// first `^C` typed into the pane, which is the keystroke the foreground child
// is supposed to receive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is supposed to receive.

() => useDenoWorkflowHost(HELPER),
useMachineSessions(),
// This host presents grids: it has a terminal to divide, and it can
// re-invoke itself for one pane.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// re-invoke itself for one pane.

// request that threw must not stop the others being asked. The watch for
// one that threw is abandoned rather than awaited — nothing is going to
// close it — and the handle is left out of `shut`, so a later call asks
// again.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// again.

Sign up for freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

✨ Open terminal grids with tmux in foreground runs (#732) - #747

Draft
taras wants to merge 15 commits into
agent/issue-731-pane-native-launchfrom
agent/issue-732-tmux-grid
Draft

✨ Open terminal grids with tmux in foreground runs (#732)#747
taras wants to merge 15 commits into
agent/issue-731-pane-native-launchfrom
agent/issue-732-tmux-grid

Conversation

@taras

@tarastaras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes#732. Fifth and final implementation Story under Quest #717, stacked on #741.

Base is agent/issue-731-pane-native-launch, not main, as the Story's verification stack directs. Branched from 8b8936c25c5b1c934f7ee1a4b003b8324c2e3452, the accepted head of #741.

Draft: checkpoint 3 of 3 is outstanding. Host installation, Node/Bun refusal, SIGHUP → structured cancellation, the CLI regressions, and the architecture inventory update are not in this branch yet. Everything below is delivered and verified.

Why

#729 froze the language, #730 made a grid execute through a replaceable provider, and #731 let native Agent sessions live in its panes — all against a controlled provider that presents nothing. This Story is the first production presentation: an authored grid opening as one foreground tmux workspace, with no tmux command or identifier anywhere in the document.

What changes

Before: xmd run on a real terminal had the whole grid contract and nothing to present it with.

After: the Deno and compiled foreground hosts open the grid through one invocation-private tmux server — panes in the authored row-major order, each running a worker that owns its pane's terminal.

How it works

prerequisites → private dir (0700) + per-pane socket + 0600 token
→ private tmux server → pane per ordinal, each on its worker
→ explicit layout + swaps into authored order
→ workers authenticate → readiness → attach
→ … → detach asked → workers quiesce → server proved gone → dir removed

packages/runtime/terminal-processes.ts is the host's answer to "is this pane free": the process table, terminal holders, signal delivery and reachability, behind one seam whose default refuses every question. Refusing is the point — "nobody is there" and "I cannot see" are the two answers a quiescence proof must never confuse. paneOccupants() snapshots before the first signal, because a killed child's children reparent to init.

packages/cli/src/terminal/layout.ts writes the layout string tmux prints and accepts, sized row-major from the authored column count. select-layout tiled cannot implement an authored columns — the same four panes become 2×2 in one terminal and 4×1 in another. tmux also fills leaves in window-list order and ignores the pane ids in them, so authored order is imposed afterwards by swapsInto().

packages/cli/src/terminal/pane-channel.ts + pane-protocol.ts + pane-worker.ts are the private channel and the worker. Exact argv, cwd and environment cross the socket as bytes; tmux's parser is told a directory and an ordinal and nothing else. The worker is xmd terminal-worker <ordinal> <dir> — this executable, so it works in the compiled distribution — dispatched at the entrypoint before main() and run under run(), because main() binds SIGINT to its own shutdown and would eat the first ^C typed into a pane.

packages/cli/src/terminal/tmux-grid.ts is the hidden composite. Three clients kept apart: the reader's visible one, a control-mode client attached -f no-output so pane bytes never reach this process, and the workers, which are not clients at all. An attach client's exit code cannot classify a close — it is 0 after detach-client, 0 after kill-session and 1 after kill-server — so the control channel does it.

packages/cli/src/terminal/attach-client.ts owns the visible client and nothing else. A pane child is settled by sweeping its group and its terminal; the reader's terminal is held by XMD, whatever started XMD, and the rest of its foreground group, so a settlement of that shape aimed at this client is aimed at the document.

Review guide

Start with:packages/cli/src/terminal/attach-client.ts — the narrowest boundary in the change, and the one with the most to lose.

Then review:

  1. packages/runtime/terminal-processes.ts — what may be claimed about a pane, and the refusing default.
  2. packages/cli/src/terminal/pane-worker.ts and pane-channel.ts — the handshake and what crosses it.
  3. packages/cli/src/terminal/tmux-grid.ts — the three clients, and stop().
  4. packages/cli/src/terminal/layout.ts — the string, and why the swaps exist.

Look carefully at:

  • escalate() in pane-child.ts excluding the worker's ancestors. A pane worker is tmux's session leader, so its group holds nothing above it — but a settlement one signal from killing the run that started the grid should not rest on the topology being what it should be. TW9 found this by sweeping the test runner.
  • stop() in tmux-grid.ts: a successful kill-server is not proof. It refuses unless the recorded pid is unreachable and the server refuses its session, and the socket file is not part of it because it outlives the server.

What must stay true

  • A pane is free only when it is proved free — enforced by the refusing seam default, checked by TP1 and TP5–TP9.
  • Nothing private in a diagnostic — enforced by TmuxCommandFailed carrying the step name alone, checked by TG10 and TG13 against planted markers in the socket, session, pane/client identifiers, worker directory, argv, environment and stderr.
  • The visible client's escalation names one pid — enforced by attach-client.ts never reading a group, descendants or holders, checked by TG11 and TG13 with bystanders in the table.
  • Authored order survives a server that ignores the layout's ids — enforced by swapsInto(), checked by TG2 against a fake that reproduces that behaviour.
  • The private directory outlives its sockets — enforced by finalizer ordering plus awaited closures, checked by TG12 counting real close events at the moment of removal.

How to verify it

deno task test packages/runtime/tests/terminal-processes.test.ts # 1 passed (9 steps)
deno task test packages/cli/tests/terminal-grid-tmux.test.ts # 3 passed (34 steps)
deno task test packages/cli/tests/session-launch-cli.test.ts # 1 passed (5 steps)

deno task check exit 0; deno task lint 0 errors.

Tier TW starts a real worker process over a real socket — no tmux at all — and covers the modes, the token's single use, three ways of failing the handshake, awkward argv (spaces, ;, both quote kinds, $HOME) arriving intact, a child that never starts, one-live-child exclusivity, display written and never read, and shutdown's final sweep.

Tier TG runs against a fake server that reproduces the two behaviours this code exists for: a split inserts its pane into the window list after the one it split, and layout leaves are filled in window-list order with the ids ignored. Not faked: the layout string, the swap decisions, and real control-mode bytes from a fixture process through the same splitter and classifier.

Every substantive claim was broken on purpose and re-run: removing the swaps fails TG2; a fake that honours the leaf ids also fails TG2; settling the visible client like a pane child fails TG11; restoring raw arguments to a failure fails TG10; not awaiting the socket closures fails TG12; discarding the final wait after SIGKILL fails TG13.

Scope

Included

  • The host process/terminal observer and its quiescence shapes.
  • Layout, swaps, the private channel, the worker, the hidden composite, the visible client.

Intentionally unchanged

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • The description matches the final diff and test results — checkpoint 3 still to come.

A terminal grid may not report a pane settled, admit the next launch into it, or
let the document continue while something a launch started can still act. A PID,
a delivered signal, an attach client going away and an elapsed timeout each
establish none of that.
`packages/runtime/terminal-processes.ts` is what does: the process table,
terminal holders, signal delivery and reachability, behind one host seam whose
own default refuses every question. Refusing is the point — "nobody is there"
and "I cannot see" are the two answers a quiescence proof must never confuse, so
a host that installs no observer stops the document rather than reporting a pane
quiet it never looked at. The POSIX handler answers with `ps` and `lsof`; the
`lsof` sweep is the expensive half and grows with the process count, which is
why it is behind the seam rather than inlined.
Two shapes carry the rule. `paneOccupants()` takes the snapshot — the child, its
descendants, its process group — and must be taken *before* the first signal,
because a killed child's children reparent to init and a later reading names
fewer processes than the launch actually started. `establishQuiescence()` asks
about every one of them and about the terminal, and reports everything still
true rather than the first thing it found.
Nothing here decides policy. It reports; the pane worker finishing a launch and
the provider tearing a grid down decide what the report means.
Tier TP proves the difference between establishing and assuming: a host with no
observer refuses, the POSIX reader finds this process in the real table, a
snapshot read after a kill names nobody, and a pane whose child is gone is still
not free while a descendant runs or anything else holds the terminal.
`select-layout tiled` picks its own column count from the window's dimensions,
so the same four panes are 2×2 in one terminal and 4×1 in another. An authored
`columns` has to be told to tmux rather than asked of it.
`packages/cli/src/terminal/layout.ts` writes the description tmux prints in
`#{window_layout}` and accepts back: a checksum, then a tree of cells sized
row-major from the pane count and the authored column count. A final row with
fewer panes than columns spans the row, because tmux has no empty cells and the
author wrote panes rather than a rectangle.
One thing the string cannot do is place a particular pane — tmux fills the
leaves in window-list order and ignores the pane ids they name — so authored
order is imposed afterwards by swaps. `swapsInto()` says which, produces none
for an order that is already right, and refuses a window that does not hold a
pane the author wrote instead of putting some other pane there.
Tier TX checks the geometry at four terminal sizes, that the cells tile exactly
with one separator between them, that the checksum tracks the tree, and all
three swap cases.
A pane's initial process is a worker that owns the pane's terminal for the
pane's whole life, and everything it does is asked of it over a socket only this
invocation can reach.
**The channel.** One directory per grid, mode 0700, directly under `$TMPDIR`
because a Unix socket path is capped at 104 bytes and a directory named after a
repository path spends most of that first. Inside it, one socket and one
mode-0600 token per pane, both written before any pane exists, so a worker that
starts finds its socket listening rather than racing it. Admission is the whole
boundary: a connection is admitted when its first frame is a `hello` naming this
pane's ordinal and carrying this pane's token, and a connection that says
anything else, says it late, names another ordinal, or arrives after that pane is
admitted is closed without being answered. The token is single-use because the
worker removes the file as it reads it.
**What crosses it.** The exact argv vector, working directory and environment.
tmux has a command parser, and a command parser is a place where an argument can
become two arguments, or a quote, or a `;`. tmux is told a directory and an
ordinal, and that is all its parser ever sees.
**The worker.** `xmd terminal-worker <ordinal> <dir>` — reusing this executable
rather than shipping a second script, which is what makes it work in the
compiled distribution. It is in no command table, so it is in no help output and
no catalog, and naming it grants nothing: without a pane's single-use token
nobody answers. It is dispatched at the entrypoint, before `main()`, and runs
under `run()`, because `main()` binds SIGINT to its own shutdown and would exit
130 on the first `^C` typed into the pane — the keystroke the foreground child is
supposed to receive. It ignores SIGINT, SIGQUIT and SIGTSTP itself so the child,
which gets default dispositions across `exec`, is the one interrupted.
**Readiness and settlement, kept apart.** Readiness is the runtime's `spawn`
event and nothing earlier; a missing executable delivers `error` instead of it,
never after it. Settlement is the escalation and sweep that follow — a child that
exited on its own may have left descendants in its group or an orphan holding the
terminal, and the pane is not free until neither is true. `exited` is reported
only after that, so the next launch is refused while a sweep that would reach it
is still running.
One hazard the evidence found: the settlement sweeps the process group it is in,
and a worker that was not a session leader would be sweeping whatever started it.
In a pane tmux makes it one — but a settlement one signal away from killing the
run that started the grid is not something to leave to the topology being what it
should be, so the sweep now never reaches an ancestor of the worker.
Tier TW proves it with a real worker process over a real socket and no tmux at
all: the modes, the removal, the handshake, three ways of failing it, awkward
argv crossing intact, a child that never starts, one-live-child exclusivity,
display written and never read, and shutdown's final sweep.
One invocation-private server per grid, on its own socket, started with
`-f /dev/null` so a reader's `.tmux.conf` cannot redecide an authored layout. A
pane per authored ordinal, each running that pane's worker — tmux's parser sees
an ordinal and a directory and never a launch's argv. Nothing is visible until
`attach()`, which core calls only after every pane has reported a start.
Three clients, kept apart because they answer different questions. The visible
one is the reader's. The control one attaches `-f no-output`, so pane bytes
never travel through this process, and what it reports is how reader detach,
server stop and control loss are told apart — an attach client's exit code
cannot tell them apart, being 0 after `detach-client`, 0 after `kill-session`
and 1 after `kill-server`. The workers are not clients at all; they are the
panes.
Teardown is registered before the first command, so a composite that fails
half-built still takes its server down. A detach is *asked for* before anything
is signalled, because a client that leaves restores the terminal and one that is
killed cannot. `stop()` establishes the server pid is unreachable and the server
refuses its session — never the socket file's absence, which outlives it.
`probeTmux()` answers the prerequisites before a server exists: a terminal to
divide, and a tmux new enough to divide it as an authored layout needs.
Tier TG runs against a fake server that reproduces the behaviours this code
exists to work around — a split inserts its pane into the window list after the
one it split, and a layout string's leaves are filled in window-list order with
the ids in them ignored. What is not faked is the composite: the same layout
string, the same swap decisions, and real control-mode lines from a fixture
process through the same splitter and classifier.
Both halves of the ordering claim were broken on purpose: removing the swaps
fails TG2, and a fake that honours the leaf ids fails TG2 as well — so the row
is passing because the composite imposes the order, not because the two happened
to coincide.
Stated plainly, and not claimed here: a fixture client inherits a pipe, so it
cannot restore a terminal it never had. That a real `tmux attach` gives the
reader's terminal back when asked to detach is #726's evidence on real tmux.
**The visible client is not a pane child.** A pane child is settled by sweeping
its process group and its terminal, because a pane's terminal belongs to the
grid. The reader's terminal belongs to the run: everything holding it is XMD,
whatever started XMD, and the rest of XMD's foreground group. A settlement of
that shape aimed at the attach client is a settlement aimed at the document.
`attach-client.ts` owns exactly one process instead — asked to detach first,
through tmux, and only then insisted on by pid, with no group, no descendants
and no terminal sweep anywhere in it.
**A successful `kill-server` is not proof.** Teardown now succeeds only once the
recorded server pid is unreachable and the server refuses its own session, and
throws a provider-neutral `TerminalTeardownFailed` when either is still unproved
at the bound. The rule is in the resource finalizer too, so a preparation that
failed halfway is held to it as well.
**Nothing private in a diagnostic.** `TmuxCommandFailed` carries the step's name
and nothing else — not the arguments, which hold the socket path, session name,
pane and client identifiers and the worker's private directory, and not stderr,
which tmux writes paths into. A provider's topology stays private on the paths
taken when something goes wrong, which are the paths a diagnostic is read on.
**Closures before removal.** The private directory is removed only after every
accepted socket and every listening server has actually closed — counted from
their own `close` events rather than from having been asked.
Three regressions, each broken on purpose and re-run:
- TG11 gives the process table company — XMD, its parent, two more in the same
group, and four holders of the reader's terminal — and proves the escalation
reaches the client's pid alone. Settling it like a pane child fails it.
- TG10 plants markers in the socket, session, pane and client identifiers, the
worker directory, the arguments and stderr, and proves none reaches the
surfaced error. Restoring raw arguments fails it.
- TG12 counts real closures at the moment of removal. Not awaiting them fails it.
Also conformed to the repository's rules: `@effectionx/fs` for stat, rm,
readTextFile and writeTextFile, with `node:fs/promises` kept only for `chmod`
and `appendFile`, both adapted through `until`; the client fixture is an
Effection operation; and the newly introduced `as const` assertions are gone in
favour of typed values.
`end()` sent SIGKILL and then discarded what the wait after it established, so
a client still holding the reader's terminal was reported as torn down. The
shared `stop()` resolved successfully on top of that, and the document carried
on.
It now establishes the client is gone, and raises a provider-neutral teardown
failure when it is not — so `stop()` rejects and the document stops instead.
`leftWithin()` also looks once more at the boundary itself rather than falling
back on the cached exit event: a client that left during the final interval is
gone, and reporting it as still there would be reporting a stale reading.
The boundary is unchanged and still narrow: detach is asked for through tmux
first, and every signal after that names the exact client pid. Nothing inspects
or signals its process group, its descendants, or the holders of the reader's
terminal — on this terminal, each of those is the run itself. The refusal
carries none of the socket, session, client name, argv, environment, terminal
or host message.
TG13 models a client that survives the ask, SIGTERM and SIGKILL: teardown
refuses, the signals delivered are exactly SIGTERM and SIGKILL to the client's
pid, three same-group bystanders and three holders of the reader's terminal are
untouched, and no planted marker reaches the refusal. TG11's successful
escalation is unchanged.
Reinstating the discarded result fails TG13 and leaves TG11 green, which is the
discrimination the two rows are for.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 7 redundant comments. Inline suggestions to remove them below.

child = spawnChild(command, args, {
cwd: options.cwd,
env: options.env,
// The reader's terminal, handed straight through.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The reader's terminal, handed straight through.

env: request.env,
// The whole point of a pane: the child reads this terminal and draws on
// it directly, so nothing between it and the reader can buffer, reorder
// or capture what passes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// or capture what passes.

found.set(id, {
ordinal: paneIds.indexOf(id),
id,
// The worker reports `ttys003`; tmux reports `/dev/ttys003`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The worker reports `ttys003`; tmux reports `/dev/ttys003`.

while (Date.now() < deadline) {
const names = yield* clientNames(tmux);
// The control client attaches with no tty of its own, so a named client is
// the visible one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// the visible one.

switch (command) {
case "new-session": {
alive = true;
// `... -c <cwd> <command...>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// `... -c <cwd> <command...>`

case "set":
return "";
case "split-window": {
// `... -t <target> -c <cwd> -P -F #{pane_id} <command...>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// `... -t <target> -c <cwd> -P -F #{pane_id} <command...>`

*reachable([pid]): Operation<boolean> {
try {
// Signal 0 delivers nothing: it asks the kernel whether the pid is
// reachable, which is the whole question here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// reachable, which is the whole question here.

@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown

PR #747: ✨ Open terminal grids with tmux in foreground runs (#732)

32 files, +7136 / -50

Scope

🔴 PR has 7186 lines changed. Split into focused PRs.

🟡 7186 lines changed. PRs under 400 receive more thorough review.

🟡 32 files changed. Are all changes related?

🟡 Changes span 8 directories.

🟡 New abstraction files: packages/cli/src/terminal/provider.ts. Verify 3+ consumers.

Structural

Oxlint structural signals:

  • no-unused-vars ×10: packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
  • no-redundant-type-constituents ×4: packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/pane-child.ts, packages/cli/src/terminal/pane-channel.ts
  • no-empty-function ×2: packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts
  • no-unnecessary-type-arguments ×2: packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/tmux-grid.ts
  • no-unnecessary-type-assertion ×2: packages/core/src/expand.ts

Slop

  • packages/runtime/terminal-processes.ts:292 (removed)
  • packages/cli/src/compiled.ts:58// is supposed to receive.
  • packages/cli/src/deno.ts:74// is supposed to receive.
  • packages/cli/src/deno.ts:122// re-invoke itself for one pane.
  • packages/cli/src/terminal/pane-channel.ts:160// again.

Oxlint slop signals:

  • no-console ×3: packages/cli/src/cli.ts
  • no-inferrable-types ×2: packages/core/src/expand.ts

Static Analysis

Oxlint: 57 diagnostics across 12 files (19 rules)
Density: 0.008 violations/added-line

no-unused-vars (10): packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
no-unsafe-type-assertion (5): packages/cli/src/deno.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
no-floating-promises (5): packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts (+1)
no-redundant-type-constituents (4): packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/pane-child.ts, packages/cli/src/terminal/pane-channel.ts
consistent-return (4): packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/provider.ts, packages/cli/src/terminal/pane-channel.ts (+1)
no-base-to-string (4): packages/core/src/expand.ts
no-shadow (3): packages/cli/src/terminal/provider.ts, packages/runtime/terminal.ts, packages/core/src/expand.ts
no-console (3): packages/cli/src/cli.ts
unbound-method (3): packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts
consistent-function-scoping (2): packages/cli/src/terminal/pane-worker.ts, packages/cli/tests/fixtures/fake-tmux.ts
no-empty-function (2): packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts
no-array-sort (2): packages/cli/src/terminal/tmux-grid.ts, packages/cli/src/terminal/layout.ts
no-inferrable-types (2): packages/core/src/expand.ts
no-unnecessary-type-arguments (2): packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/tmux-grid.ts
no-unnecessary-type-assertion (2): packages/core/src/expand.ts
no-useless-spread (1): packages/cli/src/terminal/pane-channel.ts
no-useless-fallback-in-spread (1): packages/core/src/expand.ts
restrict-template-expressions (1): packages/core/src/expand.ts
no-implied-eval (1): packages/core/src/expand.ts

Correctness

No extraneous code patterns detected.

`provider.ts` is where #730's provider-neutral request meets tmux: it prepares
the private channels, the hidden server and the panes, resolves each pane's
worker command before a server exists, and hands core a composite it drives
through its own lifecycle. Nothing tmux-shaped crosses in either direction.
The reader leaving and the host's terminal going away settle the same
`closed()`. That is deliberate: a hangup is not a second teardown path to keep
honest separately, it is the ordinary structured close every other stop uses.
The SIGHUP listener is a resource, so it is removed with the run rather than
answering for a terminal the next one is using.
`host.ts` states which hosts present grids. The Deno entrypoint and the compiled
binary supply `foregroundTerminalGrid()`; every other caller gets
`unsupportedTerminalGrid`, which still opens the installation so a grid is
validated and refused by core rather than being silently absent. Node and Bun
therefore catalog and validate the same grids and open none — threaded through
`AgentStack` beside the machine-session assembly, which is the same shape this
repository already uses for "Deno supplies the live one, Node and Bun supply the
one that installs nothing".
architecture.md's terminal-grid inventory row said "implementation unbuilt",
which four layers had made untrue. It now says what each Story built, that the
controlled provider remains the authority for core lifecycle semantics, that
this Story's evidence uses a fake tmux with real tmux behaviour remaining
#726's, and that Node and Bun install no operational provider.
Checkpoint 3's evidence is not in this commit: the Node/Bun refusal row, the
SIGHUP-through-host-installation row, and the CLI regressions are still to come.
Implements architecture commit 802b07d.
`TerminalComposite.launch(ordinal, request, spawned)` is required of every
composite. Core closes the pane-scoped launcher over it and the pane's authored
ordinal, so after claim admission and the pane flush a `<Session.Launch>`
written in a paired pane reaches *that pane's* terminal. The ordinal lives in
core's closure and enters no native request, Agent request, session key,
construction route, durable phase, result or diagnostic.
The pane launcher is now the end of the chain. Middleware written nearer the
authored launch still composes in front and may observe, wrap, refuse or
short-circuit; what it can no longer do is reach past, because past it is the
root foreground launcher and the root terminal is the one thing a pane exists
to avoid. A composite that cannot run a pane's launch refuses — there is no
fallback, because the only thing to fall back to is the wrong terminal. Root
`<Session.Launch>` is untouched.
The tmux composite sends the exact command vector, working directory and
environment over the pane's authenticated channel; tmux's parser sees a
directory and an ordinal. `shell()` stays separate and keeps deriving the
executable from live host policy. `spawned` is invoked only for the
worker-observed runtime spawn event.
Also fails closed on settlement: `requireQuiescent()` is the rule everything
downstream is conditional on, and a settlement that could not prove the pane
free no longer clears the pane, reports success, or admits another launch.
TG20 proves the endpoint against real workers on real sockets with a fake tmux
that now starts the pane commands it is given, and with no `<TestAgent>` or
nearer launcher in front: exact argv, cwd and environment arrive at the pane's
authenticated worker; a root-foreground-launcher sentinel is never entered while
a root launch still reaches it; distinct panes launch concurrently; and a pane
the composite cannot serve refuses.
Tier GN is rewired through the endpoint, which is what exposed the gap: before
this, its pane launches reached `<TestAgent>`'s launcher and nothing could tell
that from reaching the pane. GN now separates the two — `launches` at the pane
endpoint, `agentLaunches` at the root route — and GN3 and GN8 assert both.
Carries the three evidence gaps from 7511e77 and the six repairs.
**TW13 now proves the worker.** The rejected environment switch is replaced by an
injected child seam: `runPaneWorker` takes what it starts, so a suite can run the
real worker in-process against a real channel with the one thing it cannot
arrange in another process — a child whose settlement cannot say the pane is
free. After `quiet:false` the worker clears no live entry, reports no
settlement, starts no second child, and refuses.
**TG20c is a discriminator.** Each child announces itself and blocks until both
have; a serial pair would wait for a start that had not happened.
**TG20e proves cancellation.** `runInPane()` owns it: registered before the
launch is asked for, a cancellation sends the worker's cancel and waits for a
settlement that proves the pane free. A cancelled launch does not return while
its child is live — TG20e reads the child's own pid and finds it gone.
**Process observation fails closed**, in `deno-terminal-processes.ts` behind the
runtime-named boundary Deno, the compiled binary and their pane workers install.
`kill(pid, 0)` establishes absence only for ESRCH; EPERM is a process that
exists and this user may not signal, so it raises rather than reading "I may not
ask" as "nothing is there". A `ps` that would not run is not an empty table. Only
`lsof -t`'s documented exit-1-with-no-output is read as "nobody".
**Every listener is scope-owned.** No `.once()` and no `{ once: true }` in the
touched production code: named handlers, removed by the scope that installed
them, and kept installed through any wait they resolve. The worker's SIGINT,
SIGQUIT and SIGTSTP handlers are its run scope's. TW14 counts them across event
delivery, no delivery, startup failure and cancellation.
**One ordered teardown.** `tearDown()` is idempotent and covers both core's
`destroy()` and a preparation that failed halfway: detach and prove the visible
client stopped, ask every worker to shut down, await each settlement, terminal
sweep and goodbye, refuse on anything unproved, and only then stop the server and
prove it gone. Sockets, their servers and the private directory come down after
it, in the scopes that own them.
**SIGHUP is cancellation, not a reader close.** A reader who detaches selects a
close outcome; a terminal that is gone cancels the document through the ordinary
structured path, runs the whole teardown, and lets no following sibling run.
**Hosts state what they are.** Deno and compiled install the provider and the
observer together; everyone else installs neither and still validates. TH1–TH3
cover a missing terminal, an unusable tmux, and a host with no provider.
The inventory now says what was built and what the evidence is: fake tmux with
real workers and real sockets, with real tmux behaviour on macOS remaining #726's.
…ce (#732)
**Observation carries stderr, and reads only what it understands.** `lsof -t`
exits 1 saying nothing when a file has no holders, and exits 1 *with a
diagnostic* when it could not look; without stderr those are the same status,
and one means "nobody" while the other means "I do not know". Only the exact
empty shape is accepted. A successful run whose lines are not all readable, and
a `ps` reading with lines it cannot parse, now refuse rather than answering with
the subset they happened to recognise — a sweep satisfied by that is a sweep
that never saw what was there. TP2f and TP2g cover both.
**Teardown is one retry-safe lifecycle.** It is marked complete only after it
succeeds, so a repeat caller observes the same teardown rather than skipping
unfinished work, and a teardown that failed is retried rather than remembered as
done. Per worker, in order: shutdown asked, settlement required, a goodbye that
names no surviving holder, then the channel closing — a worker that was gone,
disconnected, or stopped part-way is a failure, not a success. Channels close
before the server is stopped, and the server's absence is proved before the
private paths go. Every acquired resource is still attempted after an earlier
failure, and the first failure is what surfaces.
**Every listener is scope-owned, including the frame reader.** `readFrames()` is
a resource whose named data, close and error handlers come off on delivery, on a
frame that does not parse, on cancellation and on ordinary exit. Startup
listeners are removed once startup resolves; the ones a settlement still needs
stay until the scope ends. TW14 now counts on the emitters themselves — the
child process, and the channel's sockets and servers — across delivery, no
delivery, startup failure and cancellation, with the cancellation coordinated by
the child's own start rather than a sleep.
**Host evidence.** TH4 drives the hangup through the operation the foreground
installer wraps `Execution.document` with: it stays structured cancellation,
runs the complete teardown, and lets no following sibling run. TH5 exercises the
assembly the runtime-named entrypoints call — provider and observer together, or
neither. CL6 and CL7 add the CLI grid regressions.
One thing CL6 found and records rather than hides: a grid under a pipe is
refused at the run's foreground lease, before any provider is contacted — and
the wording it gets is the foreground launcher's, which names `<Session.Launch>`
though the document writes none. The refusal is correct and early; the sentence
is aimed at the wrong feature.
The inventory no longer says the required pane endpoint remains to be
implemented, and claims the completed teardown now that it is there.
**Every registration is named and owned.** `net.createServer(cb)` and
`server.listen(cb)` both register anonymous listeners nothing can take off
again; both are now named handlers, with `connection` removed by the channel's
scope and `listening`/`error` removed synchronously once the listen resolves,
however it resolved. `readFrames()` takes all three protocol handlers off the
moment that reader terminates — a close, an error, or a frame that is not the
protocol — and tells its consumers, because a reader that detached silently
would leave them waiting on a conversation that ended. The resource cleanup
stays for the paths that terminate nothing: a cancelled scope, and a socket that
never says anything.
`spawn` and `error` are the two answers to one question, so whichever arrives
takes both off; `exit` stays, because the settlement is still waiting on it. The
same rule for the visible attach client.
**TW14 is discriminating.** It holds references to the child processes, the
accepted socket and the servers, and asserts their listener counts after each
scope ends — delivery, no delivery, startup failure and cancellation, with the
cancellation coordinated by the child's own start signal. It caught two real
misses while being written: the server's `connection` handler was still
anonymous, and the frame reader's early detach had stopped closing its queue.
**`PaneChannels.close()` publishes before it closes.** The in-flight settlement
is created and stored first, so a concurrent caller shares this close rather
than starting a second one or being told a close that has not happened had
finished. A close that fails clears it, so the next caller retries.
**CL7 asserts the concrete refusal** — the named prop and the source location —
rather than the absence of a provider message, which an unrelated failure would
also satisfy.
**The deadlock was mine, not the provider's.** A bounded reproduction — one
self-closing pane through `foregroundTerminalGrid()`, fake tmux, a real worker
on a real socket, and a shell fixture that signals its start and then stays —
traced the whole teardown in order the moment a hangup was actually delivered:
detach, worker settlement, holder-free goodbye, channels closed, server stopped.
The earlier row never delivered one. It passed `hangup` as a provider
dependency, which an earlier repair had removed, so the override was inert and
the run waited on a real SIGHUP that never came. No production change was needed
for it, and the instrumentation is gone.
**One real defect it did expose.** `useHangupCancellation` discarded what
`next(request)` returned, so every ordinary run through the installer was
refused for having "returned before the document produced a result". The result
is returned now, and `underHangup` is typed to carry it.
**TH4 is the host boundary, driven by the installed listener.** A real document
with a live grid, through everything `foregroundTerminalGrid()` installs, with
`process.kill(process.pid, "SIGHUP")` rather than a stand-in. It proves
cancellation rather than reader close, that the sibling after the grid never
ran, that the pane's child and every worker are gone, that the server is gone,
that the private directory — removed last, after its sockets close — is gone,
and that the SIGHUP listener went with the run that installed it. Every wait is
on an event: the pane child's own start file, and each worker's own exit.
Both halves of the installer are load-bearing: removing
`useHangupCancellation()` leaves TH4 hanging on a grid nothing ends, and
removing the provider registration fails it outright.
One thing recorded rather than asserted around: cancelling the document from
inside its own middleware surfaces as core's "middleware returned before the
document produced a result" rather than as `TerminalLost`, because the guard
fires on the cancelled canonical execution first. The observable contract holds
— the run fails, teardown completes, no sibling runs — so the row asserts those
and not the wording.
… handling (#732)
The teardown was the least-covered part of this provider, and covering it
found two defects.
A close *request* could fail outside the boundary that handled the waits.
`socket.destroy()` and `server.close()` were called after their closure
watch had been attached and queued, so a request that threw left a wait
nothing would ever settle — the whole close hung rather than failing. The
requests are now inside the same boundary, a watch whose request threw is
abandoned rather than awaited, and the handle stays out of the closed set so
a later call asks it again while leaving the ones that closed alone.
A retried teardown restarted rather than resumed. Every phase was re-asked,
so a worker that had already said goodbye and gone answered the second ask
as "a worker that was gone" — and that answer replaced the reason the first
attempt could not finish. The composite's own finalizer retries after a
failed `destroy()`, so this was the ordinary path: a document was told its
pane had vanished when what had actually happened was that the server would
not stop. Phases that succeeded are now remembered, and a retry resumes at
the one that failed.
The teardown itself moves out of the composite closure into
`createGridTeardown()`, which is what lets a row drive it with scripted
workers over real private sockets.
Rows: TH5 freezes the ordinary foreground-host branch — the same live grid
as TH4, ended by a reader detach through the fake control channel instead of
a hangup, asserting the exact result handed back through
`useHangupCancellation()`. It fails if either the tmux provider or the POSIX
observer is removed from `foregroundTerminalGrid()`. TH6 freezes entrypoint
selection. Tier TD covers the combined teardown: shared in-flight teardown
under concurrent destroys, the three protocol refusals, one pane's failure
stranding neither the next pane nor the channels nor the server, first-
failure preservation, the frozen order through to path removal, the retried
close request, the resumed retry, and the document-level refusal.
Real terminal restoration remains #726's real-tmux evidence.
…732)
The suite hung forever under Node and Bun. Not failed — hung, which leaves a
runtime shard running until the job's own timeout with nothing to read.
A pane's worker is this executable re-invoked under the hidden
`terminal-worker` subcommand, and only the hosts that present grids register
it: the Deno entrypoint and the compiled binary. On Node and Bun the same
argument vector names a *document* called `terminal-worker`, so the worker
exits with ENOENT before it connects and the parent waits for a pane that
will never say hello. It stalls entering TW3, the first row that spawns a
real worker.
$ tsx packages/cli/src/node.ts terminal-worker 0 <dir>
ENOENT: no such file or directory, open 'terminal-worker'
That Node and Bun install no grid provider is the design, so the fix is the
exclusion this repository already has a mechanism for rather than a portable
worker. Every other test file in this stack runs under Node unchanged; this
is the only one that cannot.
What the exclusion does and does not preserve, stated precisely because the
rationale is the reason a later reader would trust it: provider absence is
covered portably by TG9 in packages/core/tests/terminal-grid.test.ts, which
runs on all three runtimes. TH6's entrypoint-selection freeze is textual, so
proving it once under Deno proves it everywhere. TH3 makes the same claim as
TG9 but is excluded with the rest of the file and proves nothing here. What
is genuinely Deno-only is the worker, socket and fake-tmux integration.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 4 redundant comments. Inline suggestions to remove them below.

if (paneWorker !== undefined) {
// Not `main()`: it would bind SIGINT to its own shutdown and exit 130 on the
// first `^C` typed into the pane, which is the keystroke the foreground child
// is supposed to receive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is supposed to receive.

if (paneWorker !== undefined) {
// Not `main()`: it would bind SIGINT to its own shutdown and exit 130 on the
// first `^C` typed into the pane, which is the keystroke the foreground child
// is supposed to receive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is supposed to receive.

() => useDenoWorkflowHost(HELPER),
useMachineSessions(),
// This host presents grids: it has a terminal to divide, and it can
// re-invoke itself for one pane.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// re-invoke itself for one pane.

// request that threw must not stop the others being asked. The watch for
// one that threw is abandoned rather than awaited — nothing is going to
// close it — and the handle is left out of `shut`, so a later call asks
// again.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// again.

Sign up for freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

✨ Open terminal grids with tmux in foreground runs (#732) - #747

Draft
taras wants to merge 15 commits into
agent/issue-731-pane-native-launchfrom
agent/issue-732-tmux-grid
Draft

✨ Open terminal grids with tmux in foreground runs (#732)#747
taras wants to merge 15 commits into
agent/issue-731-pane-native-launchfrom
agent/issue-732-tmux-grid

Conversation

@taras

@tarastaras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes#732. Fifth and final implementation Story under Quest #717, stacked on #741.

Base is agent/issue-731-pane-native-launch, not main, as the Story's verification stack directs. Branched from 8b8936c25c5b1c934f7ee1a4b003b8324c2e3452, the accepted head of #741.

Draft: checkpoint 3 of 3 is outstanding. Host installation, Node/Bun refusal, SIGHUP → structured cancellation, the CLI regressions, and the architecture inventory update are not in this branch yet. Everything below is delivered and verified.

Why

#729 froze the language, #730 made a grid execute through a replaceable provider, and #731 let native Agent sessions live in its panes — all against a controlled provider that presents nothing. This Story is the first production presentation: an authored grid opening as one foreground tmux workspace, with no tmux command or identifier anywhere in the document.

What changes

Before: xmd run on a real terminal had the whole grid contract and nothing to present it with.

After: the Deno and compiled foreground hosts open the grid through one invocation-private tmux server — panes in the authored row-major order, each running a worker that owns its pane's terminal.

How it works

prerequisites → private dir (0700) + per-pane socket + 0600 token
→ private tmux server → pane per ordinal, each on its worker
→ explicit layout + swaps into authored order
→ workers authenticate → readiness → attach
→ … → detach asked → workers quiesce → server proved gone → dir removed

packages/runtime/terminal-processes.ts is the host's answer to "is this pane free": the process table, terminal holders, signal delivery and reachability, behind one seam whose default refuses every question. Refusing is the point — "nobody is there" and "I cannot see" are the two answers a quiescence proof must never confuse. paneOccupants() snapshots before the first signal, because a killed child's children reparent to init.

packages/cli/src/terminal/layout.ts writes the layout string tmux prints and accepts, sized row-major from the authored column count. select-layout tiled cannot implement an authored columns — the same four panes become 2×2 in one terminal and 4×1 in another. tmux also fills leaves in window-list order and ignores the pane ids in them, so authored order is imposed afterwards by swapsInto().

packages/cli/src/terminal/pane-channel.ts + pane-protocol.ts + pane-worker.ts are the private channel and the worker. Exact argv, cwd and environment cross the socket as bytes; tmux's parser is told a directory and an ordinal and nothing else. The worker is xmd terminal-worker <ordinal> <dir> — this executable, so it works in the compiled distribution — dispatched at the entrypoint before main() and run under run(), because main() binds SIGINT to its own shutdown and would eat the first ^C typed into a pane.

packages/cli/src/terminal/tmux-grid.ts is the hidden composite. Three clients kept apart: the reader's visible one, a control-mode client attached -f no-output so pane bytes never reach this process, and the workers, which are not clients at all. An attach client's exit code cannot classify a close — it is 0 after detach-client, 0 after kill-session and 1 after kill-server — so the control channel does it.

packages/cli/src/terminal/attach-client.ts owns the visible client and nothing else. A pane child is settled by sweeping its group and its terminal; the reader's terminal is held by XMD, whatever started XMD, and the rest of its foreground group, so a settlement of that shape aimed at this client is aimed at the document.

Review guide

Start with:packages/cli/src/terminal/attach-client.ts — the narrowest boundary in the change, and the one with the most to lose.

Then review:

  1. packages/runtime/terminal-processes.ts — what may be claimed about a pane, and the refusing default.
  2. packages/cli/src/terminal/pane-worker.ts and pane-channel.ts — the handshake and what crosses it.
  3. packages/cli/src/terminal/tmux-grid.ts — the three clients, and stop().
  4. packages/cli/src/terminal/layout.ts — the string, and why the swaps exist.

Look carefully at:

  • escalate() in pane-child.ts excluding the worker's ancestors. A pane worker is tmux's session leader, so its group holds nothing above it — but a settlement one signal from killing the run that started the grid should not rest on the topology being what it should be. TW9 found this by sweeping the test runner.
  • stop() in tmux-grid.ts: a successful kill-server is not proof. It refuses unless the recorded pid is unreachable and the server refuses its session, and the socket file is not part of it because it outlives the server.

What must stay true

  • A pane is free only when it is proved free — enforced by the refusing seam default, checked by TP1 and TP5–TP9.
  • Nothing private in a diagnostic — enforced by TmuxCommandFailed carrying the step name alone, checked by TG10 and TG13 against planted markers in the socket, session, pane/client identifiers, worker directory, argv, environment and stderr.
  • The visible client's escalation names one pid — enforced by attach-client.ts never reading a group, descendants or holders, checked by TG11 and TG13 with bystanders in the table.
  • Authored order survives a server that ignores the layout's ids — enforced by swapsInto(), checked by TG2 against a fake that reproduces that behaviour.
  • The private directory outlives its sockets — enforced by finalizer ordering plus awaited closures, checked by TG12 counting real close events at the moment of removal.

How to verify it

deno task test packages/runtime/tests/terminal-processes.test.ts # 1 passed (9 steps)
deno task test packages/cli/tests/terminal-grid-tmux.test.ts # 3 passed (34 steps)
deno task test packages/cli/tests/session-launch-cli.test.ts # 1 passed (5 steps)

deno task check exit 0; deno task lint 0 errors.

Tier TW starts a real worker process over a real socket — no tmux at all — and covers the modes, the token's single use, three ways of failing the handshake, awkward argv (spaces, ;, both quote kinds, $HOME) arriving intact, a child that never starts, one-live-child exclusivity, display written and never read, and shutdown's final sweep.

Tier TG runs against a fake server that reproduces the two behaviours this code exists for: a split inserts its pane into the window list after the one it split, and layout leaves are filled in window-list order with the ids ignored. Not faked: the layout string, the swap decisions, and real control-mode bytes from a fixture process through the same splitter and classifier.

Every substantive claim was broken on purpose and re-run: removing the swaps fails TG2; a fake that honours the leaf ids also fails TG2; settling the visible client like a pane child fails TG11; restoring raw arguments to a failure fails TG10; not awaiting the socket closures fails TG12; discarding the final wait after SIGKILL fails TG13.

Scope

Included

  • The host process/terminal observer and its quiescence shapes.
  • Layout, swaps, the private channel, the worker, the hidden composite, the visible client.

Intentionally unchanged

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • The description matches the final diff and test results — checkpoint 3 still to come.

A terminal grid may not report a pane settled, admit the next launch into it, or
let the document continue while something a launch started can still act. A PID,
a delivered signal, an attach client going away and an elapsed timeout each
establish none of that.
`packages/runtime/terminal-processes.ts` is what does: the process table,
terminal holders, signal delivery and reachability, behind one host seam whose
own default refuses every question. Refusing is the point — "nobody is there"
and "I cannot see" are the two answers a quiescence proof must never confuse, so
a host that installs no observer stops the document rather than reporting a pane
quiet it never looked at. The POSIX handler answers with `ps` and `lsof`; the
`lsof` sweep is the expensive half and grows with the process count, which is
why it is behind the seam rather than inlined.
Two shapes carry the rule. `paneOccupants()` takes the snapshot — the child, its
descendants, its process group — and must be taken *before* the first signal,
because a killed child's children reparent to init and a later reading names
fewer processes than the launch actually started. `establishQuiescence()` asks
about every one of them and about the terminal, and reports everything still
true rather than the first thing it found.
Nothing here decides policy. It reports; the pane worker finishing a launch and
the provider tearing a grid down decide what the report means.
Tier TP proves the difference between establishing and assuming: a host with no
observer refuses, the POSIX reader finds this process in the real table, a
snapshot read after a kill names nobody, and a pane whose child is gone is still
not free while a descendant runs or anything else holds the terminal.
`select-layout tiled` picks its own column count from the window's dimensions,
so the same four panes are 2×2 in one terminal and 4×1 in another. An authored
`columns` has to be told to tmux rather than asked of it.
`packages/cli/src/terminal/layout.ts` writes the description tmux prints in
`#{window_layout}` and accepts back: a checksum, then a tree of cells sized
row-major from the pane count and the authored column count. A final row with
fewer panes than columns spans the row, because tmux has no empty cells and the
author wrote panes rather than a rectangle.
One thing the string cannot do is place a particular pane — tmux fills the
leaves in window-list order and ignores the pane ids they name — so authored
order is imposed afterwards by swaps. `swapsInto()` says which, produces none
for an order that is already right, and refuses a window that does not hold a
pane the author wrote instead of putting some other pane there.
Tier TX checks the geometry at four terminal sizes, that the cells tile exactly
with one separator between them, that the checksum tracks the tree, and all
three swap cases.
A pane's initial process is a worker that owns the pane's terminal for the
pane's whole life, and everything it does is asked of it over a socket only this
invocation can reach.
**The channel.** One directory per grid, mode 0700, directly under `$TMPDIR`
because a Unix socket path is capped at 104 bytes and a directory named after a
repository path spends most of that first. Inside it, one socket and one
mode-0600 token per pane, both written before any pane exists, so a worker that
starts finds its socket listening rather than racing it. Admission is the whole
boundary: a connection is admitted when its first frame is a `hello` naming this
pane's ordinal and carrying this pane's token, and a connection that says
anything else, says it late, names another ordinal, or arrives after that pane is
admitted is closed without being answered. The token is single-use because the
worker removes the file as it reads it.
**What crosses it.** The exact argv vector, working directory and environment.
tmux has a command parser, and a command parser is a place where an argument can
become two arguments, or a quote, or a `;`. tmux is told a directory and an
ordinal, and that is all its parser ever sees.
**The worker.** `xmd terminal-worker <ordinal> <dir>` — reusing this executable
rather than shipping a second script, which is what makes it work in the
compiled distribution. It is in no command table, so it is in no help output and
no catalog, and naming it grants nothing: without a pane's single-use token
nobody answers. It is dispatched at the entrypoint, before `main()`, and runs
under `run()`, because `main()` binds SIGINT to its own shutdown and would exit
130 on the first `^C` typed into the pane — the keystroke the foreground child is
supposed to receive. It ignores SIGINT, SIGQUIT and SIGTSTP itself so the child,
which gets default dispositions across `exec`, is the one interrupted.
**Readiness and settlement, kept apart.** Readiness is the runtime's `spawn`
event and nothing earlier; a missing executable delivers `error` instead of it,
never after it. Settlement is the escalation and sweep that follow — a child that
exited on its own may have left descendants in its group or an orphan holding the
terminal, and the pane is not free until neither is true. `exited` is reported
only after that, so the next launch is refused while a sweep that would reach it
is still running.
One hazard the evidence found: the settlement sweeps the process group it is in,
and a worker that was not a session leader would be sweeping whatever started it.
In a pane tmux makes it one — but a settlement one signal away from killing the
run that started the grid is not something to leave to the topology being what it
should be, so the sweep now never reaches an ancestor of the worker.
Tier TW proves it with a real worker process over a real socket and no tmux at
all: the modes, the removal, the handshake, three ways of failing it, awkward
argv crossing intact, a child that never starts, one-live-child exclusivity,
display written and never read, and shutdown's final sweep.
One invocation-private server per grid, on its own socket, started with
`-f /dev/null` so a reader's `.tmux.conf` cannot redecide an authored layout. A
pane per authored ordinal, each running that pane's worker — tmux's parser sees
an ordinal and a directory and never a launch's argv. Nothing is visible until
`attach()`, which core calls only after every pane has reported a start.
Three clients, kept apart because they answer different questions. The visible
one is the reader's. The control one attaches `-f no-output`, so pane bytes
never travel through this process, and what it reports is how reader detach,
server stop and control loss are told apart — an attach client's exit code
cannot tell them apart, being 0 after `detach-client`, 0 after `kill-session`
and 1 after `kill-server`. The workers are not clients at all; they are the
panes.
Teardown is registered before the first command, so a composite that fails
half-built still takes its server down. A detach is *asked for* before anything
is signalled, because a client that leaves restores the terminal and one that is
killed cannot. `stop()` establishes the server pid is unreachable and the server
refuses its session — never the socket file's absence, which outlives it.
`probeTmux()` answers the prerequisites before a server exists: a terminal to
divide, and a tmux new enough to divide it as an authored layout needs.
Tier TG runs against a fake server that reproduces the behaviours this code
exists to work around — a split inserts its pane into the window list after the
one it split, and a layout string's leaves are filled in window-list order with
the ids in them ignored. What is not faked is the composite: the same layout
string, the same swap decisions, and real control-mode lines from a fixture
process through the same splitter and classifier.
Both halves of the ordering claim were broken on purpose: removing the swaps
fails TG2, and a fake that honours the leaf ids fails TG2 as well — so the row
is passing because the composite imposes the order, not because the two happened
to coincide.
Stated plainly, and not claimed here: a fixture client inherits a pipe, so it
cannot restore a terminal it never had. That a real `tmux attach` gives the
reader's terminal back when asked to detach is #726's evidence on real tmux.
**The visible client is not a pane child.** A pane child is settled by sweeping
its process group and its terminal, because a pane's terminal belongs to the
grid. The reader's terminal belongs to the run: everything holding it is XMD,
whatever started XMD, and the rest of XMD's foreground group. A settlement of
that shape aimed at the attach client is a settlement aimed at the document.
`attach-client.ts` owns exactly one process instead — asked to detach first,
through tmux, and only then insisted on by pid, with no group, no descendants
and no terminal sweep anywhere in it.
**A successful `kill-server` is not proof.** Teardown now succeeds only once the
recorded server pid is unreachable and the server refuses its own session, and
throws a provider-neutral `TerminalTeardownFailed` when either is still unproved
at the bound. The rule is in the resource finalizer too, so a preparation that
failed halfway is held to it as well.
**Nothing private in a diagnostic.** `TmuxCommandFailed` carries the step's name
and nothing else — not the arguments, which hold the socket path, session name,
pane and client identifiers and the worker's private directory, and not stderr,
which tmux writes paths into. A provider's topology stays private on the paths
taken when something goes wrong, which are the paths a diagnostic is read on.
**Closures before removal.** The private directory is removed only after every
accepted socket and every listening server has actually closed — counted from
their own `close` events rather than from having been asked.
Three regressions, each broken on purpose and re-run:
- TG11 gives the process table company — XMD, its parent, two more in the same
group, and four holders of the reader's terminal — and proves the escalation
reaches the client's pid alone. Settling it like a pane child fails it.
- TG10 plants markers in the socket, session, pane and client identifiers, the
worker directory, the arguments and stderr, and proves none reaches the
surfaced error. Restoring raw arguments fails it.
- TG12 counts real closures at the moment of removal. Not awaiting them fails it.
Also conformed to the repository's rules: `@effectionx/fs` for stat, rm,
readTextFile and writeTextFile, with `node:fs/promises` kept only for `chmod`
and `appendFile`, both adapted through `until`; the client fixture is an
Effection operation; and the newly introduced `as const` assertions are gone in
favour of typed values.
`end()` sent SIGKILL and then discarded what the wait after it established, so
a client still holding the reader's terminal was reported as torn down. The
shared `stop()` resolved successfully on top of that, and the document carried
on.
It now establishes the client is gone, and raises a provider-neutral teardown
failure when it is not — so `stop()` rejects and the document stops instead.
`leftWithin()` also looks once more at the boundary itself rather than falling
back on the cached exit event: a client that left during the final interval is
gone, and reporting it as still there would be reporting a stale reading.
The boundary is unchanged and still narrow: detach is asked for through tmux
first, and every signal after that names the exact client pid. Nothing inspects
or signals its process group, its descendants, or the holders of the reader's
terminal — on this terminal, each of those is the run itself. The refusal
carries none of the socket, session, client name, argv, environment, terminal
or host message.
TG13 models a client that survives the ask, SIGTERM and SIGKILL: teardown
refuses, the signals delivered are exactly SIGTERM and SIGKILL to the client's
pid, three same-group bystanders and three holders of the reader's terminal are
untouched, and no planted marker reaches the refusal. TG11's successful
escalation is unchanged.
Reinstating the discarded result fails TG13 and leaves TG11 green, which is the
discrimination the two rows are for.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 7 redundant comments. Inline suggestions to remove them below.

child = spawnChild(command, args, {
cwd: options.cwd,
env: options.env,
// The reader's terminal, handed straight through.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The reader's terminal, handed straight through.

env: request.env,
// The whole point of a pane: the child reads this terminal and draws on
// it directly, so nothing between it and the reader can buffer, reorder
// or capture what passes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// or capture what passes.

found.set(id, {
ordinal: paneIds.indexOf(id),
id,
// The worker reports `ttys003`; tmux reports `/dev/ttys003`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The worker reports `ttys003`; tmux reports `/dev/ttys003`.

while (Date.now() < deadline) {
const names = yield* clientNames(tmux);
// The control client attaches with no tty of its own, so a named client is
// the visible one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// the visible one.

switch (command) {
case "new-session": {
alive = true;
// `... -c <cwd> <command...>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// `... -c <cwd> <command...>`

case "set":
return "";
case "split-window": {
// `... -t <target> -c <cwd> -P -F #{pane_id} <command...>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// `... -t <target> -c <cwd> -P -F #{pane_id} <command...>`

*reachable([pid]): Operation<boolean> {
try {
// Signal 0 delivers nothing: it asks the kernel whether the pid is
// reachable, which is the whole question here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// reachable, which is the whole question here.

@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown

PR #747: ✨ Open terminal grids with tmux in foreground runs (#732)

32 files, +7136 / -50

Scope

🔴 PR has 7186 lines changed. Split into focused PRs.

🟡 7186 lines changed. PRs under 400 receive more thorough review.

🟡 32 files changed. Are all changes related?

🟡 Changes span 8 directories.

🟡 New abstraction files: packages/cli/src/terminal/provider.ts. Verify 3+ consumers.

Structural

Oxlint structural signals:

  • no-unused-vars ×10: packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
  • no-redundant-type-constituents ×4: packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/pane-child.ts, packages/cli/src/terminal/pane-channel.ts
  • no-empty-function ×2: packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts
  • no-unnecessary-type-arguments ×2: packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/tmux-grid.ts
  • no-unnecessary-type-assertion ×2: packages/core/src/expand.ts

Slop

  • packages/runtime/terminal-processes.ts:292 (removed)
  • packages/cli/src/compiled.ts:58// is supposed to receive.
  • packages/cli/src/deno.ts:74// is supposed to receive.
  • packages/cli/src/deno.ts:122// re-invoke itself for one pane.
  • packages/cli/src/terminal/pane-channel.ts:160// again.

Oxlint slop signals:

  • no-console ×3: packages/cli/src/cli.ts
  • no-inferrable-types ×2: packages/core/src/expand.ts

Static Analysis

Oxlint: 57 diagnostics across 12 files (19 rules)
Density: 0.008 violations/added-line

no-unused-vars (10): packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
no-unsafe-type-assertion (5): packages/cli/src/deno.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
no-floating-promises (5): packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts (+1)
no-redundant-type-constituents (4): packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/pane-child.ts, packages/cli/src/terminal/pane-channel.ts
consistent-return (4): packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/provider.ts, packages/cli/src/terminal/pane-channel.ts (+1)
no-base-to-string (4): packages/core/src/expand.ts
no-shadow (3): packages/cli/src/terminal/provider.ts, packages/runtime/terminal.ts, packages/core/src/expand.ts
no-console (3): packages/cli/src/cli.ts
unbound-method (3): packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts
consistent-function-scoping (2): packages/cli/src/terminal/pane-worker.ts, packages/cli/tests/fixtures/fake-tmux.ts
no-empty-function (2): packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts
no-array-sort (2): packages/cli/src/terminal/tmux-grid.ts, packages/cli/src/terminal/layout.ts
no-inferrable-types (2): packages/core/src/expand.ts
no-unnecessary-type-arguments (2): packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/tmux-grid.ts
no-unnecessary-type-assertion (2): packages/core/src/expand.ts
no-useless-spread (1): packages/cli/src/terminal/pane-channel.ts
no-useless-fallback-in-spread (1): packages/core/src/expand.ts
restrict-template-expressions (1): packages/core/src/expand.ts
no-implied-eval (1): packages/core/src/expand.ts

Correctness

No extraneous code patterns detected.

`provider.ts` is where #730's provider-neutral request meets tmux: it prepares
the private channels, the hidden server and the panes, resolves each pane's
worker command before a server exists, and hands core a composite it drives
through its own lifecycle. Nothing tmux-shaped crosses in either direction.
The reader leaving and the host's terminal going away settle the same
`closed()`. That is deliberate: a hangup is not a second teardown path to keep
honest separately, it is the ordinary structured close every other stop uses.
The SIGHUP listener is a resource, so it is removed with the run rather than
answering for a terminal the next one is using.
`host.ts` states which hosts present grids. The Deno entrypoint and the compiled
binary supply `foregroundTerminalGrid()`; every other caller gets
`unsupportedTerminalGrid`, which still opens the installation so a grid is
validated and refused by core rather than being silently absent. Node and Bun
therefore catalog and validate the same grids and open none — threaded through
`AgentStack` beside the machine-session assembly, which is the same shape this
repository already uses for "Deno supplies the live one, Node and Bun supply the
one that installs nothing".
architecture.md's terminal-grid inventory row said "implementation unbuilt",
which four layers had made untrue. It now says what each Story built, that the
controlled provider remains the authority for core lifecycle semantics, that
this Story's evidence uses a fake tmux with real tmux behaviour remaining
#726's, and that Node and Bun install no operational provider.
Checkpoint 3's evidence is not in this commit: the Node/Bun refusal row, the
SIGHUP-through-host-installation row, and the CLI regressions are still to come.
Implements architecture commit 802b07d.
`TerminalComposite.launch(ordinal, request, spawned)` is required of every
composite. Core closes the pane-scoped launcher over it and the pane's authored
ordinal, so after claim admission and the pane flush a `<Session.Launch>`
written in a paired pane reaches *that pane's* terminal. The ordinal lives in
core's closure and enters no native request, Agent request, session key,
construction route, durable phase, result or diagnostic.
The pane launcher is now the end of the chain. Middleware written nearer the
authored launch still composes in front and may observe, wrap, refuse or
short-circuit; what it can no longer do is reach past, because past it is the
root foreground launcher and the root terminal is the one thing a pane exists
to avoid. A composite that cannot run a pane's launch refuses — there is no
fallback, because the only thing to fall back to is the wrong terminal. Root
`<Session.Launch>` is untouched.
The tmux composite sends the exact command vector, working directory and
environment over the pane's authenticated channel; tmux's parser sees a
directory and an ordinal. `shell()` stays separate and keeps deriving the
executable from live host policy. `spawned` is invoked only for the
worker-observed runtime spawn event.
Also fails closed on settlement: `requireQuiescent()` is the rule everything
downstream is conditional on, and a settlement that could not prove the pane
free no longer clears the pane, reports success, or admits another launch.
TG20 proves the endpoint against real workers on real sockets with a fake tmux
that now starts the pane commands it is given, and with no `<TestAgent>` or
nearer launcher in front: exact argv, cwd and environment arrive at the pane's
authenticated worker; a root-foreground-launcher sentinel is never entered while
a root launch still reaches it; distinct panes launch concurrently; and a pane
the composite cannot serve refuses.
Tier GN is rewired through the endpoint, which is what exposed the gap: before
this, its pane launches reached `<TestAgent>`'s launcher and nothing could tell
that from reaching the pane. GN now separates the two — `launches` at the pane
endpoint, `agentLaunches` at the root route — and GN3 and GN8 assert both.
Carries the three evidence gaps from 7511e77 and the six repairs.
**TW13 now proves the worker.** The rejected environment switch is replaced by an
injected child seam: `runPaneWorker` takes what it starts, so a suite can run the
real worker in-process against a real channel with the one thing it cannot
arrange in another process — a child whose settlement cannot say the pane is
free. After `quiet:false` the worker clears no live entry, reports no
settlement, starts no second child, and refuses.
**TG20c is a discriminator.** Each child announces itself and blocks until both
have; a serial pair would wait for a start that had not happened.
**TG20e proves cancellation.** `runInPane()` owns it: registered before the
launch is asked for, a cancellation sends the worker's cancel and waits for a
settlement that proves the pane free. A cancelled launch does not return while
its child is live — TG20e reads the child's own pid and finds it gone.
**Process observation fails closed**, in `deno-terminal-processes.ts` behind the
runtime-named boundary Deno, the compiled binary and their pane workers install.
`kill(pid, 0)` establishes absence only for ESRCH; EPERM is a process that
exists and this user may not signal, so it raises rather than reading "I may not
ask" as "nothing is there". A `ps` that would not run is not an empty table. Only
`lsof -t`'s documented exit-1-with-no-output is read as "nobody".
**Every listener is scope-owned.** No `.once()` and no `{ once: true }` in the
touched production code: named handlers, removed by the scope that installed
them, and kept installed through any wait they resolve. The worker's SIGINT,
SIGQUIT and SIGTSTP handlers are its run scope's. TW14 counts them across event
delivery, no delivery, startup failure and cancellation.
**One ordered teardown.** `tearDown()` is idempotent and covers both core's
`destroy()` and a preparation that failed halfway: detach and prove the visible
client stopped, ask every worker to shut down, await each settlement, terminal
sweep and goodbye, refuse on anything unproved, and only then stop the server and
prove it gone. Sockets, their servers and the private directory come down after
it, in the scopes that own them.
**SIGHUP is cancellation, not a reader close.** A reader who detaches selects a
close outcome; a terminal that is gone cancels the document through the ordinary
structured path, runs the whole teardown, and lets no following sibling run.
**Hosts state what they are.** Deno and compiled install the provider and the
observer together; everyone else installs neither and still validates. TH1–TH3
cover a missing terminal, an unusable tmux, and a host with no provider.
The inventory now says what was built and what the evidence is: fake tmux with
real workers and real sockets, with real tmux behaviour on macOS remaining #726's.
…ce (#732)
**Observation carries stderr, and reads only what it understands.** `lsof -t`
exits 1 saying nothing when a file has no holders, and exits 1 *with a
diagnostic* when it could not look; without stderr those are the same status,
and one means "nobody" while the other means "I do not know". Only the exact
empty shape is accepted. A successful run whose lines are not all readable, and
a `ps` reading with lines it cannot parse, now refuse rather than answering with
the subset they happened to recognise — a sweep satisfied by that is a sweep
that never saw what was there. TP2f and TP2g cover both.
**Teardown is one retry-safe lifecycle.** It is marked complete only after it
succeeds, so a repeat caller observes the same teardown rather than skipping
unfinished work, and a teardown that failed is retried rather than remembered as
done. Per worker, in order: shutdown asked, settlement required, a goodbye that
names no surviving holder, then the channel closing — a worker that was gone,
disconnected, or stopped part-way is a failure, not a success. Channels close
before the server is stopped, and the server's absence is proved before the
private paths go. Every acquired resource is still attempted after an earlier
failure, and the first failure is what surfaces.
**Every listener is scope-owned, including the frame reader.** `readFrames()` is
a resource whose named data, close and error handlers come off on delivery, on a
frame that does not parse, on cancellation and on ordinary exit. Startup
listeners are removed once startup resolves; the ones a settlement still needs
stay until the scope ends. TW14 now counts on the emitters themselves — the
child process, and the channel's sockets and servers — across delivery, no
delivery, startup failure and cancellation, with the cancellation coordinated by
the child's own start rather than a sleep.
**Host evidence.** TH4 drives the hangup through the operation the foreground
installer wraps `Execution.document` with: it stays structured cancellation,
runs the complete teardown, and lets no following sibling run. TH5 exercises the
assembly the runtime-named entrypoints call — provider and observer together, or
neither. CL6 and CL7 add the CLI grid regressions.
One thing CL6 found and records rather than hides: a grid under a pipe is
refused at the run's foreground lease, before any provider is contacted — and
the wording it gets is the foreground launcher's, which names `<Session.Launch>`
though the document writes none. The refusal is correct and early; the sentence
is aimed at the wrong feature.
The inventory no longer says the required pane endpoint remains to be
implemented, and claims the completed teardown now that it is there.
**Every registration is named and owned.** `net.createServer(cb)` and
`server.listen(cb)` both register anonymous listeners nothing can take off
again; both are now named handlers, with `connection` removed by the channel's
scope and `listening`/`error` removed synchronously once the listen resolves,
however it resolved. `readFrames()` takes all three protocol handlers off the
moment that reader terminates — a close, an error, or a frame that is not the
protocol — and tells its consumers, because a reader that detached silently
would leave them waiting on a conversation that ended. The resource cleanup
stays for the paths that terminate nothing: a cancelled scope, and a socket that
never says anything.
`spawn` and `error` are the two answers to one question, so whichever arrives
takes both off; `exit` stays, because the settlement is still waiting on it. The
same rule for the visible attach client.
**TW14 is discriminating.** It holds references to the child processes, the
accepted socket and the servers, and asserts their listener counts after each
scope ends — delivery, no delivery, startup failure and cancellation, with the
cancellation coordinated by the child's own start signal. It caught two real
misses while being written: the server's `connection` handler was still
anonymous, and the frame reader's early detach had stopped closing its queue.
**`PaneChannels.close()` publishes before it closes.** The in-flight settlement
is created and stored first, so a concurrent caller shares this close rather
than starting a second one or being told a close that has not happened had
finished. A close that fails clears it, so the next caller retries.
**CL7 asserts the concrete refusal** — the named prop and the source location —
rather than the absence of a provider message, which an unrelated failure would
also satisfy.
**The deadlock was mine, not the provider's.** A bounded reproduction — one
self-closing pane through `foregroundTerminalGrid()`, fake tmux, a real worker
on a real socket, and a shell fixture that signals its start and then stays —
traced the whole teardown in order the moment a hangup was actually delivered:
detach, worker settlement, holder-free goodbye, channels closed, server stopped.
The earlier row never delivered one. It passed `hangup` as a provider
dependency, which an earlier repair had removed, so the override was inert and
the run waited on a real SIGHUP that never came. No production change was needed
for it, and the instrumentation is gone.
**One real defect it did expose.** `useHangupCancellation` discarded what
`next(request)` returned, so every ordinary run through the installer was
refused for having "returned before the document produced a result". The result
is returned now, and `underHangup` is typed to carry it.
**TH4 is the host boundary, driven by the installed listener.** A real document
with a live grid, through everything `foregroundTerminalGrid()` installs, with
`process.kill(process.pid, "SIGHUP")` rather than a stand-in. It proves
cancellation rather than reader close, that the sibling after the grid never
ran, that the pane's child and every worker are gone, that the server is gone,
that the private directory — removed last, after its sockets close — is gone,
and that the SIGHUP listener went with the run that installed it. Every wait is
on an event: the pane child's own start file, and each worker's own exit.
Both halves of the installer are load-bearing: removing
`useHangupCancellation()` leaves TH4 hanging on a grid nothing ends, and
removing the provider registration fails it outright.
One thing recorded rather than asserted around: cancelling the document from
inside its own middleware surfaces as core's "middleware returned before the
document produced a result" rather than as `TerminalLost`, because the guard
fires on the cancelled canonical execution first. The observable contract holds
— the run fails, teardown completes, no sibling runs — so the row asserts those
and not the wording.
… handling (#732)
The teardown was the least-covered part of this provider, and covering it
found two defects.
A close *request* could fail outside the boundary that handled the waits.
`socket.destroy()` and `server.close()` were called after their closure
watch had been attached and queued, so a request that threw left a wait
nothing would ever settle — the whole close hung rather than failing. The
requests are now inside the same boundary, a watch whose request threw is
abandoned rather than awaited, and the handle stays out of the closed set so
a later call asks it again while leaving the ones that closed alone.
A retried teardown restarted rather than resumed. Every phase was re-asked,
so a worker that had already said goodbye and gone answered the second ask
as "a worker that was gone" — and that answer replaced the reason the first
attempt could not finish. The composite's own finalizer retries after a
failed `destroy()`, so this was the ordinary path: a document was told its
pane had vanished when what had actually happened was that the server would
not stop. Phases that succeeded are now remembered, and a retry resumes at
the one that failed.
The teardown itself moves out of the composite closure into
`createGridTeardown()`, which is what lets a row drive it with scripted
workers over real private sockets.
Rows: TH5 freezes the ordinary foreground-host branch — the same live grid
as TH4, ended by a reader detach through the fake control channel instead of
a hangup, asserting the exact result handed back through
`useHangupCancellation()`. It fails if either the tmux provider or the POSIX
observer is removed from `foregroundTerminalGrid()`. TH6 freezes entrypoint
selection. Tier TD covers the combined teardown: shared in-flight teardown
under concurrent destroys, the three protocol refusals, one pane's failure
stranding neither the next pane nor the channels nor the server, first-
failure preservation, the frozen order through to path removal, the retried
close request, the resumed retry, and the document-level refusal.
Real terminal restoration remains #726's real-tmux evidence.
…732)
The suite hung forever under Node and Bun. Not failed — hung, which leaves a
runtime shard running until the job's own timeout with nothing to read.
A pane's worker is this executable re-invoked under the hidden
`terminal-worker` subcommand, and only the hosts that present grids register
it: the Deno entrypoint and the compiled binary. On Node and Bun the same
argument vector names a *document* called `terminal-worker`, so the worker
exits with ENOENT before it connects and the parent waits for a pane that
will never say hello. It stalls entering TW3, the first row that spawns a
real worker.
$ tsx packages/cli/src/node.ts terminal-worker 0 <dir>
ENOENT: no such file or directory, open 'terminal-worker'
That Node and Bun install no grid provider is the design, so the fix is the
exclusion this repository already has a mechanism for rather than a portable
worker. Every other test file in this stack runs under Node unchanged; this
is the only one that cannot.
What the exclusion does and does not preserve, stated precisely because the
rationale is the reason a later reader would trust it: provider absence is
covered portably by TG9 in packages/core/tests/terminal-grid.test.ts, which
runs on all three runtimes. TH6's entrypoint-selection freeze is textual, so
proving it once under Deno proves it everywhere. TH3 makes the same claim as
TG9 but is excluded with the rest of the file and proves nothing here. What
is genuinely Deno-only is the worker, socket and fake-tmux integration.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 4 redundant comments. Inline suggestions to remove them below.

if (paneWorker !== undefined) {
// Not `main()`: it would bind SIGINT to its own shutdown and exit 130 on the
// first `^C` typed into the pane, which is the keystroke the foreground child
// is supposed to receive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is supposed to receive.

if (paneWorker !== undefined) {
// Not `main()`: it would bind SIGINT to its own shutdown and exit 130 on the
// first `^C` typed into the pane, which is the keystroke the foreground child
// is supposed to receive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is supposed to receive.

() => useDenoWorkflowHost(HELPER),
useMachineSessions(),
// This host presents grids: it has a terminal to divide, and it can
// re-invoke itself for one pane.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// re-invoke itself for one pane.

// request that threw must not stop the others being asked. The watch for
// one that threw is abandoned rather than awaited — nothing is going to
// close it — and the handle is left out of `shut`, so a later call asks
// again.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// again.

Sign up for freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

✨ Open terminal grids with tmux in foreground runs (#732) - #747

Draft
taras wants to merge 15 commits into
agent/issue-731-pane-native-launchfrom
agent/issue-732-tmux-grid
Draft

✨ Open terminal grids with tmux in foreground runs (#732)#747
taras wants to merge 15 commits into
agent/issue-731-pane-native-launchfrom
agent/issue-732-tmux-grid

Conversation

@taras

@tarastaras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes#732. Fifth and final implementation Story under Quest #717, stacked on #741.

Base is agent/issue-731-pane-native-launch, not main, as the Story's verification stack directs. Branched from 8b8936c25c5b1c934f7ee1a4b003b8324c2e3452, the accepted head of #741.

Draft: checkpoint 3 of 3 is outstanding. Host installation, Node/Bun refusal, SIGHUP → structured cancellation, the CLI regressions, and the architecture inventory update are not in this branch yet. Everything below is delivered and verified.

Why

#729 froze the language, #730 made a grid execute through a replaceable provider, and #731 let native Agent sessions live in its panes — all against a controlled provider that presents nothing. This Story is the first production presentation: an authored grid opening as one foreground tmux workspace, with no tmux command or identifier anywhere in the document.

What changes

Before: xmd run on a real terminal had the whole grid contract and nothing to present it with.

After: the Deno and compiled foreground hosts open the grid through one invocation-private tmux server — panes in the authored row-major order, each running a worker that owns its pane's terminal.

How it works

prerequisites → private dir (0700) + per-pane socket + 0600 token
→ private tmux server → pane per ordinal, each on its worker
→ explicit layout + swaps into authored order
→ workers authenticate → readiness → attach
→ … → detach asked → workers quiesce → server proved gone → dir removed

packages/runtime/terminal-processes.ts is the host's answer to "is this pane free": the process table, terminal holders, signal delivery and reachability, behind one seam whose default refuses every question. Refusing is the point — "nobody is there" and "I cannot see" are the two answers a quiescence proof must never confuse. paneOccupants() snapshots before the first signal, because a killed child's children reparent to init.

packages/cli/src/terminal/layout.ts writes the layout string tmux prints and accepts, sized row-major from the authored column count. select-layout tiled cannot implement an authored columns — the same four panes become 2×2 in one terminal and 4×1 in another. tmux also fills leaves in window-list order and ignores the pane ids in them, so authored order is imposed afterwards by swapsInto().

packages/cli/src/terminal/pane-channel.ts + pane-protocol.ts + pane-worker.ts are the private channel and the worker. Exact argv, cwd and environment cross the socket as bytes; tmux's parser is told a directory and an ordinal and nothing else. The worker is xmd terminal-worker <ordinal> <dir> — this executable, so it works in the compiled distribution — dispatched at the entrypoint before main() and run under run(), because main() binds SIGINT to its own shutdown and would eat the first ^C typed into a pane.

packages/cli/src/terminal/tmux-grid.ts is the hidden composite. Three clients kept apart: the reader's visible one, a control-mode client attached -f no-output so pane bytes never reach this process, and the workers, which are not clients at all. An attach client's exit code cannot classify a close — it is 0 after detach-client, 0 after kill-session and 1 after kill-server — so the control channel does it.

packages/cli/src/terminal/attach-client.ts owns the visible client and nothing else. A pane child is settled by sweeping its group and its terminal; the reader's terminal is held by XMD, whatever started XMD, and the rest of its foreground group, so a settlement of that shape aimed at this client is aimed at the document.

Review guide

Start with:packages/cli/src/terminal/attach-client.ts — the narrowest boundary in the change, and the one with the most to lose.

Then review:

  1. packages/runtime/terminal-processes.ts — what may be claimed about a pane, and the refusing default.
  2. packages/cli/src/terminal/pane-worker.ts and pane-channel.ts — the handshake and what crosses it.
  3. packages/cli/src/terminal/tmux-grid.ts — the three clients, and stop().
  4. packages/cli/src/terminal/layout.ts — the string, and why the swaps exist.

Look carefully at:

  • escalate() in pane-child.ts excluding the worker's ancestors. A pane worker is tmux's session leader, so its group holds nothing above it — but a settlement one signal from killing the run that started the grid should not rest on the topology being what it should be. TW9 found this by sweeping the test runner.
  • stop() in tmux-grid.ts: a successful kill-server is not proof. It refuses unless the recorded pid is unreachable and the server refuses its session, and the socket file is not part of it because it outlives the server.

What must stay true

  • A pane is free only when it is proved free — enforced by the refusing seam default, checked by TP1 and TP5–TP9.
  • Nothing private in a diagnostic — enforced by TmuxCommandFailed carrying the step name alone, checked by TG10 and TG13 against planted markers in the socket, session, pane/client identifiers, worker directory, argv, environment and stderr.
  • The visible client's escalation names one pid — enforced by attach-client.ts never reading a group, descendants or holders, checked by TG11 and TG13 with bystanders in the table.
  • Authored order survives a server that ignores the layout's ids — enforced by swapsInto(), checked by TG2 against a fake that reproduces that behaviour.
  • The private directory outlives its sockets — enforced by finalizer ordering plus awaited closures, checked by TG12 counting real close events at the moment of removal.

How to verify it

deno task test packages/runtime/tests/terminal-processes.test.ts # 1 passed (9 steps)
deno task test packages/cli/tests/terminal-grid-tmux.test.ts # 3 passed (34 steps)
deno task test packages/cli/tests/session-launch-cli.test.ts # 1 passed (5 steps)

deno task check exit 0; deno task lint 0 errors.

Tier TW starts a real worker process over a real socket — no tmux at all — and covers the modes, the token's single use, three ways of failing the handshake, awkward argv (spaces, ;, both quote kinds, $HOME) arriving intact, a child that never starts, one-live-child exclusivity, display written and never read, and shutdown's final sweep.

Tier TG runs against a fake server that reproduces the two behaviours this code exists for: a split inserts its pane into the window list after the one it split, and layout leaves are filled in window-list order with the ids ignored. Not faked: the layout string, the swap decisions, and real control-mode bytes from a fixture process through the same splitter and classifier.

Every substantive claim was broken on purpose and re-run: removing the swaps fails TG2; a fake that honours the leaf ids also fails TG2; settling the visible client like a pane child fails TG11; restoring raw arguments to a failure fails TG10; not awaiting the socket closures fails TG12; discarding the final wait after SIGKILL fails TG13.

Scope

Included

  • The host process/terminal observer and its quiescence shapes.
  • Layout, swaps, the private channel, the worker, the hidden composite, the visible client.

Intentionally unchanged

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • The description matches the final diff and test results — checkpoint 3 still to come.

A terminal grid may not report a pane settled, admit the next launch into it, or
let the document continue while something a launch started can still act. A PID,
a delivered signal, an attach client going away and an elapsed timeout each
establish none of that.
`packages/runtime/terminal-processes.ts` is what does: the process table,
terminal holders, signal delivery and reachability, behind one host seam whose
own default refuses every question. Refusing is the point — "nobody is there"
and "I cannot see" are the two answers a quiescence proof must never confuse, so
a host that installs no observer stops the document rather than reporting a pane
quiet it never looked at. The POSIX handler answers with `ps` and `lsof`; the
`lsof` sweep is the expensive half and grows with the process count, which is
why it is behind the seam rather than inlined.
Two shapes carry the rule. `paneOccupants()` takes the snapshot — the child, its
descendants, its process group — and must be taken *before* the first signal,
because a killed child's children reparent to init and a later reading names
fewer processes than the launch actually started. `establishQuiescence()` asks
about every one of them and about the terminal, and reports everything still
true rather than the first thing it found.
Nothing here decides policy. It reports; the pane worker finishing a launch and
the provider tearing a grid down decide what the report means.
Tier TP proves the difference between establishing and assuming: a host with no
observer refuses, the POSIX reader finds this process in the real table, a
snapshot read after a kill names nobody, and a pane whose child is gone is still
not free while a descendant runs or anything else holds the terminal.
`select-layout tiled` picks its own column count from the window's dimensions,
so the same four panes are 2×2 in one terminal and 4×1 in another. An authored
`columns` has to be told to tmux rather than asked of it.
`packages/cli/src/terminal/layout.ts` writes the description tmux prints in
`#{window_layout}` and accepts back: a checksum, then a tree of cells sized
row-major from the pane count and the authored column count. A final row with
fewer panes than columns spans the row, because tmux has no empty cells and the
author wrote panes rather than a rectangle.
One thing the string cannot do is place a particular pane — tmux fills the
leaves in window-list order and ignores the pane ids they name — so authored
order is imposed afterwards by swaps. `swapsInto()` says which, produces none
for an order that is already right, and refuses a window that does not hold a
pane the author wrote instead of putting some other pane there.
Tier TX checks the geometry at four terminal sizes, that the cells tile exactly
with one separator between them, that the checksum tracks the tree, and all
three swap cases.
A pane's initial process is a worker that owns the pane's terminal for the
pane's whole life, and everything it does is asked of it over a socket only this
invocation can reach.
**The channel.** One directory per grid, mode 0700, directly under `$TMPDIR`
because a Unix socket path is capped at 104 bytes and a directory named after a
repository path spends most of that first. Inside it, one socket and one
mode-0600 token per pane, both written before any pane exists, so a worker that
starts finds its socket listening rather than racing it. Admission is the whole
boundary: a connection is admitted when its first frame is a `hello` naming this
pane's ordinal and carrying this pane's token, and a connection that says
anything else, says it late, names another ordinal, or arrives after that pane is
admitted is closed without being answered. The token is single-use because the
worker removes the file as it reads it.
**What crosses it.** The exact argv vector, working directory and environment.
tmux has a command parser, and a command parser is a place where an argument can
become two arguments, or a quote, or a `;`. tmux is told a directory and an
ordinal, and that is all its parser ever sees.
**The worker.** `xmd terminal-worker <ordinal> <dir>` — reusing this executable
rather than shipping a second script, which is what makes it work in the
compiled distribution. It is in no command table, so it is in no help output and
no catalog, and naming it grants nothing: without a pane's single-use token
nobody answers. It is dispatched at the entrypoint, before `main()`, and runs
under `run()`, because `main()` binds SIGINT to its own shutdown and would exit
130 on the first `^C` typed into the pane — the keystroke the foreground child is
supposed to receive. It ignores SIGINT, SIGQUIT and SIGTSTP itself so the child,
which gets default dispositions across `exec`, is the one interrupted.
**Readiness and settlement, kept apart.** Readiness is the runtime's `spawn`
event and nothing earlier; a missing executable delivers `error` instead of it,
never after it. Settlement is the escalation and sweep that follow — a child that
exited on its own may have left descendants in its group or an orphan holding the
terminal, and the pane is not free until neither is true. `exited` is reported
only after that, so the next launch is refused while a sweep that would reach it
is still running.
One hazard the evidence found: the settlement sweeps the process group it is in,
and a worker that was not a session leader would be sweeping whatever started it.
In a pane tmux makes it one — but a settlement one signal away from killing the
run that started the grid is not something to leave to the topology being what it
should be, so the sweep now never reaches an ancestor of the worker.
Tier TW proves it with a real worker process over a real socket and no tmux at
all: the modes, the removal, the handshake, three ways of failing it, awkward
argv crossing intact, a child that never starts, one-live-child exclusivity,
display written and never read, and shutdown's final sweep.
One invocation-private server per grid, on its own socket, started with
`-f /dev/null` so a reader's `.tmux.conf` cannot redecide an authored layout. A
pane per authored ordinal, each running that pane's worker — tmux's parser sees
an ordinal and a directory and never a launch's argv. Nothing is visible until
`attach()`, which core calls only after every pane has reported a start.
Three clients, kept apart because they answer different questions. The visible
one is the reader's. The control one attaches `-f no-output`, so pane bytes
never travel through this process, and what it reports is how reader detach,
server stop and control loss are told apart — an attach client's exit code
cannot tell them apart, being 0 after `detach-client`, 0 after `kill-session`
and 1 after `kill-server`. The workers are not clients at all; they are the
panes.
Teardown is registered before the first command, so a composite that fails
half-built still takes its server down. A detach is *asked for* before anything
is signalled, because a client that leaves restores the terminal and one that is
killed cannot. `stop()` establishes the server pid is unreachable and the server
refuses its session — never the socket file's absence, which outlives it.
`probeTmux()` answers the prerequisites before a server exists: a terminal to
divide, and a tmux new enough to divide it as an authored layout needs.
Tier TG runs against a fake server that reproduces the behaviours this code
exists to work around — a split inserts its pane into the window list after the
one it split, and a layout string's leaves are filled in window-list order with
the ids in them ignored. What is not faked is the composite: the same layout
string, the same swap decisions, and real control-mode lines from a fixture
process through the same splitter and classifier.
Both halves of the ordering claim were broken on purpose: removing the swaps
fails TG2, and a fake that honours the leaf ids fails TG2 as well — so the row
is passing because the composite imposes the order, not because the two happened
to coincide.
Stated plainly, and not claimed here: a fixture client inherits a pipe, so it
cannot restore a terminal it never had. That a real `tmux attach` gives the
reader's terminal back when asked to detach is #726's evidence on real tmux.
**The visible client is not a pane child.** A pane child is settled by sweeping
its process group and its terminal, because a pane's terminal belongs to the
grid. The reader's terminal belongs to the run: everything holding it is XMD,
whatever started XMD, and the rest of XMD's foreground group. A settlement of
that shape aimed at the attach client is a settlement aimed at the document.
`attach-client.ts` owns exactly one process instead — asked to detach first,
through tmux, and only then insisted on by pid, with no group, no descendants
and no terminal sweep anywhere in it.
**A successful `kill-server` is not proof.** Teardown now succeeds only once the
recorded server pid is unreachable and the server refuses its own session, and
throws a provider-neutral `TerminalTeardownFailed` when either is still unproved
at the bound. The rule is in the resource finalizer too, so a preparation that
failed halfway is held to it as well.
**Nothing private in a diagnostic.** `TmuxCommandFailed` carries the step's name
and nothing else — not the arguments, which hold the socket path, session name,
pane and client identifiers and the worker's private directory, and not stderr,
which tmux writes paths into. A provider's topology stays private on the paths
taken when something goes wrong, which are the paths a diagnostic is read on.
**Closures before removal.** The private directory is removed only after every
accepted socket and every listening server has actually closed — counted from
their own `close` events rather than from having been asked.
Three regressions, each broken on purpose and re-run:
- TG11 gives the process table company — XMD, its parent, two more in the same
group, and four holders of the reader's terminal — and proves the escalation
reaches the client's pid alone. Settling it like a pane child fails it.
- TG10 plants markers in the socket, session, pane and client identifiers, the
worker directory, the arguments and stderr, and proves none reaches the
surfaced error. Restoring raw arguments fails it.
- TG12 counts real closures at the moment of removal. Not awaiting them fails it.
Also conformed to the repository's rules: `@effectionx/fs` for stat, rm,
readTextFile and writeTextFile, with `node:fs/promises` kept only for `chmod`
and `appendFile`, both adapted through `until`; the client fixture is an
Effection operation; and the newly introduced `as const` assertions are gone in
favour of typed values.
`end()` sent SIGKILL and then discarded what the wait after it established, so
a client still holding the reader's terminal was reported as torn down. The
shared `stop()` resolved successfully on top of that, and the document carried
on.
It now establishes the client is gone, and raises a provider-neutral teardown
failure when it is not — so `stop()` rejects and the document stops instead.
`leftWithin()` also looks once more at the boundary itself rather than falling
back on the cached exit event: a client that left during the final interval is
gone, and reporting it as still there would be reporting a stale reading.
The boundary is unchanged and still narrow: detach is asked for through tmux
first, and every signal after that names the exact client pid. Nothing inspects
or signals its process group, its descendants, or the holders of the reader's
terminal — on this terminal, each of those is the run itself. The refusal
carries none of the socket, session, client name, argv, environment, terminal
or host message.
TG13 models a client that survives the ask, SIGTERM and SIGKILL: teardown
refuses, the signals delivered are exactly SIGTERM and SIGKILL to the client's
pid, three same-group bystanders and three holders of the reader's terminal are
untouched, and no planted marker reaches the refusal. TG11's successful
escalation is unchanged.
Reinstating the discarded result fails TG13 and leaves TG11 green, which is the
discrimination the two rows are for.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 7 redundant comments. Inline suggestions to remove them below.

child = spawnChild(command, args, {
cwd: options.cwd,
env: options.env,
// The reader's terminal, handed straight through.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The reader's terminal, handed straight through.

env: request.env,
// The whole point of a pane: the child reads this terminal and draws on
// it directly, so nothing between it and the reader can buffer, reorder
// or capture what passes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// or capture what passes.

found.set(id, {
ordinal: paneIds.indexOf(id),
id,
// The worker reports `ttys003`; tmux reports `/dev/ttys003`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The worker reports `ttys003`; tmux reports `/dev/ttys003`.

while (Date.now() < deadline) {
const names = yield* clientNames(tmux);
// The control client attaches with no tty of its own, so a named client is
// the visible one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// the visible one.

switch (command) {
case "new-session": {
alive = true;
// `... -c <cwd> <command...>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// `... -c <cwd> <command...>`

case "set":
return "";
case "split-window": {
// `... -t <target> -c <cwd> -P -F #{pane_id} <command...>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// `... -t <target> -c <cwd> -P -F #{pane_id} <command...>`

*reachable([pid]): Operation<boolean> {
try {
// Signal 0 delivers nothing: it asks the kernel whether the pid is
// reachable, which is the whole question here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// reachable, which is the whole question here.

@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown

PR #747: ✨ Open terminal grids with tmux in foreground runs (#732)

32 files, +7136 / -50

Scope

🔴 PR has 7186 lines changed. Split into focused PRs.

🟡 7186 lines changed. PRs under 400 receive more thorough review.

🟡 32 files changed. Are all changes related?

🟡 Changes span 8 directories.

🟡 New abstraction files: packages/cli/src/terminal/provider.ts. Verify 3+ consumers.

Structural

Oxlint structural signals:

  • no-unused-vars ×10: packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
  • no-redundant-type-constituents ×4: packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/pane-child.ts, packages/cli/src/terminal/pane-channel.ts
  • no-empty-function ×2: packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts
  • no-unnecessary-type-arguments ×2: packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/tmux-grid.ts
  • no-unnecessary-type-assertion ×2: packages/core/src/expand.ts

Slop

  • packages/runtime/terminal-processes.ts:292 (removed)
  • packages/cli/src/compiled.ts:58// is supposed to receive.
  • packages/cli/src/deno.ts:74// is supposed to receive.
  • packages/cli/src/deno.ts:122// re-invoke itself for one pane.
  • packages/cli/src/terminal/pane-channel.ts:160// again.

Oxlint slop signals:

  • no-console ×3: packages/cli/src/cli.ts
  • no-inferrable-types ×2: packages/core/src/expand.ts

Static Analysis

Oxlint: 57 diagnostics across 12 files (19 rules)
Density: 0.008 violations/added-line

no-unused-vars (10): packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
no-unsafe-type-assertion (5): packages/cli/src/deno.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
no-floating-promises (5): packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts (+1)
no-redundant-type-constituents (4): packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/pane-child.ts, packages/cli/src/terminal/pane-channel.ts
consistent-return (4): packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/provider.ts, packages/cli/src/terminal/pane-channel.ts (+1)
no-base-to-string (4): packages/core/src/expand.ts
no-shadow (3): packages/cli/src/terminal/provider.ts, packages/runtime/terminal.ts, packages/core/src/expand.ts
no-console (3): packages/cli/src/cli.ts
unbound-method (3): packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts
consistent-function-scoping (2): packages/cli/src/terminal/pane-worker.ts, packages/cli/tests/fixtures/fake-tmux.ts
no-empty-function (2): packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts
no-array-sort (2): packages/cli/src/terminal/tmux-grid.ts, packages/cli/src/terminal/layout.ts
no-inferrable-types (2): packages/core/src/expand.ts
no-unnecessary-type-arguments (2): packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/tmux-grid.ts
no-unnecessary-type-assertion (2): packages/core/src/expand.ts
no-useless-spread (1): packages/cli/src/terminal/pane-channel.ts
no-useless-fallback-in-spread (1): packages/core/src/expand.ts
restrict-template-expressions (1): packages/core/src/expand.ts
no-implied-eval (1): packages/core/src/expand.ts

Correctness

No extraneous code patterns detected.

`provider.ts` is where #730's provider-neutral request meets tmux: it prepares
the private channels, the hidden server and the panes, resolves each pane's
worker command before a server exists, and hands core a composite it drives
through its own lifecycle. Nothing tmux-shaped crosses in either direction.
The reader leaving and the host's terminal going away settle the same
`closed()`. That is deliberate: a hangup is not a second teardown path to keep
honest separately, it is the ordinary structured close every other stop uses.
The SIGHUP listener is a resource, so it is removed with the run rather than
answering for a terminal the next one is using.
`host.ts` states which hosts present grids. The Deno entrypoint and the compiled
binary supply `foregroundTerminalGrid()`; every other caller gets
`unsupportedTerminalGrid`, which still opens the installation so a grid is
validated and refused by core rather than being silently absent. Node and Bun
therefore catalog and validate the same grids and open none — threaded through
`AgentStack` beside the machine-session assembly, which is the same shape this
repository already uses for "Deno supplies the live one, Node and Bun supply the
one that installs nothing".
architecture.md's terminal-grid inventory row said "implementation unbuilt",
which four layers had made untrue. It now says what each Story built, that the
controlled provider remains the authority for core lifecycle semantics, that
this Story's evidence uses a fake tmux with real tmux behaviour remaining
#726's, and that Node and Bun install no operational provider.
Checkpoint 3's evidence is not in this commit: the Node/Bun refusal row, the
SIGHUP-through-host-installation row, and the CLI regressions are still to come.
Implements architecture commit 802b07d.
`TerminalComposite.launch(ordinal, request, spawned)` is required of every
composite. Core closes the pane-scoped launcher over it and the pane's authored
ordinal, so after claim admission and the pane flush a `<Session.Launch>`
written in a paired pane reaches *that pane's* terminal. The ordinal lives in
core's closure and enters no native request, Agent request, session key,
construction route, durable phase, result or diagnostic.
The pane launcher is now the end of the chain. Middleware written nearer the
authored launch still composes in front and may observe, wrap, refuse or
short-circuit; what it can no longer do is reach past, because past it is the
root foreground launcher and the root terminal is the one thing a pane exists
to avoid. A composite that cannot run a pane's launch refuses — there is no
fallback, because the only thing to fall back to is the wrong terminal. Root
`<Session.Launch>` is untouched.
The tmux composite sends the exact command vector, working directory and
environment over the pane's authenticated channel; tmux's parser sees a
directory and an ordinal. `shell()` stays separate and keeps deriving the
executable from live host policy. `spawned` is invoked only for the
worker-observed runtime spawn event.
Also fails closed on settlement: `requireQuiescent()` is the rule everything
downstream is conditional on, and a settlement that could not prove the pane
free no longer clears the pane, reports success, or admits another launch.
TG20 proves the endpoint against real workers on real sockets with a fake tmux
that now starts the pane commands it is given, and with no `<TestAgent>` or
nearer launcher in front: exact argv, cwd and environment arrive at the pane's
authenticated worker; a root-foreground-launcher sentinel is never entered while
a root launch still reaches it; distinct panes launch concurrently; and a pane
the composite cannot serve refuses.
Tier GN is rewired through the endpoint, which is what exposed the gap: before
this, its pane launches reached `<TestAgent>`'s launcher and nothing could tell
that from reaching the pane. GN now separates the two — `launches` at the pane
endpoint, `agentLaunches` at the root route — and GN3 and GN8 assert both.
Carries the three evidence gaps from 7511e77 and the six repairs.
**TW13 now proves the worker.** The rejected environment switch is replaced by an
injected child seam: `runPaneWorker` takes what it starts, so a suite can run the
real worker in-process against a real channel with the one thing it cannot
arrange in another process — a child whose settlement cannot say the pane is
free. After `quiet:false` the worker clears no live entry, reports no
settlement, starts no second child, and refuses.
**TG20c is a discriminator.** Each child announces itself and blocks until both
have; a serial pair would wait for a start that had not happened.
**TG20e proves cancellation.** `runInPane()` owns it: registered before the
launch is asked for, a cancellation sends the worker's cancel and waits for a
settlement that proves the pane free. A cancelled launch does not return while
its child is live — TG20e reads the child's own pid and finds it gone.
**Process observation fails closed**, in `deno-terminal-processes.ts` behind the
runtime-named boundary Deno, the compiled binary and their pane workers install.
`kill(pid, 0)` establishes absence only for ESRCH; EPERM is a process that
exists and this user may not signal, so it raises rather than reading "I may not
ask" as "nothing is there". A `ps` that would not run is not an empty table. Only
`lsof -t`'s documented exit-1-with-no-output is read as "nobody".
**Every listener is scope-owned.** No `.once()` and no `{ once: true }` in the
touched production code: named handlers, removed by the scope that installed
them, and kept installed through any wait they resolve. The worker's SIGINT,
SIGQUIT and SIGTSTP handlers are its run scope's. TW14 counts them across event
delivery, no delivery, startup failure and cancellation.
**One ordered teardown.** `tearDown()` is idempotent and covers both core's
`destroy()` and a preparation that failed halfway: detach and prove the visible
client stopped, ask every worker to shut down, await each settlement, terminal
sweep and goodbye, refuse on anything unproved, and only then stop the server and
prove it gone. Sockets, their servers and the private directory come down after
it, in the scopes that own them.
**SIGHUP is cancellation, not a reader close.** A reader who detaches selects a
close outcome; a terminal that is gone cancels the document through the ordinary
structured path, runs the whole teardown, and lets no following sibling run.
**Hosts state what they are.** Deno and compiled install the provider and the
observer together; everyone else installs neither and still validates. TH1–TH3
cover a missing terminal, an unusable tmux, and a host with no provider.
The inventory now says what was built and what the evidence is: fake tmux with
real workers and real sockets, with real tmux behaviour on macOS remaining #726's.
…ce (#732)
**Observation carries stderr, and reads only what it understands.** `lsof -t`
exits 1 saying nothing when a file has no holders, and exits 1 *with a
diagnostic* when it could not look; without stderr those are the same status,
and one means "nobody" while the other means "I do not know". Only the exact
empty shape is accepted. A successful run whose lines are not all readable, and
a `ps` reading with lines it cannot parse, now refuse rather than answering with
the subset they happened to recognise — a sweep satisfied by that is a sweep
that never saw what was there. TP2f and TP2g cover both.
**Teardown is one retry-safe lifecycle.** It is marked complete only after it
succeeds, so a repeat caller observes the same teardown rather than skipping
unfinished work, and a teardown that failed is retried rather than remembered as
done. Per worker, in order: shutdown asked, settlement required, a goodbye that
names no surviving holder, then the channel closing — a worker that was gone,
disconnected, or stopped part-way is a failure, not a success. Channels close
before the server is stopped, and the server's absence is proved before the
private paths go. Every acquired resource is still attempted after an earlier
failure, and the first failure is what surfaces.
**Every listener is scope-owned, including the frame reader.** `readFrames()` is
a resource whose named data, close and error handlers come off on delivery, on a
frame that does not parse, on cancellation and on ordinary exit. Startup
listeners are removed once startup resolves; the ones a settlement still needs
stay until the scope ends. TW14 now counts on the emitters themselves — the
child process, and the channel's sockets and servers — across delivery, no
delivery, startup failure and cancellation, with the cancellation coordinated by
the child's own start rather than a sleep.
**Host evidence.** TH4 drives the hangup through the operation the foreground
installer wraps `Execution.document` with: it stays structured cancellation,
runs the complete teardown, and lets no following sibling run. TH5 exercises the
assembly the runtime-named entrypoints call — provider and observer together, or
neither. CL6 and CL7 add the CLI grid regressions.
One thing CL6 found and records rather than hides: a grid under a pipe is
refused at the run's foreground lease, before any provider is contacted — and
the wording it gets is the foreground launcher's, which names `<Session.Launch>`
though the document writes none. The refusal is correct and early; the sentence
is aimed at the wrong feature.
The inventory no longer says the required pane endpoint remains to be
implemented, and claims the completed teardown now that it is there.
**Every registration is named and owned.** `net.createServer(cb)` and
`server.listen(cb)` both register anonymous listeners nothing can take off
again; both are now named handlers, with `connection` removed by the channel's
scope and `listening`/`error` removed synchronously once the listen resolves,
however it resolved. `readFrames()` takes all three protocol handlers off the
moment that reader terminates — a close, an error, or a frame that is not the
protocol — and tells its consumers, because a reader that detached silently
would leave them waiting on a conversation that ended. The resource cleanup
stays for the paths that terminate nothing: a cancelled scope, and a socket that
never says anything.
`spawn` and `error` are the two answers to one question, so whichever arrives
takes both off; `exit` stays, because the settlement is still waiting on it. The
same rule for the visible attach client.
**TW14 is discriminating.** It holds references to the child processes, the
accepted socket and the servers, and asserts their listener counts after each
scope ends — delivery, no delivery, startup failure and cancellation, with the
cancellation coordinated by the child's own start signal. It caught two real
misses while being written: the server's `connection` handler was still
anonymous, and the frame reader's early detach had stopped closing its queue.
**`PaneChannels.close()` publishes before it closes.** The in-flight settlement
is created and stored first, so a concurrent caller shares this close rather
than starting a second one or being told a close that has not happened had
finished. A close that fails clears it, so the next caller retries.
**CL7 asserts the concrete refusal** — the named prop and the source location —
rather than the absence of a provider message, which an unrelated failure would
also satisfy.
**The deadlock was mine, not the provider's.** A bounded reproduction — one
self-closing pane through `foregroundTerminalGrid()`, fake tmux, a real worker
on a real socket, and a shell fixture that signals its start and then stays —
traced the whole teardown in order the moment a hangup was actually delivered:
detach, worker settlement, holder-free goodbye, channels closed, server stopped.
The earlier row never delivered one. It passed `hangup` as a provider
dependency, which an earlier repair had removed, so the override was inert and
the run waited on a real SIGHUP that never came. No production change was needed
for it, and the instrumentation is gone.
**One real defect it did expose.** `useHangupCancellation` discarded what
`next(request)` returned, so every ordinary run through the installer was
refused for having "returned before the document produced a result". The result
is returned now, and `underHangup` is typed to carry it.
**TH4 is the host boundary, driven by the installed listener.** A real document
with a live grid, through everything `foregroundTerminalGrid()` installs, with
`process.kill(process.pid, "SIGHUP")` rather than a stand-in. It proves
cancellation rather than reader close, that the sibling after the grid never
ran, that the pane's child and every worker are gone, that the server is gone,
that the private directory — removed last, after its sockets close — is gone,
and that the SIGHUP listener went with the run that installed it. Every wait is
on an event: the pane child's own start file, and each worker's own exit.
Both halves of the installer are load-bearing: removing
`useHangupCancellation()` leaves TH4 hanging on a grid nothing ends, and
removing the provider registration fails it outright.
One thing recorded rather than asserted around: cancelling the document from
inside its own middleware surfaces as core's "middleware returned before the
document produced a result" rather than as `TerminalLost`, because the guard
fires on the cancelled canonical execution first. The observable contract holds
— the run fails, teardown completes, no sibling runs — so the row asserts those
and not the wording.
… handling (#732)
The teardown was the least-covered part of this provider, and covering it
found two defects.
A close *request* could fail outside the boundary that handled the waits.
`socket.destroy()` and `server.close()` were called after their closure
watch had been attached and queued, so a request that threw left a wait
nothing would ever settle — the whole close hung rather than failing. The
requests are now inside the same boundary, a watch whose request threw is
abandoned rather than awaited, and the handle stays out of the closed set so
a later call asks it again while leaving the ones that closed alone.
A retried teardown restarted rather than resumed. Every phase was re-asked,
so a worker that had already said goodbye and gone answered the second ask
as "a worker that was gone" — and that answer replaced the reason the first
attempt could not finish. The composite's own finalizer retries after a
failed `destroy()`, so this was the ordinary path: a document was told its
pane had vanished when what had actually happened was that the server would
not stop. Phases that succeeded are now remembered, and a retry resumes at
the one that failed.
The teardown itself moves out of the composite closure into
`createGridTeardown()`, which is what lets a row drive it with scripted
workers over real private sockets.
Rows: TH5 freezes the ordinary foreground-host branch — the same live grid
as TH4, ended by a reader detach through the fake control channel instead of
a hangup, asserting the exact result handed back through
`useHangupCancellation()`. It fails if either the tmux provider or the POSIX
observer is removed from `foregroundTerminalGrid()`. TH6 freezes entrypoint
selection. Tier TD covers the combined teardown: shared in-flight teardown
under concurrent destroys, the three protocol refusals, one pane's failure
stranding neither the next pane nor the channels nor the server, first-
failure preservation, the frozen order through to path removal, the retried
close request, the resumed retry, and the document-level refusal.
Real terminal restoration remains #726's real-tmux evidence.
…732)
The suite hung forever under Node and Bun. Not failed — hung, which leaves a
runtime shard running until the job's own timeout with nothing to read.
A pane's worker is this executable re-invoked under the hidden
`terminal-worker` subcommand, and only the hosts that present grids register
it: the Deno entrypoint and the compiled binary. On Node and Bun the same
argument vector names a *document* called `terminal-worker`, so the worker
exits with ENOENT before it connects and the parent waits for a pane that
will never say hello. It stalls entering TW3, the first row that spawns a
real worker.
$ tsx packages/cli/src/node.ts terminal-worker 0 <dir>
ENOENT: no such file or directory, open 'terminal-worker'
That Node and Bun install no grid provider is the design, so the fix is the
exclusion this repository already has a mechanism for rather than a portable
worker. Every other test file in this stack runs under Node unchanged; this
is the only one that cannot.
What the exclusion does and does not preserve, stated precisely because the
rationale is the reason a later reader would trust it: provider absence is
covered portably by TG9 in packages/core/tests/terminal-grid.test.ts, which
runs on all three runtimes. TH6's entrypoint-selection freeze is textual, so
proving it once under Deno proves it everywhere. TH3 makes the same claim as
TG9 but is excluded with the rest of the file and proves nothing here. What
is genuinely Deno-only is the worker, socket and fake-tmux integration.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 4 redundant comments. Inline suggestions to remove them below.

if (paneWorker !== undefined) {
// Not `main()`: it would bind SIGINT to its own shutdown and exit 130 on the
// first `^C` typed into the pane, which is the keystroke the foreground child
// is supposed to receive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is supposed to receive.

if (paneWorker !== undefined) {
// Not `main()`: it would bind SIGINT to its own shutdown and exit 130 on the
// first `^C` typed into the pane, which is the keystroke the foreground child
// is supposed to receive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is supposed to receive.

() => useDenoWorkflowHost(HELPER),
useMachineSessions(),
// This host presents grids: it has a terminal to divide, and it can
// re-invoke itself for one pane.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// re-invoke itself for one pane.

// request that threw must not stop the others being asked. The watch for
// one that threw is abandoned rather than awaited — nothing is going to
// close it — and the handle is left out of `shut`, so a later call asks
// again.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// again.

Sign up for freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

✨ Open terminal grids with tmux in foreground runs (#732) - #747

Draft
taras wants to merge 15 commits into
agent/issue-731-pane-native-launchfrom
agent/issue-732-tmux-grid
Draft

✨ Open terminal grids with tmux in foreground runs (#732)#747
taras wants to merge 15 commits into
agent/issue-731-pane-native-launchfrom
agent/issue-732-tmux-grid

Conversation

@taras

@tarastaras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes#732. Fifth and final implementation Story under Quest #717, stacked on #741.

Base is agent/issue-731-pane-native-launch, not main, as the Story's verification stack directs. Branched from 8b8936c25c5b1c934f7ee1a4b003b8324c2e3452, the accepted head of #741.

Draft: checkpoint 3 of 3 is outstanding. Host installation, Node/Bun refusal, SIGHUP → structured cancellation, the CLI regressions, and the architecture inventory update are not in this branch yet. Everything below is delivered and verified.

Why

#729 froze the language, #730 made a grid execute through a replaceable provider, and #731 let native Agent sessions live in its panes — all against a controlled provider that presents nothing. This Story is the first production presentation: an authored grid opening as one foreground tmux workspace, with no tmux command or identifier anywhere in the document.

What changes

Before: xmd run on a real terminal had the whole grid contract and nothing to present it with.

After: the Deno and compiled foreground hosts open the grid through one invocation-private tmux server — panes in the authored row-major order, each running a worker that owns its pane's terminal.

How it works

prerequisites → private dir (0700) + per-pane socket + 0600 token
→ private tmux server → pane per ordinal, each on its worker
→ explicit layout + swaps into authored order
→ workers authenticate → readiness → attach
→ … → detach asked → workers quiesce → server proved gone → dir removed

packages/runtime/terminal-processes.ts is the host's answer to "is this pane free": the process table, terminal holders, signal delivery and reachability, behind one seam whose default refuses every question. Refusing is the point — "nobody is there" and "I cannot see" are the two answers a quiescence proof must never confuse. paneOccupants() snapshots before the first signal, because a killed child's children reparent to init.

packages/cli/src/terminal/layout.ts writes the layout string tmux prints and accepts, sized row-major from the authored column count. select-layout tiled cannot implement an authored columns — the same four panes become 2×2 in one terminal and 4×1 in another. tmux also fills leaves in window-list order and ignores the pane ids in them, so authored order is imposed afterwards by swapsInto().

packages/cli/src/terminal/pane-channel.ts + pane-protocol.ts + pane-worker.ts are the private channel and the worker. Exact argv, cwd and environment cross the socket as bytes; tmux's parser is told a directory and an ordinal and nothing else. The worker is xmd terminal-worker <ordinal> <dir> — this executable, so it works in the compiled distribution — dispatched at the entrypoint before main() and run under run(), because main() binds SIGINT to its own shutdown and would eat the first ^C typed into a pane.

packages/cli/src/terminal/tmux-grid.ts is the hidden composite. Three clients kept apart: the reader's visible one, a control-mode client attached -f no-output so pane bytes never reach this process, and the workers, which are not clients at all. An attach client's exit code cannot classify a close — it is 0 after detach-client, 0 after kill-session and 1 after kill-server — so the control channel does it.

packages/cli/src/terminal/attach-client.ts owns the visible client and nothing else. A pane child is settled by sweeping its group and its terminal; the reader's terminal is held by XMD, whatever started XMD, and the rest of its foreground group, so a settlement of that shape aimed at this client is aimed at the document.

Review guide

Start with:packages/cli/src/terminal/attach-client.ts — the narrowest boundary in the change, and the one with the most to lose.

Then review:

  1. packages/runtime/terminal-processes.ts — what may be claimed about a pane, and the refusing default.
  2. packages/cli/src/terminal/pane-worker.ts and pane-channel.ts — the handshake and what crosses it.
  3. packages/cli/src/terminal/tmux-grid.ts — the three clients, and stop().
  4. packages/cli/src/terminal/layout.ts — the string, and why the swaps exist.

Look carefully at:

  • escalate() in pane-child.ts excluding the worker's ancestors. A pane worker is tmux's session leader, so its group holds nothing above it — but a settlement one signal from killing the run that started the grid should not rest on the topology being what it should be. TW9 found this by sweeping the test runner.
  • stop() in tmux-grid.ts: a successful kill-server is not proof. It refuses unless the recorded pid is unreachable and the server refuses its session, and the socket file is not part of it because it outlives the server.

What must stay true

  • A pane is free only when it is proved free — enforced by the refusing seam default, checked by TP1 and TP5–TP9.
  • Nothing private in a diagnostic — enforced by TmuxCommandFailed carrying the step name alone, checked by TG10 and TG13 against planted markers in the socket, session, pane/client identifiers, worker directory, argv, environment and stderr.
  • The visible client's escalation names one pid — enforced by attach-client.ts never reading a group, descendants or holders, checked by TG11 and TG13 with bystanders in the table.
  • Authored order survives a server that ignores the layout's ids — enforced by swapsInto(), checked by TG2 against a fake that reproduces that behaviour.
  • The private directory outlives its sockets — enforced by finalizer ordering plus awaited closures, checked by TG12 counting real close events at the moment of removal.

How to verify it

deno task test packages/runtime/tests/terminal-processes.test.ts # 1 passed (9 steps)
deno task test packages/cli/tests/terminal-grid-tmux.test.ts # 3 passed (34 steps)
deno task test packages/cli/tests/session-launch-cli.test.ts # 1 passed (5 steps)

deno task check exit 0; deno task lint 0 errors.

Tier TW starts a real worker process over a real socket — no tmux at all — and covers the modes, the token's single use, three ways of failing the handshake, awkward argv (spaces, ;, both quote kinds, $HOME) arriving intact, a child that never starts, one-live-child exclusivity, display written and never read, and shutdown's final sweep.

Tier TG runs against a fake server that reproduces the two behaviours this code exists for: a split inserts its pane into the window list after the one it split, and layout leaves are filled in window-list order with the ids ignored. Not faked: the layout string, the swap decisions, and real control-mode bytes from a fixture process through the same splitter and classifier.

Every substantive claim was broken on purpose and re-run: removing the swaps fails TG2; a fake that honours the leaf ids also fails TG2; settling the visible client like a pane child fails TG11; restoring raw arguments to a failure fails TG10; not awaiting the socket closures fails TG12; discarding the final wait after SIGKILL fails TG13.

Scope

Included

  • The host process/terminal observer and its quiescence shapes.
  • Layout, swaps, the private channel, the worker, the hidden composite, the visible client.

Intentionally unchanged

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • The description matches the final diff and test results — checkpoint 3 still to come.

A terminal grid may not report a pane settled, admit the next launch into it, or
let the document continue while something a launch started can still act. A PID,
a delivered signal, an attach client going away and an elapsed timeout each
establish none of that.
`packages/runtime/terminal-processes.ts` is what does: the process table,
terminal holders, signal delivery and reachability, behind one host seam whose
own default refuses every question. Refusing is the point — "nobody is there"
and "I cannot see" are the two answers a quiescence proof must never confuse, so
a host that installs no observer stops the document rather than reporting a pane
quiet it never looked at. The POSIX handler answers with `ps` and `lsof`; the
`lsof` sweep is the expensive half and grows with the process count, which is
why it is behind the seam rather than inlined.
Two shapes carry the rule. `paneOccupants()` takes the snapshot — the child, its
descendants, its process group — and must be taken *before* the first signal,
because a killed child's children reparent to init and a later reading names
fewer processes than the launch actually started. `establishQuiescence()` asks
about every one of them and about the terminal, and reports everything still
true rather than the first thing it found.
Nothing here decides policy. It reports; the pane worker finishing a launch and
the provider tearing a grid down decide what the report means.
Tier TP proves the difference between establishing and assuming: a host with no
observer refuses, the POSIX reader finds this process in the real table, a
snapshot read after a kill names nobody, and a pane whose child is gone is still
not free while a descendant runs or anything else holds the terminal.
`select-layout tiled` picks its own column count from the window's dimensions,
so the same four panes are 2×2 in one terminal and 4×1 in another. An authored
`columns` has to be told to tmux rather than asked of it.
`packages/cli/src/terminal/layout.ts` writes the description tmux prints in
`#{window_layout}` and accepts back: a checksum, then a tree of cells sized
row-major from the pane count and the authored column count. A final row with
fewer panes than columns spans the row, because tmux has no empty cells and the
author wrote panes rather than a rectangle.
One thing the string cannot do is place a particular pane — tmux fills the
leaves in window-list order and ignores the pane ids they name — so authored
order is imposed afterwards by swaps. `swapsInto()` says which, produces none
for an order that is already right, and refuses a window that does not hold a
pane the author wrote instead of putting some other pane there.
Tier TX checks the geometry at four terminal sizes, that the cells tile exactly
with one separator between them, that the checksum tracks the tree, and all
three swap cases.
A pane's initial process is a worker that owns the pane's terminal for the
pane's whole life, and everything it does is asked of it over a socket only this
invocation can reach.
**The channel.** One directory per grid, mode 0700, directly under `$TMPDIR`
because a Unix socket path is capped at 104 bytes and a directory named after a
repository path spends most of that first. Inside it, one socket and one
mode-0600 token per pane, both written before any pane exists, so a worker that
starts finds its socket listening rather than racing it. Admission is the whole
boundary: a connection is admitted when its first frame is a `hello` naming this
pane's ordinal and carrying this pane's token, and a connection that says
anything else, says it late, names another ordinal, or arrives after that pane is
admitted is closed without being answered. The token is single-use because the
worker removes the file as it reads it.
**What crosses it.** The exact argv vector, working directory and environment.
tmux has a command parser, and a command parser is a place where an argument can
become two arguments, or a quote, or a `;`. tmux is told a directory and an
ordinal, and that is all its parser ever sees.
**The worker.** `xmd terminal-worker <ordinal> <dir>` — reusing this executable
rather than shipping a second script, which is what makes it work in the
compiled distribution. It is in no command table, so it is in no help output and
no catalog, and naming it grants nothing: without a pane's single-use token
nobody answers. It is dispatched at the entrypoint, before `main()`, and runs
under `run()`, because `main()` binds SIGINT to its own shutdown and would exit
130 on the first `^C` typed into the pane — the keystroke the foreground child is
supposed to receive. It ignores SIGINT, SIGQUIT and SIGTSTP itself so the child,
which gets default dispositions across `exec`, is the one interrupted.
**Readiness and settlement, kept apart.** Readiness is the runtime's `spawn`
event and nothing earlier; a missing executable delivers `error` instead of it,
never after it. Settlement is the escalation and sweep that follow — a child that
exited on its own may have left descendants in its group or an orphan holding the
terminal, and the pane is not free until neither is true. `exited` is reported
only after that, so the next launch is refused while a sweep that would reach it
is still running.
One hazard the evidence found: the settlement sweeps the process group it is in,
and a worker that was not a session leader would be sweeping whatever started it.
In a pane tmux makes it one — but a settlement one signal away from killing the
run that started the grid is not something to leave to the topology being what it
should be, so the sweep now never reaches an ancestor of the worker.
Tier TW proves it with a real worker process over a real socket and no tmux at
all: the modes, the removal, the handshake, three ways of failing it, awkward
argv crossing intact, a child that never starts, one-live-child exclusivity,
display written and never read, and shutdown's final sweep.
One invocation-private server per grid, on its own socket, started with
`-f /dev/null` so a reader's `.tmux.conf` cannot redecide an authored layout. A
pane per authored ordinal, each running that pane's worker — tmux's parser sees
an ordinal and a directory and never a launch's argv. Nothing is visible until
`attach()`, which core calls only after every pane has reported a start.
Three clients, kept apart because they answer different questions. The visible
one is the reader's. The control one attaches `-f no-output`, so pane bytes
never travel through this process, and what it reports is how reader detach,
server stop and control loss are told apart — an attach client's exit code
cannot tell them apart, being 0 after `detach-client`, 0 after `kill-session`
and 1 after `kill-server`. The workers are not clients at all; they are the
panes.
Teardown is registered before the first command, so a composite that fails
half-built still takes its server down. A detach is *asked for* before anything
is signalled, because a client that leaves restores the terminal and one that is
killed cannot. `stop()` establishes the server pid is unreachable and the server
refuses its session — never the socket file's absence, which outlives it.
`probeTmux()` answers the prerequisites before a server exists: a terminal to
divide, and a tmux new enough to divide it as an authored layout needs.
Tier TG runs against a fake server that reproduces the behaviours this code
exists to work around — a split inserts its pane into the window list after the
one it split, and a layout string's leaves are filled in window-list order with
the ids in them ignored. What is not faked is the composite: the same layout
string, the same swap decisions, and real control-mode lines from a fixture
process through the same splitter and classifier.
Both halves of the ordering claim were broken on purpose: removing the swaps
fails TG2, and a fake that honours the leaf ids fails TG2 as well — so the row
is passing because the composite imposes the order, not because the two happened
to coincide.
Stated plainly, and not claimed here: a fixture client inherits a pipe, so it
cannot restore a terminal it never had. That a real `tmux attach` gives the
reader's terminal back when asked to detach is #726's evidence on real tmux.
**The visible client is not a pane child.** A pane child is settled by sweeping
its process group and its terminal, because a pane's terminal belongs to the
grid. The reader's terminal belongs to the run: everything holding it is XMD,
whatever started XMD, and the rest of XMD's foreground group. A settlement of
that shape aimed at the attach client is a settlement aimed at the document.
`attach-client.ts` owns exactly one process instead — asked to detach first,
through tmux, and only then insisted on by pid, with no group, no descendants
and no terminal sweep anywhere in it.
**A successful `kill-server` is not proof.** Teardown now succeeds only once the
recorded server pid is unreachable and the server refuses its own session, and
throws a provider-neutral `TerminalTeardownFailed` when either is still unproved
at the bound. The rule is in the resource finalizer too, so a preparation that
failed halfway is held to it as well.
**Nothing private in a diagnostic.** `TmuxCommandFailed` carries the step's name
and nothing else — not the arguments, which hold the socket path, session name,
pane and client identifiers and the worker's private directory, and not stderr,
which tmux writes paths into. A provider's topology stays private on the paths
taken when something goes wrong, which are the paths a diagnostic is read on.
**Closures before removal.** The private directory is removed only after every
accepted socket and every listening server has actually closed — counted from
their own `close` events rather than from having been asked.
Three regressions, each broken on purpose and re-run:
- TG11 gives the process table company — XMD, its parent, two more in the same
group, and four holders of the reader's terminal — and proves the escalation
reaches the client's pid alone. Settling it like a pane child fails it.
- TG10 plants markers in the socket, session, pane and client identifiers, the
worker directory, the arguments and stderr, and proves none reaches the
surfaced error. Restoring raw arguments fails it.
- TG12 counts real closures at the moment of removal. Not awaiting them fails it.
Also conformed to the repository's rules: `@effectionx/fs` for stat, rm,
readTextFile and writeTextFile, with `node:fs/promises` kept only for `chmod`
and `appendFile`, both adapted through `until`; the client fixture is an
Effection operation; and the newly introduced `as const` assertions are gone in
favour of typed values.
`end()` sent SIGKILL and then discarded what the wait after it established, so
a client still holding the reader's terminal was reported as torn down. The
shared `stop()` resolved successfully on top of that, and the document carried
on.
It now establishes the client is gone, and raises a provider-neutral teardown
failure when it is not — so `stop()` rejects and the document stops instead.
`leftWithin()` also looks once more at the boundary itself rather than falling
back on the cached exit event: a client that left during the final interval is
gone, and reporting it as still there would be reporting a stale reading.
The boundary is unchanged and still narrow: detach is asked for through tmux
first, and every signal after that names the exact client pid. Nothing inspects
or signals its process group, its descendants, or the holders of the reader's
terminal — on this terminal, each of those is the run itself. The refusal
carries none of the socket, session, client name, argv, environment, terminal
or host message.
TG13 models a client that survives the ask, SIGTERM and SIGKILL: teardown
refuses, the signals delivered are exactly SIGTERM and SIGKILL to the client's
pid, three same-group bystanders and three holders of the reader's terminal are
untouched, and no planted marker reaches the refusal. TG11's successful
escalation is unchanged.
Reinstating the discarded result fails TG13 and leaves TG11 green, which is the
discrimination the two rows are for.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 7 redundant comments. Inline suggestions to remove them below.

child = spawnChild(command, args, {
cwd: options.cwd,
env: options.env,
// The reader's terminal, handed straight through.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The reader's terminal, handed straight through.

env: request.env,
// The whole point of a pane: the child reads this terminal and draws on
// it directly, so nothing between it and the reader can buffer, reorder
// or capture what passes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// or capture what passes.

found.set(id, {
ordinal: paneIds.indexOf(id),
id,
// The worker reports `ttys003`; tmux reports `/dev/ttys003`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The worker reports `ttys003`; tmux reports `/dev/ttys003`.

while (Date.now() < deadline) {
const names = yield* clientNames(tmux);
// The control client attaches with no tty of its own, so a named client is
// the visible one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// the visible one.

switch (command) {
case "new-session": {
alive = true;
// `... -c <cwd> <command...>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// `... -c <cwd> <command...>`

case "set":
return "";
case "split-window": {
// `... -t <target> -c <cwd> -P -F #{pane_id} <command...>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// `... -t <target> -c <cwd> -P -F #{pane_id} <command...>`

*reachable([pid]): Operation<boolean> {
try {
// Signal 0 delivers nothing: it asks the kernel whether the pid is
// reachable, which is the whole question here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// reachable, which is the whole question here.

@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown

PR #747: ✨ Open terminal grids with tmux in foreground runs (#732)

32 files, +7136 / -50

Scope

🔴 PR has 7186 lines changed. Split into focused PRs.

🟡 7186 lines changed. PRs under 400 receive more thorough review.

🟡 32 files changed. Are all changes related?

🟡 Changes span 8 directories.

🟡 New abstraction files: packages/cli/src/terminal/provider.ts. Verify 3+ consumers.

Structural

Oxlint structural signals:

  • no-unused-vars ×10: packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
  • no-redundant-type-constituents ×4: packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/pane-child.ts, packages/cli/src/terminal/pane-channel.ts
  • no-empty-function ×2: packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts
  • no-unnecessary-type-arguments ×2: packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/tmux-grid.ts
  • no-unnecessary-type-assertion ×2: packages/core/src/expand.ts

Slop

  • packages/runtime/terminal-processes.ts:292 (removed)
  • packages/cli/src/compiled.ts:58// is supposed to receive.
  • packages/cli/src/deno.ts:74// is supposed to receive.
  • packages/cli/src/deno.ts:122// re-invoke itself for one pane.
  • packages/cli/src/terminal/pane-channel.ts:160// again.

Oxlint slop signals:

  • no-console ×3: packages/cli/src/cli.ts
  • no-inferrable-types ×2: packages/core/src/expand.ts

Static Analysis

Oxlint: 57 diagnostics across 12 files (19 rules)
Density: 0.008 violations/added-line

no-unused-vars (10): packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
no-unsafe-type-assertion (5): packages/cli/src/deno.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
no-floating-promises (5): packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts (+1)
no-redundant-type-constituents (4): packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/pane-child.ts, packages/cli/src/terminal/pane-channel.ts
consistent-return (4): packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/provider.ts, packages/cli/src/terminal/pane-channel.ts (+1)
no-base-to-string (4): packages/core/src/expand.ts
no-shadow (3): packages/cli/src/terminal/provider.ts, packages/runtime/terminal.ts, packages/core/src/expand.ts
no-console (3): packages/cli/src/cli.ts
unbound-method (3): packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts
consistent-function-scoping (2): packages/cli/src/terminal/pane-worker.ts, packages/cli/tests/fixtures/fake-tmux.ts
no-empty-function (2): packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts
no-array-sort (2): packages/cli/src/terminal/tmux-grid.ts, packages/cli/src/terminal/layout.ts
no-inferrable-types (2): packages/core/src/expand.ts
no-unnecessary-type-arguments (2): packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/tmux-grid.ts
no-unnecessary-type-assertion (2): packages/core/src/expand.ts
no-useless-spread (1): packages/cli/src/terminal/pane-channel.ts
no-useless-fallback-in-spread (1): packages/core/src/expand.ts
restrict-template-expressions (1): packages/core/src/expand.ts
no-implied-eval (1): packages/core/src/expand.ts

Correctness

No extraneous code patterns detected.

`provider.ts` is where #730's provider-neutral request meets tmux: it prepares
the private channels, the hidden server and the panes, resolves each pane's
worker command before a server exists, and hands core a composite it drives
through its own lifecycle. Nothing tmux-shaped crosses in either direction.
The reader leaving and the host's terminal going away settle the same
`closed()`. That is deliberate: a hangup is not a second teardown path to keep
honest separately, it is the ordinary structured close every other stop uses.
The SIGHUP listener is a resource, so it is removed with the run rather than
answering for a terminal the next one is using.
`host.ts` states which hosts present grids. The Deno entrypoint and the compiled
binary supply `foregroundTerminalGrid()`; every other caller gets
`unsupportedTerminalGrid`, which still opens the installation so a grid is
validated and refused by core rather than being silently absent. Node and Bun
therefore catalog and validate the same grids and open none — threaded through
`AgentStack` beside the machine-session assembly, which is the same shape this
repository already uses for "Deno supplies the live one, Node and Bun supply the
one that installs nothing".
architecture.md's terminal-grid inventory row said "implementation unbuilt",
which four layers had made untrue. It now says what each Story built, that the
controlled provider remains the authority for core lifecycle semantics, that
this Story's evidence uses a fake tmux with real tmux behaviour remaining
#726's, and that Node and Bun install no operational provider.
Checkpoint 3's evidence is not in this commit: the Node/Bun refusal row, the
SIGHUP-through-host-installation row, and the CLI regressions are still to come.
Implements architecture commit 802b07d.
`TerminalComposite.launch(ordinal, request, spawned)` is required of every
composite. Core closes the pane-scoped launcher over it and the pane's authored
ordinal, so after claim admission and the pane flush a `<Session.Launch>`
written in a paired pane reaches *that pane's* terminal. The ordinal lives in
core's closure and enters no native request, Agent request, session key,
construction route, durable phase, result or diagnostic.
The pane launcher is now the end of the chain. Middleware written nearer the
authored launch still composes in front and may observe, wrap, refuse or
short-circuit; what it can no longer do is reach past, because past it is the
root foreground launcher and the root terminal is the one thing a pane exists
to avoid. A composite that cannot run a pane's launch refuses — there is no
fallback, because the only thing to fall back to is the wrong terminal. Root
`<Session.Launch>` is untouched.
The tmux composite sends the exact command vector, working directory and
environment over the pane's authenticated channel; tmux's parser sees a
directory and an ordinal. `shell()` stays separate and keeps deriving the
executable from live host policy. `spawned` is invoked only for the
worker-observed runtime spawn event.
Also fails closed on settlement: `requireQuiescent()` is the rule everything
downstream is conditional on, and a settlement that could not prove the pane
free no longer clears the pane, reports success, or admits another launch.
TG20 proves the endpoint against real workers on real sockets with a fake tmux
that now starts the pane commands it is given, and with no `<TestAgent>` or
nearer launcher in front: exact argv, cwd and environment arrive at the pane's
authenticated worker; a root-foreground-launcher sentinel is never entered while
a root launch still reaches it; distinct panes launch concurrently; and a pane
the composite cannot serve refuses.
Tier GN is rewired through the endpoint, which is what exposed the gap: before
this, its pane launches reached `<TestAgent>`'s launcher and nothing could tell
that from reaching the pane. GN now separates the two — `launches` at the pane
endpoint, `agentLaunches` at the root route — and GN3 and GN8 assert both.
Carries the three evidence gaps from 7511e77 and the six repairs.
**TW13 now proves the worker.** The rejected environment switch is replaced by an
injected child seam: `runPaneWorker` takes what it starts, so a suite can run the
real worker in-process against a real channel with the one thing it cannot
arrange in another process — a child whose settlement cannot say the pane is
free. After `quiet:false` the worker clears no live entry, reports no
settlement, starts no second child, and refuses.
**TG20c is a discriminator.** Each child announces itself and blocks until both
have; a serial pair would wait for a start that had not happened.
**TG20e proves cancellation.** `runInPane()` owns it: registered before the
launch is asked for, a cancellation sends the worker's cancel and waits for a
settlement that proves the pane free. A cancelled launch does not return while
its child is live — TG20e reads the child's own pid and finds it gone.
**Process observation fails closed**, in `deno-terminal-processes.ts` behind the
runtime-named boundary Deno, the compiled binary and their pane workers install.
`kill(pid, 0)` establishes absence only for ESRCH; EPERM is a process that
exists and this user may not signal, so it raises rather than reading "I may not
ask" as "nothing is there". A `ps` that would not run is not an empty table. Only
`lsof -t`'s documented exit-1-with-no-output is read as "nobody".
**Every listener is scope-owned.** No `.once()` and no `{ once: true }` in the
touched production code: named handlers, removed by the scope that installed
them, and kept installed through any wait they resolve. The worker's SIGINT,
SIGQUIT and SIGTSTP handlers are its run scope's. TW14 counts them across event
delivery, no delivery, startup failure and cancellation.
**One ordered teardown.** `tearDown()` is idempotent and covers both core's
`destroy()` and a preparation that failed halfway: detach and prove the visible
client stopped, ask every worker to shut down, await each settlement, terminal
sweep and goodbye, refuse on anything unproved, and only then stop the server and
prove it gone. Sockets, their servers and the private directory come down after
it, in the scopes that own them.
**SIGHUP is cancellation, not a reader close.** A reader who detaches selects a
close outcome; a terminal that is gone cancels the document through the ordinary
structured path, runs the whole teardown, and lets no following sibling run.
**Hosts state what they are.** Deno and compiled install the provider and the
observer together; everyone else installs neither and still validates. TH1–TH3
cover a missing terminal, an unusable tmux, and a host with no provider.
The inventory now says what was built and what the evidence is: fake tmux with
real workers and real sockets, with real tmux behaviour on macOS remaining #726's.
…ce (#732)
**Observation carries stderr, and reads only what it understands.** `lsof -t`
exits 1 saying nothing when a file has no holders, and exits 1 *with a
diagnostic* when it could not look; without stderr those are the same status,
and one means "nobody" while the other means "I do not know". Only the exact
empty shape is accepted. A successful run whose lines are not all readable, and
a `ps` reading with lines it cannot parse, now refuse rather than answering with
the subset they happened to recognise — a sweep satisfied by that is a sweep
that never saw what was there. TP2f and TP2g cover both.
**Teardown is one retry-safe lifecycle.** It is marked complete only after it
succeeds, so a repeat caller observes the same teardown rather than skipping
unfinished work, and a teardown that failed is retried rather than remembered as
done. Per worker, in order: shutdown asked, settlement required, a goodbye that
names no surviving holder, then the channel closing — a worker that was gone,
disconnected, or stopped part-way is a failure, not a success. Channels close
before the server is stopped, and the server's absence is proved before the
private paths go. Every acquired resource is still attempted after an earlier
failure, and the first failure is what surfaces.
**Every listener is scope-owned, including the frame reader.** `readFrames()` is
a resource whose named data, close and error handlers come off on delivery, on a
frame that does not parse, on cancellation and on ordinary exit. Startup
listeners are removed once startup resolves; the ones a settlement still needs
stay until the scope ends. TW14 now counts on the emitters themselves — the
child process, and the channel's sockets and servers — across delivery, no
delivery, startup failure and cancellation, with the cancellation coordinated by
the child's own start rather than a sleep.
**Host evidence.** TH4 drives the hangup through the operation the foreground
installer wraps `Execution.document` with: it stays structured cancellation,
runs the complete teardown, and lets no following sibling run. TH5 exercises the
assembly the runtime-named entrypoints call — provider and observer together, or
neither. CL6 and CL7 add the CLI grid regressions.
One thing CL6 found and records rather than hides: a grid under a pipe is
refused at the run's foreground lease, before any provider is contacted — and
the wording it gets is the foreground launcher's, which names `<Session.Launch>`
though the document writes none. The refusal is correct and early; the sentence
is aimed at the wrong feature.
The inventory no longer says the required pane endpoint remains to be
implemented, and claims the completed teardown now that it is there.
**Every registration is named and owned.** `net.createServer(cb)` and
`server.listen(cb)` both register anonymous listeners nothing can take off
again; both are now named handlers, with `connection` removed by the channel's
scope and `listening`/`error` removed synchronously once the listen resolves,
however it resolved. `readFrames()` takes all three protocol handlers off the
moment that reader terminates — a close, an error, or a frame that is not the
protocol — and tells its consumers, because a reader that detached silently
would leave them waiting on a conversation that ended. The resource cleanup
stays for the paths that terminate nothing: a cancelled scope, and a socket that
never says anything.
`spawn` and `error` are the two answers to one question, so whichever arrives
takes both off; `exit` stays, because the settlement is still waiting on it. The
same rule for the visible attach client.
**TW14 is discriminating.** It holds references to the child processes, the
accepted socket and the servers, and asserts their listener counts after each
scope ends — delivery, no delivery, startup failure and cancellation, with the
cancellation coordinated by the child's own start signal. It caught two real
misses while being written: the server's `connection` handler was still
anonymous, and the frame reader's early detach had stopped closing its queue.
**`PaneChannels.close()` publishes before it closes.** The in-flight settlement
is created and stored first, so a concurrent caller shares this close rather
than starting a second one or being told a close that has not happened had
finished. A close that fails clears it, so the next caller retries.
**CL7 asserts the concrete refusal** — the named prop and the source location —
rather than the absence of a provider message, which an unrelated failure would
also satisfy.
**The deadlock was mine, not the provider's.** A bounded reproduction — one
self-closing pane through `foregroundTerminalGrid()`, fake tmux, a real worker
on a real socket, and a shell fixture that signals its start and then stays —
traced the whole teardown in order the moment a hangup was actually delivered:
detach, worker settlement, holder-free goodbye, channels closed, server stopped.
The earlier row never delivered one. It passed `hangup` as a provider
dependency, which an earlier repair had removed, so the override was inert and
the run waited on a real SIGHUP that never came. No production change was needed
for it, and the instrumentation is gone.
**One real defect it did expose.** `useHangupCancellation` discarded what
`next(request)` returned, so every ordinary run through the installer was
refused for having "returned before the document produced a result". The result
is returned now, and `underHangup` is typed to carry it.
**TH4 is the host boundary, driven by the installed listener.** A real document
with a live grid, through everything `foregroundTerminalGrid()` installs, with
`process.kill(process.pid, "SIGHUP")` rather than a stand-in. It proves
cancellation rather than reader close, that the sibling after the grid never
ran, that the pane's child and every worker are gone, that the server is gone,
that the private directory — removed last, after its sockets close — is gone,
and that the SIGHUP listener went with the run that installed it. Every wait is
on an event: the pane child's own start file, and each worker's own exit.
Both halves of the installer are load-bearing: removing
`useHangupCancellation()` leaves TH4 hanging on a grid nothing ends, and
removing the provider registration fails it outright.
One thing recorded rather than asserted around: cancelling the document from
inside its own middleware surfaces as core's "middleware returned before the
document produced a result" rather than as `TerminalLost`, because the guard
fires on the cancelled canonical execution first. The observable contract holds
— the run fails, teardown completes, no sibling runs — so the row asserts those
and not the wording.
… handling (#732)
The teardown was the least-covered part of this provider, and covering it
found two defects.
A close *request* could fail outside the boundary that handled the waits.
`socket.destroy()` and `server.close()` were called after their closure
watch had been attached and queued, so a request that threw left a wait
nothing would ever settle — the whole close hung rather than failing. The
requests are now inside the same boundary, a watch whose request threw is
abandoned rather than awaited, and the handle stays out of the closed set so
a later call asks it again while leaving the ones that closed alone.
A retried teardown restarted rather than resumed. Every phase was re-asked,
so a worker that had already said goodbye and gone answered the second ask
as "a worker that was gone" — and that answer replaced the reason the first
attempt could not finish. The composite's own finalizer retries after a
failed `destroy()`, so this was the ordinary path: a document was told its
pane had vanished when what had actually happened was that the server would
not stop. Phases that succeeded are now remembered, and a retry resumes at
the one that failed.
The teardown itself moves out of the composite closure into
`createGridTeardown()`, which is what lets a row drive it with scripted
workers over real private sockets.
Rows: TH5 freezes the ordinary foreground-host branch — the same live grid
as TH4, ended by a reader detach through the fake control channel instead of
a hangup, asserting the exact result handed back through
`useHangupCancellation()`. It fails if either the tmux provider or the POSIX
observer is removed from `foregroundTerminalGrid()`. TH6 freezes entrypoint
selection. Tier TD covers the combined teardown: shared in-flight teardown
under concurrent destroys, the three protocol refusals, one pane's failure
stranding neither the next pane nor the channels nor the server, first-
failure preservation, the frozen order through to path removal, the retried
close request, the resumed retry, and the document-level refusal.
Real terminal restoration remains #726's real-tmux evidence.
…732)
The suite hung forever under Node and Bun. Not failed — hung, which leaves a
runtime shard running until the job's own timeout with nothing to read.
A pane's worker is this executable re-invoked under the hidden
`terminal-worker` subcommand, and only the hosts that present grids register
it: the Deno entrypoint and the compiled binary. On Node and Bun the same
argument vector names a *document* called `terminal-worker`, so the worker
exits with ENOENT before it connects and the parent waits for a pane that
will never say hello. It stalls entering TW3, the first row that spawns a
real worker.
$ tsx packages/cli/src/node.ts terminal-worker 0 <dir>
ENOENT: no such file or directory, open 'terminal-worker'
That Node and Bun install no grid provider is the design, so the fix is the
exclusion this repository already has a mechanism for rather than a portable
worker. Every other test file in this stack runs under Node unchanged; this
is the only one that cannot.
What the exclusion does and does not preserve, stated precisely because the
rationale is the reason a later reader would trust it: provider absence is
covered portably by TG9 in packages/core/tests/terminal-grid.test.ts, which
runs on all three runtimes. TH6's entrypoint-selection freeze is textual, so
proving it once under Deno proves it everywhere. TH3 makes the same claim as
TG9 but is excluded with the rest of the file and proves nothing here. What
is genuinely Deno-only is the worker, socket and fake-tmux integration.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 4 redundant comments. Inline suggestions to remove them below.

if (paneWorker !== undefined) {
// Not `main()`: it would bind SIGINT to its own shutdown and exit 130 on the
// first `^C` typed into the pane, which is the keystroke the foreground child
// is supposed to receive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is supposed to receive.

if (paneWorker !== undefined) {
// Not `main()`: it would bind SIGINT to its own shutdown and exit 130 on the
// first `^C` typed into the pane, which is the keystroke the foreground child
// is supposed to receive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is supposed to receive.

() => useDenoWorkflowHost(HELPER),
useMachineSessions(),
// This host presents grids: it has a terminal to divide, and it can
// re-invoke itself for one pane.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// re-invoke itself for one pane.

// request that threw must not stop the others being asked. The watch for
// one that threw is abandoned rather than awaited — nothing is going to
// close it — and the handle is left out of `shut`, so a later call asks
// again.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// again.

Sign up for freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

✨ Open terminal grids with tmux in foreground runs (#732) - #747

Draft
taras wants to merge 15 commits into
agent/issue-731-pane-native-launchfrom
agent/issue-732-tmux-grid
Draft

✨ Open terminal grids with tmux in foreground runs (#732)#747
taras wants to merge 15 commits into
agent/issue-731-pane-native-launchfrom
agent/issue-732-tmux-grid

Conversation

@taras

@tarastaras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes#732. Fifth and final implementation Story under Quest #717, stacked on #741.

Base is agent/issue-731-pane-native-launch, not main, as the Story's verification stack directs. Branched from 8b8936c25c5b1c934f7ee1a4b003b8324c2e3452, the accepted head of #741.

Draft: checkpoint 3 of 3 is outstanding. Host installation, Node/Bun refusal, SIGHUP → structured cancellation, the CLI regressions, and the architecture inventory update are not in this branch yet. Everything below is delivered and verified.

Why

#729 froze the language, #730 made a grid execute through a replaceable provider, and #731 let native Agent sessions live in its panes — all against a controlled provider that presents nothing. This Story is the first production presentation: an authored grid opening as one foreground tmux workspace, with no tmux command or identifier anywhere in the document.

What changes

Before: xmd run on a real terminal had the whole grid contract and nothing to present it with.

After: the Deno and compiled foreground hosts open the grid through one invocation-private tmux server — panes in the authored row-major order, each running a worker that owns its pane's terminal.

How it works

prerequisites → private dir (0700) + per-pane socket + 0600 token
→ private tmux server → pane per ordinal, each on its worker
→ explicit layout + swaps into authored order
→ workers authenticate → readiness → attach
→ … → detach asked → workers quiesce → server proved gone → dir removed

packages/runtime/terminal-processes.ts is the host's answer to "is this pane free": the process table, terminal holders, signal delivery and reachability, behind one seam whose default refuses every question. Refusing is the point — "nobody is there" and "I cannot see" are the two answers a quiescence proof must never confuse. paneOccupants() snapshots before the first signal, because a killed child's children reparent to init.

packages/cli/src/terminal/layout.ts writes the layout string tmux prints and accepts, sized row-major from the authored column count. select-layout tiled cannot implement an authored columns — the same four panes become 2×2 in one terminal and 4×1 in another. tmux also fills leaves in window-list order and ignores the pane ids in them, so authored order is imposed afterwards by swapsInto().

packages/cli/src/terminal/pane-channel.ts + pane-protocol.ts + pane-worker.ts are the private channel and the worker. Exact argv, cwd and environment cross the socket as bytes; tmux's parser is told a directory and an ordinal and nothing else. The worker is xmd terminal-worker <ordinal> <dir> — this executable, so it works in the compiled distribution — dispatched at the entrypoint before main() and run under run(), because main() binds SIGINT to its own shutdown and would eat the first ^C typed into a pane.

packages/cli/src/terminal/tmux-grid.ts is the hidden composite. Three clients kept apart: the reader's visible one, a control-mode client attached -f no-output so pane bytes never reach this process, and the workers, which are not clients at all. An attach client's exit code cannot classify a close — it is 0 after detach-client, 0 after kill-session and 1 after kill-server — so the control channel does it.

packages/cli/src/terminal/attach-client.ts owns the visible client and nothing else. A pane child is settled by sweeping its group and its terminal; the reader's terminal is held by XMD, whatever started XMD, and the rest of its foreground group, so a settlement of that shape aimed at this client is aimed at the document.

Review guide

Start with:packages/cli/src/terminal/attach-client.ts — the narrowest boundary in the change, and the one with the most to lose.

Then review:

  1. packages/runtime/terminal-processes.ts — what may be claimed about a pane, and the refusing default.
  2. packages/cli/src/terminal/pane-worker.ts and pane-channel.ts — the handshake and what crosses it.
  3. packages/cli/src/terminal/tmux-grid.ts — the three clients, and stop().
  4. packages/cli/src/terminal/layout.ts — the string, and why the swaps exist.

Look carefully at:

  • escalate() in pane-child.ts excluding the worker's ancestors. A pane worker is tmux's session leader, so its group holds nothing above it — but a settlement one signal from killing the run that started the grid should not rest on the topology being what it should be. TW9 found this by sweeping the test runner.
  • stop() in tmux-grid.ts: a successful kill-server is not proof. It refuses unless the recorded pid is unreachable and the server refuses its session, and the socket file is not part of it because it outlives the server.

What must stay true

  • A pane is free only when it is proved free — enforced by the refusing seam default, checked by TP1 and TP5–TP9.
  • Nothing private in a diagnostic — enforced by TmuxCommandFailed carrying the step name alone, checked by TG10 and TG13 against planted markers in the socket, session, pane/client identifiers, worker directory, argv, environment and stderr.
  • The visible client's escalation names one pid — enforced by attach-client.ts never reading a group, descendants or holders, checked by TG11 and TG13 with bystanders in the table.
  • Authored order survives a server that ignores the layout's ids — enforced by swapsInto(), checked by TG2 against a fake that reproduces that behaviour.
  • The private directory outlives its sockets — enforced by finalizer ordering plus awaited closures, checked by TG12 counting real close events at the moment of removal.

How to verify it

deno task test packages/runtime/tests/terminal-processes.test.ts # 1 passed (9 steps)
deno task test packages/cli/tests/terminal-grid-tmux.test.ts # 3 passed (34 steps)
deno task test packages/cli/tests/session-launch-cli.test.ts # 1 passed (5 steps)

deno task check exit 0; deno task lint 0 errors.

Tier TW starts a real worker process over a real socket — no tmux at all — and covers the modes, the token's single use, three ways of failing the handshake, awkward argv (spaces, ;, both quote kinds, $HOME) arriving intact, a child that never starts, one-live-child exclusivity, display written and never read, and shutdown's final sweep.

Tier TG runs against a fake server that reproduces the two behaviours this code exists for: a split inserts its pane into the window list after the one it split, and layout leaves are filled in window-list order with the ids ignored. Not faked: the layout string, the swap decisions, and real control-mode bytes from a fixture process through the same splitter and classifier.

Every substantive claim was broken on purpose and re-run: removing the swaps fails TG2; a fake that honours the leaf ids also fails TG2; settling the visible client like a pane child fails TG11; restoring raw arguments to a failure fails TG10; not awaiting the socket closures fails TG12; discarding the final wait after SIGKILL fails TG13.

Scope

Included

  • The host process/terminal observer and its quiescence shapes.
  • Layout, swaps, the private channel, the worker, the hidden composite, the visible client.

Intentionally unchanged

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • The description matches the final diff and test results — checkpoint 3 still to come.

A terminal grid may not report a pane settled, admit the next launch into it, or
let the document continue while something a launch started can still act. A PID,
a delivered signal, an attach client going away and an elapsed timeout each
establish none of that.
`packages/runtime/terminal-processes.ts` is what does: the process table,
terminal holders, signal delivery and reachability, behind one host seam whose
own default refuses every question. Refusing is the point — "nobody is there"
and "I cannot see" are the two answers a quiescence proof must never confuse, so
a host that installs no observer stops the document rather than reporting a pane
quiet it never looked at. The POSIX handler answers with `ps` and `lsof`; the
`lsof` sweep is the expensive half and grows with the process count, which is
why it is behind the seam rather than inlined.
Two shapes carry the rule. `paneOccupants()` takes the snapshot — the child, its
descendants, its process group — and must be taken *before* the first signal,
because a killed child's children reparent to init and a later reading names
fewer processes than the launch actually started. `establishQuiescence()` asks
about every one of them and about the terminal, and reports everything still
true rather than the first thing it found.
Nothing here decides policy. It reports; the pane worker finishing a launch and
the provider tearing a grid down decide what the report means.
Tier TP proves the difference between establishing and assuming: a host with no
observer refuses, the POSIX reader finds this process in the real table, a
snapshot read after a kill names nobody, and a pane whose child is gone is still
not free while a descendant runs or anything else holds the terminal.
`select-layout tiled` picks its own column count from the window's dimensions,
so the same four panes are 2×2 in one terminal and 4×1 in another. An authored
`columns` has to be told to tmux rather than asked of it.
`packages/cli/src/terminal/layout.ts` writes the description tmux prints in
`#{window_layout}` and accepts back: a checksum, then a tree of cells sized
row-major from the pane count and the authored column count. A final row with
fewer panes than columns spans the row, because tmux has no empty cells and the
author wrote panes rather than a rectangle.
One thing the string cannot do is place a particular pane — tmux fills the
leaves in window-list order and ignores the pane ids they name — so authored
order is imposed afterwards by swaps. `swapsInto()` says which, produces none
for an order that is already right, and refuses a window that does not hold a
pane the author wrote instead of putting some other pane there.
Tier TX checks the geometry at four terminal sizes, that the cells tile exactly
with one separator between them, that the checksum tracks the tree, and all
three swap cases.
A pane's initial process is a worker that owns the pane's terminal for the
pane's whole life, and everything it does is asked of it over a socket only this
invocation can reach.
**The channel.** One directory per grid, mode 0700, directly under `$TMPDIR`
because a Unix socket path is capped at 104 bytes and a directory named after a
repository path spends most of that first. Inside it, one socket and one
mode-0600 token per pane, both written before any pane exists, so a worker that
starts finds its socket listening rather than racing it. Admission is the whole
boundary: a connection is admitted when its first frame is a `hello` naming this
pane's ordinal and carrying this pane's token, and a connection that says
anything else, says it late, names another ordinal, or arrives after that pane is
admitted is closed without being answered. The token is single-use because the
worker removes the file as it reads it.
**What crosses it.** The exact argv vector, working directory and environment.
tmux has a command parser, and a command parser is a place where an argument can
become two arguments, or a quote, or a `;`. tmux is told a directory and an
ordinal, and that is all its parser ever sees.
**The worker.** `xmd terminal-worker <ordinal> <dir>` — reusing this executable
rather than shipping a second script, which is what makes it work in the
compiled distribution. It is in no command table, so it is in no help output and
no catalog, and naming it grants nothing: without a pane's single-use token
nobody answers. It is dispatched at the entrypoint, before `main()`, and runs
under `run()`, because `main()` binds SIGINT to its own shutdown and would exit
130 on the first `^C` typed into the pane — the keystroke the foreground child is
supposed to receive. It ignores SIGINT, SIGQUIT and SIGTSTP itself so the child,
which gets default dispositions across `exec`, is the one interrupted.
**Readiness and settlement, kept apart.** Readiness is the runtime's `spawn`
event and nothing earlier; a missing executable delivers `error` instead of it,
never after it. Settlement is the escalation and sweep that follow — a child that
exited on its own may have left descendants in its group or an orphan holding the
terminal, and the pane is not free until neither is true. `exited` is reported
only after that, so the next launch is refused while a sweep that would reach it
is still running.
One hazard the evidence found: the settlement sweeps the process group it is in,
and a worker that was not a session leader would be sweeping whatever started it.
In a pane tmux makes it one — but a settlement one signal away from killing the
run that started the grid is not something to leave to the topology being what it
should be, so the sweep now never reaches an ancestor of the worker.
Tier TW proves it with a real worker process over a real socket and no tmux at
all: the modes, the removal, the handshake, three ways of failing it, awkward
argv crossing intact, a child that never starts, one-live-child exclusivity,
display written and never read, and shutdown's final sweep.
One invocation-private server per grid, on its own socket, started with
`-f /dev/null` so a reader's `.tmux.conf` cannot redecide an authored layout. A
pane per authored ordinal, each running that pane's worker — tmux's parser sees
an ordinal and a directory and never a launch's argv. Nothing is visible until
`attach()`, which core calls only after every pane has reported a start.
Three clients, kept apart because they answer different questions. The visible
one is the reader's. The control one attaches `-f no-output`, so pane bytes
never travel through this process, and what it reports is how reader detach,
server stop and control loss are told apart — an attach client's exit code
cannot tell them apart, being 0 after `detach-client`, 0 after `kill-session`
and 1 after `kill-server`. The workers are not clients at all; they are the
panes.
Teardown is registered before the first command, so a composite that fails
half-built still takes its server down. A detach is *asked for* before anything
is signalled, because a client that leaves restores the terminal and one that is
killed cannot. `stop()` establishes the server pid is unreachable and the server
refuses its session — never the socket file's absence, which outlives it.
`probeTmux()` answers the prerequisites before a server exists: a terminal to
divide, and a tmux new enough to divide it as an authored layout needs.
Tier TG runs against a fake server that reproduces the behaviours this code
exists to work around — a split inserts its pane into the window list after the
one it split, and a layout string's leaves are filled in window-list order with
the ids in them ignored. What is not faked is the composite: the same layout
string, the same swap decisions, and real control-mode lines from a fixture
process through the same splitter and classifier.
Both halves of the ordering claim were broken on purpose: removing the swaps
fails TG2, and a fake that honours the leaf ids fails TG2 as well — so the row
is passing because the composite imposes the order, not because the two happened
to coincide.
Stated plainly, and not claimed here: a fixture client inherits a pipe, so it
cannot restore a terminal it never had. That a real `tmux attach` gives the
reader's terminal back when asked to detach is #726's evidence on real tmux.
**The visible client is not a pane child.** A pane child is settled by sweeping
its process group and its terminal, because a pane's terminal belongs to the
grid. The reader's terminal belongs to the run: everything holding it is XMD,
whatever started XMD, and the rest of XMD's foreground group. A settlement of
that shape aimed at the attach client is a settlement aimed at the document.
`attach-client.ts` owns exactly one process instead — asked to detach first,
through tmux, and only then insisted on by pid, with no group, no descendants
and no terminal sweep anywhere in it.
**A successful `kill-server` is not proof.** Teardown now succeeds only once the
recorded server pid is unreachable and the server refuses its own session, and
throws a provider-neutral `TerminalTeardownFailed` when either is still unproved
at the bound. The rule is in the resource finalizer too, so a preparation that
failed halfway is held to it as well.
**Nothing private in a diagnostic.** `TmuxCommandFailed` carries the step's name
and nothing else — not the arguments, which hold the socket path, session name,
pane and client identifiers and the worker's private directory, and not stderr,
which tmux writes paths into. A provider's topology stays private on the paths
taken when something goes wrong, which are the paths a diagnostic is read on.
**Closures before removal.** The private directory is removed only after every
accepted socket and every listening server has actually closed — counted from
their own `close` events rather than from having been asked.
Three regressions, each broken on purpose and re-run:
- TG11 gives the process table company — XMD, its parent, two more in the same
group, and four holders of the reader's terminal — and proves the escalation
reaches the client's pid alone. Settling it like a pane child fails it.
- TG10 plants markers in the socket, session, pane and client identifiers, the
worker directory, the arguments and stderr, and proves none reaches the
surfaced error. Restoring raw arguments fails it.
- TG12 counts real closures at the moment of removal. Not awaiting them fails it.
Also conformed to the repository's rules: `@effectionx/fs` for stat, rm,
readTextFile and writeTextFile, with `node:fs/promises` kept only for `chmod`
and `appendFile`, both adapted through `until`; the client fixture is an
Effection operation; and the newly introduced `as const` assertions are gone in
favour of typed values.
`end()` sent SIGKILL and then discarded what the wait after it established, so
a client still holding the reader's terminal was reported as torn down. The
shared `stop()` resolved successfully on top of that, and the document carried
on.
It now establishes the client is gone, and raises a provider-neutral teardown
failure when it is not — so `stop()` rejects and the document stops instead.
`leftWithin()` also looks once more at the boundary itself rather than falling
back on the cached exit event: a client that left during the final interval is
gone, and reporting it as still there would be reporting a stale reading.
The boundary is unchanged and still narrow: detach is asked for through tmux
first, and every signal after that names the exact client pid. Nothing inspects
or signals its process group, its descendants, or the holders of the reader's
terminal — on this terminal, each of those is the run itself. The refusal
carries none of the socket, session, client name, argv, environment, terminal
or host message.
TG13 models a client that survives the ask, SIGTERM and SIGKILL: teardown
refuses, the signals delivered are exactly SIGTERM and SIGKILL to the client's
pid, three same-group bystanders and three holders of the reader's terminal are
untouched, and no planted marker reaches the refusal. TG11's successful
escalation is unchanged.
Reinstating the discarded result fails TG13 and leaves TG11 green, which is the
discrimination the two rows are for.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 7 redundant comments. Inline suggestions to remove them below.

child = spawnChild(command, args, {
cwd: options.cwd,
env: options.env,
// The reader's terminal, handed straight through.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The reader's terminal, handed straight through.

env: request.env,
// The whole point of a pane: the child reads this terminal and draws on
// it directly, so nothing between it and the reader can buffer, reorder
// or capture what passes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// or capture what passes.

found.set(id, {
ordinal: paneIds.indexOf(id),
id,
// The worker reports `ttys003`; tmux reports `/dev/ttys003`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The worker reports `ttys003`; tmux reports `/dev/ttys003`.

while (Date.now() < deadline) {
const names = yield* clientNames(tmux);
// The control client attaches with no tty of its own, so a named client is
// the visible one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// the visible one.

switch (command) {
case "new-session": {
alive = true;
// `... -c <cwd> <command...>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// `... -c <cwd> <command...>`

case "set":
return "";
case "split-window": {
// `... -t <target> -c <cwd> -P -F #{pane_id} <command...>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// `... -t <target> -c <cwd> -P -F #{pane_id} <command...>`

*reachable([pid]): Operation<boolean> {
try {
// Signal 0 delivers nothing: it asks the kernel whether the pid is
// reachable, which is the whole question here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// reachable, which is the whole question here.

@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown

PR #747: ✨ Open terminal grids with tmux in foreground runs (#732)

32 files, +7136 / -50

Scope

🔴 PR has 7186 lines changed. Split into focused PRs.

🟡 7186 lines changed. PRs under 400 receive more thorough review.

🟡 32 files changed. Are all changes related?

🟡 Changes span 8 directories.

🟡 New abstraction files: packages/cli/src/terminal/provider.ts. Verify 3+ consumers.

Structural

Oxlint structural signals:

  • no-unused-vars ×10: packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
  • no-redundant-type-constituents ×4: packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/pane-child.ts, packages/cli/src/terminal/pane-channel.ts
  • no-empty-function ×2: packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts
  • no-unnecessary-type-arguments ×2: packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/tmux-grid.ts
  • no-unnecessary-type-assertion ×2: packages/core/src/expand.ts

Slop

  • packages/runtime/terminal-processes.ts:292 (removed)
  • packages/cli/src/compiled.ts:58// is supposed to receive.
  • packages/cli/src/deno.ts:74// is supposed to receive.
  • packages/cli/src/deno.ts:122// re-invoke itself for one pane.
  • packages/cli/src/terminal/pane-channel.ts:160// again.

Oxlint slop signals:

  • no-console ×3: packages/cli/src/cli.ts
  • no-inferrable-types ×2: packages/core/src/expand.ts

Static Analysis

Oxlint: 57 diagnostics across 12 files (19 rules)
Density: 0.008 violations/added-line

no-unused-vars (10): packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
no-unsafe-type-assertion (5): packages/cli/src/deno.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
no-floating-promises (5): packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts (+1)
no-redundant-type-constituents (4): packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/pane-child.ts, packages/cli/src/terminal/pane-channel.ts
consistent-return (4): packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/provider.ts, packages/cli/src/terminal/pane-channel.ts (+1)
no-base-to-string (4): packages/core/src/expand.ts
no-shadow (3): packages/cli/src/terminal/provider.ts, packages/runtime/terminal.ts, packages/core/src/expand.ts
no-console (3): packages/cli/src/cli.ts
unbound-method (3): packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts
consistent-function-scoping (2): packages/cli/src/terminal/pane-worker.ts, packages/cli/tests/fixtures/fake-tmux.ts
no-empty-function (2): packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts
no-array-sort (2): packages/cli/src/terminal/tmux-grid.ts, packages/cli/src/terminal/layout.ts
no-inferrable-types (2): packages/core/src/expand.ts
no-unnecessary-type-arguments (2): packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/tmux-grid.ts
no-unnecessary-type-assertion (2): packages/core/src/expand.ts
no-useless-spread (1): packages/cli/src/terminal/pane-channel.ts
no-useless-fallback-in-spread (1): packages/core/src/expand.ts
restrict-template-expressions (1): packages/core/src/expand.ts
no-implied-eval (1): packages/core/src/expand.ts

Correctness

No extraneous code patterns detected.

`provider.ts` is where #730's provider-neutral request meets tmux: it prepares
the private channels, the hidden server and the panes, resolves each pane's
worker command before a server exists, and hands core a composite it drives
through its own lifecycle. Nothing tmux-shaped crosses in either direction.
The reader leaving and the host's terminal going away settle the same
`closed()`. That is deliberate: a hangup is not a second teardown path to keep
honest separately, it is the ordinary structured close every other stop uses.
The SIGHUP listener is a resource, so it is removed with the run rather than
answering for a terminal the next one is using.
`host.ts` states which hosts present grids. The Deno entrypoint and the compiled
binary supply `foregroundTerminalGrid()`; every other caller gets
`unsupportedTerminalGrid`, which still opens the installation so a grid is
validated and refused by core rather than being silently absent. Node and Bun
therefore catalog and validate the same grids and open none — threaded through
`AgentStack` beside the machine-session assembly, which is the same shape this
repository already uses for "Deno supplies the live one, Node and Bun supply the
one that installs nothing".
architecture.md's terminal-grid inventory row said "implementation unbuilt",
which four layers had made untrue. It now says what each Story built, that the
controlled provider remains the authority for core lifecycle semantics, that
this Story's evidence uses a fake tmux with real tmux behaviour remaining
#726's, and that Node and Bun install no operational provider.
Checkpoint 3's evidence is not in this commit: the Node/Bun refusal row, the
SIGHUP-through-host-installation row, and the CLI regressions are still to come.
Implements architecture commit 802b07d.
`TerminalComposite.launch(ordinal, request, spawned)` is required of every
composite. Core closes the pane-scoped launcher over it and the pane's authored
ordinal, so after claim admission and the pane flush a `<Session.Launch>`
written in a paired pane reaches *that pane's* terminal. The ordinal lives in
core's closure and enters no native request, Agent request, session key,
construction route, durable phase, result or diagnostic.
The pane launcher is now the end of the chain. Middleware written nearer the
authored launch still composes in front and may observe, wrap, refuse or
short-circuit; what it can no longer do is reach past, because past it is the
root foreground launcher and the root terminal is the one thing a pane exists
to avoid. A composite that cannot run a pane's launch refuses — there is no
fallback, because the only thing to fall back to is the wrong terminal. Root
`<Session.Launch>` is untouched.
The tmux composite sends the exact command vector, working directory and
environment over the pane's authenticated channel; tmux's parser sees a
directory and an ordinal. `shell()` stays separate and keeps deriving the
executable from live host policy. `spawned` is invoked only for the
worker-observed runtime spawn event.
Also fails closed on settlement: `requireQuiescent()` is the rule everything
downstream is conditional on, and a settlement that could not prove the pane
free no longer clears the pane, reports success, or admits another launch.
TG20 proves the endpoint against real workers on real sockets with a fake tmux
that now starts the pane commands it is given, and with no `<TestAgent>` or
nearer launcher in front: exact argv, cwd and environment arrive at the pane's
authenticated worker; a root-foreground-launcher sentinel is never entered while
a root launch still reaches it; distinct panes launch concurrently; and a pane
the composite cannot serve refuses.
Tier GN is rewired through the endpoint, which is what exposed the gap: before
this, its pane launches reached `<TestAgent>`'s launcher and nothing could tell
that from reaching the pane. GN now separates the two — `launches` at the pane
endpoint, `agentLaunches` at the root route — and GN3 and GN8 assert both.
Carries the three evidence gaps from 7511e77 and the six repairs.
**TW13 now proves the worker.** The rejected environment switch is replaced by an
injected child seam: `runPaneWorker` takes what it starts, so a suite can run the
real worker in-process against a real channel with the one thing it cannot
arrange in another process — a child whose settlement cannot say the pane is
free. After `quiet:false` the worker clears no live entry, reports no
settlement, starts no second child, and refuses.
**TG20c is a discriminator.** Each child announces itself and blocks until both
have; a serial pair would wait for a start that had not happened.
**TG20e proves cancellation.** `runInPane()` owns it: registered before the
launch is asked for, a cancellation sends the worker's cancel and waits for a
settlement that proves the pane free. A cancelled launch does not return while
its child is live — TG20e reads the child's own pid and finds it gone.
**Process observation fails closed**, in `deno-terminal-processes.ts` behind the
runtime-named boundary Deno, the compiled binary and their pane workers install.
`kill(pid, 0)` establishes absence only for ESRCH; EPERM is a process that
exists and this user may not signal, so it raises rather than reading "I may not
ask" as "nothing is there". A `ps` that would not run is not an empty table. Only
`lsof -t`'s documented exit-1-with-no-output is read as "nobody".
**Every listener is scope-owned.** No `.once()` and no `{ once: true }` in the
touched production code: named handlers, removed by the scope that installed
them, and kept installed through any wait they resolve. The worker's SIGINT,
SIGQUIT and SIGTSTP handlers are its run scope's. TW14 counts them across event
delivery, no delivery, startup failure and cancellation.
**One ordered teardown.** `tearDown()` is idempotent and covers both core's
`destroy()` and a preparation that failed halfway: detach and prove the visible
client stopped, ask every worker to shut down, await each settlement, terminal
sweep and goodbye, refuse on anything unproved, and only then stop the server and
prove it gone. Sockets, their servers and the private directory come down after
it, in the scopes that own them.
**SIGHUP is cancellation, not a reader close.** A reader who detaches selects a
close outcome; a terminal that is gone cancels the document through the ordinary
structured path, runs the whole teardown, and lets no following sibling run.
**Hosts state what they are.** Deno and compiled install the provider and the
observer together; everyone else installs neither and still validates. TH1–TH3
cover a missing terminal, an unusable tmux, and a host with no provider.
The inventory now says what was built and what the evidence is: fake tmux with
real workers and real sockets, with real tmux behaviour on macOS remaining #726's.
…ce (#732)
**Observation carries stderr, and reads only what it understands.** `lsof -t`
exits 1 saying nothing when a file has no holders, and exits 1 *with a
diagnostic* when it could not look; without stderr those are the same status,
and one means "nobody" while the other means "I do not know". Only the exact
empty shape is accepted. A successful run whose lines are not all readable, and
a `ps` reading with lines it cannot parse, now refuse rather than answering with
the subset they happened to recognise — a sweep satisfied by that is a sweep
that never saw what was there. TP2f and TP2g cover both.
**Teardown is one retry-safe lifecycle.** It is marked complete only after it
succeeds, so a repeat caller observes the same teardown rather than skipping
unfinished work, and a teardown that failed is retried rather than remembered as
done. Per worker, in order: shutdown asked, settlement required, a goodbye that
names no surviving holder, then the channel closing — a worker that was gone,
disconnected, or stopped part-way is a failure, not a success. Channels close
before the server is stopped, and the server's absence is proved before the
private paths go. Every acquired resource is still attempted after an earlier
failure, and the first failure is what surfaces.
**Every listener is scope-owned, including the frame reader.** `readFrames()` is
a resource whose named data, close and error handlers come off on delivery, on a
frame that does not parse, on cancellation and on ordinary exit. Startup
listeners are removed once startup resolves; the ones a settlement still needs
stay until the scope ends. TW14 now counts on the emitters themselves — the
child process, and the channel's sockets and servers — across delivery, no
delivery, startup failure and cancellation, with the cancellation coordinated by
the child's own start rather than a sleep.
**Host evidence.** TH4 drives the hangup through the operation the foreground
installer wraps `Execution.document` with: it stays structured cancellation,
runs the complete teardown, and lets no following sibling run. TH5 exercises the
assembly the runtime-named entrypoints call — provider and observer together, or
neither. CL6 and CL7 add the CLI grid regressions.
One thing CL6 found and records rather than hides: a grid under a pipe is
refused at the run's foreground lease, before any provider is contacted — and
the wording it gets is the foreground launcher's, which names `<Session.Launch>`
though the document writes none. The refusal is correct and early; the sentence
is aimed at the wrong feature.
The inventory no longer says the required pane endpoint remains to be
implemented, and claims the completed teardown now that it is there.
**Every registration is named and owned.** `net.createServer(cb)` and
`server.listen(cb)` both register anonymous listeners nothing can take off
again; both are now named handlers, with `connection` removed by the channel's
scope and `listening`/`error` removed synchronously once the listen resolves,
however it resolved. `readFrames()` takes all three protocol handlers off the
moment that reader terminates — a close, an error, or a frame that is not the
protocol — and tells its consumers, because a reader that detached silently
would leave them waiting on a conversation that ended. The resource cleanup
stays for the paths that terminate nothing: a cancelled scope, and a socket that
never says anything.
`spawn` and `error` are the two answers to one question, so whichever arrives
takes both off; `exit` stays, because the settlement is still waiting on it. The
same rule for the visible attach client.
**TW14 is discriminating.** It holds references to the child processes, the
accepted socket and the servers, and asserts their listener counts after each
scope ends — delivery, no delivery, startup failure and cancellation, with the
cancellation coordinated by the child's own start signal. It caught two real
misses while being written: the server's `connection` handler was still
anonymous, and the frame reader's early detach had stopped closing its queue.
**`PaneChannels.close()` publishes before it closes.** The in-flight settlement
is created and stored first, so a concurrent caller shares this close rather
than starting a second one or being told a close that has not happened had
finished. A close that fails clears it, so the next caller retries.
**CL7 asserts the concrete refusal** — the named prop and the source location —
rather than the absence of a provider message, which an unrelated failure would
also satisfy.
**The deadlock was mine, not the provider's.** A bounded reproduction — one
self-closing pane through `foregroundTerminalGrid()`, fake tmux, a real worker
on a real socket, and a shell fixture that signals its start and then stays —
traced the whole teardown in order the moment a hangup was actually delivered:
detach, worker settlement, holder-free goodbye, channels closed, server stopped.
The earlier row never delivered one. It passed `hangup` as a provider
dependency, which an earlier repair had removed, so the override was inert and
the run waited on a real SIGHUP that never came. No production change was needed
for it, and the instrumentation is gone.
**One real defect it did expose.** `useHangupCancellation` discarded what
`next(request)` returned, so every ordinary run through the installer was
refused for having "returned before the document produced a result". The result
is returned now, and `underHangup` is typed to carry it.
**TH4 is the host boundary, driven by the installed listener.** A real document
with a live grid, through everything `foregroundTerminalGrid()` installs, with
`process.kill(process.pid, "SIGHUP")` rather than a stand-in. It proves
cancellation rather than reader close, that the sibling after the grid never
ran, that the pane's child and every worker are gone, that the server is gone,
that the private directory — removed last, after its sockets close — is gone,
and that the SIGHUP listener went with the run that installed it. Every wait is
on an event: the pane child's own start file, and each worker's own exit.
Both halves of the installer are load-bearing: removing
`useHangupCancellation()` leaves TH4 hanging on a grid nothing ends, and
removing the provider registration fails it outright.
One thing recorded rather than asserted around: cancelling the document from
inside its own middleware surfaces as core's "middleware returned before the
document produced a result" rather than as `TerminalLost`, because the guard
fires on the cancelled canonical execution first. The observable contract holds
— the run fails, teardown completes, no sibling runs — so the row asserts those
and not the wording.
… handling (#732)
The teardown was the least-covered part of this provider, and covering it
found two defects.
A close *request* could fail outside the boundary that handled the waits.
`socket.destroy()` and `server.close()` were called after their closure
watch had been attached and queued, so a request that threw left a wait
nothing would ever settle — the whole close hung rather than failing. The
requests are now inside the same boundary, a watch whose request threw is
abandoned rather than awaited, and the handle stays out of the closed set so
a later call asks it again while leaving the ones that closed alone.
A retried teardown restarted rather than resumed. Every phase was re-asked,
so a worker that had already said goodbye and gone answered the second ask
as "a worker that was gone" — and that answer replaced the reason the first
attempt could not finish. The composite's own finalizer retries after a
failed `destroy()`, so this was the ordinary path: a document was told its
pane had vanished when what had actually happened was that the server would
not stop. Phases that succeeded are now remembered, and a retry resumes at
the one that failed.
The teardown itself moves out of the composite closure into
`createGridTeardown()`, which is what lets a row drive it with scripted
workers over real private sockets.
Rows: TH5 freezes the ordinary foreground-host branch — the same live grid
as TH4, ended by a reader detach through the fake control channel instead of
a hangup, asserting the exact result handed back through
`useHangupCancellation()`. It fails if either the tmux provider or the POSIX
observer is removed from `foregroundTerminalGrid()`. TH6 freezes entrypoint
selection. Tier TD covers the combined teardown: shared in-flight teardown
under concurrent destroys, the three protocol refusals, one pane's failure
stranding neither the next pane nor the channels nor the server, first-
failure preservation, the frozen order through to path removal, the retried
close request, the resumed retry, and the document-level refusal.
Real terminal restoration remains #726's real-tmux evidence.
…732)
The suite hung forever under Node and Bun. Not failed — hung, which leaves a
runtime shard running until the job's own timeout with nothing to read.
A pane's worker is this executable re-invoked under the hidden
`terminal-worker` subcommand, and only the hosts that present grids register
it: the Deno entrypoint and the compiled binary. On Node and Bun the same
argument vector names a *document* called `terminal-worker`, so the worker
exits with ENOENT before it connects and the parent waits for a pane that
will never say hello. It stalls entering TW3, the first row that spawns a
real worker.
$ tsx packages/cli/src/node.ts terminal-worker 0 <dir>
ENOENT: no such file or directory, open 'terminal-worker'
That Node and Bun install no grid provider is the design, so the fix is the
exclusion this repository already has a mechanism for rather than a portable
worker. Every other test file in this stack runs under Node unchanged; this
is the only one that cannot.
What the exclusion does and does not preserve, stated precisely because the
rationale is the reason a later reader would trust it: provider absence is
covered portably by TG9 in packages/core/tests/terminal-grid.test.ts, which
runs on all three runtimes. TH6's entrypoint-selection freeze is textual, so
proving it once under Deno proves it everywhere. TH3 makes the same claim as
TG9 but is excluded with the rest of the file and proves nothing here. What
is genuinely Deno-only is the worker, socket and fake-tmux integration.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 4 redundant comments. Inline suggestions to remove them below.

if (paneWorker !== undefined) {
// Not `main()`: it would bind SIGINT to its own shutdown and exit 130 on the
// first `^C` typed into the pane, which is the keystroke the foreground child
// is supposed to receive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is supposed to receive.

if (paneWorker !== undefined) {
// Not `main()`: it would bind SIGINT to its own shutdown and exit 130 on the
// first `^C` typed into the pane, which is the keystroke the foreground child
// is supposed to receive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is supposed to receive.

() => useDenoWorkflowHost(HELPER),
useMachineSessions(),
// This host presents grids: it has a terminal to divide, and it can
// re-invoke itself for one pane.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// re-invoke itself for one pane.

// request that threw must not stop the others being asked. The watch for
// one that threw is abandoned rather than awaited — nothing is going to
// close it — and the handle is left out of `shut`, so a later call asks
// again.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// again.

Sign up for freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

✨ Open terminal grids with tmux in foreground runs (#732) - #747

Draft
taras wants to merge 15 commits into
agent/issue-731-pane-native-launchfrom
agent/issue-732-tmux-grid
Draft

✨ Open terminal grids with tmux in foreground runs (#732)#747
taras wants to merge 15 commits into
agent/issue-731-pane-native-launchfrom
agent/issue-732-tmux-grid

Conversation

@taras

@tarastaras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes#732. Fifth and final implementation Story under Quest #717, stacked on #741.

Base is agent/issue-731-pane-native-launch, not main, as the Story's verification stack directs. Branched from 8b8936c25c5b1c934f7ee1a4b003b8324c2e3452, the accepted head of #741.

Draft: checkpoint 3 of 3 is outstanding. Host installation, Node/Bun refusal, SIGHUP → structured cancellation, the CLI regressions, and the architecture inventory update are not in this branch yet. Everything below is delivered and verified.

Why

#729 froze the language, #730 made a grid execute through a replaceable provider, and #731 let native Agent sessions live in its panes — all against a controlled provider that presents nothing. This Story is the first production presentation: an authored grid opening as one foreground tmux workspace, with no tmux command or identifier anywhere in the document.

What changes

Before: xmd run on a real terminal had the whole grid contract and nothing to present it with.

After: the Deno and compiled foreground hosts open the grid through one invocation-private tmux server — panes in the authored row-major order, each running a worker that owns its pane's terminal.

How it works

prerequisites → private dir (0700) + per-pane socket + 0600 token
→ private tmux server → pane per ordinal, each on its worker
→ explicit layout + swaps into authored order
→ workers authenticate → readiness → attach
→ … → detach asked → workers quiesce → server proved gone → dir removed

packages/runtime/terminal-processes.ts is the host's answer to "is this pane free": the process table, terminal holders, signal delivery and reachability, behind one seam whose default refuses every question. Refusing is the point — "nobody is there" and "I cannot see" are the two answers a quiescence proof must never confuse. paneOccupants() snapshots before the first signal, because a killed child's children reparent to init.

packages/cli/src/terminal/layout.ts writes the layout string tmux prints and accepts, sized row-major from the authored column count. select-layout tiled cannot implement an authored columns — the same four panes become 2×2 in one terminal and 4×1 in another. tmux also fills leaves in window-list order and ignores the pane ids in them, so authored order is imposed afterwards by swapsInto().

packages/cli/src/terminal/pane-channel.ts + pane-protocol.ts + pane-worker.ts are the private channel and the worker. Exact argv, cwd and environment cross the socket as bytes; tmux's parser is told a directory and an ordinal and nothing else. The worker is xmd terminal-worker <ordinal> <dir> — this executable, so it works in the compiled distribution — dispatched at the entrypoint before main() and run under run(), because main() binds SIGINT to its own shutdown and would eat the first ^C typed into a pane.

packages/cli/src/terminal/tmux-grid.ts is the hidden composite. Three clients kept apart: the reader's visible one, a control-mode client attached -f no-output so pane bytes never reach this process, and the workers, which are not clients at all. An attach client's exit code cannot classify a close — it is 0 after detach-client, 0 after kill-session and 1 after kill-server — so the control channel does it.

packages/cli/src/terminal/attach-client.ts owns the visible client and nothing else. A pane child is settled by sweeping its group and its terminal; the reader's terminal is held by XMD, whatever started XMD, and the rest of its foreground group, so a settlement of that shape aimed at this client is aimed at the document.

Review guide

Start with:packages/cli/src/terminal/attach-client.ts — the narrowest boundary in the change, and the one with the most to lose.

Then review:

  1. packages/runtime/terminal-processes.ts — what may be claimed about a pane, and the refusing default.
  2. packages/cli/src/terminal/pane-worker.ts and pane-channel.ts — the handshake and what crosses it.
  3. packages/cli/src/terminal/tmux-grid.ts — the three clients, and stop().
  4. packages/cli/src/terminal/layout.ts — the string, and why the swaps exist.

Look carefully at:

  • escalate() in pane-child.ts excluding the worker's ancestors. A pane worker is tmux's session leader, so its group holds nothing above it — but a settlement one signal from killing the run that started the grid should not rest on the topology being what it should be. TW9 found this by sweeping the test runner.
  • stop() in tmux-grid.ts: a successful kill-server is not proof. It refuses unless the recorded pid is unreachable and the server refuses its session, and the socket file is not part of it because it outlives the server.

What must stay true

  • A pane is free only when it is proved free — enforced by the refusing seam default, checked by TP1 and TP5–TP9.
  • Nothing private in a diagnostic — enforced by TmuxCommandFailed carrying the step name alone, checked by TG10 and TG13 against planted markers in the socket, session, pane/client identifiers, worker directory, argv, environment and stderr.
  • The visible client's escalation names one pid — enforced by attach-client.ts never reading a group, descendants or holders, checked by TG11 and TG13 with bystanders in the table.
  • Authored order survives a server that ignores the layout's ids — enforced by swapsInto(), checked by TG2 against a fake that reproduces that behaviour.
  • The private directory outlives its sockets — enforced by finalizer ordering plus awaited closures, checked by TG12 counting real close events at the moment of removal.

How to verify it

deno task test packages/runtime/tests/terminal-processes.test.ts # 1 passed (9 steps)
deno task test packages/cli/tests/terminal-grid-tmux.test.ts # 3 passed (34 steps)
deno task test packages/cli/tests/session-launch-cli.test.ts # 1 passed (5 steps)

deno task check exit 0; deno task lint 0 errors.

Tier TW starts a real worker process over a real socket — no tmux at all — and covers the modes, the token's single use, three ways of failing the handshake, awkward argv (spaces, ;, both quote kinds, $HOME) arriving intact, a child that never starts, one-live-child exclusivity, display written and never read, and shutdown's final sweep.

Tier TG runs against a fake server that reproduces the two behaviours this code exists for: a split inserts its pane into the window list after the one it split, and layout leaves are filled in window-list order with the ids ignored. Not faked: the layout string, the swap decisions, and real control-mode bytes from a fixture process through the same splitter and classifier.

Every substantive claim was broken on purpose and re-run: removing the swaps fails TG2; a fake that honours the leaf ids also fails TG2; settling the visible client like a pane child fails TG11; restoring raw arguments to a failure fails TG10; not awaiting the socket closures fails TG12; discarding the final wait after SIGKILL fails TG13.

Scope

Included

  • The host process/terminal observer and its quiescence shapes.
  • Layout, swaps, the private channel, the worker, the hidden composite, the visible client.

Intentionally unchanged

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • The description matches the final diff and test results — checkpoint 3 still to come.

A terminal grid may not report a pane settled, admit the next launch into it, or
let the document continue while something a launch started can still act. A PID,
a delivered signal, an attach client going away and an elapsed timeout each
establish none of that.
`packages/runtime/terminal-processes.ts` is what does: the process table,
terminal holders, signal delivery and reachability, behind one host seam whose
own default refuses every question. Refusing is the point — "nobody is there"
and "I cannot see" are the two answers a quiescence proof must never confuse, so
a host that installs no observer stops the document rather than reporting a pane
quiet it never looked at. The POSIX handler answers with `ps` and `lsof`; the
`lsof` sweep is the expensive half and grows with the process count, which is
why it is behind the seam rather than inlined.
Two shapes carry the rule. `paneOccupants()` takes the snapshot — the child, its
descendants, its process group — and must be taken *before* the first signal,
because a killed child's children reparent to init and a later reading names
fewer processes than the launch actually started. `establishQuiescence()` asks
about every one of them and about the terminal, and reports everything still
true rather than the first thing it found.
Nothing here decides policy. It reports; the pane worker finishing a launch and
the provider tearing a grid down decide what the report means.
Tier TP proves the difference between establishing and assuming: a host with no
observer refuses, the POSIX reader finds this process in the real table, a
snapshot read after a kill names nobody, and a pane whose child is gone is still
not free while a descendant runs or anything else holds the terminal.
`select-layout tiled` picks its own column count from the window's dimensions,
so the same four panes are 2×2 in one terminal and 4×1 in another. An authored
`columns` has to be told to tmux rather than asked of it.
`packages/cli/src/terminal/layout.ts` writes the description tmux prints in
`#{window_layout}` and accepts back: a checksum, then a tree of cells sized
row-major from the pane count and the authored column count. A final row with
fewer panes than columns spans the row, because tmux has no empty cells and the
author wrote panes rather than a rectangle.
One thing the string cannot do is place a particular pane — tmux fills the
leaves in window-list order and ignores the pane ids they name — so authored
order is imposed afterwards by swaps. `swapsInto()` says which, produces none
for an order that is already right, and refuses a window that does not hold a
pane the author wrote instead of putting some other pane there.
Tier TX checks the geometry at four terminal sizes, that the cells tile exactly
with one separator between them, that the checksum tracks the tree, and all
three swap cases.
A pane's initial process is a worker that owns the pane's terminal for the
pane's whole life, and everything it does is asked of it over a socket only this
invocation can reach.
**The channel.** One directory per grid, mode 0700, directly under `$TMPDIR`
because a Unix socket path is capped at 104 bytes and a directory named after a
repository path spends most of that first. Inside it, one socket and one
mode-0600 token per pane, both written before any pane exists, so a worker that
starts finds its socket listening rather than racing it. Admission is the whole
boundary: a connection is admitted when its first frame is a `hello` naming this
pane's ordinal and carrying this pane's token, and a connection that says
anything else, says it late, names another ordinal, or arrives after that pane is
admitted is closed without being answered. The token is single-use because the
worker removes the file as it reads it.
**What crosses it.** The exact argv vector, working directory and environment.
tmux has a command parser, and a command parser is a place where an argument can
become two arguments, or a quote, or a `;`. tmux is told a directory and an
ordinal, and that is all its parser ever sees.
**The worker.** `xmd terminal-worker <ordinal> <dir>` — reusing this executable
rather than shipping a second script, which is what makes it work in the
compiled distribution. It is in no command table, so it is in no help output and
no catalog, and naming it grants nothing: without a pane's single-use token
nobody answers. It is dispatched at the entrypoint, before `main()`, and runs
under `run()`, because `main()` binds SIGINT to its own shutdown and would exit
130 on the first `^C` typed into the pane — the keystroke the foreground child is
supposed to receive. It ignores SIGINT, SIGQUIT and SIGTSTP itself so the child,
which gets default dispositions across `exec`, is the one interrupted.
**Readiness and settlement, kept apart.** Readiness is the runtime's `spawn`
event and nothing earlier; a missing executable delivers `error` instead of it,
never after it. Settlement is the escalation and sweep that follow — a child that
exited on its own may have left descendants in its group or an orphan holding the
terminal, and the pane is not free until neither is true. `exited` is reported
only after that, so the next launch is refused while a sweep that would reach it
is still running.
One hazard the evidence found: the settlement sweeps the process group it is in,
and a worker that was not a session leader would be sweeping whatever started it.
In a pane tmux makes it one — but a settlement one signal away from killing the
run that started the grid is not something to leave to the topology being what it
should be, so the sweep now never reaches an ancestor of the worker.
Tier TW proves it with a real worker process over a real socket and no tmux at
all: the modes, the removal, the handshake, three ways of failing it, awkward
argv crossing intact, a child that never starts, one-live-child exclusivity,
display written and never read, and shutdown's final sweep.
One invocation-private server per grid, on its own socket, started with
`-f /dev/null` so a reader's `.tmux.conf` cannot redecide an authored layout. A
pane per authored ordinal, each running that pane's worker — tmux's parser sees
an ordinal and a directory and never a launch's argv. Nothing is visible until
`attach()`, which core calls only after every pane has reported a start.
Three clients, kept apart because they answer different questions. The visible
one is the reader's. The control one attaches `-f no-output`, so pane bytes
never travel through this process, and what it reports is how reader detach,
server stop and control loss are told apart — an attach client's exit code
cannot tell them apart, being 0 after `detach-client`, 0 after `kill-session`
and 1 after `kill-server`. The workers are not clients at all; they are the
panes.
Teardown is registered before the first command, so a composite that fails
half-built still takes its server down. A detach is *asked for* before anything
is signalled, because a client that leaves restores the terminal and one that is
killed cannot. `stop()` establishes the server pid is unreachable and the server
refuses its session — never the socket file's absence, which outlives it.
`probeTmux()` answers the prerequisites before a server exists: a terminal to
divide, and a tmux new enough to divide it as an authored layout needs.
Tier TG runs against a fake server that reproduces the behaviours this code
exists to work around — a split inserts its pane into the window list after the
one it split, and a layout string's leaves are filled in window-list order with
the ids in them ignored. What is not faked is the composite: the same layout
string, the same swap decisions, and real control-mode lines from a fixture
process through the same splitter and classifier.
Both halves of the ordering claim were broken on purpose: removing the swaps
fails TG2, and a fake that honours the leaf ids fails TG2 as well — so the row
is passing because the composite imposes the order, not because the two happened
to coincide.
Stated plainly, and not claimed here: a fixture client inherits a pipe, so it
cannot restore a terminal it never had. That a real `tmux attach` gives the
reader's terminal back when asked to detach is #726's evidence on real tmux.
**The visible client is not a pane child.** A pane child is settled by sweeping
its process group and its terminal, because a pane's terminal belongs to the
grid. The reader's terminal belongs to the run: everything holding it is XMD,
whatever started XMD, and the rest of XMD's foreground group. A settlement of
that shape aimed at the attach client is a settlement aimed at the document.
`attach-client.ts` owns exactly one process instead — asked to detach first,
through tmux, and only then insisted on by pid, with no group, no descendants
and no terminal sweep anywhere in it.
**A successful `kill-server` is not proof.** Teardown now succeeds only once the
recorded server pid is unreachable and the server refuses its own session, and
throws a provider-neutral `TerminalTeardownFailed` when either is still unproved
at the bound. The rule is in the resource finalizer too, so a preparation that
failed halfway is held to it as well.
**Nothing private in a diagnostic.** `TmuxCommandFailed` carries the step's name
and nothing else — not the arguments, which hold the socket path, session name,
pane and client identifiers and the worker's private directory, and not stderr,
which tmux writes paths into. A provider's topology stays private on the paths
taken when something goes wrong, which are the paths a diagnostic is read on.
**Closures before removal.** The private directory is removed only after every
accepted socket and every listening server has actually closed — counted from
their own `close` events rather than from having been asked.
Three regressions, each broken on purpose and re-run:
- TG11 gives the process table company — XMD, its parent, two more in the same
group, and four holders of the reader's terminal — and proves the escalation
reaches the client's pid alone. Settling it like a pane child fails it.
- TG10 plants markers in the socket, session, pane and client identifiers, the
worker directory, the arguments and stderr, and proves none reaches the
surfaced error. Restoring raw arguments fails it.
- TG12 counts real closures at the moment of removal. Not awaiting them fails it.
Also conformed to the repository's rules: `@effectionx/fs` for stat, rm,
readTextFile and writeTextFile, with `node:fs/promises` kept only for `chmod`
and `appendFile`, both adapted through `until`; the client fixture is an
Effection operation; and the newly introduced `as const` assertions are gone in
favour of typed values.
`end()` sent SIGKILL and then discarded what the wait after it established, so
a client still holding the reader's terminal was reported as torn down. The
shared `stop()` resolved successfully on top of that, and the document carried
on.
It now establishes the client is gone, and raises a provider-neutral teardown
failure when it is not — so `stop()` rejects and the document stops instead.
`leftWithin()` also looks once more at the boundary itself rather than falling
back on the cached exit event: a client that left during the final interval is
gone, and reporting it as still there would be reporting a stale reading.
The boundary is unchanged and still narrow: detach is asked for through tmux
first, and every signal after that names the exact client pid. Nothing inspects
or signals its process group, its descendants, or the holders of the reader's
terminal — on this terminal, each of those is the run itself. The refusal
carries none of the socket, session, client name, argv, environment, terminal
or host message.
TG13 models a client that survives the ask, SIGTERM and SIGKILL: teardown
refuses, the signals delivered are exactly SIGTERM and SIGKILL to the client's
pid, three same-group bystanders and three holders of the reader's terminal are
untouched, and no planted marker reaches the refusal. TG11's successful
escalation is unchanged.
Reinstating the discarded result fails TG13 and leaves TG11 green, which is the
discrimination the two rows are for.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 7 redundant comments. Inline suggestions to remove them below.

child = spawnChild(command, args, {
cwd: options.cwd,
env: options.env,
// The reader's terminal, handed straight through.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The reader's terminal, handed straight through.

env: request.env,
// The whole point of a pane: the child reads this terminal and draws on
// it directly, so nothing between it and the reader can buffer, reorder
// or capture what passes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// or capture what passes.

found.set(id, {
ordinal: paneIds.indexOf(id),
id,
// The worker reports `ttys003`; tmux reports `/dev/ttys003`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The worker reports `ttys003`; tmux reports `/dev/ttys003`.

while (Date.now() < deadline) {
const names = yield* clientNames(tmux);
// The control client attaches with no tty of its own, so a named client is
// the visible one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// the visible one.

switch (command) {
case "new-session": {
alive = true;
// `... -c <cwd> <command...>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// `... -c <cwd> <command...>`

case "set":
return "";
case "split-window": {
// `... -t <target> -c <cwd> -P -F #{pane_id} <command...>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// `... -t <target> -c <cwd> -P -F #{pane_id} <command...>`

*reachable([pid]): Operation<boolean> {
try {
// Signal 0 delivers nothing: it asks the kernel whether the pid is
// reachable, which is the whole question here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// reachable, which is the whole question here.

@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown

PR #747: ✨ Open terminal grids with tmux in foreground runs (#732)

32 files, +7136 / -50

Scope

🔴 PR has 7186 lines changed. Split into focused PRs.

🟡 7186 lines changed. PRs under 400 receive more thorough review.

🟡 32 files changed. Are all changes related?

🟡 Changes span 8 directories.

🟡 New abstraction files: packages/cli/src/terminal/provider.ts. Verify 3+ consumers.

Structural

Oxlint structural signals:

  • no-unused-vars ×10: packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
  • no-redundant-type-constituents ×4: packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/pane-child.ts, packages/cli/src/terminal/pane-channel.ts
  • no-empty-function ×2: packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts
  • no-unnecessary-type-arguments ×2: packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/tmux-grid.ts
  • no-unnecessary-type-assertion ×2: packages/core/src/expand.ts

Slop

  • packages/runtime/terminal-processes.ts:292 (removed)
  • packages/cli/src/compiled.ts:58// is supposed to receive.
  • packages/cli/src/deno.ts:74// is supposed to receive.
  • packages/cli/src/deno.ts:122// re-invoke itself for one pane.
  • packages/cli/src/terminal/pane-channel.ts:160// again.

Oxlint slop signals:

  • no-console ×3: packages/cli/src/cli.ts
  • no-inferrable-types ×2: packages/core/src/expand.ts

Static Analysis

Oxlint: 57 diagnostics across 12 files (19 rules)
Density: 0.008 violations/added-line

no-unused-vars (10): packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
no-unsafe-type-assertion (5): packages/cli/src/deno.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
no-floating-promises (5): packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts (+1)
no-redundant-type-constituents (4): packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/pane-child.ts, packages/cli/src/terminal/pane-channel.ts
consistent-return (4): packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/provider.ts, packages/cli/src/terminal/pane-channel.ts (+1)
no-base-to-string (4): packages/core/src/expand.ts
no-shadow (3): packages/cli/src/terminal/provider.ts, packages/runtime/terminal.ts, packages/core/src/expand.ts
no-console (3): packages/cli/src/cli.ts
unbound-method (3): packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts
consistent-function-scoping (2): packages/cli/src/terminal/pane-worker.ts, packages/cli/tests/fixtures/fake-tmux.ts
no-empty-function (2): packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts
no-array-sort (2): packages/cli/src/terminal/tmux-grid.ts, packages/cli/src/terminal/layout.ts
no-inferrable-types (2): packages/core/src/expand.ts
no-unnecessary-type-arguments (2): packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/tmux-grid.ts
no-unnecessary-type-assertion (2): packages/core/src/expand.ts
no-useless-spread (1): packages/cli/src/terminal/pane-channel.ts
no-useless-fallback-in-spread (1): packages/core/src/expand.ts
restrict-template-expressions (1): packages/core/src/expand.ts
no-implied-eval (1): packages/core/src/expand.ts

Correctness

No extraneous code patterns detected.

`provider.ts` is where #730's provider-neutral request meets tmux: it prepares
the private channels, the hidden server and the panes, resolves each pane's
worker command before a server exists, and hands core a composite it drives
through its own lifecycle. Nothing tmux-shaped crosses in either direction.
The reader leaving and the host's terminal going away settle the same
`closed()`. That is deliberate: a hangup is not a second teardown path to keep
honest separately, it is the ordinary structured close every other stop uses.
The SIGHUP listener is a resource, so it is removed with the run rather than
answering for a terminal the next one is using.
`host.ts` states which hosts present grids. The Deno entrypoint and the compiled
binary supply `foregroundTerminalGrid()`; every other caller gets
`unsupportedTerminalGrid`, which still opens the installation so a grid is
validated and refused by core rather than being silently absent. Node and Bun
therefore catalog and validate the same grids and open none — threaded through
`AgentStack` beside the machine-session assembly, which is the same shape this
repository already uses for "Deno supplies the live one, Node and Bun supply the
one that installs nothing".
architecture.md's terminal-grid inventory row said "implementation unbuilt",
which four layers had made untrue. It now says what each Story built, that the
controlled provider remains the authority for core lifecycle semantics, that
this Story's evidence uses a fake tmux with real tmux behaviour remaining
#726's, and that Node and Bun install no operational provider.
Checkpoint 3's evidence is not in this commit: the Node/Bun refusal row, the
SIGHUP-through-host-installation row, and the CLI regressions are still to come.
Implements architecture commit 802b07d.
`TerminalComposite.launch(ordinal, request, spawned)` is required of every
composite. Core closes the pane-scoped launcher over it and the pane's authored
ordinal, so after claim admission and the pane flush a `<Session.Launch>`
written in a paired pane reaches *that pane's* terminal. The ordinal lives in
core's closure and enters no native request, Agent request, session key,
construction route, durable phase, result or diagnostic.
The pane launcher is now the end of the chain. Middleware written nearer the
authored launch still composes in front and may observe, wrap, refuse or
short-circuit; what it can no longer do is reach past, because past it is the
root foreground launcher and the root terminal is the one thing a pane exists
to avoid. A composite that cannot run a pane's launch refuses — there is no
fallback, because the only thing to fall back to is the wrong terminal. Root
`<Session.Launch>` is untouched.
The tmux composite sends the exact command vector, working directory and
environment over the pane's authenticated channel; tmux's parser sees a
directory and an ordinal. `shell()` stays separate and keeps deriving the
executable from live host policy. `spawned` is invoked only for the
worker-observed runtime spawn event.
Also fails closed on settlement: `requireQuiescent()` is the rule everything
downstream is conditional on, and a settlement that could not prove the pane
free no longer clears the pane, reports success, or admits another launch.
TG20 proves the endpoint against real workers on real sockets with a fake tmux
that now starts the pane commands it is given, and with no `<TestAgent>` or
nearer launcher in front: exact argv, cwd and environment arrive at the pane's
authenticated worker; a root-foreground-launcher sentinel is never entered while
a root launch still reaches it; distinct panes launch concurrently; and a pane
the composite cannot serve refuses.
Tier GN is rewired through the endpoint, which is what exposed the gap: before
this, its pane launches reached `<TestAgent>`'s launcher and nothing could tell
that from reaching the pane. GN now separates the two — `launches` at the pane
endpoint, `agentLaunches` at the root route — and GN3 and GN8 assert both.
Carries the three evidence gaps from 7511e77 and the six repairs.
**TW13 now proves the worker.** The rejected environment switch is replaced by an
injected child seam: `runPaneWorker` takes what it starts, so a suite can run the
real worker in-process against a real channel with the one thing it cannot
arrange in another process — a child whose settlement cannot say the pane is
free. After `quiet:false` the worker clears no live entry, reports no
settlement, starts no second child, and refuses.
**TG20c is a discriminator.** Each child announces itself and blocks until both
have; a serial pair would wait for a start that had not happened.
**TG20e proves cancellation.** `runInPane()` owns it: registered before the
launch is asked for, a cancellation sends the worker's cancel and waits for a
settlement that proves the pane free. A cancelled launch does not return while
its child is live — TG20e reads the child's own pid and finds it gone.
**Process observation fails closed**, in `deno-terminal-processes.ts` behind the
runtime-named boundary Deno, the compiled binary and their pane workers install.
`kill(pid, 0)` establishes absence only for ESRCH; EPERM is a process that
exists and this user may not signal, so it raises rather than reading "I may not
ask" as "nothing is there". A `ps` that would not run is not an empty table. Only
`lsof -t`'s documented exit-1-with-no-output is read as "nobody".
**Every listener is scope-owned.** No `.once()` and no `{ once: true }` in the
touched production code: named handlers, removed by the scope that installed
them, and kept installed through any wait they resolve. The worker's SIGINT,
SIGQUIT and SIGTSTP handlers are its run scope's. TW14 counts them across event
delivery, no delivery, startup failure and cancellation.
**One ordered teardown.** `tearDown()` is idempotent and covers both core's
`destroy()` and a preparation that failed halfway: detach and prove the visible
client stopped, ask every worker to shut down, await each settlement, terminal
sweep and goodbye, refuse on anything unproved, and only then stop the server and
prove it gone. Sockets, their servers and the private directory come down after
it, in the scopes that own them.
**SIGHUP is cancellation, not a reader close.** A reader who detaches selects a
close outcome; a terminal that is gone cancels the document through the ordinary
structured path, runs the whole teardown, and lets no following sibling run.
**Hosts state what they are.** Deno and compiled install the provider and the
observer together; everyone else installs neither and still validates. TH1–TH3
cover a missing terminal, an unusable tmux, and a host with no provider.
The inventory now says what was built and what the evidence is: fake tmux with
real workers and real sockets, with real tmux behaviour on macOS remaining #726's.
…ce (#732)
**Observation carries stderr, and reads only what it understands.** `lsof -t`
exits 1 saying nothing when a file has no holders, and exits 1 *with a
diagnostic* when it could not look; without stderr those are the same status,
and one means "nobody" while the other means "I do not know". Only the exact
empty shape is accepted. A successful run whose lines are not all readable, and
a `ps` reading with lines it cannot parse, now refuse rather than answering with
the subset they happened to recognise — a sweep satisfied by that is a sweep
that never saw what was there. TP2f and TP2g cover both.
**Teardown is one retry-safe lifecycle.** It is marked complete only after it
succeeds, so a repeat caller observes the same teardown rather than skipping
unfinished work, and a teardown that failed is retried rather than remembered as
done. Per worker, in order: shutdown asked, settlement required, a goodbye that
names no surviving holder, then the channel closing — a worker that was gone,
disconnected, or stopped part-way is a failure, not a success. Channels close
before the server is stopped, and the server's absence is proved before the
private paths go. Every acquired resource is still attempted after an earlier
failure, and the first failure is what surfaces.
**Every listener is scope-owned, including the frame reader.** `readFrames()` is
a resource whose named data, close and error handlers come off on delivery, on a
frame that does not parse, on cancellation and on ordinary exit. Startup
listeners are removed once startup resolves; the ones a settlement still needs
stay until the scope ends. TW14 now counts on the emitters themselves — the
child process, and the channel's sockets and servers — across delivery, no
delivery, startup failure and cancellation, with the cancellation coordinated by
the child's own start rather than a sleep.
**Host evidence.** TH4 drives the hangup through the operation the foreground
installer wraps `Execution.document` with: it stays structured cancellation,
runs the complete teardown, and lets no following sibling run. TH5 exercises the
assembly the runtime-named entrypoints call — provider and observer together, or
neither. CL6 and CL7 add the CLI grid regressions.
One thing CL6 found and records rather than hides: a grid under a pipe is
refused at the run's foreground lease, before any provider is contacted — and
the wording it gets is the foreground launcher's, which names `<Session.Launch>`
though the document writes none. The refusal is correct and early; the sentence
is aimed at the wrong feature.
The inventory no longer says the required pane endpoint remains to be
implemented, and claims the completed teardown now that it is there.
**Every registration is named and owned.** `net.createServer(cb)` and
`server.listen(cb)` both register anonymous listeners nothing can take off
again; both are now named handlers, with `connection` removed by the channel's
scope and `listening`/`error` removed synchronously once the listen resolves,
however it resolved. `readFrames()` takes all three protocol handlers off the
moment that reader terminates — a close, an error, or a frame that is not the
protocol — and tells its consumers, because a reader that detached silently
would leave them waiting on a conversation that ended. The resource cleanup
stays for the paths that terminate nothing: a cancelled scope, and a socket that
never says anything.
`spawn` and `error` are the two answers to one question, so whichever arrives
takes both off; `exit` stays, because the settlement is still waiting on it. The
same rule for the visible attach client.
**TW14 is discriminating.** It holds references to the child processes, the
accepted socket and the servers, and asserts their listener counts after each
scope ends — delivery, no delivery, startup failure and cancellation, with the
cancellation coordinated by the child's own start signal. It caught two real
misses while being written: the server's `connection` handler was still
anonymous, and the frame reader's early detach had stopped closing its queue.
**`PaneChannels.close()` publishes before it closes.** The in-flight settlement
is created and stored first, so a concurrent caller shares this close rather
than starting a second one or being told a close that has not happened had
finished. A close that fails clears it, so the next caller retries.
**CL7 asserts the concrete refusal** — the named prop and the source location —
rather than the absence of a provider message, which an unrelated failure would
also satisfy.
**The deadlock was mine, not the provider's.** A bounded reproduction — one
self-closing pane through `foregroundTerminalGrid()`, fake tmux, a real worker
on a real socket, and a shell fixture that signals its start and then stays —
traced the whole teardown in order the moment a hangup was actually delivered:
detach, worker settlement, holder-free goodbye, channels closed, server stopped.
The earlier row never delivered one. It passed `hangup` as a provider
dependency, which an earlier repair had removed, so the override was inert and
the run waited on a real SIGHUP that never came. No production change was needed
for it, and the instrumentation is gone.
**One real defect it did expose.** `useHangupCancellation` discarded what
`next(request)` returned, so every ordinary run through the installer was
refused for having "returned before the document produced a result". The result
is returned now, and `underHangup` is typed to carry it.
**TH4 is the host boundary, driven by the installed listener.** A real document
with a live grid, through everything `foregroundTerminalGrid()` installs, with
`process.kill(process.pid, "SIGHUP")` rather than a stand-in. It proves
cancellation rather than reader close, that the sibling after the grid never
ran, that the pane's child and every worker are gone, that the server is gone,
that the private directory — removed last, after its sockets close — is gone,
and that the SIGHUP listener went with the run that installed it. Every wait is
on an event: the pane child's own start file, and each worker's own exit.
Both halves of the installer are load-bearing: removing
`useHangupCancellation()` leaves TH4 hanging on a grid nothing ends, and
removing the provider registration fails it outright.
One thing recorded rather than asserted around: cancelling the document from
inside its own middleware surfaces as core's "middleware returned before the
document produced a result" rather than as `TerminalLost`, because the guard
fires on the cancelled canonical execution first. The observable contract holds
— the run fails, teardown completes, no sibling runs — so the row asserts those
and not the wording.
… handling (#732)
The teardown was the least-covered part of this provider, and covering it
found two defects.
A close *request* could fail outside the boundary that handled the waits.
`socket.destroy()` and `server.close()` were called after their closure
watch had been attached and queued, so a request that threw left a wait
nothing would ever settle — the whole close hung rather than failing. The
requests are now inside the same boundary, a watch whose request threw is
abandoned rather than awaited, and the handle stays out of the closed set so
a later call asks it again while leaving the ones that closed alone.
A retried teardown restarted rather than resumed. Every phase was re-asked,
so a worker that had already said goodbye and gone answered the second ask
as "a worker that was gone" — and that answer replaced the reason the first
attempt could not finish. The composite's own finalizer retries after a
failed `destroy()`, so this was the ordinary path: a document was told its
pane had vanished when what had actually happened was that the server would
not stop. Phases that succeeded are now remembered, and a retry resumes at
the one that failed.
The teardown itself moves out of the composite closure into
`createGridTeardown()`, which is what lets a row drive it with scripted
workers over real private sockets.
Rows: TH5 freezes the ordinary foreground-host branch — the same live grid
as TH4, ended by a reader detach through the fake control channel instead of
a hangup, asserting the exact result handed back through
`useHangupCancellation()`. It fails if either the tmux provider or the POSIX
observer is removed from `foregroundTerminalGrid()`. TH6 freezes entrypoint
selection. Tier TD covers the combined teardown: shared in-flight teardown
under concurrent destroys, the three protocol refusals, one pane's failure
stranding neither the next pane nor the channels nor the server, first-
failure preservation, the frozen order through to path removal, the retried
close request, the resumed retry, and the document-level refusal.
Real terminal restoration remains #726's real-tmux evidence.
…732)
The suite hung forever under Node and Bun. Not failed — hung, which leaves a
runtime shard running until the job's own timeout with nothing to read.
A pane's worker is this executable re-invoked under the hidden
`terminal-worker` subcommand, and only the hosts that present grids register
it: the Deno entrypoint and the compiled binary. On Node and Bun the same
argument vector names a *document* called `terminal-worker`, so the worker
exits with ENOENT before it connects and the parent waits for a pane that
will never say hello. It stalls entering TW3, the first row that spawns a
real worker.
$ tsx packages/cli/src/node.ts terminal-worker 0 <dir>
ENOENT: no such file or directory, open 'terminal-worker'
That Node and Bun install no grid provider is the design, so the fix is the
exclusion this repository already has a mechanism for rather than a portable
worker. Every other test file in this stack runs under Node unchanged; this
is the only one that cannot.
What the exclusion does and does not preserve, stated precisely because the
rationale is the reason a later reader would trust it: provider absence is
covered portably by TG9 in packages/core/tests/terminal-grid.test.ts, which
runs on all three runtimes. TH6's entrypoint-selection freeze is textual, so
proving it once under Deno proves it everywhere. TH3 makes the same claim as
TG9 but is excluded with the rest of the file and proves nothing here. What
is genuinely Deno-only is the worker, socket and fake-tmux integration.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 4 redundant comments. Inline suggestions to remove them below.

if (paneWorker !== undefined) {
// Not `main()`: it would bind SIGINT to its own shutdown and exit 130 on the
// first `^C` typed into the pane, which is the keystroke the foreground child
// is supposed to receive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is supposed to receive.

if (paneWorker !== undefined) {
// Not `main()`: it would bind SIGINT to its own shutdown and exit 130 on the
// first `^C` typed into the pane, which is the keystroke the foreground child
// is supposed to receive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is supposed to receive.

() => useDenoWorkflowHost(HELPER),
useMachineSessions(),
// This host presents grids: it has a terminal to divide, and it can
// re-invoke itself for one pane.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// re-invoke itself for one pane.

// request that threw must not stop the others being asked. The watch for
// one that threw is abandoned rather than awaited — nothing is going to
// close it — and the handle is left out of `shut`, so a later call asks
// again.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// again.

Sign up for freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

✨ Open terminal grids with tmux in foreground runs (#732) - #747

Draft
taras wants to merge 15 commits into
agent/issue-731-pane-native-launchfrom
agent/issue-732-tmux-grid
Draft

✨ Open terminal grids with tmux in foreground runs (#732)#747
taras wants to merge 15 commits into
agent/issue-731-pane-native-launchfrom
agent/issue-732-tmux-grid

Conversation

@taras

@tarastaras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes#732. Fifth and final implementation Story under Quest #717, stacked on #741.

Base is agent/issue-731-pane-native-launch, not main, as the Story's verification stack directs. Branched from 8b8936c25c5b1c934f7ee1a4b003b8324c2e3452, the accepted head of #741.

Draft: checkpoint 3 of 3 is outstanding. Host installation, Node/Bun refusal, SIGHUP → structured cancellation, the CLI regressions, and the architecture inventory update are not in this branch yet. Everything below is delivered and verified.

Why

#729 froze the language, #730 made a grid execute through a replaceable provider, and #731 let native Agent sessions live in its panes — all against a controlled provider that presents nothing. This Story is the first production presentation: an authored grid opening as one foreground tmux workspace, with no tmux command or identifier anywhere in the document.

What changes

Before: xmd run on a real terminal had the whole grid contract and nothing to present it with.

After: the Deno and compiled foreground hosts open the grid through one invocation-private tmux server — panes in the authored row-major order, each running a worker that owns its pane's terminal.

How it works

prerequisites → private dir (0700) + per-pane socket + 0600 token
→ private tmux server → pane per ordinal, each on its worker
→ explicit layout + swaps into authored order
→ workers authenticate → readiness → attach
→ … → detach asked → workers quiesce → server proved gone → dir removed

packages/runtime/terminal-processes.ts is the host's answer to "is this pane free": the process table, terminal holders, signal delivery and reachability, behind one seam whose default refuses every question. Refusing is the point — "nobody is there" and "I cannot see" are the two answers a quiescence proof must never confuse. paneOccupants() snapshots before the first signal, because a killed child's children reparent to init.

packages/cli/src/terminal/layout.ts writes the layout string tmux prints and accepts, sized row-major from the authored column count. select-layout tiled cannot implement an authored columns — the same four panes become 2×2 in one terminal and 4×1 in another. tmux also fills leaves in window-list order and ignores the pane ids in them, so authored order is imposed afterwards by swapsInto().

packages/cli/src/terminal/pane-channel.ts + pane-protocol.ts + pane-worker.ts are the private channel and the worker. Exact argv, cwd and environment cross the socket as bytes; tmux's parser is told a directory and an ordinal and nothing else. The worker is xmd terminal-worker <ordinal> <dir> — this executable, so it works in the compiled distribution — dispatched at the entrypoint before main() and run under run(), because main() binds SIGINT to its own shutdown and would eat the first ^C typed into a pane.

packages/cli/src/terminal/tmux-grid.ts is the hidden composite. Three clients kept apart: the reader's visible one, a control-mode client attached -f no-output so pane bytes never reach this process, and the workers, which are not clients at all. An attach client's exit code cannot classify a close — it is 0 after detach-client, 0 after kill-session and 1 after kill-server — so the control channel does it.

packages/cli/src/terminal/attach-client.ts owns the visible client and nothing else. A pane child is settled by sweeping its group and its terminal; the reader's terminal is held by XMD, whatever started XMD, and the rest of its foreground group, so a settlement of that shape aimed at this client is aimed at the document.

Review guide

Start with:packages/cli/src/terminal/attach-client.ts — the narrowest boundary in the change, and the one with the most to lose.

Then review:

  1. packages/runtime/terminal-processes.ts — what may be claimed about a pane, and the refusing default.
  2. packages/cli/src/terminal/pane-worker.ts and pane-channel.ts — the handshake and what crosses it.
  3. packages/cli/src/terminal/tmux-grid.ts — the three clients, and stop().
  4. packages/cli/src/terminal/layout.ts — the string, and why the swaps exist.

Look carefully at:

  • escalate() in pane-child.ts excluding the worker's ancestors. A pane worker is tmux's session leader, so its group holds nothing above it — but a settlement one signal from killing the run that started the grid should not rest on the topology being what it should be. TW9 found this by sweeping the test runner.
  • stop() in tmux-grid.ts: a successful kill-server is not proof. It refuses unless the recorded pid is unreachable and the server refuses its session, and the socket file is not part of it because it outlives the server.

What must stay true

  • A pane is free only when it is proved free — enforced by the refusing seam default, checked by TP1 and TP5–TP9.
  • Nothing private in a diagnostic — enforced by TmuxCommandFailed carrying the step name alone, checked by TG10 and TG13 against planted markers in the socket, session, pane/client identifiers, worker directory, argv, environment and stderr.
  • The visible client's escalation names one pid — enforced by attach-client.ts never reading a group, descendants or holders, checked by TG11 and TG13 with bystanders in the table.
  • Authored order survives a server that ignores the layout's ids — enforced by swapsInto(), checked by TG2 against a fake that reproduces that behaviour.
  • The private directory outlives its sockets — enforced by finalizer ordering plus awaited closures, checked by TG12 counting real close events at the moment of removal.

How to verify it

deno task test packages/runtime/tests/terminal-processes.test.ts # 1 passed (9 steps)
deno task test packages/cli/tests/terminal-grid-tmux.test.ts # 3 passed (34 steps)
deno task test packages/cli/tests/session-launch-cli.test.ts # 1 passed (5 steps)

deno task check exit 0; deno task lint 0 errors.

Tier TW starts a real worker process over a real socket — no tmux at all — and covers the modes, the token's single use, three ways of failing the handshake, awkward argv (spaces, ;, both quote kinds, $HOME) arriving intact, a child that never starts, one-live-child exclusivity, display written and never read, and shutdown's final sweep.

Tier TG runs against a fake server that reproduces the two behaviours this code exists for: a split inserts its pane into the window list after the one it split, and layout leaves are filled in window-list order with the ids ignored. Not faked: the layout string, the swap decisions, and real control-mode bytes from a fixture process through the same splitter and classifier.

Every substantive claim was broken on purpose and re-run: removing the swaps fails TG2; a fake that honours the leaf ids also fails TG2; settling the visible client like a pane child fails TG11; restoring raw arguments to a failure fails TG10; not awaiting the socket closures fails TG12; discarding the final wait after SIGKILL fails TG13.

Scope

Included

  • The host process/terminal observer and its quiescence shapes.
  • Layout, swaps, the private channel, the worker, the hidden composite, the visible client.

Intentionally unchanged

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • The description matches the final diff and test results — checkpoint 3 still to come.

A terminal grid may not report a pane settled, admit the next launch into it, or
let the document continue while something a launch started can still act. A PID,
a delivered signal, an attach client going away and an elapsed timeout each
establish none of that.
`packages/runtime/terminal-processes.ts` is what does: the process table,
terminal holders, signal delivery and reachability, behind one host seam whose
own default refuses every question. Refusing is the point — "nobody is there"
and "I cannot see" are the two answers a quiescence proof must never confuse, so
a host that installs no observer stops the document rather than reporting a pane
quiet it never looked at. The POSIX handler answers with `ps` and `lsof`; the
`lsof` sweep is the expensive half and grows with the process count, which is
why it is behind the seam rather than inlined.
Two shapes carry the rule. `paneOccupants()` takes the snapshot — the child, its
descendants, its process group — and must be taken *before* the first signal,
because a killed child's children reparent to init and a later reading names
fewer processes than the launch actually started. `establishQuiescence()` asks
about every one of them and about the terminal, and reports everything still
true rather than the first thing it found.
Nothing here decides policy. It reports; the pane worker finishing a launch and
the provider tearing a grid down decide what the report means.
Tier TP proves the difference between establishing and assuming: a host with no
observer refuses, the POSIX reader finds this process in the real table, a
snapshot read after a kill names nobody, and a pane whose child is gone is still
not free while a descendant runs or anything else holds the terminal.
`select-layout tiled` picks its own column count from the window's dimensions,
so the same four panes are 2×2 in one terminal and 4×1 in another. An authored
`columns` has to be told to tmux rather than asked of it.
`packages/cli/src/terminal/layout.ts` writes the description tmux prints in
`#{window_layout}` and accepts back: a checksum, then a tree of cells sized
row-major from the pane count and the authored column count. A final row with
fewer panes than columns spans the row, because tmux has no empty cells and the
author wrote panes rather than a rectangle.
One thing the string cannot do is place a particular pane — tmux fills the
leaves in window-list order and ignores the pane ids they name — so authored
order is imposed afterwards by swaps. `swapsInto()` says which, produces none
for an order that is already right, and refuses a window that does not hold a
pane the author wrote instead of putting some other pane there.
Tier TX checks the geometry at four terminal sizes, that the cells tile exactly
with one separator between them, that the checksum tracks the tree, and all
three swap cases.
A pane's initial process is a worker that owns the pane's terminal for the
pane's whole life, and everything it does is asked of it over a socket only this
invocation can reach.
**The channel.** One directory per grid, mode 0700, directly under `$TMPDIR`
because a Unix socket path is capped at 104 bytes and a directory named after a
repository path spends most of that first. Inside it, one socket and one
mode-0600 token per pane, both written before any pane exists, so a worker that
starts finds its socket listening rather than racing it. Admission is the whole
boundary: a connection is admitted when its first frame is a `hello` naming this
pane's ordinal and carrying this pane's token, and a connection that says
anything else, says it late, names another ordinal, or arrives after that pane is
admitted is closed without being answered. The token is single-use because the
worker removes the file as it reads it.
**What crosses it.** The exact argv vector, working directory and environment.
tmux has a command parser, and a command parser is a place where an argument can
become two arguments, or a quote, or a `;`. tmux is told a directory and an
ordinal, and that is all its parser ever sees.
**The worker.** `xmd terminal-worker <ordinal> <dir>` — reusing this executable
rather than shipping a second script, which is what makes it work in the
compiled distribution. It is in no command table, so it is in no help output and
no catalog, and naming it grants nothing: without a pane's single-use token
nobody answers. It is dispatched at the entrypoint, before `main()`, and runs
under `run()`, because `main()` binds SIGINT to its own shutdown and would exit
130 on the first `^C` typed into the pane — the keystroke the foreground child is
supposed to receive. It ignores SIGINT, SIGQUIT and SIGTSTP itself so the child,
which gets default dispositions across `exec`, is the one interrupted.
**Readiness and settlement, kept apart.** Readiness is the runtime's `spawn`
event and nothing earlier; a missing executable delivers `error` instead of it,
never after it. Settlement is the escalation and sweep that follow — a child that
exited on its own may have left descendants in its group or an orphan holding the
terminal, and the pane is not free until neither is true. `exited` is reported
only after that, so the next launch is refused while a sweep that would reach it
is still running.
One hazard the evidence found: the settlement sweeps the process group it is in,
and a worker that was not a session leader would be sweeping whatever started it.
In a pane tmux makes it one — but a settlement one signal away from killing the
run that started the grid is not something to leave to the topology being what it
should be, so the sweep now never reaches an ancestor of the worker.
Tier TW proves it with a real worker process over a real socket and no tmux at
all: the modes, the removal, the handshake, three ways of failing it, awkward
argv crossing intact, a child that never starts, one-live-child exclusivity,
display written and never read, and shutdown's final sweep.
One invocation-private server per grid, on its own socket, started with
`-f /dev/null` so a reader's `.tmux.conf` cannot redecide an authored layout. A
pane per authored ordinal, each running that pane's worker — tmux's parser sees
an ordinal and a directory and never a launch's argv. Nothing is visible until
`attach()`, which core calls only after every pane has reported a start.
Three clients, kept apart because they answer different questions. The visible
one is the reader's. The control one attaches `-f no-output`, so pane bytes
never travel through this process, and what it reports is how reader detach,
server stop and control loss are told apart — an attach client's exit code
cannot tell them apart, being 0 after `detach-client`, 0 after `kill-session`
and 1 after `kill-server`. The workers are not clients at all; they are the
panes.
Teardown is registered before the first command, so a composite that fails
half-built still takes its server down. A detach is *asked for* before anything
is signalled, because a client that leaves restores the terminal and one that is
killed cannot. `stop()` establishes the server pid is unreachable and the server
refuses its session — never the socket file's absence, which outlives it.
`probeTmux()` answers the prerequisites before a server exists: a terminal to
divide, and a tmux new enough to divide it as an authored layout needs.
Tier TG runs against a fake server that reproduces the behaviours this code
exists to work around — a split inserts its pane into the window list after the
one it split, and a layout string's leaves are filled in window-list order with
the ids in them ignored. What is not faked is the composite: the same layout
string, the same swap decisions, and real control-mode lines from a fixture
process through the same splitter and classifier.
Both halves of the ordering claim were broken on purpose: removing the swaps
fails TG2, and a fake that honours the leaf ids fails TG2 as well — so the row
is passing because the composite imposes the order, not because the two happened
to coincide.
Stated plainly, and not claimed here: a fixture client inherits a pipe, so it
cannot restore a terminal it never had. That a real `tmux attach` gives the
reader's terminal back when asked to detach is #726's evidence on real tmux.
**The visible client is not a pane child.** A pane child is settled by sweeping
its process group and its terminal, because a pane's terminal belongs to the
grid. The reader's terminal belongs to the run: everything holding it is XMD,
whatever started XMD, and the rest of XMD's foreground group. A settlement of
that shape aimed at the attach client is a settlement aimed at the document.
`attach-client.ts` owns exactly one process instead — asked to detach first,
through tmux, and only then insisted on by pid, with no group, no descendants
and no terminal sweep anywhere in it.
**A successful `kill-server` is not proof.** Teardown now succeeds only once the
recorded server pid is unreachable and the server refuses its own session, and
throws a provider-neutral `TerminalTeardownFailed` when either is still unproved
at the bound. The rule is in the resource finalizer too, so a preparation that
failed halfway is held to it as well.
**Nothing private in a diagnostic.** `TmuxCommandFailed` carries the step's name
and nothing else — not the arguments, which hold the socket path, session name,
pane and client identifiers and the worker's private directory, and not stderr,
which tmux writes paths into. A provider's topology stays private on the paths
taken when something goes wrong, which are the paths a diagnostic is read on.
**Closures before removal.** The private directory is removed only after every
accepted socket and every listening server has actually closed — counted from
their own `close` events rather than from having been asked.
Three regressions, each broken on purpose and re-run:
- TG11 gives the process table company — XMD, its parent, two more in the same
group, and four holders of the reader's terminal — and proves the escalation
reaches the client's pid alone. Settling it like a pane child fails it.
- TG10 plants markers in the socket, session, pane and client identifiers, the
worker directory, the arguments and stderr, and proves none reaches the
surfaced error. Restoring raw arguments fails it.
- TG12 counts real closures at the moment of removal. Not awaiting them fails it.
Also conformed to the repository's rules: `@effectionx/fs` for stat, rm,
readTextFile and writeTextFile, with `node:fs/promises` kept only for `chmod`
and `appendFile`, both adapted through `until`; the client fixture is an
Effection operation; and the newly introduced `as const` assertions are gone in
favour of typed values.
`end()` sent SIGKILL and then discarded what the wait after it established, so
a client still holding the reader's terminal was reported as torn down. The
shared `stop()` resolved successfully on top of that, and the document carried
on.
It now establishes the client is gone, and raises a provider-neutral teardown
failure when it is not — so `stop()` rejects and the document stops instead.
`leftWithin()` also looks once more at the boundary itself rather than falling
back on the cached exit event: a client that left during the final interval is
gone, and reporting it as still there would be reporting a stale reading.
The boundary is unchanged and still narrow: detach is asked for through tmux
first, and every signal after that names the exact client pid. Nothing inspects
or signals its process group, its descendants, or the holders of the reader's
terminal — on this terminal, each of those is the run itself. The refusal
carries none of the socket, session, client name, argv, environment, terminal
or host message.
TG13 models a client that survives the ask, SIGTERM and SIGKILL: teardown
refuses, the signals delivered are exactly SIGTERM and SIGKILL to the client's
pid, three same-group bystanders and three holders of the reader's terminal are
untouched, and no planted marker reaches the refusal. TG11's successful
escalation is unchanged.
Reinstating the discarded result fails TG13 and leaves TG11 green, which is the
discrimination the two rows are for.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 7 redundant comments. Inline suggestions to remove them below.

child = spawnChild(command, args, {
cwd: options.cwd,
env: options.env,
// The reader's terminal, handed straight through.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The reader's terminal, handed straight through.

env: request.env,
// The whole point of a pane: the child reads this terminal and draws on
// it directly, so nothing between it and the reader can buffer, reorder
// or capture what passes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// or capture what passes.

found.set(id, {
ordinal: paneIds.indexOf(id),
id,
// The worker reports `ttys003`; tmux reports `/dev/ttys003`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The worker reports `ttys003`; tmux reports `/dev/ttys003`.

while (Date.now() < deadline) {
const names = yield* clientNames(tmux);
// The control client attaches with no tty of its own, so a named client is
// the visible one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// the visible one.

switch (command) {
case "new-session": {
alive = true;
// `... -c <cwd> <command...>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// `... -c <cwd> <command...>`

case "set":
return "";
case "split-window": {
// `... -t <target> -c <cwd> -P -F #{pane_id} <command...>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// `... -t <target> -c <cwd> -P -F #{pane_id} <command...>`

*reachable([pid]): Operation<boolean> {
try {
// Signal 0 delivers nothing: it asks the kernel whether the pid is
// reachable, which is the whole question here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// reachable, which is the whole question here.

@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown

PR #747: ✨ Open terminal grids with tmux in foreground runs (#732)

32 files, +7136 / -50

Scope

🔴 PR has 7186 lines changed. Split into focused PRs.

🟡 7186 lines changed. PRs under 400 receive more thorough review.

🟡 32 files changed. Are all changes related?

🟡 Changes span 8 directories.

🟡 New abstraction files: packages/cli/src/terminal/provider.ts. Verify 3+ consumers.

Structural

Oxlint structural signals:

  • no-unused-vars ×10: packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
  • no-redundant-type-constituents ×4: packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/pane-child.ts, packages/cli/src/terminal/pane-channel.ts
  • no-empty-function ×2: packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts
  • no-unnecessary-type-arguments ×2: packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/tmux-grid.ts
  • no-unnecessary-type-assertion ×2: packages/core/src/expand.ts

Slop

  • packages/runtime/terminal-processes.ts:292 (removed)
  • packages/cli/src/compiled.ts:58// is supposed to receive.
  • packages/cli/src/deno.ts:74// is supposed to receive.
  • packages/cli/src/deno.ts:122// re-invoke itself for one pane.
  • packages/cli/src/terminal/pane-channel.ts:160// again.

Oxlint slop signals:

  • no-console ×3: packages/cli/src/cli.ts
  • no-inferrable-types ×2: packages/core/src/expand.ts

Static Analysis

Oxlint: 57 diagnostics across 12 files (19 rules)
Density: 0.008 violations/added-line

no-unused-vars (10): packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
no-unsafe-type-assertion (5): packages/cli/src/deno.ts, packages/core/src/expand.ts, packages/cli/src/cli.ts
no-floating-promises (5): packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts (+1)
no-redundant-type-constituents (4): packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/pane-child.ts, packages/cli/src/terminal/pane-channel.ts
consistent-return (4): packages/cli/src/terminal/attach-client.ts, packages/cli/src/terminal/provider.ts, packages/cli/src/terminal/pane-channel.ts (+1)
no-base-to-string (4): packages/core/src/expand.ts
no-shadow (3): packages/cli/src/terminal/provider.ts, packages/runtime/terminal.ts, packages/core/src/expand.ts
no-console (3): packages/cli/src/cli.ts
unbound-method (3): packages/cli/src/terminal/provider.ts, packages/core/src/expand.ts
consistent-function-scoping (2): packages/cli/src/terminal/pane-worker.ts, packages/cli/tests/fixtures/fake-tmux.ts
no-empty-function (2): packages/cli/src/terminal/pane-worker.ts, packages/cli/src/cli.ts
no-array-sort (2): packages/cli/src/terminal/tmux-grid.ts, packages/cli/src/terminal/layout.ts
no-inferrable-types (2): packages/core/src/expand.ts
no-unnecessary-type-arguments (2): packages/cli/src/terminal/pane-channel.ts, packages/cli/src/terminal/tmux-grid.ts
no-unnecessary-type-assertion (2): packages/core/src/expand.ts
no-useless-spread (1): packages/cli/src/terminal/pane-channel.ts
no-useless-fallback-in-spread (1): packages/core/src/expand.ts
restrict-template-expressions (1): packages/core/src/expand.ts
no-implied-eval (1): packages/core/src/expand.ts

Correctness

No extraneous code patterns detected.

`provider.ts` is where #730's provider-neutral request meets tmux: it prepares
the private channels, the hidden server and the panes, resolves each pane's
worker command before a server exists, and hands core a composite it drives
through its own lifecycle. Nothing tmux-shaped crosses in either direction.
The reader leaving and the host's terminal going away settle the same
`closed()`. That is deliberate: a hangup is not a second teardown path to keep
honest separately, it is the ordinary structured close every other stop uses.
The SIGHUP listener is a resource, so it is removed with the run rather than
answering for a terminal the next one is using.
`host.ts` states which hosts present grids. The Deno entrypoint and the compiled
binary supply `foregroundTerminalGrid()`; every other caller gets
`unsupportedTerminalGrid`, which still opens the installation so a grid is
validated and refused by core rather than being silently absent. Node and Bun
therefore catalog and validate the same grids and open none — threaded through
`AgentStack` beside the machine-session assembly, which is the same shape this
repository already uses for "Deno supplies the live one, Node and Bun supply the
one that installs nothing".
architecture.md's terminal-grid inventory row said "implementation unbuilt",
which four layers had made untrue. It now says what each Story built, that the
controlled provider remains the authority for core lifecycle semantics, that
this Story's evidence uses a fake tmux with real tmux behaviour remaining
#726's, and that Node and Bun install no operational provider.
Checkpoint 3's evidence is not in this commit: the Node/Bun refusal row, the
SIGHUP-through-host-installation row, and the CLI regressions are still to come.
Implements architecture commit 802b07d.
`TerminalComposite.launch(ordinal, request, spawned)` is required of every
composite. Core closes the pane-scoped launcher over it and the pane's authored
ordinal, so after claim admission and the pane flush a `<Session.Launch>`
written in a paired pane reaches *that pane's* terminal. The ordinal lives in
core's closure and enters no native request, Agent request, session key,
construction route, durable phase, result or diagnostic.
The pane launcher is now the end of the chain. Middleware written nearer the
authored launch still composes in front and may observe, wrap, refuse or
short-circuit; what it can no longer do is reach past, because past it is the
root foreground launcher and the root terminal is the one thing a pane exists
to avoid. A composite that cannot run a pane's launch refuses — there is no
fallback, because the only thing to fall back to is the wrong terminal. Root
`<Session.Launch>` is untouched.
The tmux composite sends the exact command vector, working directory and
environment over the pane's authenticated channel; tmux's parser sees a
directory and an ordinal. `shell()` stays separate and keeps deriving the
executable from live host policy. `spawned` is invoked only for the
worker-observed runtime spawn event.
Also fails closed on settlement: `requireQuiescent()` is the rule everything
downstream is conditional on, and a settlement that could not prove the pane
free no longer clears the pane, reports success, or admits another launch.
TG20 proves the endpoint against real workers on real sockets with a fake tmux
that now starts the pane commands it is given, and with no `<TestAgent>` or
nearer launcher in front: exact argv, cwd and environment arrive at the pane's
authenticated worker; a root-foreground-launcher sentinel is never entered while
a root launch still reaches it; distinct panes launch concurrently; and a pane
the composite cannot serve refuses.
Tier GN is rewired through the endpoint, which is what exposed the gap: before
this, its pane launches reached `<TestAgent>`'s launcher and nothing could tell
that from reaching the pane. GN now separates the two — `launches` at the pane
endpoint, `agentLaunches` at the root route — and GN3 and GN8 assert both.
Carries the three evidence gaps from 7511e77 and the six repairs.
**TW13 now proves the worker.** The rejected environment switch is replaced by an
injected child seam: `runPaneWorker` takes what it starts, so a suite can run the
real worker in-process against a real channel with the one thing it cannot
arrange in another process — a child whose settlement cannot say the pane is
free. After `quiet:false` the worker clears no live entry, reports no
settlement, starts no second child, and refuses.
**TG20c is a discriminator.** Each child announces itself and blocks until both
have; a serial pair would wait for a start that had not happened.
**TG20e proves cancellation.** `runInPane()` owns it: registered before the
launch is asked for, a cancellation sends the worker's cancel and waits for a
settlement that proves the pane free. A cancelled launch does not return while
its child is live — TG20e reads the child's own pid and finds it gone.
**Process observation fails closed**, in `deno-terminal-processes.ts` behind the
runtime-named boundary Deno, the compiled binary and their pane workers install.
`kill(pid, 0)` establishes absence only for ESRCH; EPERM is a process that
exists and this user may not signal, so it raises rather than reading "I may not
ask" as "nothing is there". A `ps` that would not run is not an empty table. Only
`lsof -t`'s documented exit-1-with-no-output is read as "nobody".
**Every listener is scope-owned.** No `.once()` and no `{ once: true }` in the
touched production code: named handlers, removed by the scope that installed
them, and kept installed through any wait they resolve. The worker's SIGINT,
SIGQUIT and SIGTSTP handlers are its run scope's. TW14 counts them across event
delivery, no delivery, startup failure and cancellation.
**One ordered teardown.** `tearDown()` is idempotent and covers both core's
`destroy()` and a preparation that failed halfway: detach and prove the visible
client stopped, ask every worker to shut down, await each settlement, terminal
sweep and goodbye, refuse on anything unproved, and only then stop the server and
prove it gone. Sockets, their servers and the private directory come down after
it, in the scopes that own them.
**SIGHUP is cancellation, not a reader close.** A reader who detaches selects a
close outcome; a terminal that is gone cancels the document through the ordinary
structured path, runs the whole teardown, and lets no following sibling run.
**Hosts state what they are.** Deno and compiled install the provider and the
observer together; everyone else installs neither and still validates. TH1–TH3
cover a missing terminal, an unusable tmux, and a host with no provider.
The inventory now says what was built and what the evidence is: fake tmux with
real workers and real sockets, with real tmux behaviour on macOS remaining #726's.
…ce (#732)
**Observation carries stderr, and reads only what it understands.** `lsof -t`
exits 1 saying nothing when a file has no holders, and exits 1 *with a
diagnostic* when it could not look; without stderr those are the same status,
and one means "nobody" while the other means "I do not know". Only the exact
empty shape is accepted. A successful run whose lines are not all readable, and
a `ps` reading with lines it cannot parse, now refuse rather than answering with
the subset they happened to recognise — a sweep satisfied by that is a sweep
that never saw what was there. TP2f and TP2g cover both.
**Teardown is one retry-safe lifecycle.** It is marked complete only after it
succeeds, so a repeat caller observes the same teardown rather than skipping
unfinished work, and a teardown that failed is retried rather than remembered as
done. Per worker, in order: shutdown asked, settlement required, a goodbye that
names no surviving holder, then the channel closing — a worker that was gone,
disconnected, or stopped part-way is a failure, not a success. Channels close
before the server is stopped, and the server's absence is proved before the
private paths go. Every acquired resource is still attempted after an earlier
failure, and the first failure is what surfaces.
**Every listener is scope-owned, including the frame reader.** `readFrames()` is
a resource whose named data, close and error handlers come off on delivery, on a
frame that does not parse, on cancellation and on ordinary exit. Startup
listeners are removed once startup resolves; the ones a settlement still needs
stay until the scope ends. TW14 now counts on the emitters themselves — the
child process, and the channel's sockets and servers — across delivery, no
delivery, startup failure and cancellation, with the cancellation coordinated by
the child's own start rather than a sleep.
**Host evidence.** TH4 drives the hangup through the operation the foreground
installer wraps `Execution.document` with: it stays structured cancellation,
runs the complete teardown, and lets no following sibling run. TH5 exercises the
assembly the runtime-named entrypoints call — provider and observer together, or
neither. CL6 and CL7 add the CLI grid regressions.
One thing CL6 found and records rather than hides: a grid under a pipe is
refused at the run's foreground lease, before any provider is contacted — and
the wording it gets is the foreground launcher's, which names `<Session.Launch>`
though the document writes none. The refusal is correct and early; the sentence
is aimed at the wrong feature.
The inventory no longer says the required pane endpoint remains to be
implemented, and claims the completed teardown now that it is there.
**Every registration is named and owned.** `net.createServer(cb)` and
`server.listen(cb)` both register anonymous listeners nothing can take off
again; both are now named handlers, with `connection` removed by the channel's
scope and `listening`/`error` removed synchronously once the listen resolves,
however it resolved. `readFrames()` takes all three protocol handlers off the
moment that reader terminates — a close, an error, or a frame that is not the
protocol — and tells its consumers, because a reader that detached silently
would leave them waiting on a conversation that ended. The resource cleanup
stays for the paths that terminate nothing: a cancelled scope, and a socket that
never says anything.
`spawn` and `error` are the two answers to one question, so whichever arrives
takes both off; `exit` stays, because the settlement is still waiting on it. The
same rule for the visible attach client.
**TW14 is discriminating.** It holds references to the child processes, the
accepted socket and the servers, and asserts their listener counts after each
scope ends — delivery, no delivery, startup failure and cancellation, with the
cancellation coordinated by the child's own start signal. It caught two real
misses while being written: the server's `connection` handler was still
anonymous, and the frame reader's early detach had stopped closing its queue.
**`PaneChannels.close()` publishes before it closes.** The in-flight settlement
is created and stored first, so a concurrent caller shares this close rather
than starting a second one or being told a close that has not happened had
finished. A close that fails clears it, so the next caller retries.
**CL7 asserts the concrete refusal** — the named prop and the source location —
rather than the absence of a provider message, which an unrelated failure would
also satisfy.
**The deadlock was mine, not the provider's.** A bounded reproduction — one
self-closing pane through `foregroundTerminalGrid()`, fake tmux, a real worker
on a real socket, and a shell fixture that signals its start and then stays —
traced the whole teardown in order the moment a hangup was actually delivered:
detach, worker settlement, holder-free goodbye, channels closed, server stopped.
The earlier row never delivered one. It passed `hangup` as a provider
dependency, which an earlier repair had removed, so the override was inert and
the run waited on a real SIGHUP that never came. No production change was needed
for it, and the instrumentation is gone.
**One real defect it did expose.** `useHangupCancellation` discarded what
`next(request)` returned, so every ordinary run through the installer was
refused for having "returned before the document produced a result". The result
is returned now, and `underHangup` is typed to carry it.
**TH4 is the host boundary, driven by the installed listener.** A real document
with a live grid, through everything `foregroundTerminalGrid()` installs, with
`process.kill(process.pid, "SIGHUP")` rather than a stand-in. It proves
cancellation rather than reader close, that the sibling after the grid never
ran, that the pane's child and every worker are gone, that the server is gone,
that the private directory — removed last, after its sockets close — is gone,
and that the SIGHUP listener went with the run that installed it. Every wait is
on an event: the pane child's own start file, and each worker's own exit.
Both halves of the installer are load-bearing: removing
`useHangupCancellation()` leaves TH4 hanging on a grid nothing ends, and
removing the provider registration fails it outright.
One thing recorded rather than asserted around: cancelling the document from
inside its own middleware surfaces as core's "middleware returned before the
document produced a result" rather than as `TerminalLost`, because the guard
fires on the cancelled canonical execution first. The observable contract holds
— the run fails, teardown completes, no sibling runs — so the row asserts those
and not the wording.
… handling (#732)
The teardown was the least-covered part of this provider, and covering it
found two defects.
A close *request* could fail outside the boundary that handled the waits.
`socket.destroy()` and `server.close()` were called after their closure
watch had been attached and queued, so a request that threw left a wait
nothing would ever settle — the whole close hung rather than failing. The
requests are now inside the same boundary, a watch whose request threw is
abandoned rather than awaited, and the handle stays out of the closed set so
a later call asks it again while leaving the ones that closed alone.
A retried teardown restarted rather than resumed. Every phase was re-asked,
so a worker that had already said goodbye and gone answered the second ask
as "a worker that was gone" — and that answer replaced the reason the first
attempt could not finish. The composite's own finalizer retries after a
failed `destroy()`, so this was the ordinary path: a document was told its
pane had vanished when what had actually happened was that the server would
not stop. Phases that succeeded are now remembered, and a retry resumes at
the one that failed.
The teardown itself moves out of the composite closure into
`createGridTeardown()`, which is what lets a row drive it with scripted
workers over real private sockets.
Rows: TH5 freezes the ordinary foreground-host branch — the same live grid
as TH4, ended by a reader detach through the fake control channel instead of
a hangup, asserting the exact result handed back through
`useHangupCancellation()`. It fails if either the tmux provider or the POSIX
observer is removed from `foregroundTerminalGrid()`. TH6 freezes entrypoint
selection. Tier TD covers the combined teardown: shared in-flight teardown
under concurrent destroys, the three protocol refusals, one pane's failure
stranding neither the next pane nor the channels nor the server, first-
failure preservation, the frozen order through to path removal, the retried
close request, the resumed retry, and the document-level refusal.
Real terminal restoration remains #726's real-tmux evidence.
…732)
The suite hung forever under Node and Bun. Not failed — hung, which leaves a
runtime shard running until the job's own timeout with nothing to read.
A pane's worker is this executable re-invoked under the hidden
`terminal-worker` subcommand, and only the hosts that present grids register
it: the Deno entrypoint and the compiled binary. On Node and Bun the same
argument vector names a *document* called `terminal-worker`, so the worker
exits with ENOENT before it connects and the parent waits for a pane that
will never say hello. It stalls entering TW3, the first row that spawns a
real worker.
$ tsx packages/cli/src/node.ts terminal-worker 0 <dir>
ENOENT: no such file or directory, open 'terminal-worker'
That Node and Bun install no grid provider is the design, so the fix is the
exclusion this repository already has a mechanism for rather than a portable
worker. Every other test file in this stack runs under Node unchanged; this
is the only one that cannot.
What the exclusion does and does not preserve, stated precisely because the
rationale is the reason a later reader would trust it: provider absence is
covered portably by TG9 in packages/core/tests/terminal-grid.test.ts, which
runs on all three runtimes. TH6's entrypoint-selection freeze is textual, so
proving it once under Deno proves it everywhere. TH3 makes the same claim as
TG9 but is excluded with the rest of the file and proves nothing here. What
is genuinely Deno-only is the worker, socket and fake-tmux integration.

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 4 redundant comments. Inline suggestions to remove them below.

if (paneWorker !== undefined) {
// Not `main()`: it would bind SIGINT to its own shutdown and exit 130 on the
// first `^C` typed into the pane, which is the keystroke the foreground child
// is supposed to receive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is supposed to receive.

if (paneWorker !== undefined) {
// Not `main()`: it would bind SIGINT to its own shutdown and exit 130 on the
// first `^C` typed into the pane, which is the keystroke the foreground child
// is supposed to receive.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is supposed to receive.

() => useDenoWorkflowHost(HELPER),
useMachineSessions(),
// This host presents grids: it has a terminal to divide, and it can
// re-invoke itself for one pane.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// re-invoke itself for one pane.

// request that threw must not stop the others being asked. The watch for
// one that threw is abandoned rather than awaited — nothing is going to
// close it — and the handle is left out of `shut`, so a later call asks
// again.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// again.

Sign up for freeto 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

@taras