Repository navigation
✨ Reuse root JSON across REPL entries and cold reopen (#939) - #943
Conversation
A root `<Let>` and an ordinary component capture bound into the environment
they were written in and recorded nothing, so the entry after them could not
read the value: `<Let as="saved" value="alpha" />` in one entry left `{saved}`
undefined in the next, and a cold reopen found nothing either. Only an `eval`
block's exports crossed, because evaluating one is a durable effect and a
capture was not.
Retention is now a second trusted-host input beside the initial bindings,
`rootBindingPublications: "json-v1"`, which the REPL states for each entry it
admits. Core builds the publisher beside the canonical root binding
environment and hands it to expansion as an argument, so what may publish is a
capture whose actual assignment target is that exact object — a component
body, an `<Each>` iteration, a `<Spawn>` and a generated fragment each write a
different environment and none of them acquires root ownership from the file
it was written in. Each successful root capture is one ordinary durable record
whose value is a detached snapshot taken through the one strict JSON parser
the initial bindings are copied through; the binding itself stays the exact
value the producer made, so reference identity, resource lifetime and later
mutation are unchanged and none of them reaches the record.
Values outside that JSON domain stay in the entry that made them. They bind
and remain usable, publish no replacement — so a name keeps the last value
anything did publish under it — and produce one value-free outcome, which
removes nothing and warns about nothing. One record per successful capture
whichever outcome it is, so a partial continuation replays the positions it
recorded.
The mode travels on the root import's own description, and root-history
admission compares it against the invoking execution's before a terminal is
reused, before public replay policy and before authored work. An absent field
is a history written before this existed: it agrees with an execution that
publishes nothing, which is what lets an unfinished entry from such a file
continue in its own mode through its own close with its completed effects
restored rather than repeated. Cold projection applies the same rule
independently. Ordinary `execute()` states nothing and keeps the exact LET11
inventory it had.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review of 3fc3f29 found that core consumed a retained publication without parsing it. `publish()` discarded what `retain()` handed back, and `retain()` returned the durable operation's stored value unread — so on a continuation a record this version cannot read was accepted as history: the live operand was bound over it, the suffix no record describes ran, and a completion was appended on top. The CLI projector parses the same bytes and refuses them, which is exactly why its parser cannot be the only enforcement: the trusted host reaches `executeInstalled()` directly. `publish()` now reads back what the journal holds and parses it before the caller binds and before any later work. Live, the record parsed is the one just written and the check is free; on a continuation it is the retained record, and an unreadable one raises `StaleInputError` — the same classification a stale loop terminal raises, so it escapes as the run's own durability failure rather than becoming a document error that a printing boundary could absorb. One record contract serves both readers. `parseRootBindingRecord()` holds the version, the binding name, its agreement with the effect name replayed under, and the closed outcome; the event reader applies it after its own event checks. Two readings of one persisted shape would be two contracts, and the day they disagreed is the day a history one of them refused continued through the other. A recognized retained outcome stays authoritative: parsing reads it and never refreshes it from today's operand, so an entry whose eligibility changed on reconstruction still replays the positions it recorded. Ordinary execution, legacy old-mode continuation and entry-local non-JSON behavior are unchanged. The review also named four frozen assertions the evidence had not supplied, and each is completed in the row that already owned it rather than in a new one: - P3 now writes an ordinary component capture inside the `<Each>` iteration, inside two spawned branches and inside an admitted generated fragment. Each runs — five retained symbol reads and the fragment's own admission prove it — and none publishes, reaches the root binding map or reaches the successor. - P9 gains the malformed-version representative on the core continuation path, a record whose binding disagrees with the effect name it replayed under, and the incompatible-retention comparison at core's own owned admission in both directions over a history that holds its terminal, each with a sound control. - P2 mutates a member one level down rather than a scalar, so the retained value's detachment is pinned at that depth in the entry's own binding, in the next entry and at the position that retained the first publication. - G2 opens the completed-prefix execution it then asserts is unchanged, so an append to the history being inspected would be caught rather than hidden behind an untouched copy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…al (#939) Review of ceebc74 found F1 closed for an unfinished prefix and still open for terminal reuse. Parsing the retained record where a capture publishes only protects a history expansion walks: a history standing behind a recorded completion reuses that terminal without expanding anything, so the publisher is never entered. An unreadable publication with its own Close was therefore accepted as history and its recorded success returned as authoritative — bytes the CLI projector refuses. `admitRootHistory()` now collects every retained publication in the walk it already makes and holds each one to the shared record parser, beside the retained-mode comparison it already performed. That is inside `readAll`, where every other root-history admission lives, so the refusal is ahead of terminal reuse, public replay policy, authored work and any append. It raises `StaleInputError` with the same diagnostic the publish path raises, so which boundary discovered the unreadable record does not change what the run reports or how the failure is classified. One parser, two boundaries. A record the projector refuses is not a record core continues through, whether the continuation runs the body or reuses a terminal. A history holding no publication is unaffected, which is every ordinary run and every entry admitted before this existed. Recognized retained outcomes stay authoritative and are not refreshed from today's operand; live assignment still binds the original operand by reference; ordinary execution, accepted entry-local behavior and legacy old-mode continuation are unchanged. The existing P9 version-2 representative now covers both boundaries: the unfinished prefix refuses as before, the same record with its retained Close refuses rather than reusing recorded success, and the untouched history remains as the control that reuses its completion, runs nothing again and appends nothing. F2–F5 and their evidence are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| // through the ordinary public Component Api, exactly as it always has. | ||
| // through the ordinary public Component Api, exactly as it always has. And | ||
| // no root publisher: an expansion with no execution has no root environment | ||
| // of an execution's to publish for. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // of an execution's to publish for. |
| environment, | ||
| undefined, | ||
| // A fragment writes an environment of its own, so it has no root | ||
| // capture to publish and no root publisher to be given one. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // capture to publish and no root publisher to be given one. |
| // document execution that admitted it. | ||
| undefined, | ||
| // And no root publisher: a fragment's locals are its own, and a name it | ||
| // binds is not something a later entry inherits. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // binds is not something a later entry inherits. |
| // defined: `values["__proto__"] = …` reaches `Object.prototype`'s inherited | ||
| // setter, and what that does depends on the engine. This is the environment a | ||
| // retained name is read back into, so a capture of that name has to be the | ||
| // same own data property every runtime. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // same own data property every runtime. |
| name, | ||
| ...sourceDescription(position), | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
PR #943: ✨ Reuse root JSON across REPL entries and cold reopen (#939)20 files, +3063 / -78 Scope🔴 PR has 3141 lines changed. Split into focused PRs. 🟡 3141 lines changed. PRs under 400 receive more thorough review. StructuralOxlint structural signals:
Slop
Static AnalysisOxlint: 60 diagnostics across 5 files (16 rules) no-shadow (14): packages/core/src/components/Fail.ts, packages/cli/src/repl/session.ts, packages/core/src/execute.ts CorrectnessNo extraneous code patterns detected. |
The per-binding publication protocol #939 shipped is removed whole: no producer, mode, admission field, parser, counter, outcome marker or expansion plumbing, and nothing renamed in its place. `expand.ts`, `generated-xmd.ts`, `host.ts` and the CLI's `entries.ts` return to the pre-#939 base byte for byte. In its place, an execution computes the root data it hands back. A binding is a byproduct of running the document, so one private owner per canonical invocation is given the actual root environment by identity, and at the result boundary it reads that root's current eligible data onto the ordinary `DocumentResult` — on an authored failure as well as on success, because a failure is an outcome. The journal gains no operation, and `parseDocumentResult` stays the single interpretation point, so a cold reader answers from the record it already parses. Reading is owned the same way. `useSecretDetection` now returns the disclosure policy beside the journal gate, so the owned read closes over the exact scanner the gate closes over and a later same-name Context cannot weaken it. `HostExecution.bindings()` exposes that read, and `ReplSession.bindings()` layers it over the retained head, rechecking the running entry's identity after the read suspends. Ordinary authored failures now normalize inside the canonical result boundary. Before this, a plain `<Fail>` left no `DocumentResult` at all — the root Close held the durable protocol's serialized error — so its rendered text was lost to every later reader. Durability and fatal Files failures still escape ahead of it.
Repairs the five findings both exact-head reviews returned on 10f3cc8, which stays as it was. The owner is now an Effection resource, acquired in the canonical invocation's own scope before the work it holds anything for, so its finalizer is established before there is a live reference to release. It releases the document's root environment and this execution's scanner on its own teardown, whichever way the run ends, and a run halted or fatally failed before it settled has nothing to read and no live environment to read instead. The manually paired `release()` is gone. Settling keeps the accepted data and lets go of the root and the scanner. What arrives there came back through the root's own close, so the gate that decides what may be written has already vetted it — which is what lets the handle stay readable for its promised lifetime without holding a live reference open to serve a read, and what keeps a replayed run from scanning its own journal again. Every successful read is copied rather than shared, live or settled. Two reads of one settled execution are two graphs, so a reader cannot edit an outcome the journal has already recorded. A fragment expands against the containing document's environment, as it always has, so a name it binds with `as` is written in that execution's root and is eligible there. The earlier premise that it writes an environment of its own was wrong; a generated root is the separate case. Released fragment semantics are unchanged and nothing tracks provenance. Completes two frozen observations that were claimed but not exercised: a bound command's outcome crossing to a successor by its three channels, and a transient JSON value overwritten by a function before anything retained it, absent from the result, the next entry and a cold reading. Cancellation carries no computed map of its own and does not unretain the facts recorded before it. Its representative is now the real unfinished question prefix with a cancellation recorded on a copy of it, and the guide and spec say that distinction instead of "hands on nothing".
…own (#939) Completes the two selected observations the Planner asked for twice, in the core owned-handle controls. No production change: the resource and copy fixes at babfb6f already behave this way, and these rows are what makes that visible to a test rather than only to a trace. A new C5 row captures safe nested data, holds the run at an explicit barrier — a component that reports it was reached and then suspends, so the invocation is genuinely unfinished rather than waiting on a timer — reads the handle successfully there, then leaves the scope so the owner is halted and joined. The same retained handle then refuses with no map. The successful read before the halt is what makes that a refusal rather than an owner that never had anything. The C2 control now writes through the nested member of one returned settled answer and reads again. The accepted value is unchanged, the retained bytes are identical, and the separate cold execution over them still reads what the run decided. Both rows discriminate: reintroducing the settled-read alias fails C2, and a finalizer that stops clearing its live references fails C5.
| terminal = settled; | ||
| // After the eval facts this segment already recorded, and through the | ||
| // same publishing path: the ordinary result is the last thing this entry | ||
| // said, so a name it carries wins over an earlier value for that name. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // said, so a name it carries wins over an earlier value for that name. |
| } | ||
| // Read whole or not at all: a member this version cannot read refuses the | ||
| // outcome rather than being dropped, because dropping it would hand a later | ||
| // entry a smaller environment than the record describes. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // entry a smaller environment than the record describes. |
| // Suspended, so this is a different moment. If the entry that owned the | ||
| // read is no longer the entry running, what it computed is either in the | ||
| // head now or was never acknowledged at all; either way the file is the | ||
| // only honest answer and the live half is dropped rather than carried. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // only honest answer and the live half is dropped rather than carried. |
| return head; | ||
| } | ||
| // The live values win: they are this entry's current root, and the head | ||
| // holds what was true before this entry started. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // holds what was true before this entry started. |
| // The session's own read, for the same reason `printing` is: the live | ||
| // half belongs to the entry running in this process, and only the | ||
| // session knows which entry that is and whether it is still that one | ||
| // when the read answers. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // when the read answers. |
| // Whichever way this invocation ended. A run that settled keeps the | ||
| // answer it settled — that answer is data, and the handle was promised | ||
| // it — while a run halted or failed before settling has nothing to read | ||
| // and no live environment left to read instead. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // and no live environment left to read instead. |
| // Defined rather than assigned, for the reason the root's own initial | ||
| // bindings are defined: `values["__proto__"] = …` reaches | ||
| // `Object.prototype`'s inherited setter, and what that does depends on the | ||
| // engine. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // engine. |
| const wrapper = caught instanceof Error ? caught : new Error(String(caught)); | ||
| // Without a printed description the failure speaks for itself: an authored | ||
| // `<Fail>` carries its own message and no separate segment account, so the | ||
| // record repeats that message rather than inventing a second one. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // record repeats that message rather than inventing a second one. |
| } | ||
| liveEnvironment(rootEnv); | ||
| // The one place the owner learns which object this run's root is. Identity, | ||
| // not a lookup: what may be reported is this environment's own data. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // not a lookup: what may be reported is this environment's own data. |
| const result = parseDocumentResult(returned); | ||
| // Accepted, live or restored: from here the owner answers from the result | ||
| // rather than from a root environment, which is what lets a completed run | ||
| // be read without being executed again. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // be read without being executed again. |
Repairs the three PR-caused CI failures at 1043460. The base and the pre-correction PR head were both green, so all of them were this correction's. 10f3cc8 made every undescribed error the document's own outcome, which is the opposite of what the settling boundary has always decided: `documentationFailure` reports a document's failure as its own outcome and lets anything else escape as infrastructure. Git protocol refusals, retained-history rejections, assertion aborts and a missing launcher were being reported as document results instead of escaping. The guard is restored with one narrow exception. A valid `<Fail>` marks the Error it raises with a stable namespaced non-enumerable own property, and the boundary reads that mark off the object it was handed. So the one undescribed failure that is a decision the document made keeps its ordinary result, its rendered text and its computed data, and every other undescribed failure is rethrown unchanged. Nothing identifies it by class, name or message: `<Fail>` still raises an ordinary Error carrying exactly the author's sentence, through ordinary component reporting and recovery, and an invocation `<Fail>` refuses is never marked. The mark is non-enumerable and is never searched for underneath a wrapper, so a failure that merely carries an authored stop as its cause is not adopted as one. Separately, babfb6f pushed a binding checkpoint at the root Close's own marker, and the REPL keys checkpoints by marker — so one marker had two children with one key and the drawer refused to reconcile. The Close's terminal checkpoint already owns that selectable position and the computed data arrives with that outcome, so the second checkpoint is gone. The binding transcript row, the terminal's bindings and the merged entry data are unchanged. And the new JSDoc line that failed the banned-words check is reworded.
| // The session's own read, for the same reason `printing` is: the live | ||
| // half belongs to the entry running in this process, and only the | ||
| // session knows which entry that is and whether it is still that one | ||
| // when the read answers. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // when the read answers. |
| // The accepted data is kept, not the things that produced it. From | ||
| // here the root the document expanded against and the scanner that | ||
| // vetted it on its way into the journal are this owner's no longer, | ||
| // whichever way the run ends. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // whichever way the run ends. |
| // Whichever way this invocation ended. A run that settled keeps the | ||
| // answer it settled — that answer is data, and the handle was promised | ||
| // it — while a run halted or failed before settling has nothing to read | ||
| // and no live environment left to read instead. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // and no live environment left to read instead. |
| // Defined rather than assigned, for the reason the root's own initial | ||
| // bindings are defined: `values["__proto__"] = …` reaches | ||
| // `Object.prototype`'s inherited setter, and what that does depends on the | ||
| // engine. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // engine. |
| // boundary was handed, never searched for underneath it, so a wrapper or a | ||
| // teardown failure that merely carries an authored stop is not adopted as | ||
| // one. `describeFailure` reads the wrapper for its own account when there | ||
| // is no description to read. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // is no description to read. |
| const result = parseDocumentResult(returned); | ||
| // Accepted, live or restored: from here the owner answers from the result | ||
| // rather than from a root environment, which is what lets a completed run | ||
| // be read without being executed again. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // be read without being executed again. |
After #943 merged, GitHub rebased this branch onto the new main, so the remote held 368aa6d while the signed integration held 3d10513. Both carried the same tree. This merges the remote head into the signed branch, which keeps every published object reachable, makes the remote an ancestor again, and leaves a normal fast-forward as the way to publish. The merge changes nothing: the tree is still 9fd3794, the tree both sides already had and the one F1-F4 and the parent-boundary suites passed on. Nothing was rebased, force pushed or reset, the deleted parent branch stays deleted, and the pull request keeps main as its base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Why
Root
<Let>and ordinary component captures now reach the next REPL entry, explicit Bindings reads and a cold reopen. Reusing a captured value, such as a Git commit ID, does not repeat completed effects.Closes #939.
What changes
Entry 1:
After it settles,
<REPL.Bindings />includessaved: { answer: 42 }, and Entry 2 rendersAnswer: 42fromAnswer: {saved.answer}, including after reopening the journal.The retained value is the eligible root data when the entry finishes. Each entry receives isolated copies of inherited objects: mutations made before its ordinary result is retained carry forward without re-exporting the name. Earlier entries and historical readings remain unchanged.
How it works
Canonical execution carries computed root JSON on its ordinary document result, without individual binding journal publications or publication modes. Rendered output, typed return values and computed bindings remain distinct. Both ordinary success and authored failure retain their computed data; cold reconstruction reads recorded outcomes without repeating completed work. A valid core
<Fail>preserves its authored Error and ordinary recovery; unrelated undescribed errors continue to escape. Each root Close has one selectable checkpoint while its binding rows and data remain available.Explicit Bindings reads combine retained data with the running entry's safe live root. Historical panes remain limited to their selected prefix. Each host-handle read returns an independent detached map. An Effection resource is acquired before owned work; its own finalizer releases live root/policy references. Halt or fatal completion without an accepted result refuses later reads, while accepted JSON remains readable for the handle's promised lifetime without retaining live resources or rescanning replayed data.
Risks and limitations
7490f014; the fixed939runtime remains there for active sessions.How to verify it
All seven focused groups pass with zero failures:
All seven groups ran on the published repair tree
b3a65dbc1392d1c7459e6bbbf423ecfbf56a046a. They cover actual capture/successor handoff, cold and unfinished replay without repeated Git effects, joined-halt refusal, returned-map mutation isolation, cancellation facts and secret/append enforcement. The affected Fail/recovery, escaping-error and REPL model/drawer journeys also pass (9/53, 6/67 and 74/179 suites/steps);deno task lintanddeno task checkexit 0.Both original reviewers explicitly covered the resource lifetime/detachment requirement and this narrow CI repair on the published head. These are agent verdicts, not GitHub account approvals:
0217795b5cc38c7be6c20e755c28f3850232385e.0217795b5cc38c7be6c20e755c28f3850232385e.The complete reviews and focused logs remain in the central #939 delivery records.
CI run 39135245943, attempt 1 completed successfully on exact
0217795b5cc38c7be6c20e755c28f3850232385e: 41 jobs succeeded, and onlycomposabilitywas correctly skipped for this ordinary pull request. Required aggregategreensucceeded at 14:26:17 UTC on 2026-10-18.Stack and dependency scope
The healthy, approved corrected parent
0217795b5cc38c7be6c20e755c28f3850232385eis published on this PR and available for future authorized safe integration. All downstream tracks (#940, #934, #942 and #932) remain PARKED. Each delivery still needs its own corrected-parent integration, affected evidence, both original reviewers inspecting one identical final integrated head, and its own Planner title/body APPLY/VERIFY. Earlier-parent reviews do not cover that integrated head. Lexical visibility (#110) andREPL.Entry.{Source,Prompt,Output,Journal}(#932) retain their separate scope.