Skip to content

🔬 Prove persistent tmux pane workers for <Terminal.Grid> (#726) - #727

Closed
taras wants to merge 4 commits into
mainfrom
spike/tmux-pane-workers
Closed

🔬 Prove persistent tmux pane workers for <Terminal.Grid> (#726)#727
taras wants to merge 4 commits into
mainfrom
spike/tmux-pane-workers

Conversation

@taras

@taras taras commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Why

#717 (<Terminal.Grid>) needs a process topology that gives reliable readiness, pane output, sequential native launches and complete teardown. #726 asks for an executable proof of the persistent pane-worker topology and a report so the Planner can adopt it or the Architect can narrow the lifecycle contract. This PR is that proof and report; it decides #717's next step and ships nothing in the product binary.

What changes

Before: the topology was a hypothesis. tmux's pane_pid cannot say whether a native process started, and a native command as the pane command leaves no endpoint for document text or a second launch.

After: deno task proof:tmux-pane-workers runs one 2×2 workspace (and the smaller topologies each check needs) end to end and writes evidence.json + summary.md. plans/tmux-pane-workers-proof.md records the run at b0fcee9 — 8/8 checks, 115 claims — the measured lifecycle for 2/4/8 panes, what teardown can and cannot prove, the smallest interfaces the evidence supports, and the recommended decision: adopt the pane-worker topology, with the teardown claim narrowed to process group, ancestry at teardown start, and terminal holders.

How it works

proof.ts → outer private tmux (a terminal for the visible attach) → proof.ts --inner
  → per check: private 0700 dir → pane sockets + 0600 tokens → hidden tmux server,
    explicit row-major layout, worker.ts as each pane's initial process
    → workers connect over IPC → launch/display/cancel/shutdown
    → interactive-process resource in the worker (spawn event = ready; exit = settled;
      SIGINT → SIGKILL → snapshot sweep; terminal-holder sweep at shutdown)
    → control client (-C, no-output) classifies detach / control loss / server stop
  → evidence.json, summary.md

Review guide

Start with: plans/tmux-pane-workers-proof.md — the decision, the acceptance table, and What teardown can and cannot prove.

Then review:

  1. scripts/proofs/tmux-pane-workers/interactive-process.ts — the product-boundary resource derived from packages/runtime/launcher.ts: readiness vs settlement, escalation, the pre-signal snapshot.
  2. worker.ts and ipc.ts — what a pane worker is, what crosses the socket, admission by token, exited after the sweep, the shutdown terminal sweep; run() not main().
  3. provider.ts and layout.ts — hidden server, explicit layout + swap-pane ordering, control-mode classification, detach-before-signal on the visible client, stop() proof.
  4. checks.ts — one check per acceptance item; evidence.ts, workspace.ts, proof.ts are harness.

Look carefully at:

  • the orphan rows in the report's teardown table: the one honest negative, and where each sweep has to live (the worker at shutdown, not the provider).
  • plans/tmux-pane-workers-evidence.json is the raw run with IPC tokens redacted; its pane ids, ttys, pids and socket paths describe one run and nothing durable.

What must stay true

  • Readiness is the child-process spawn event and nothing earlier; startup-failed never follows ready — enforced by useInteractiveProcess, checked by readiness-boundary.
  • Argv, cwd and env never pass through tmux's command parser — enforced by the IPC protocol; checked by the journey's byte-for-byte argv claim.
  • A pane is free for the next child only after its sweep — enforced by the worker sending exited after stop(); checked by the journey's sequential-reuse claim.
  • Every acquired resource has one scope and one finalizer — the ownership diagram in the report is the code's structure; checked by cancellation-points at five phases.

How to verify it

  • deno task proof:tmux-pane-workers (≈5 min, unattended; needs tmux 3.6a-class and Deno) proves every claim in the report and fails if any pid, server, socket directory or terminal setting survives teardown.
  • deno task proof:tmux-pane-workers -- --only negative-children proves descendant coverage and fails if the pre-signal snapshot or the worker's terminal sweep is removed (both were removed on purpose during development and did fail).
  • -- --only layout-geometry fails if swap-pane ordering is skipped (tmux ignores leaf ids in layout strings).
  • -- --only readiness-boundary fails if ready is sent before the spawn event.
  • -- --only cancellation-points fails if stop() skips kill-server.
  • -- --attach shows the journey on your own terminal and waits for you to detach.
  • deno task lint and deno task check are green; the proof lives under scripts/proofs/, outside test discovery, so no CI suite runs it.

Scope

Included

  • The proof, its task, the report and the redacted evidence of one run.

Intentionally unchanged

  • architecture.md, the specs and packages/runtime/launcher.ts: the decision is recommended here for the Architect to record; nothing in the product moves.

Risks and limitations

  • Platform-specific by design: measured on macOS arm64 with tmux 3.6a; ps -o tpgid, lsof -t and the pty-revoke behaviour are the macOS facts the report names.
  • A descendant that left the session, closed the pane terminal and outlived its parent cannot be proven stopped by any parent process; the report says so and proposes the narrowed contract.
  • Cancellation between new-session forking the server and the server listening is a window the proof records rather than tests.

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

An executable proof of the pane-worker topology #717 needs: one persistent
worker per tmux pane owning the pane's terminal, driven over an
invocation-private Unix socket, starting interactive children that inherit
the terminal through an Effection process resource derived from the native
launcher. The proof runs the reader-visible journey, the readiness boundary,
explicit row-major layout, atomic attach, the three close signals, negative
children, cancellation at every phase, and 2/4/8-pane timings, and records
what teardown can and cannot prove.

    deno task proof:tmux-pane-workers
Evidence from b0fcee9: 8/8 checks, 115 claims, 2/4/8-pane timings over 20
runs each. Recommends adopting the persistent pane-worker topology for #717
with the teardown claim narrowed to process group, ancestry at teardown start
and terminal holders — a descendant that left the session, closed the pane's
terminal and outlived its parent is outside what a parent can prove.

@github-actions github-actions Bot 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.

rows === Math.ceil(shape.panes / shape.columns),
rows,
);
// What `select-layout tiled` would have done with the same panes.

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
// What `select-layout tiled` would have done with the same panes.

workspace.links.every((link) => link.connected()),
);

