Skip to content

Commit a0a4ea7

Browse files
authored
✨ Slices C and D of #827: a draft, an Entries catalog, and the journey that proves them (#869)
* ✨ Slice C of #827: route, draft and a windowed Entries catalog A draft is now execution-wide state for the next entry. It coexists with a selected entry, a nested scope, either surface, drawers, a Sessions filter and a frozen history position, and it stays editable while an entry runs and while a prefix is being inspected — an admitted entry is immutable, so there is never an entry those keystrokes could be editing. Run is available only at the live head. The reducer refuses it at a frozen position, because the only inheritance boundary there that is not a fork is the head; whether this is a moment to submit at all stays the session's answer. No refusal touches the draft: it clears only once it has become an entry, and the entry it became carries the key the session really assigned. The Entries surface is a catalog of every retained entry in admission order, each row carrying its own outcome — unfinished, ok, err or cancelled — and keyed by its stable entry key rather than by its place in the window. The catalog is windowed rather than mounted whole and clipped: rows outside the window are not described, mounted, drawn or targetable, every row becomes reachable by scrolling, and a scroll is applied from the clamped position the frame is drawing so the first press after a resize moves the screen. A location naming an entry its prefix never admitted refuses whole. When the history action selects a position earlier than the selected entry's admission, the loop clears that now-invalid entry and scope suffix and guesses no replacement, keeping the draft, the surface, the filter, the History drawer and the marker. * 🐛 Slice C review: the transcript locus, the outcome and the surface round trip Four corrections from the Slice C review of f2c73c6. The transcript now belongs to the entry that is selected. It was always `model.transcript`, so selecting an entry moved the route and nothing a reader could see: every entry's rows showed under every selection, and a running entry's live overlay appeared while a settled earlier entry was being read. An entry supplies its own retained rows, the overlay is drawn only where the entry producing it is the locus, and selecting nothing is still the whole execution — which is what a one-entry execution always showed. The outcome comes before the name. The catalog promises every accepted frame shows an entry's outcome, and `N. <name> · <outcome>` let an arbitrary root name push that promise past a 28-column sidebar. `N. [ok] <name>` keeps it inside the drawn row whatever the entry is called. A location carries the selected entry on either surface. Going to Sessions used to leave the route unspellable — `encodeLocation` raised rather than refusing — so there was no way back to the entry a person left. Which surface is being read and which entry is selected are independent members of one location, and the grammar now says so; what a surface still constrains is its drawers. The decoder's own documentation no longer claims a draft beside a structure or a history position is refused. * ✅ Slice C review: prove the surface round trip through the mounted controls Two review items, no production change. The declared drawer replaces a type assertion. A shape asserted into place is not the shape the grammar defines, and the rule against it is the repository's. The selected-entry round trip is proven where a person performs it. The reducer was being called directly at a wide size, which showed the decision and not that anything mounted asks for it. The new row drives a narrow frame: the second entry is selected through its own catalog row, and each surface control is reached by Enter and by a pointer resolved against the exact frame that drew it, with the two asserted equal. Across both crossings the selection, the draft, the conversation filter and the frozen position all stand; Sessions stays the whole execution's chronology and is byte-for-byte the reading it is with no entry selected; and returning lands on the same transcript locus under one location. `sessions-forgets-the-entry` reddens it. * 📝 Slice D of #827: the integrated journey, cold reconstruction and the documents The last slice of the sequential-entries Story, and it changes no production code: the integrated journey found nothing the earlier slices had wrong, so what it adds is the evidence that the whole thing works as one product, and the two documents that still described a REPL admitting one entry. EJ1 drives three entries through one `runReplProgram()`. Entry 1 publishes a value and stops at a question; the next entry is drafted while it is still running and Run refuses, leaving the draft byte for byte and the history where it was; answering settles Entry 1 and the same draft becomes Entry 2, which reads what Entry 1 published and then fails after publishing its own; Entry 3 follows that failure and still reads Entry 1's value. The three rows stand in admission order with the outcomes they reached, and selecting the first one shows its transcript and not the third's. EC1 reopens a two-entry journal cold, at a location naming the second entry, and gets the same catalog, the same selection and the same inherited value — with no eval block compiled, no component source read, and the file byte-identical afterwards. A second EC1 row pins the thing a cold reopen and a live successor must agree on: an edit an entry made to a value it never republished reaches no successor, because inheritance is a reading of the file rather than of the run before it. EH1 reads one two-entry history at three positions — before the second admission, at it, and at its outcome — and each shows exactly the entries and outcomes retained by then, while the draft stays editable at all of them and `[live]` brings back the head catalog with it. `specs/repl-spec.md` and `architecture.md` describe sequential entries once: the catalog and its order, the draft that is always the next entry's, the submission gate, what a later entry starts from, the private segment adapter, the host-only initial bindings, entry-local Prompt sequence beside one global Sessions chronology, and what a frozen position does and does not freeze. A new D1 row holds the spec to the code, so the one-entry ceiling cannot come back in prose after it has gone from the product. * ✅ Slice D review: provider-cold evidence, real-time waits, and two corrections Four review items, no production change. EC1 had no Agent work in it, so "nothing was asked" was a claim about a journal with nobody to ask. A second cold row runs two entries that each hold one Prompt — one Prompt per execution, so each records sequence 0, which is the collision a cold reader has to resolve the same way a live one did. Reopened at a location naming the second entry, with a recording provider: no prompt sent, no runtime created, no session ensured, no agent started; the drawn Sessions rows are the rows the live process drew, both entries' turns under one conversation; the transcript column holds the selected entry's output and not its predecessor's, while that predecessor's turn is still on Sessions beside it; and the file is byte-identical. The record carries the rest — both turns recorded, each owned by the entry that ran it — because the sidebar is narrower than the words for it. The new journey waits counted attempts, which counts how busy the machine is. They are one deadline-bounded waiter now, and it reports the screen when it gives up. The spec said an interrupted entry hands values on. It does not: teardown without a retained root close leaves the entry unfinished, and an unfinished entry permits no successor. What permits one is a retained err or cancelled close; an interrupted entry resumes and settles first. The boundary row's type assertion is a declared `readonly ReplSurface[]`.
1 parent e732512 commit a0a4ea7

