Skip to content

✨ Launch native Agent sessions in independent terminal panes (#731) - #741

Open
taras wants to merge 4 commits into
agent/issue-730-terminal-executionfrom
agent/issue-731-pane-native-launch
Open

✨ Launch native Agent sessions in independent terminal panes (#731)#741
taras wants to merge 4 commits into
agent/issue-730-terminal-executionfrom
agent/issue-731-pane-native-launch

Conversation

@taras

@taras taras commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes #731. Third implementation layer under Quest #717, stacked on #738.

Why

A root <Session.Launch> owns the run's foreground terminal. Reusing that launcher inside a grid would serialize panes or deadlock against the grid's lease. A launch inside a pane must instead use that pane's terminal without learning any pane identity.

What changes

<Session.Launch> inside a paired pane reaches a pane-scoped native launcher. The launcher closes over the pane claim and routes the exact native request to the composite's required pane-execution endpoint.

pane scope → reserve this pane → flush this pane's output
           → composite launches exact request in this pane
           → runtime spawn event acknowledges readiness
           → complete child and session settlement releases the pane

Distinct panes may launch concurrently. One pane admits one live interactive operation. Two panes naming the same logical Agent session still contend through the unchanged session coordinator.

The final authored surface is <Grid> and <Pane>. This layer's exact diff predates #781 and retains the provisional <Terminal.Grid>/<Terminal> names and paths; PR #797 applies the final rename in #771 without changing launch semantics.

Contract

  • Pane identity never enters AgentLaunchRequest, session keys, construction routes, durable phases, results, or diagnostics.
  • The composite is the physical terminal endpoint; a pane launch never falls through to the root launcher.
  • Exact argv, cwd, environment, and provider-native identity remain the values produced by the Agent provider.
  • Readiness is reported only by the child's runtime spawn event.
  • The pane remains owned until child teardown, session settlement, and pane quiescence complete.
  • Root launch and completed replay remain unchanged.

Review and evidence

Start with the pane-launcher implementation and the authored TestAgent grid journey, then run the focused suites present at this layer:

deno task test packages/core/tests/agent-session-launch.test.ts
deno task test packages/runtime/tests/native-launcher.test.ts
deno task test packages/test-agent/tests/native-launch.test.ts
deno task test packages/test-agent/tests/terminal-grid-native-launch.test.ts

SP, FL, and GN distinguish pane-local routing, concurrent panes, spawn readiness, same-session contention, startup refusal, close, replay, and sequential reuse after complete settlement.

Scope

Included: pane-scoped native launch, pane output flushing, spawn acknowledgement, and deterministic TestAgent integration evidence.

Excluded: tmux, new Agent advertisements, and changes to session identity or coordinator authority.

Stack order: #738#741#747.

@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 1 redundant comment. Inline suggestions to remove them below.


// The runtime's own start event, and the only thing reported as one. A
// spawn that fails emits `error` instead, so a child that never ran never
// reports having started.

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
// reports having started.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

PR #741: ✨ Launch native Agent sessions in independent terminal panes (#731)

12 files, +1509 / -34

Scope

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

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

Structural

Oxlint structural signals:

  • no-unused-vars ×10: packages/acp/src/provider.ts, packages/core/src/expand.ts
  • no-empty-function ×3: packages/runtime/launcher.ts, packages/acp/src/provider.ts
  • no-unnecessary-type-assertion ×2: packages/core/src/expand.ts
  • no-redundant-type-constituents ×1: packages/runtime/launcher.ts

Slop

Oxlint slop signals:

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

Static Analysis

Oxlint: 51 diagnostics across 3 files (15 rules)
Density: 0.034 violations/added-line

no-unused-vars (10): packages/acp/src/provider.ts, packages/core/src/expand.ts
consistent-function-scoping (10): packages/acp/src/provider.ts
no-shadow (5): packages/runtime/launcher.ts, packages/acp/src/provider.ts, packages/core/src/expand.ts
no-unsafe-type-assertion (4): packages/acp/src/provider.ts, packages/core/src/expand.ts
no-base-to-string (4): packages/core/src/expand.ts
no-empty-function (3): packages/runtime/launcher.ts, packages/acp/src/provider.ts
no-useless-spread (3): packages/acp/src/provider.ts
no-inferrable-types (2): packages/core/src/expand.ts
no-floating-promises (2): packages/acp/src/provider.ts
no-unnecessary-type-assertion (2): packages/core/src/expand.ts
unbound-method (2): packages/core/src/expand.ts
no-useless-fallback-in-spread (1): packages/core/src/expand.ts
no-redundant-type-constituents (1): packages/runtime/launcher.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.

@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 1 redundant comment. Inline suggestions to remove them below.


// The runtime's own start event, and the only thing reported as one. A
// spawn that fails emits `error` instead, so a child that never ran never
// reports having started.

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
// reports having started.

@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 1 redundant comment. Inline suggestions to remove them below.


// The runtime's own start event, and the only thing reported as one. A
// spawn that fails emits `error` instead, so a child that never ran never
// reports having started.

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
// reports having started.

@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 4 redundant comments. Inline suggestions to remove them below.

// the reader closing a terminal grid is one — unwinds past every
// statement after it, so a decision written down here would never be
// reached; written as this scope's cleanup, it is reached on every
// path there is.

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
// path there is.

// prepared and never handed over, leaves one. Either one
// missing leaves the session owned rather than looking
// finished, which is what the next owner is told to recover
// deliberately.

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
// deliberately.

// A `<Session.Launch>` written in this pane finds this launcher simply
// by being here: it reserves and flushes this pane instead of competing
// for the run's one foreground lease, and the child it starts is what
// makes this pane ready.

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
// makes this pane ready.


// The runtime's own start event, and the only thing reported as one. A
// spawn that fails emits `error` instead, so a child that never ran never
// reports having started.

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
// reports having started.

A `<Session.Launch>` written at the root takes the run's one foreground
terminal, so native UIs are sequential. Inside a `<Terminal>` that would defeat
the point of a grid, where every pane is interactive at the same time.

So core installs a native launcher in each paired pane's scope, closed over that
pane's claim. `<Session.Launch>` finds it by being written there: it is handed
no pane, ordinal, token or mode, and its request, result and retained phases are
the ones a root launch would have. What changes is which terminal answers
`reserve` and `flush` — the pane's, through its claim, so two panes do not
contend and one pane admits one live launch at a time. A pane also flushes what
it has rendered before the UI draws over it, which is the root rule in the one
place a pane's text goes.

Readiness now has a boundary a launch can report. `NativeLauncherHandler.launch`
takes the runtime's child-start event as a parameter — not a request member, not
a context, not a result — and the foreground launcher reports it from the
child's own `spawn` event, before it waits for the exit. The pane launcher
listens and trips its claim's latch there and nowhere else: preparation, the
reservation, the flush and an allocated PID are not a start, and a child that
never ran never reports one. `nativeLaunch()` is unchanged for adapters, which
hear nothing about the start.

Terminal ownership and Agent-session ownership stay independent. Nothing pane-
derived enters the coordinator key, the launch request, the retained record or a
diagnostic, and two panes naming one logical session still contend through the
existing non-waiting coordinator.

No tmux, no new Agent advertisement, and root launch behavior is unchanged.

Evidence: SP1–SP5 in the core launch suite (pane lease, concurrency, readiness,
a failure before the start, one-live-launch-per-pane), FL8–FL9 in the runtime
launcher (the start event, and a child that never starts), and Tier GN over the
checked-in journey `TerminalGridNativeLaunch.test.md` through the whole TestAgent
stack. Removing the pane launcher fails SP1–SP4 and GN1–GN4; never reporting
readiness fails SP1, SP2, SP3 and SP5.
No production change. The pane-scoped launcher, the runtime spawn callback, the
authority boundaries and root-launch behavior are exactly as reviewed.

The checked-in journey is now the 2×2 grid TG5 asks for: three native Agent
sessions and the host's default shell. All four children report their start
before the composite attaches, each waits for its siblings while holding its own
pane, and the row reads back the authored row-major positions and forms.

Four rows added, all driven by signals this run produced:

- GN7: after attachment one native UI exits nonzero while its sibling is live.
  Only that pane fails; the sibling is observed alive on the far side of the
  failure and stops only when the reader leaves; the grid ends on the pane that
  failed, and the close's cancellation is not a second failure.
- GN8: the reader leaves with both launches live. Both are cancelled where they
  stood, neither pane fails, the composite comes down — and a root launch after
  the grid, naming a session a pane held, proves both leases came back. Which
  refusal it gets is the proof: not "already holds this run's terminal", not
  "another owner is using session", but the #517 recovery tombstone a cancelled
  native UI leaves behind.
- GN9: a pane admits its next user only once the last one is wholly done, with
  the launch and the prompt that follows it going through the real coordinator.
- GN10: a grid interrupted with a live pane launch, resumed on the same journal.
  It rebuilds the composite, starts the native child on the identity the first
  attempt retained, prepares nothing, and the retained record comes back
  unchanged — identity, route, binding and phase alike.

SP5 now proves the pane stays held through both halves: refused while the child
is live, refused again once the child has gone but the lease around it is still
unwinding, admitted only after both. A launch that merely returned showed only
the first.

Two harness repairs. Pane states are read as a set of panes rather than a count
of messages — a pane still live when the reader leaves is told twice, once from
the outcome close decided and once from its own settlement, and that is display
rather than a second settlement. And a generated variant is written to a
directory of its own with copies of the scenarios it names, so a killed run
leaves nothing in the repository.
The harness's interrupted branch built a `Result` as an object literal and cast
it. Effection has a constructor for exactly that, so it uses it: no cast, and
the type is the constructor's rather than an assertion's.

Behavior and evidence are unchanged.
An orderly cancellation that finished proves everything a normal return proves,
and must say so. It did not: `ownership.quiesced()` was a statement after
`authority.perform()`, and cancellation unwinds past every statement after the
operation it cancels. A reader closing a terminal grid therefore left every
session its panes had launched carrying a recovery tombstone, and the next owner
was told to recover a session nothing was using.

The launch now runs in a scope the ownership body owns, and the acknowledgement
is that scope's cleanup — reached on every path there is, cancellation included.
It brings the launch down deliberately and reads the outcome of doing so, so the
two facts it needs are facts rather than inferences: the native child and its
cleanup settled, and this provider holds no handle for the session. A teardown
that could not prove the child stopped throws out of `destroy()` and is not
quiescence — and is still a failure, so it propagates rather than passing
quietly.

Nothing grid-specific reaches the provider. Reader close is the ordinary launch
cancellation path, and this is the ordinary launch cancellation path's rule.

The conservative cases keep their tombstone: a detach that failed or a session
prepared and never handed over leaves a handle, and a child or provider cleanup
that failed leaves the acknowledgement unmade. Cancellation, a released lease, a
PID and elapsed time still prove nothing on their own.

CX1 asserted the behavior this replaces — that a cancelled launch stays owned —
so it now asserts the accepted one. CX2 is new and holds the other half: a
cleanup that could not finish withholds quiescence, and the record stays active.
GN8 is rebuilt as directed: two pane children held on unresolved operations,
signals from each child's own teardown, teardown proven to finish after both,
and a root launch afterwards on one of the same logical sessions that acquires
ownership and starts — receiving neither session-busy nor
session-recovery-required, and reclaiming the root foreground lease as it goes.

Broken on purpose and re-run: acknowledging only on a normal return fails CX1
and GN8; acknowledging without proving the cleanup settled fails CX2, and only
CX2.
@taras
taras force-pushed the agent/issue-731-pane-native-launch branch from e380dd1 to c279fe1 Compare September 10, 2026 16:28
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.

Launch native Agent sessions in independent terminal panes

1 participant