✨ Launch native Agent sessions in independent terminal panes (#731) - #741
✨ Launch native Agent sessions in independent terminal panes (#731)#741taras wants to merge 4 commits into
Conversation
|
|
||
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // reports having started. |
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. StructuralOxlint structural signals:
SlopOxlint slop signals:
Static AnalysisOxlint: 51 diagnostics across 3 files (15 rules) no-unused-vars (10): packages/acp/src/provider.ts, packages/core/src/expand.ts CorrectnessNo extraneous code patterns detected. |
|
|
||
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // reports having started. |
|
|
||
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // reports having started. |
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // reports having started. |
8b8936c to
d5dee3f
Compare
d5dee3f to
e380dd1
Compare
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.
e380dd1 to
c279fe1
Compare
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.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
AgentLaunchRequest, session keys, construction routes, durable phases, results, or diagnostics.Review and evidence
Start with the pane-launcher implementation and the authored TestAgent grid journey, then run the focused suites present at this layer:
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.