13 files changed

Lines changed: 2775 additions & 169 deletions

‎architecture.md‎

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5486,6 +5486,19 @@ concurrent Prompts that publish out of order do not reorder each other. The
54865486
slot, its order and its live key are process-local — no route, model, Journal
54875487
record or public Core Api carries one.
54885488

5489+
A Prompt's sequence number is local to the execution that recorded it, so it
5490+
restarts with every entry and two entries may each hold sequence `0`. Turns are
5491+
therefore ordered by entry admission first and by sequence within that entry,
5492+
and uniqueness is required only inside the owning entry. Both retained and live
5493+
turns carry their entry beside their scope, and a publication is correlated by
5494+
the pair — a durable Prompt name says where it was written, so two entries
5495+
running one source hold the same name at the same position, and correlating by
5496+
the name alone shows one turn twice and the other not at all.
5497+
5498+
Sessions stays one execution-wide chronology grouped by the provider's session
5499+
key and by nothing else. Selecting an entry changes which transcript is being
5500+
read and never which conversations are listed.
5501+
54895502
Filtering is by the conversation key a provider actually issued; an authored
54905503
`<Session>` name is not one. Selecting or clearing changes `route.session` and
54915504
nothing else, and background Agent work never changes it.
@@ -5608,7 +5621,8 @@ DurableEvents -> frozen ReplModel -> resolved immutable view
56085621
| `model.ts` | projecting one validated Journal prefix into a frozen model |
56095622
| `route.ts` | the location grammar, and resolving one against a model |
56105623
| `journal.ts` | the retained NDJSON stream and the repository over a root |
5611-
| `session.ts` | admitting one entry, reopening one history, and the live overlay |
5624+
| `session.ts` | admitting entries in turn, reopening one history, and the live overlay |
5625+
| `entries.ts` | partitioning one physical journal into entry segments, and the private stream view over one of them |
56125626
| `expansion.ts` | pausing and continuing expansion over the public seams |
56135627
| `elicitation.ts` | the one question shape this REPL presents |
56145628
| `description.ts` | opaque immutable descriptions and the closed action boundary |
@@ -5622,6 +5636,53 @@ DurableEvents -> frozen ReplModel -> resolved immutable view
56225636
| `components/` · `application.ts` | the screens, and the one state transition boundary |
56235637
| `storage.ts` · `program.ts` | where histories live, and the command as one scope |
56245638

