✨ Execute concurrent terminal panes through a replaceable provider (#730) - #738
✨ Execute concurrent terminal panes through a replaceable provider (#730)#738taras wants to merge 15 commits into
Conversation
The replaceable seam a terminal grid executes through, before any of the execution that uses it. `packages/runtime/terminal.ts` is the contextual provider: `prepare()` builds the whole composite while it stays hidden, `attach()` shows it once every pane is ready, and `destroy()` gives the root terminal back. The request is provider-neutral — columns, rows, and the authored panes with their derived positions — and names no terminal, socket, process or window. Middleware may observe, narrow, refuse, wrap or delegate; presentation never decides an outcome, so `update()` receives states core has already settled on. `packages/core/src/terminal/authority.ts` mints one-use pane claims for one request's ordinals. A claim admits one interactive operation at a time on its pane and holds that pane's readiness latch. Two claims do not contend, which is what lets panes stay interactive together. `packages/core/src/terminal/pane.ts` is the seam interactive work inside a pane reaches for, so it runs as that pane's owner instead of competing for the root foreground lease. Absence means "not in a pane". Evidence: `packages/runtime/tests/terminal-provider.test.ts`, 11 rows.
`runTerminalGrid()` owns the lifecycle the reader sees: it takes the run's one foreground-terminal lease, flushes root output, prepares the composite while it stays hidden, starts every pane concurrently, and attaches only once every pane has reported a spawn through its claim. Ordering is the contract. The lease and the composite are both scope-owned, so success, failure and cancellation all release the terminal and destroy exactly the composite that was prepared — there is no path that skips teardown. A pane that settles without ever reporting a spawn fails startup rather than being presented as a running pane. Before the barrier a pane failure fails the whole grid closed; after it, the failure is that pane's status and its siblings keep running. Close cancels a live pane as `closed`, which is not a failed pane, and the grid fails with the first failed pane in authored order. `display()` and an `onUpdate` hook complete the provider surface: a pane's rendered text goes to that pane, and a suite reacts to a state the grid decided rather than waiting and hoping. Evidence: `packages/core/tests/terminal-grid.test.ts` (15 rows) and `packages/runtime/tests/terminal-provider.test.ts` (11 rows). The readiness barrier row was verified by removing the barrier: it fails without it.
`<Terminal.Grid>` now executes. Each authored pane becomes a concurrent child of the grid: a self-closing pane runs the host's default shell through its claim, and a paired pane expands its own content in a scope of its own. A pane inherits the bindings, providers, configuration and working directory visible where the grid was written, and keeps everything it creates afterwards. Its `<Break>` has no loop to exit, its `<Return>` has no enclosing value body to claim, and its checked failures settle the pane rather than reaching the root or a sibling. A pane's rendered text is displayed in that pane; the grid itself renders `""`, so the root output holds what surrounds the grid and no pane display at all. #729's five execution-dependent rows move here, where they assert the layout against the request the provider actually receives rather than reading it off a refusal's cause — the structural suite keeps the grammar, placement and pure-layout rows it owns. Moving them was authorized rather than assumed. Evidence: 22 rows in `packages/core/tests/terminal-grid.test.ts` and 11 in `packages/runtime/tests/terminal-provider.test.ts`; the whole Deno core (350) and runtime (14) suites pass.
PR #738: ✨ Execute concurrent terminal panes through a replaceable provider (#730)26 files, +5399 / -226 Scope🔴 PR has 5625 lines changed. Split into focused PRs. 🟡 5625 lines changed. PRs under 400 receive more thorough review. 🟡 26 files changed. Are all changes related? 🟡 New abstraction files: packages/core/src/terminal/provider-api.ts. Verify 3+ consumers. StructuralOxlint structural signals:
Slop
Oxlint slop signals:
Static AnalysisOxlint: 43 diagnostics across 5 files (12 rules) no-unnecessary-type-assertion (11): packages/core/src/terminal/journal.ts, packages/durable-streams/retained.ts, packages/durable-streams/combinators.ts (+2) CorrectnessNo extraneous code patterns detected. |
`paneWork()` never read the grid element it was handed. Its caller has it, and a pane's own diagnostics are positioned at the pane.
| }, | ||
| // The grid renders nothing into the document: what a pane shows belongs to | ||
| // that pane, and the sibling after `</Terminal.Grid>` renders to the root | ||
| // again only once the provider has restored it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // again only once the provider has restored it. |
| *run(claim, composite) { | ||
| yield* scoped(function* () { | ||
| // A pane is not inside the loop the grid was written in, so a <Break> | ||
| // in its content has no loop to exit and says so. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // in its content has no loop to exit and says so. |
| const siteEnv = yield* env; | ||
| // Starts from what the grid site can see and keeps its own writes: a | ||
| // binding this pane makes is visible to later work in this pane and to | ||
| // nothing else. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // nothing else. |
| ), | ||
| 0, | ||
| // The pane's own ledger: a checked failure settles this pane and | ||
| // cannot reach the root or a sibling. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // cannot reach the root or a sibling. |
| // and a host with no terminal refuses here — before any pane has done work. | ||
| yield* reserveTerminal(); | ||
| // Everything the document has produced so far reaches the reader before the | ||
| // grid covers it up. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // grid covers it up. |
| yield* pane.run(claim, composite); | ||
| if (!readiness.acknowledged) { | ||
| // Settled without ever starting: that is a startup failure even | ||
| // though the work itself raised nothing. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // though the work itself raised nothing. |
| attached = true; | ||
| // The composite stays visible after its panes settle. The reader leaving is | ||
| // what finishes the grid, not the last pane exiting. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // what finishes the grid, not the last pane exiting. |
| export const TerminalProvider: Api<TerminalProviderHandler> = createApi<TerminalProviderHandler>( | ||
| "runtime.terminalProvider", | ||
| { | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| } | ||
| log.events.push(`attach:${generation}`); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| log.events.push(`state:${generation}:${ordinal}:${state}`); | ||
| options.onUpdate?.(ordinal, state); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
…#730) Restores the authority boundary on the `AgentProviders` handshake, and puts each pane on its own durable child coroutine. **The boundary.** `TerminalGrids` is routing and only routing: `open()` answers `unknown` and core throws the answer away, so middleware may observe, narrow, refuse, wrap or delegate but can never authorize. The capability that takes the leases, mints pane claims and settles a grid is a non-contextual authority delivered straight to the registered provider through a one-use install handshake. Core mints one identity-bearing request per expansion; presenting a copy, a rebuilt lookalike, a changed request, an already-presented one, or one from a superseded installation generation authorizes nothing, and a handler that answers without presenting settles nothing. **Durable children.** Each pane is a durable child of the grid, allocated in authored order, so a pane's identity follows its ordinal rather than the order the runtime scheduled it in. The layout is recorded in the parent coroutine before the lease and before any provider is contacted. **Ordering.** A pane that settles before attach keeps the status it settled to instead of being overwritten with `running`, and simultaneous startup failures are selected by authored ordinal rather than by whichever rejected first. Each pane also expands under a counter of its own, so two concurrent panes cannot take block identities that depend on which ran first. `durableSpawn` could not be used: the task it returns is spawned inside the ephemeral effect's own scope, which closes as the effect resolves, so awaiting it throws `halted`. It has no call sites or tests upstream. `durableAll` is the exercised primitive and is what the panes and the grid child use. Evidence: 30 rows in `packages/core/tests/terminal-grid.test.ts` and 10 in `packages/runtime/tests/terminal-provider.test.ts`; core 349, runtime 15.
| // Recorded in this coroutine, before the lease and before any provider is | ||
| // contacted: a resumed run whose grid changed is refused while nothing has | ||
| // been opened. It cannot live inside the grid child, because a completed | ||
| // child never runs. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // child never runs. |
| *run(claim, composite) { | ||
| yield* scoped(function* () { | ||
| // A pane is not inside the loop the grid was written in, so a <Break> | ||
| // in its content has no loop to exit and says so. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // in its content has no loop to exit and says so. |
| const siteEnv = yield* env; | ||
| // Starts from what the grid site can see and keeps its own writes: a | ||
| // binding this pane makes is visible to later work in this pane and to | ||
| // nothing else. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // nothing else. |
| containedLedger(site.checkedFailures), | ||
| site.authority, | ||
| // No enclosing value body: a <Return> written in a pane cannot claim | ||
| // one outside the grid. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // one outside the grid. |
| // The one foreground-terminal lease, taken before any provider is asked for | ||
| // anything. A root <Session.Launch> and a grid contend for exactly this, so | ||
| // neither can begin while the other holds it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // neither can begin while the other holds it. |
| ): Operation<RetainedGrid> { | ||
| return scoped(function* (): Operation<RetainedGrid> { | ||
| // Registered before a single pane starts: a composite that was presented is | ||
| // owed a destroy even if the next line is what fails. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // owed a destroy even if the next line is what fails. |
| attached = true; | ||
| // The composite stays visible after its panes settle. The reader leaving is | ||
| // what finishes the grid, not the last pane exiting. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // what finishes the grid, not the last pane exiting. |
| yield* pane.run(claim, composite); | ||
| if (!readiness.acknowledged) { | ||
| // Settled without ever starting: a startup failure even though the work | ||
| // itself raised nothing. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // itself raised nothing. |
| // deno-lint-ignore require-yield | ||
| *install(call: TerminalProviderCall): Operation<unknown> { | ||
| if (call.intent === "install") { | ||
| // Reaching the terminal means no registered provider consumed it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Reaching the terminal means no registered provider consumed it. |
`durableSpawn` returned a task spawned inside the `ephemeral` effect's own scope, and that scope closed as the effect resolved — so every `yield* task` threw `halted`. It had no call sites and no tests. It now starts the child in the routine's own scope, so the task outlives the call and can be awaited or halted by whoever asked for it. A retained `Close(cancelled)` meant one thing to the code and two things in practice. Under `durableRace` and `durableAll` it is a race loser or a fail-fast sibling, and the same combinator cancels it again — those keep DEC-024 exactly. Under `durableSpawn` nobody cancels it a second time, so suspending hung the resumed run forever. `runDurableChild` now takes an explicit `CancelledChildPolicy`, fixed at each combinator's call site and never chosen by a caller. Resuming uses a new internal `ReplayIndex.reopen()`, which forgets one coroutine's retained Close while keeping its yields — so the child continues its own history rather than restarting, and the divergence guard stops reading the remaining effects as a coroutine continuing past its own close. Neither it nor `disableReplay` is exported. DEC-039 records the policy and marks DEC-024's invariant as superseded in part: it assumed every cancelled child belongs to race or all. The grid uses the repaired primitive: the whole grid is one durable child, each pane is its own durable child allocated in authored ordinal order, and each pane task is observed outside its child — so a replayed pane's retained outcome publishes its status and satisfies the readiness barrier without entering a body, a shell, or a launcher. Evidence: 9 rows in `packages/durable-streams/tests/durable-spawn.test.ts` (lifetime, completed replay, interrupted resume, retained-history continuation, and both combinators keeping their own policy); 30 rows in `packages/core/tests/terminal-grid.test.ts`. durable-streams 32, core 349, runtime 15.
| // Recorded in this coroutine, before the lease and before any provider is | ||
| // contacted: a resumed run whose grid changed is refused while nothing has | ||
| // been opened. It cannot live inside the grid child, because a completed | ||
| // child never runs. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // child never runs. |
| *run(claim, composite) { | ||
| yield* scoped(function* () { | ||
| // A pane is not inside the loop the grid was written in, so a <Break> | ||
| // in its content has no loop to exit and says so. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // in its content has no loop to exit and says so. |
| const siteEnv = yield* env; | ||
| // Starts from what the grid site can see and keeps its own writes: a | ||
| // binding this pane makes is visible to later work in this pane and to | ||
| // nothing else. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // nothing else. |
| containedLedger(site.checkedFailures), | ||
| site.authority, | ||
| // No enclosing value body: a <Return> written in a pane cannot claim | ||
| // one outside the grid. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // one outside the grid. |
| // The one foreground-terminal lease, taken before any provider is asked for | ||
| // anything. A root <Session.Launch> and a grid contend for exactly this, so | ||
| // neither can begin while the other holds it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // neither can begin while the other holds it. |
| return (function* (): Operation<Task<RetainedPaneOutcome>> { | ||
| const durable = yield* DurableContext.get(); | ||
| if (durable === undefined) { | ||
| // No journal behind this run: an ordinary spawned child. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // No journal behind this run: an ordinary spawned child. |
| // deno-lint-ignore require-yield | ||
| *install(call: TerminalProviderCall): Operation<unknown> { | ||
| if (call.intent === "install") { | ||
| // Reaching the terminal means no registered provider consumed it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Reaching the terminal means no registered provider consumed it. |
| throw new TerminalProviderInstallError(`Unknown terminal provider "${call.name}"`); | ||
| } | ||
| // Object identity, not shape: a request rebuilt with the same members | ||
| // describes the same ask and authorizes nothing. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // describes the same ask and authorizes nothing. |
| if (!replayIndex.hasClose(childId)) { | ||
| // (e.g., a cancelled child being replayed via suspend()). A child that | ||
| // resumed from a retained cancelled Close is the exception: the record it | ||
| // reached this time is the one that describes the work that actually ran. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // reached this time is the one that describes the work that actually ran. |
| } | ||
| // The default shell starts: a suite that says nothing about a pane | ||
| // wants a pane that works, and one that never reported a spawn would | ||
| // hang the readiness barrier instead. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // hang the readiness barrier instead. |
Two decisions exposed while implementing #730, and no implementation. **DEC-040 — a cancelled child records why.** DEC-039's `"resume"` fired on every retained `Close(cancelled)` under an incomplete parent, which revives work a caller deliberately halted: the record of a deliberate `task.halt()` and the record of an interrupted run are the same event. The cancelled close now carries `cancellation: "caller"` or `"unwound"`, written by whichever path cancelled the child, and `"resume"` continues only `"unwound"`. A deliberate stop suspends, which is DEC-024's reproduction argument applied to a caller instead of a combinator; a legacy record with no reason reads as `"caller"`, because refusing to revive is the safe direction. The reason is retained evidence, not authority: nothing outside `runDurableChild` reads it, and no caller chooses a policy. Terminal grids need nothing wider. A grid halts its pane tasks at close, so those retain `"caller"` — and the grid child completes, so a resumed run short-circuits the region and never reaches them. The case that must resume, an interrupted run, unwinds and retains `"unwound"`. **TG17 narrows to the resolved layout.** A continuation executes the root the journal retained; the supplied source is not read, compared or refused (proved in #722). A grid's authored structure — pane count, order, form — is therefore fixed for the life of a journal and cannot differ between runs, so comparing it compares a value with itself, which is why the refusal never fired. What a fixed retained document still resolves differently is `columns` and each `title`, through prop-borne values, since props are not restored. Those refuse before the lease and before provider contact. Authored-structure change is a root-definition compatibility question, not a grid one. Root-definition authority is preserved rather than overridden by a pre-replay comparison against the current file, and the versioned root boundary that would refuse a changed source stays open work.
**DEC-040.** A cancelled Close now records why: `cancellation: "caller"`
when the owner halts the task `durableSpawn` returned, `"unwound"` for
anything involuntary. `durableSpawn` resumes only `"unwound"`; a deliberate
stop suspends until the caller's deterministic control flow halts it again,
and a record with no reason reads as `"caller"` so nothing legacy is
revived. `durableAll` and `durableRace` keep DEC-024 whatever the reason
says.
The halt is intercepted without changing the public `Task` surface: the
returned task carries every member the real one defines, copied with its
prototype, and only `halt` is replaced. A proxy cannot do this — a task's
members are read-only and non-configurable, so a `get` trap is required to
hand back exactly what the target holds.
The reason had to survive three boundaries that were dropping it: the
protocol parser, the observable copy, and — the one that actually mattered —
`detachResult`, which froze every cancellation down to `{ status }`.
**TG15.** The harness's `attached` and `pastGrid` signals are now separate,
and a run that expects its grid to complete waits for the sibling *after*
the grid before halting the root at `<Hold />`. That is what leaves a
completed grid child under an incomplete root, which is the only state in
which a completed region can be observed replaying at all. Both a successful
grid and a contained failed one replay their exact retained result with no
provider, pane content, shell or launcher work, and each row asserts the
grid child genuinely recorded a terminal close. No timeouts.
**TG17.** Prop-borne `columns` and `title` change independently against one
fixed retained document — the only things a fixed retained root can still
resolve differently — and each refuses with zero provider observation. For
supplied-file changes to pane count, order and form, the continuation opens
the retained structure rather than the file's, asserted request-for-request.
The retained record carries every authored pane's ordinal, title, form and
derived position.
`readLayout()` parses totally: the layout object and every pane field, with
missing, extra, mistyped, out-of-position and self-inconsistent records all
refused rather than half-read.
Evidence: durable-spawn 14 rows, terminal-grid 36 rows, structural 13,
provider 10. Packages: durable-streams 33, core 349, runtime 15,
workflow 172.#730) **The harness cannot pass a hung replay any more.** `runInterrupted()` had a 1500ms timer racing its signals, so a replay that hung returned a DocumentRun that looked finished; it also slept a fixed 5ms to let records land. Both are gone. It now waits only on events the run produced: `attached`, `pastGrid`, and a new `panesSettled` for the rows that read pane records — a pane's status is published only after its durable child returned, so counting settled panes is also counting durable pane closes. A replay that hangs now reaches none of them and hangs the row. **TG15's failed case is a real contained failure.** A pane that fails before attachment fails the whole region, so the old document could not both fail and continue. The failing pane is now a shell that starts, waits for attachment, and only then exits badly — contained as that pane's status, with the grid settling as failed and the document carrying on. Both runs capture the printed errors, and the row asserts the replayed run produced the same ones, reached `PAST_THE_GRID`, and did no provider, pane, shell or launcher work. **DEC-040 gets boundary tests where the evidence actually travels.** `parse.test.ts` round-trips both reasons to the same bytes, keeps a legacy absence absent, and refuses an unrecognised reason at `$.result.cancellation`. `retained.test.ts` proves retention and `consumable()` carry both reasons, leave a legacy absence absent, drop an unrecognised one to the safe default, and that the reason reaches the replay index. The DEC-040 rows in `durable-spawn.test.ts` no longer coordinate by delay: a child says when it is running, and the caller says when it has halted. **Malformed retained layouts** are covered by replaying a real journal with only its layout entry replaced — a missing member, an extra one, a mistyped one, a pane out of position, and a record that disagrees with itself. Each refuses with zero provider observation. The `durableSpawn` doc comment no longer says every retained cancellation is an interrupted run.
The DEC-040 block still slept where it meant to synchronise — my previous replacements silently failed to match after the file was reformatted, so none of them landed. The block is rewritten rather than patched. Every row now waits on something the run reported. A shared `living()` child resolves a `started` signal and then suspends, so each row halts or unwinds a child that is provably live rather than one a delay happened to reach. The caller resolves `halted` after performing its deliberate halt, so a run is interrupted only once both facts — the deliberate stop and the interruption — are in the journal. Non-revival is established by control flow rather than by waiting: the resumed run reaches its own `task.halt()` and says so, and a revived child would have recorded its mark before the caller could get there. The legacy-absence row signals once the child has been asked for and the request returned. No new timeout, and `sleep` stays imported because the lifetime rows above still use it deliberately. `retained.test.ts` drops the cast and the row it supported: rejecting an unrecognised reason is the parser's, proved there, and retention proves only that `"caller"`, `"unwound"` and a legacy absence survive.
| }, | ||
| // The grid renders nothing into the document: what a pane shows belongs to | ||
| // that pane, and the sibling after `</Terminal.Grid>` renders to the root | ||
| // again only once the provider has restored it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // again only once the provider has restored it. |
| // Recorded in this coroutine, before the lease and before any provider is | ||
| // contacted: a resumed run whose grid changed is refused while nothing has | ||
| // been opened. It cannot live inside the grid child, because a completed | ||
| // child never runs. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // child never runs. |
| *run(claim, composite) { | ||
| yield* scoped(function* () { | ||
| // A pane is not inside the loop the grid was written in, so a <Break> | ||
| // in its content has no loop to exit and says so. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // in its content has no loop to exit and says so. |
| const siteEnv = yield* env; | ||
| // Starts from what the grid site can see and keeps its own writes: a | ||
| // binding this pane makes is visible to later work in this pane and to | ||
| // nothing else. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // nothing else. |
| containedLedger(site.checkedFailures), | ||
| site.authority, | ||
| // No enclosing value body: a <Return> written in a pane cannot claim | ||
| // one outside the grid. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // one outside the grid. |
| // neither can begin while the other holds it. | ||
| yield* reserveTerminal(); | ||
| // Everything the document has produced so far reaches the reader before the | ||
| // grid covers it up. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // grid covers it up. |
| // grid covers it up. | ||
| yield* flushOutput(); | ||
| // Routed, and the answer thrown away. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Routed, and the answer thrown away. |
| ): Operation<RetainedGrid> { | ||
| return scoped(function* (): Operation<RetainedGrid> { | ||
| // Registered before a single pane starts: a composite that was presented is | ||
| // owed a destroy even if the next line is what fails. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // owed a destroy even if the next line is what fails. |
| const grid = createTerminalGridClaims(request); | ||
| // Nothing new is admitted once teardown begins, so a pane that was about to | ||
| // start an interactive child is refused rather than racing the close. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // start an interactive child is refused rather than racing the close. |
| // to schedule it in. Each task is observed *outside* its child: a replayed | ||
| // completed pane returns its retained outcome without entering a body, a | ||
| // shell, or a launcher, and that outcome is what publishes its status and | ||
| // satisfies the readiness barrier. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // satisfies the readiness barrier. |
…730) **`Symbol.asyncDispose` bypassed the deliberate-stop evidence.** The task `durableSpawn` returns copied it from the original unchanged, so `await using` — or an explicit `task[Symbol.asyncDispose]()` — recorded `cancellation: "unwound"` and the next run revived the child. `halt()` and the async dispose are the same decision spelled two ways, and both are now observed. Awaiting a task is not a stop and is left exactly as it was. A regression disposes a live task, asserts the retained reason is `"caller"`, resumes the journal, and proves the body is not entered again. **Reader close no longer halts panes.** It asks them to stop: a pane races its work against a close signal, settles as `closed`, and records that outcome as its own. Nothing on the ordinary close path is a caller-cancelled child any more, so a resumed run restores a pane the reader closed rather than finding a cancelled child it must either re-enter or wait on forever. Statuses are published before anything is awaited, so a pane with slow finalizers cannot delay the outcome the grid already knows. **§6.21 now agrees with architecture.md and TG17.** Partial replay compares the resolved layout — columns and titles. Pane count, order and form come from the retained root and cannot diverge within a continuation, so a changed supplied file is ignored in favour of the retained structure; refusing a changed authored structure is a root-definition boundary this specification does not yet define. DEC-040 is unchanged and nothing deliberately stopped is revived.
| }, | ||
| // The grid renders nothing into the document: what a pane shows belongs to | ||
| // that pane, and the sibling after `</Terminal.Grid>` renders to the root | ||
| // again only once the provider has restored it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // again only once the provider has restored it. |
| // Recorded in this coroutine, before the lease and before any provider is | ||
| // contacted: a resumed run whose grid changed is refused while nothing has | ||
| // been opened. It cannot live inside the grid child, because a completed | ||
| // child never runs. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // child never runs. |
| *run(claim, composite) { | ||
| yield* scoped(function* () { | ||
| // A pane is not inside the loop the grid was written in, so a <Break> | ||
| // in its content has no loop to exit and says so. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // in its content has no loop to exit and says so. |
| const siteEnv = yield* env; | ||
| // Starts from what the grid site can see and keeps its own writes: a | ||
| // binding this pane makes is visible to later work in this pane and to | ||
| // nothing else. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // nothing else. |
| containedLedger(site.checkedFailures), | ||
| site.authority, | ||
| // No enclosing value body: a <Return> written in a pane cannot claim | ||
| // one outside the grid. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // one outside the grid. |
| // deno-lint-ignore require-yield | ||
| *install(call: TerminalProviderCall): Operation<unknown> { | ||
| if (call.intent === "install") { | ||
| // Reaching the terminal means no registered provider consumed it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Reaching the terminal means no registered provider consumed it. |
| throw new TerminalProviderInstallError(`Unknown terminal provider "${call.name}"`); | ||
| } | ||
| // Object identity, not shape: a request rebuilt with the same members | ||
| // describes the same ask and authorizes nothing. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // describes the same ask and authorizes nothing. |
| if (!replayIndex.hasClose(childId)) { | ||
| // (e.g., a cancelled child being replayed via suspend()). A child that | ||
| // resumed from a retained cancelled Close is the exception: the record it | ||
| // reached this time is the one that describes the work that actually ran. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // reached this time is the one that describes the work that actually ran. |
| return { status: "cancelled" }; | ||
| // The reason travels with the copy: a resumed spawned region reads it to tell | ||
| // a deliberate stop from an interrupted run (DEC-040), and dropping it here | ||
| // would make every retained cancellation look deliberate. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // would make every retained cancellation look deliberate. |
| *destroy() { | ||
| // Destroying twice would make the record say a composite was taken down | ||
| // more times than it was built, which is exactly the ordering claim a | ||
| // suite reads this log for. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // suite reads this log for. |
| const siteEnv = yield* env; | ||
| // Starts from what the grid site can see and keeps its own writes: a | ||
| // binding this pane makes is visible to later work in this pane and to | ||
| // nothing else. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // nothing else. |
| containedLedger(site.checkedFailures), | ||
| site.authority, | ||
| // No enclosing value body: a <Return> written in a pane cannot claim | ||
| // one outside the grid. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // one outside the grid. |
| }, | ||
| ready() { | ||
| // Idempotent by construction: readiness is a fact about the pane, and a | ||
| // provider that reports the same spawn twice has not started two panes. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // provider that reports the same spawn twice has not started two panes. |
| // The one foreground-terminal lease, taken before any provider is asked for | ||
| // anything. A root <Session.Launch> and a grid contend for exactly this, so | ||
| // neither can begin while the other holds it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // neither can begin while the other holds it. |
| ): Operation<RetainedGrid> { | ||
| return scoped(function* (): Operation<RetainedGrid> { | ||
| // Registered before a single pane starts: a composite that was presented is | ||
| // owed a destroy even if the next line is what fails. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // owed a destroy even if the next line is what fails. |
| if (!replayIndex.hasClose(childId)) { | ||
| // (e.g., a cancelled child being replayed via suspend()). A child that | ||
| // resumed from a retained cancelled Close is the exception: the record it | ||
| // reached this time is the one that describes the work that actually ran. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // reached this time is the one that describes the work that actually ran. |
| const cancellation = members.get("cancellation"); | ||
| if (cancellation === undefined) { | ||
| // A record written before this evidence existed. DEC-040 reads the | ||
| // absence as a deliberate stop, so nothing it left behind is revived. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // absence as a deliberate stop, so nothing it left behind is revived. |
| return { status: "cancelled" }; | ||
| // The reason travels with the copy: a resumed spawned region reads it to tell | ||
| // a deliberate stop from an interrupted run (DEC-040), and dropping it here | ||
| // would make every retained cancellation look deliberate. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // would make every retained cancellation look deliberate. |
| } | ||
| // The default shell starts: a suite that says nothing about a pane | ||
| // wants a pane that works, and one that never reported a spawn would | ||
| // hang the readiness barrier instead. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // hang the readiness barrier instead. |
| *destroy() { | ||
| // Destroying twice would make the record say a composite was taken down | ||
| // more times than it was built, which is exactly the ordering claim a | ||
| // suite reads this log for. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // suite reads this log for. |
8f85400 to
eb92602Compare| const siteEnv = yield* env; | ||
| // Starts from what the grid site can see and keeps its own writes: a | ||
| // binding this pane makes is visible to later work in this pane and to | ||
| // nothing else. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // nothing else. |
| containedLedger(site.checkedFailures), | ||
| site.authority, | ||
| // No enclosing value body: a <Return> written in a pane cannot claim | ||
| // one outside the grid. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // one outside the grid. |
| } | ||
| for (const [index] of work.entries()) { | ||
| // Awaited, not halted. Each pane settles on the close signal and records | ||
| // the outcome it reached, which is what a resumed run reads. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // the outcome it reached, which is what a resumed run reads. |
| // the reader leaves, this settles as `closed` straight away and the work | ||
| // comes down in the enclosing scope's own teardown — so a pane whose | ||
| // finalizers are slow cannot hold up the outcome the grid already knows, | ||
| // and the record a resumed run reads is written either way. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // and the record a resumed run reads is written either way. |
| return (function* (): Operation<Task<RetainedPaneOutcome>> { | ||
| const durable = yield* DurableContext.get(); | ||
| if (durable === undefined) { | ||
| // No journal behind this run: an ordinary spawned child. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // No journal behind this run: an ordinary spawned child. |
| // deno-lint-ignore require-yield | ||
| *install(call: TerminalProviderCall): Operation<unknown> { | ||
| if (call.intent === "install") { | ||
| // Reaching the terminal means no registered provider consumed it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Reaching the terminal means no registered provider consumed it. |
| throw new TerminalProviderInstallError(`Unknown terminal provider "${call.name}"`); | ||
| } | ||
| // Object identity, not shape: a request rebuilt with the same members | ||
| // describes the same ask and authorizes nothing. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // describes the same ask and authorizes nothing. |
| return { status: "cancelled" }; | ||
| // The reason travels with the copy: a resumed spawned region reads it to tell | ||
| // a deliberate stop from an interrupted run (DEC-040), and dropping it here | ||
| // would make every retained cancellation look deliberate. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // would make every retained cancellation look deliberate. |
| } | ||
| // The default shell starts: a suite that says nothing about a pane | ||
| // wants a pane that works, and one that never reported a spawn would | ||
| // hang the readiness barrier instead. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // hang the readiness barrier instead. |
Implements the amendment at 1833870 without revising it. **The live handshake.** `composite.closed()` settling now only *proposes* the boundary. The grid's durable child publishes that proposal and waits; the owner awaiting the child acknowledges it; and only then does the grid seal admission and ask its panes to close. The handshake is one live rendezvous — no provider identity, nothing journaled. **Committing an outcome before the scope finishes unwinding.** A durable child can now declare its terminal value, and `runDurableChild` records that value if the child never reaches a normal ending. That is the piece the contract needs: the grid commits its retained record as the boundary is crossed, and each pane live at that moment commits `closed`, so a cancellation arriving while pane and provider finalizers are still running records what close decided rather than a cancellation. Committing is live state; it reaches the journal only as the ordinary `Close`. A child that returns or throws normally overrides it, and a child that never committed still records the cancellation it actually reached — DEC-040 untouched. Cancellation stays deferred because Effection completes a child's teardown — pane finalizers, provider destroy, terminal restoration, lease release, the `Close` append and the task's settlement — before the halt reaches the owner. **Pane work stays inside its ordinal-derived durable child.** Reader close asks the pane to close; it never halts the pane's durable task. The pane commits `closed`, stops its live nested work through its own scope, and settles only once that work and its finalizers have settled. No durable closing marker was added, and completed replay is unchanged.
| }, | ||
| // The grid renders nothing into the document: what a pane shows belongs to | ||
| // that pane, and the sibling after `</Terminal.Grid>` renders to the root | ||
| // again only once the provider has restored it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // again only once the provider has restored it. |
| // Recorded in this coroutine, before the lease and before any provider is | ||
| // contacted: a resumed run whose grid changed is refused while nothing has | ||
| // been opened. It cannot live inside the grid child, because a completed | ||
| // child never runs. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // child never runs. |
| *run(claim, composite) { | ||
| yield* scoped(function* () { | ||
| // A pane is not inside the loop the grid was written in, so a <Break> | ||
| // in its content has no loop to exit and says so. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // in its content has no loop to exit and says so. |
| const siteEnv = yield* env; | ||
| // Starts from what the grid site can see and keeps its own writes: a | ||
| // binding this pane makes is visible to later work in this pane and to | ||
| // nothing else. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // nothing else. |
| containedLedger(site.checkedFailures), | ||
| site.authority, | ||
| // No enclosing value body: a <Return> written in a pane cannot claim | ||
| // one outside the grid. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // one outside the grid. |
| // The one foreground-terminal lease, taken before any provider is asked for | ||
| // anything. A root <Session.Launch> and a grid contend for exactly this, so | ||
| // neither can begin while the other holds it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // neither can begin while the other holds it. |
| // neither can begin while the other holds it. | ||
| yield* reserveTerminal(); | ||
| // Everything the document has produced so far reaches the reader before the | ||
| // grid covers it up. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // grid covers it up. |
| // grid covers it up. | ||
| yield* flushOutput(); | ||
| // Routed, and the answer thrown away. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Routed, and the answer thrown away. |
| ): Operation<RetainedGrid> { | ||
| return scoped(function* (): Operation<RetainedGrid> { | ||
| // Registered before a single pane starts: a composite that was presented is | ||
| // owed a destroy even if the next line is what fails. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // owed a destroy even if the next line is what fails. |
| const grid = createTerminalGridClaims(request); | ||
| // Nothing new is admitted once teardown begins, so a pane that was about to | ||
| // start an interactive child is refused rather than racing the close. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // start an interactive child is refused rather than racing the close. |
Implements architecture 1833870 at the owner boundary. The grid's durable child now runs in a scope of its own — a child of the owner's, so it inherits every context the document runs under, and its own so that tearing the owner down does not reach it first. A finalizer registered after that scope exists runs before it is destroyed, and that is the cancellation-deferred await: once the owner has acknowledged the provider's close proposal, the grid and its panes finish teardown and append their ordinary completed Close records, and only then does the cancellation carry on to the parent. Removes the exported CommitOutcome/durableSpawn(commit) API. Cancellation is never turned into success inside runDurableChild; durableSpawnIn only says where a child lives, and grants nothing a caller does not already have. TG19 proves the ordering with signals alone: a live pane arms a blocking finalizer, the reader leaves, the finalizer is entered and held, cancellation begins, the finalizer is released, and the run ends with the composite destroyed, a completed grid Close retained, the live pane retained as closed — and the sibling after the grid never reached. The continuation then replays past it with no provider, no pane body and no finalizer re-entered. TG6 isolates paired-pane sequencing on its own: the reader leaves only once the pane's second component has run.
The production lifecycle is unchanged. TG19 now reads counters and journal records rather than a log's shape. The controlled composite keeps live resource counters — composites prepared, composites attached, shells started — each raised when it takes something and lowered when it gives it back, however it left. TG19 reads them once while a pane finalizer is blocked, so it knows they went up, and again when the cancellation has completed, so it knows they came back down. The harness now says when a blocked finalizer *leaves*, not only when it is entered: a finalizer that was entered and then cancelled reaches the first hook and never the second. And after every interrupted run it takes the foreground lease and gives it back twice — the first proves the grid returned it, the second proves the harness did. TG19 adds: one grid Close(ok) retaining close: "reader"; two pane Closes, both completed, with no cancellation recorded at either level; the finalizer entered and left exactly once; destroy:0 exactly once. The first-attempt claim that no following sibling ran and the replay tripwires are unchanged. Every one of these was broken on purpose and re-run: dropping any of the three counter releases, the deferral, the finalizer-exit hook, or double-logging destroy fails TG19, and a second holder of the foreground lease is refused.
Closes#730. Second implementation Story under Quest #717, stacked on #733.
Base is
agent/issue-729-terminal-grid, notmain, as the Story's verification stack directs. Branched from97be06321cbdf08d84ef53dbefb88407f54326fe, the reviewed head of #733.Why
#729 froze the language: an author can write a terminal grid, and every runtime can validate one. Nothing could run one. This Story makes a grid execute, through a boundary a host replaces — so the document keeps one lifecycle contract whether the presentation is tmux, a controlled test surface, or something native later.
What changes
Before: a valid
<Terminal.Grid>derived its layout and refused, because nothing could present it.After: it runs. Core takes the run's one foreground-terminal lease, flushes root output, prepares the composite while it stays hidden, starts every pane concurrently, and attaches only once every pane has actually started.
<Terminal.Grid columns={2}> <Terminaltitle="Agent">…</Terminal> <Terminaltitle="Shell" /> </Terminal.Grid>How it works
packages/runtime/terminal.tsis the replaceable seam. The request is provider-neutral — columns, rows, and the authored panes with their derived positions — and names no terminal, socket, process or window. Middleware may observe, narrow, refuse, wrap or delegate. Presentation never decides an outcome:update()receives states core has already settled on, and its return value is ignored.packages/core/src/terminal/authority.tsmints one-use pane claims for one request's ordinals. A claim admits one interactive operation at a time on its pane and holds that pane's readiness latch. Two claims do not contend, which is what lets panes stay interactive together.packages/core/src/terminal/pane.tsis the seam anything interactive inside a pane reaches, so it runs as that pane's owner instead of competing for the root lease. Absence means "not in a pane".packages/core/src/terminal/grid.tsowns the lifecycle. The lease and the composite are both scope-owned, so success, failure and cancellation all release the terminal and destroy exactly the composite that was prepared — there is no path that skips teardown.Review guide
Start with:
packages/core/tests/terminal-grid.test.tsThen review:
packages/runtime/terminal.ts— the boundary and what a handler may do.packages/core/src/terminal/authority.ts— claims, contention, the readiness latch.packages/core/src/terminal/grid.ts— the ordering, and where teardown is registered.packages/core/src/expand.ts—paneWork(), which is where a pane's isolation is established.Look carefully at:
runTerminalGrid().ensure(() => composite.destroy())is registered before any pane starts, so a composite that was prepared is owed a destroy even if the next line fails.attachedflag. Before the barrier a pane failure fails the whole grid closed; after it, the failure is that pane's status and its siblings keep running.paneWork()'s paired branch:ActiveLoop.set(undefined), a derived env,containedLedger(), andundefinedfor the return body are the four things that make a pane a scope rather than a region.What must stay true
ensure, checked by TG12 and by the controlled composite refusing a second destroy.update()returning void and claims never leaving core, checked by the middleware rows in the provider suite.How to verify it
closedrather than failed.execute()path.Whole Deno core suite: 350 passed. Runtime suite: 14 passed.
Scope
Included
Not included — this Story is not finished
These acceptance points landed in later commits on this branch, and the section
that once said durability was absent is no longer true:
child and each pane is its own durable child allocated in authored ordinal
order. A completed region replays its exact retained result — successful and
contained-failed alike — contacting no provider and running no pane content,
shell or launcher. An interrupted region rebuilds a fresh composite. The
layout is recorded in the parent coroutine before the lease and before any
provider is contacted, and a malformed retained layout refuses there.
executes the retained root, so pane count, order and form cannot differ and
are proved retained-and-honoured rather than refused;
columnsand eachtitlereach the grid through props, which are not restored, and each refusesindependently with zero provider observation.
durableSpawnwas repaired to make any of this possible: its task nowoutlives the call, and a cancelled child records why it stopped (DEC-040) so
a deliberately halted region is never revived while an interrupted one
resumes.
What remains outside this PR:
controlled provider cannot exercise it.
Story, as the issue states.
a pane whose own finalizer is slow leaves that pane recorded as unwound
rather than closed, so a resumed run re-enters it. Reader close is now
cooperative — panes are asked to stop and record
closedthemselves ratherthan being halted — which removes caller-cancelled panes from the ordinary
close path, but the blocked-finalizer overlap is not yet covered by a
regression.
Intentionally unchanged
New abstractions
TerminalProvider/TerminalCompositeexist because a grid's presentation has to be replaceable; the controlled provider and the future tmux adapter are the two concrete uses.TerminalPaneClaimexists because pane terminal ownership must be unforgeable and per-ordinal; consumed by the grid and by the pane seam.PaneTerminalexists because interactive work inside a pane must not compete for the root lease; consumed by the suite's controlled interactive child now and by<Session.Launch>in Launch native Agent sessions in independent terminal panes #731.Each new abstraction has multiple concrete uses or a clear justification.
No speculative functionality is included.
Risks and limitations
cause.<Break>inside a pane fails the pane rather than printing as it would at the root. That is consistent with the pane-settlement contract, but it is a behavioural difference worth a reviewer's eye.Scope confirmation