Conversation
A `<Prompt>` returns only once its provider turn has settled and its ordinary `agent_prompt` record has appended. Between those two moments there is a turn nobody could read. This adds the private live owner that reports it — queued, streaming, finished-but-unpublished — and that holds and settles the interactive permission requests a turn makes. The observer wraps `Agent.prompt()`'s cold stream rather than subscribing to it, so `<Prompt>` stays the one subscriber and the one owner of provider cancellation, and each provider event travels back untouched. Correlation is by coroutine, not by resemblance. Two `<Spawn>` children may run identical prompts against identical responses and settle in either order, so nothing about a turn's content identifies it. Expansion inside one coroutine is sequential and a record appends on the coroutine that made it, so the Nth turn observed on a coroutine is the Nth record appended there. A record and the overlay it replaces change in one transition, so no snapshot holds a turn twice or neither time. An interactive request belongs to the live turn on whose coroutine the provider asked. Where there is not exactly one, nothing is published, nothing is denied, and the session owner is terminated — reporting only into the permission operation would leave the REPL running without the identity it needs. A post-admission correlation failure ends the owner the same way: authority is withdrawn, the execution is halted and joined, and `join()` returns that exact failure. Dismissal while the session is live denies once through Core's own rule, which `@executablemd/core` now exports. Whole-session teardown is structured cancellation instead, and claims no denial outcome. Preflight now validates a submitted entry against the vocabulary its execution installs. A host that declares `<Session>` to the execution would otherwise have every entry naming one refused before it ran.
A turn the REPL ran retained no permission audit at all. The observation was
installed per Prompt, inside the turn; the REPL's authority is installed with the
session's execution installations, outside it. A policy decides every request
inside its scope without deferring outward — that is what deciding means — so the
observer never saw the call it exists to record, and `PromptRecord.permissions`
was empty for every REPL turn while Slice A's contract held everywhere else.
One observer now sits with the Agent installation itself, outside every policy
installed after it. Each Prompt places a private ledger where its own requests
will find it, and a request raised while that Prompt's provider stream is being
consumed inherits that ledger through its own scope — so two turns asking at once
keep their audits apart with nothing correlating by recency, prompt text, agent,
conversation or the order decisions settled in. The observer reserves the
request's place on the way in, delegates exactly once, and copies the returned
outcome into that same place, so two overlapping requests from one turn keep the
order they were asked in however their answers interleave. A place nobody
completed is published as nothing: a policy that raised, and a wait cancelled with
its session, leave no member rather than an outcome nobody reached.
The ledger and its context are private to the journal module. Nothing new is
exported, no authority is added, and the safe fields are still copied one by one —
`rawInput`, the Session and the provider's own objects reach neither the bytes nor
the model.
`{ at: "max" }` is not outside a later default install: it *is* the default, and
an outer scope's middleware ends up outermost. That is why this is installed in
the Agent installation rather than once per execution.
…lish `spawn()` returns before its child body runs, and a Signal drops whatever it sends while no subscription is active. Both admission watchers created their subscription inside the spawned child, so an announcement the execution made immediately — the queued Agent turn or the elicitation that is the only evidence admitting a cold session — could be sent into no subscriber at all, leaving `start()` waiting for an admission that had already happened. Effection's contract is explicit: create the subscription in the enclosing scope, iterate it in the child. Spawning the consumer earlier is not equivalent, because what must precede the document is the subscription, not the task that reads it. The elicitation watcher had the same lossy ordering and predates this slice. Both belong to one admission boundary, so both are corrected rather than leaving one known race beside the fixed one.
The REPL observed every public `Agent.prompt()` call and drained a per-coroutine queue whenever an `agent_prompt` record appended. `Agent` is exported, so a registered component may call it directly: that call was observed, journalled nothing, and left its queue entry behind — and the next canonical record then retired the direct call's entry instead of its own. The canonical turn stayed in the live overlay while also being durable, and a permission it was granted was audited against the wrong turn. A queue cannot establish that identity, because it holds calls that can never produce a record. The journal boundary already knows which calls are its own, so it now says so: core calls the host publisher's `begin()` at the one moment that is both canonical and live — in the turn's own scope, as its private audit ledger is placed — and hands the same value back on the publication that ends it. `append()` remains the single durable handoff, and the live turn it began is removed inside that same transition, so no announced snapshot holds a turn both ways or neither. The handle is opaque to core, ephemeral, process-local and never journalled. `PromptAudit` stays private, `AgentApi` is untouched, and no parallel host API is introduced: `begin()` is an optional addition to the existing `AgentPromptPublisher`, so a publisher that declares none behaves exactly as before. A direct `Agent.prompt()` begins nothing, so it gets no reading and can neither claim a record nor be claimed by one. A replayed turn asks no provider, so it begins nothing either. P6 covers it: a direct public prompt completing before a canonical `<Prompt>` on the same coroutine, and the same exact handoff for failed and cancelled turns, where no `association` exists and only the handle identifies the turn.
`place()` installed this Prompt's ledger and then trusted its caller to have opened a scope of exactly the right size first. Two things that must be inseparable were separate: installing the ledger, and running this Prompt's provider stream beneath it. Called one scope too high it would have leaked silently into sibling work, and nothing in the signature said so. `within(body)` is a lexical bracket instead. The ledger exists for exactly as long as `body` runs, descendants inherit it, and it is restored when `body` returns, raises or is cancelled. No caller can install it without naming the work it governs, so the invariant is structural rather than remembered. The canonical turn is begun inside that same bracket, which is the one moment that is both canonical and live. No behavior changes: the audit rows that prove two requests from one turn keep their arrival order, and that a later request from an earlier turn is still that turn's, pass unchanged.
…iptions Auditing was something the canonical `<Prompt>` had to remember to install. It now belongs to Core's outer Agent middleware, which owns both sides of it. The `prompt` handler makes one private ledger for that exact invocation and wraps the cold stream it returns. Subscribing and every `next()` run beneath `PromptAudit.with(ledger, …)`, so a provider that asks for permission the moment it is subscribed already finds the right ledger, and the context is restored whichever way each step finishes. The `requestPermission` handler reads the inherited ledger, reserves the request before delegating, delegates once, and records what came back. `<Prompt>` no longer creates, places or brackets anything. It reads its audit from the exact stream it consumed, through a private association held in a WeakMap that belongs to the installation rather than the module. Holding the stream is the only way to name an entry, so a bystander cannot read one. `PromptPermissionAudit.within()` is gone from the component-facing protocol. `scoped()` stays: it owns provider cancellation, permission routing and the session lock, which have nothing to do with audit correlation. Admission's ordering is now structural rather than remembered. `consumeAdmissions()` takes a `Subscription`, not a `Stream`, so the losing shape — a consumer that subscribes as its first act, a turn too late — cannot be written through the signature at all. Both session subscriptions are created in the enclosing scope before the document spawns. Evidence, with the controls that make each row mean something: - AD1/AD2 hold the admission ordering: AD2 shows the replaced shape dropping a value announced before its consumer began, which is what AD1 proves survives. - P6 covers a direct public `Agent.prompt()` beside a canonical `<Prompt>` on one coroutine and across concurrent spawns, and the same handoff for failed and cancelled turns where no association exists. - AFP0/AFP1/AFP2 hold publication failure: a publisher that raises and one that returns without appending both fail through the durability path and leave no `agent_prompt`, while the same harness with a publisher that appends succeeds and records exactly one.
…by lookup Four corrections to the canonical-correlation work. A publication that fails retained nothing, but the live turn it began stayed mounted — a terminal overlay waiting for a record that was never going to arrive. The failure path now retires exactly that turn and announces the removal before the failure travels on. The handle is read back by lookup instead of by assertion. `begin()` mints an opaque object token and remembers what it stood for; `publish()` narrows at runtime and asks the map. A value this owner did not mint is simply not a key, so nothing has to claim it is a turn. The two `as const` assertions in the new rows are gone too. Admission filtering composes with the stream API. `filter()` is applied in the enclosing scope before either subscription exists, so `consumeAdmissions()` is a generic drain of an already-active subscription and cannot know or care what admits. `@executablemd/cli` declares `@effectionx/stream-helpers`, which the lockfile already held; the delta is one line. The published contracts now describe what is actually there: `publication.ts` documents a publisher seen at two moments rather than only at completion, `components.ts` and both specs describe middleware that owns each invocation's ledger and wraps its stream, and the Prompt-owned-ledger and FIFO-correlation wording is gone. Evidence: the refused-publication row is REPL-level and drives a journal that refuses the record, proving the turn is neither retained nor left mounted. It reddens when the failure path stops retiring the turn.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #854. Slice B of five.
Stacked on two merged foundations, both already on
main:<All>/<Spawn>concurrent document work.Why
Slice A let the REPL project retained Agent conversations from the Journal.
It could not run one. A live
<Prompt>had nowhere to report its streamingreading, a permission request arriving mid-turn had no owner that could answer
it, and the decision the REPL's own authority made never reached the owning
Prompt's durable audit.
What changes
Before:
<Prompt>in a REPL entry produced no live overlay; only retained turnsexisted.
After:
Journal remains retained truth.
policy, and each live turn is tied to its durable record by the journal
boundary itself.
How it works
Auditing is owned by Core's outer Agent middleware, not by the component.
Its
prompthandler makes one private ledger for that exactAgent.prompt()invocation and wraps the cold stream it returns; subscribing and every
next()run beneath that ledger, so a provider that asks for permission themoment it is subscribed already finds the right one, and the context is
restored however each step finishes. Its
requestPermissionhandler reads theinherited ledger, reserves the request before delegating, delegates exactly
once, and records what came back. Only completed places are published, so a
request still in flight at teardown is omitted rather than invented.
<Prompt>installs nothing. It reads its audit from the exact stream itconsumed, through a private association held per Agent installation. Holding
the stream is the only way to name an entry.
Correlation is the canonical publication. Core calls the host publisher's
begin(input)immediately before the provider is asked, and hands the sameopaque handle back on the publication that ends the turn.
append()is thesingle durable handoff, and the live turn it began is retired inside that same
transition — so no announced snapshot holds a turn both ways or neither. When
publication fails, the turn is retired and the removal announced before the
failure travels on, because nothing was retained and nothing may still be
shown as though it is about to be.
A direct public
Agent.prompt()begins nothing: it gets no reading and canneither claim a record nor be claimed by one. A replayed turn asks no
provider, so it begins nothing either. Nothing correlates by coroutine queue,
prompt text, session, completion order or recency.
Admission ordering is structural. Both session subscriptions are created
in the enclosing scope, filtered with
filter()before the document spawns;consumeAdmissions()takes aSubscriptionrather than aStream, so thelosing shape — a consumer that subscribes as its first act, a turn too late —
cannot be written through the signature.
Review guide
Start with:
packages/core/src/agent/journal.ts— the middleware that ownsboth sides of auditing, the wrapped stream, and the per-installation
association.
Then review:
packages/core/src/agent/publication.ts—begin()/begun, the hostboundary correlation is built on.
packages/cli/src/repl/agent.ts— the REPL publisher, handle lookup, andthe retirement path on success and on failure.
packages/cli/src/repl/admission.tsandsession.ts— subscriptionownership.
What must stay true
compile work. The cold row performs a real component read and eval compile
on the live run, proves both are zero on the cold run, calls no provider,
and compares the cold execution's serialized journal event-for-event with
the live bytes.
AgentApi,component props or
core/mod.ts.begin()is an optional addition to theexisting
useAgentPromptPublisherhost boundary; a publisher declaring nonebehaves exactly as before.
How to verify it
Each control below was applied alone and reverted byte-for-byte:
next()Agent.prompt()calls tooAFP0is the other side of the publication-failure comparison: the sameharness with a publisher that appends succeeds and records exactly one event,
where
AFP1(raises) andAFP2(returns without appending) both fail throughthe durability path and leave none.
Scope
Included
Intentionally unchanged
Slice E command, packaged Plan and journeys — including
specs/repl-spec.md,which the plan assigns to Slice E.
New dependencies
@executablemd/clinow declares@effectionx/stream-helpers@0.8.3, alreadyin the lockfile and already used elsewhere in the workspace. The lock delta
is one line. Used for
filter(), so admission filtering composes with thestream API instead of being hand-rolled inside the consumer.
Risks and limitations
ReplAgentTurn.sequenceis Prompt schedulingchronology, not authored order.
<All>separately preserves authored outputorder. A surface wanting authored order cannot read it from
sequence.deno task verify:cleanwas not run for the dependency change. The delta isa single member-scope reference to a package the lock already pinned, and
deno task check:jsrpasses, but the repository's dependency-mutationprocedure asks for it before pushing anything that could move dependency
state.
Scope confirmation