5639+
### Sequential entries over one physical journal
5640+
5641+
One execution is one append-only stream, and entries are ranges of it. A segment
5642+
begins at the root `import_component` admission and ends at that root's `Close`;
5643+
only the final one may be unfinished. Each live execution is given a private
5644+
view over exactly its own segment, so a repeated root name and a reused
5645+
coroutine id cannot correlate across entries — the view reads only its range,
5646+
delegates every append to the physical stream, updates what it believes only
5647+
after that append is acknowledged, and refuses to write at all once its segment
5648+
is closed or is no longer the final one. No physical offset reaches Core; the
5649+
command's own observer holds the acknowledged prefix and computes the resulting
5650+
marker. This partitioning is private to the CLI: `DurableStream` is unchanged
5651+
and there is no physical-offset API.
5652+
5653+
The whole prefix is partitioned and validated before any entry is projected, and
5654+
a malformed boundary refuses the model whole rather than yielding a partial
5655+
catalog — work before the first admission, a nested or repeated admission inside
5656+
an unfinished segment, a close with no open segment, work between a terminal
5657+
close and the next admission, a second unfinished segment, or a segment the
5658+
one-entry reader cannot read.
5659+
5660+
Entry keys are `entry-1`, `entry-2`, and so on, from immutable admission order;
5661+
they are not Journal markers. The first entry keeps every marker spelling it
5662+
ever had, so every location written before this existed still resolves, and
5663+
later entries namespace their segment-local markers with their key. Projection
5664+
converts between the two at that private boundary, and Core sees only the
5665+
segment-local history it already understands.
5666+
5667+
Values cross between entries through one trusted-host input: `executeInstalled()`
5668+
takes the root environment's initial bare bindings beside the source, exported
5669+
from `@executablemd/core/host` and from nowhere else — not a Context, component,
5670+
middleware answer, document prop or durable record, and not on ordinary
5671+
`execute()`. Core parses, name-validates and recursively copies the whole record
5672+
before any installation or document code runs, and the detached values are
5673+
ordinarily mutable inside the execution that receives them. The REPL derives
5674+
that record only from values durably retained before the new entry's admission,
5675+
so replay derives the identical record from the identical bytes.
5676+
5677+
The session owns at most one entry task. Starting another needs two separate
5678+
facts — the preceding entry has a retained terminal close, *and* its task has
5679+
completely torn down and joined — because observing a terminal outcome is not
5680+
the same as the work being over. There is no queue: a submission that arrives
5681+
before both hold is refused, having started nothing, and teardown stops a
5682+
pending one from starting rather than appending on its behalf. Settled segments
5683+
are projected and never replayed; only an unfinished final segment is given back
5684+
to Core to continue.
5685+
56255686
A location is resolved against the retained history before a session opens.
56265687
Opening one starts or resumes the execution, and replay past the retained prefix
56275688
appends — so a route naming a scope or drawer the file never held is answered

0 commit comments

Comments
 (0)