// 3. The server stops underneath everything.

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
// 3. The server stops underneath everything.


await main(function* () {
const options = parseOptions(process.argv.slice(2));
// tmux and ps output is collected, never echoed.

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
// tmux and ps output is collected, never echoed.

// tmux and ps output is collected, never echoed.
yield* Stdio.around({
*stdout() {
// Collected by the caller, never echoed.

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
// Collected by the caller, never echoed.

// Collected by the caller, never echoed.
},
*stderr() {
// Collected by the caller, never echoed.

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
// Collected by the caller, never echoed.

// A child that exited on its own may have left descendants in the
// pane's process group. They are swept before `exited` is reported,
// because `exited` is what makes the pane free for the next child —
// and a sweep running beside a new child would reach that child too.

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
// and a sweep running beside a new child would reach that child too.

limitMs: number = DEFAULT_LIMIT_MS,
): Operation<T> {
// Subscribe before scanning, so an event between the scan and the wait
// is not lost.

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 not lost.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

PR #727: 🔬 Prove persistent tmux pane workers for <Terminal.Grid> (#726)

14 files, +6544 / -0

Scope

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

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

🟡 PR mixes config and source changes.

🟡 New abstraction files: scripts/proofs/tmux-pane-workers/provider.ts. Verify 3+ consumers.

Structural

Oxlint structural signals:

  • no-unnecessary-type-arguments ×2: scripts/proofs/tmux-pane-workers/ipc.ts, scripts/proofs/tmux-pane-workers/workspace.ts
  • no-redundant-type-constituents ×1: scripts/proofs/tmux-pane-workers/interactive-process.ts

Slop

  • scripts/proofs/tmux-pane-workers/proof.ts:238// Collected by the caller, never echoed.
  • scripts/proofs/tmux-pane-workers/proof.ts:241// Collected by the caller, never echoed.
  • scripts/proofs/tmux-pane-workers/interactive-process.ts:206// Already released.

Static Analysis

Oxlint: 10 diagnostics across 6 files (4 rules)
Density: 0.002 violations/added-line

no-floating-promises (5): scripts/proofs/tmux-pane-workers/provider.ts, scripts/proofs/tmux-pane-workers/ipc.ts, scripts/proofs/tmux-pane-workers/worker.ts (+1)
no-unnecessary-type-arguments (2): scripts/proofs/tmux-pane-workers/ipc.ts, scripts/proofs/tmux-pane-workers/workspace.ts
no-unnecessary-template-expression (2): scripts/proofs/tmux-pane-workers/checks.ts
no-redundant-type-constituents (1): scripts/proofs/tmux-pane-workers/interactive-process.ts

Correctness

No extraneous code patterns detected.

A worker's settlement is now the escalation followed by the terminal-holder
sweep, on natural exit as well as cancel and shutdown, and `exited` is
reported only after it. A new sequential-handoff regression starts a child
whose descendant setsid()s, keeps the pane terminal and outlives its parent,
and proves a launch sent during settlement is refused, the descendant is
named and stopped before `exited`, and only the worker and the second child
hold the terminal after admission. Measurements gain the handoff and
relaunch latencies the sweep introduces. The setsid() + closed-terminal
orphan remains the recorded unprovable boundary.
Evidence from 650510b: 9/9 checks, 127 claims. Adds the handoff and
relaunch columns to the measurements and the sequential-handoff regression
to the teardown account; the setsid() + closed-terminal orphan stays the
recorded unprovable boundary.

@github-actions github-actions Bot 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 3 redundant comments. Inline suggestions to remove them below.

// tmux and ps output is collected, never echoed.
yield* Stdio.around({
*stdout() {
// Collected by the caller, never echoed.

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
// Collected by the caller, never echoed.

// Collected by the caller, never echoed.
},
*stderr() {
// Collected by the caller, never echoed.

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
// Collected by the caller, never echoed.

try {
child.unref();
} catch {
// Already released.

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
// Already released.

@taras

taras commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

Closing without merge. This PR was a spike whose purpose was to decide the persistent tmux pane-worker topology, not to add maintained product code.

The accepted result is preserved in #726's PASS record and #717's architecture decisions. PR #734 carries the lasting contract, and PR #747 owns the production tmux implementation and regression evidence. The spike branch and this PR remain available as historical evidence at head 80770ea3e20748109b05583d5292d6160cccc398.

@taras taras closed this Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant