Skip to content

🏭 Build remote workflow provider checkpoints D1–D3 (#698) - #763

Draft
minkimcello wants to merge 47 commits into
taras:codex/github-actions-software-factory-architecturefrom
minkimcello:claude/698-remote-workflow-provider
Draft

🏭 Build remote workflow provider checkpoints D1–D3 (#698)#763
minkimcello wants to merge 47 commits into
taras:codex/github-actions-software-factory-architecturefrom
minkimcello:claude/698-remote-workflow-provider

Conversation

@minkimcello

Copy link
Copy Markdown

Why

This stacked change implements the remote-workflow foundation from #698 on top of the software-factory contract in #736. It lets a disposable runner work against workflow and Workspace state durably owned by a Cloudflare Durable Object without moving provider-specific authority into shared workflow code.

What changes

Before:

Executable.md had the software-factory contract and local Deno workflow storage, but no concrete remote owner, private executor transport, coherent remote reads, or atomic remote Workspace publication path.

After:

A Cloudflare-backed owner can admit an executor over a private bounded connection, serve coherent retained snapshots and content, expose a complete internal remote WorkflowRunDatabase, run Workspace effects on the runner, and atomically retain the resulting root, mappings, journal entries, and execution record. The runner remains disposable and provider-neutral code sees only semantic records and failures.

How it works

executor acquisition -> private owner connection -> coherent retained snapshot -> runner-owned Workspace attempt -> one sealed commit -> atomic Durable Object publication

Cloudflare-specific codecs, authentication, SQLite storage, retry decisions, and Durable Object routing stay in the adapter. Provider-neutral remote modules own materialization, transaction collection, database behavior, mapping reconciliation, and the Workspace coordinator.

Review guide

Stack dependency: #736

Start with: packages/workflow/tests/cloudflare/remote-workspace.vitest.ts

Then review:

  1. packages/workflow/src/remote/workspace.ts and the remote Workspace/database tests for the runner-side contract
  2. packages/workflow/src/remote/collector.ts and packages/workflow/src/remote/database.ts for transaction lifetime and publication authority
  3. packages/workflow/src/cloudflare/owner.ts, owner-reads.ts, owner-transaction.ts, and publish.ts for durable owner behavior
  4. packages/workflow/src/cloudflare/acquisition.ts, dispatcher.ts, and client.ts for private transport and strict wire validation
  5. specs/workflow-workspace-spec.md, specs/workflow-spec.md, and architecture.md for the settled contracts

Look carefully at:

  • owner/executor authority separation, retry identity, hibernation-safe performed decisions, all-or-nothing publication, symlink containment, post-teardown capability closure, and the one-owner-link construction invariant

What must stay true

  • The Durable Object is the sole durable owner and the runner is disposable — enforced by the owner transaction and staged-attempt protocol and checked on real workerd.
  • One publication is accepted completely or changes nothing — enforced by the sealed commit intent and one owner-side transaction and checked by rollback, retry, corruption, and lost-response regressions.
  • Reads and commits for one run cannot be assembled from different owners — enforced by RemoteWorkspaceLink as one construction value and checked by the two-owner regression.
  • Shared production modules remain provider- and runtime-neutral — enforced by package boundaries and checked by host-neutrality and public-entrypoint tests.
  • Credentials, acquisition identifiers, SQL rows, private commands, and provider refusals do not cross the semantic boundary — enforced by strict adapter translation and checked by privacy and malformed-answer regressions.

How to verify it

  • pnpm test:cloudflare runs the real-workerd acquisition, storage, publication, retry, rollback, hibernation, and remote Workspace integration suite.
  • pnpm check:cloudflare typechecks the Cloudflare-only runtime boundary.
  • deno task test packages/workflow/tests/remote-client.test.ts packages/workflow/tests/remote-database.test.ts packages/workflow/tests/remote-interoperability.test.ts packages/workflow/tests/remote-materialization.test.ts packages/workflow/tests/remote-publication.test.ts packages/workflow/tests/remote-read.test.ts packages/workflow/tests/remote-transaction.test.ts packages/workflow/tests/remote-workspace-files.test.ts packages/workflow/tests/remote-workspace.test.ts checks the provider-neutral remote contract and native Workspace filesystem containment.
  • deno task check, deno task lint, and deno task check:jsr passed at feedback head 5b04ad217f79eff5704efabed4ef1b6f1ff724ec.

Scope

Included

  • Neutral factory-run identity and lifecycle contracts required by the remote provider
  • Cloudflare Durable Object storage, schema recognition, executor acquisition, private transport, strict reads, and retry decisions
  • Atomic remote journal and Workspace publication with retained decision recovery
  • Complete internal remote workflow database and runner-side Workspace coordinator
  • Real-workerd integration coverage and its required CI job

Intentionally unchanged

  • D4 lifecycle commands, stale-running recovery, answer delivery and consumption, inspection/history, and completed replay
  • D5 configured WorkflowHost, final public Cloudflare client assembly, CLI selection/configuration, and delivery reconciliation
  • The existing four-method WorkflowHost boundary

New abstractions

  • The provider-neutral remote link, collector, materialization, database, and Workspace coordinator types separate semantic workflow behavior from the Cloudflare wire and storage adapter.
  • RemoteWorkspaceLink carries retained reads and publication through one owner authority so supported construction cannot split a run across owners.
  • Each new abstraction has multiple concrete uses or a clear authority/lifecycle justification.
  • No speculative lifecycle or delivery functionality is included.

New dependencies

  • @cloudflare/vitest-plugin, @cloudflare/workers-types, and Vitest runner packages provide the real workerd test and typecheck boundary for the Cloudflare adapter.

Generated or mechanical changes

  • pnpm-lock.yaml and deno.lock record the Cloudflare test-runtime dependency graph.
  • The Cloudflare-only source and tests are excluded from Deno's host graph and checked through check:cloudflare and test:cloudflare instead.

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and reported focused test results.

minkimcello and others added 30 commits September 2, 2026 09:33
Turn PR taras#630's draft into a decision-complete first-release architecture. No
product, deployment, authority, identity, ownership, failure, replay, public
form, permission or ordering decision is left for a later Implementor.

The factory specification now settles: the canonical `github-issue-v1` run-ID
derivation and unsupported issue-transfer drift; `User` as the only Stage 1
spelling; the deployment topology of one SQLite-backed Cloudflare Durable
Object, an authenticated executor WebSocket, an ephemeral Actions runner and a
dedicated GitHub App; authenticated ingress order, bounded intake, dispatch
payload and OIDC claims; the exact App permissions, host ceilings, human
permission floor and `.github/workflows/**` denial; suspend-on-every-conflict
as the single Stage 4 profile; the Stage 7 trusted merge, `Git.PublishTarget`
compare-and-swap and remote-effect-before-terminal ordering; and the exact
public contract inventory. Section 8's remaining material decisions are gone.

architecture.md gains the terminology, the remote storage and executor
topology, the delivery-plane generalization, the Project-provider boundary,
comments/readiness/closure, ordered merge, target publication, trusted evidence
execution, terminal settlement, the split trusted host, and ten construct
inventory rows marked "specified by taras#710; implementation unbuilt".

The workflow and Workspace specifications gain the remote host, the executor
connection, which requests need it and which do not, the host-derived public
run ID, and provider-neutral contracts for `Git.Merge`, `Git.PublishTarget`,
`PullRequest.Comment`/`Ready`/`Close`, `Issue.Comment`/`Close`, `Evidence.Run`
and `Project.Status` — none of them reachable by an Agent or generated XMD,
whose write table stays exactly `File:write`, `Dir` and `File.Delete`. The
executable-MDX specification gains the exact authored forms and the frozen
WRH/WGI/WGE/WGM/WER/WFP/WFL acceptance tiers.

Documentation only: no production code, workflow YAML, dependency, generated
artifact or executable fixture changes, and the ownership SVG already agrees
with the amended text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Answer the Architect's three blocking findings on 86a4694. The public contracts
are now closed shapes rather than descriptions of shapes, run identity has one
spelling, and the two terminal paths are separate everywhere.

Finding 1 — exact bindings and durable records. specs/workflow-workspace-spec.md
is the normative schema location for every authored construct: §7.8 defines the
`GitMergeResult` clean/conflicted union, the conflict-entry record, its sorting,
duplicate and missing-side rules, and restoration failure; §7.9 defines the
`Git.PublishTarget` request and result with five exhaustive pre-states; §7.10
defines the comment request/result and how an engine-derived effect identity is
made remotely observable without using body text, plus literal `Ready`/`Close`
bindings; §10.3 does the same for `Issue.Comment` and `Issue.Close`; §10.5
settles `Evidence.Run` — the whole list runs, separate bounded stdout and stderr
with stated truncation, and an explicit table of which cases bind, which fail
and which commit nothing, with cancellation and teardown precedence fixed; §10.6
defines `Project.Status`. The factory specification §11.2 defines the versioned
closed factory protocol schemas — subject, stage, revision, handoff, actor,
outcome, invalidation, verdicts, conflict suspension, Stage 7 decisions, the
frontier and its complete reduction table, and the two asymmetric terminals.

Finding 1 also adds `PullRequest.Merged` (§7.11), the reconciled Git-host
observation that owns the post-publication merged fact, and settles the remote
host boundary (§13.2): the existing four-method `WorkflowHost` stays the host
assembly contract with a Cloudflare implementation beside the Deno one, the
runner-to-owner transport is a closed versioned envelope that refuses rather
than adapts, and an ownership table says which side owns each concern.

Finding 2 — every restatement of the run-ID derivation now uses `canonical
GitHub authority` byte for byte; the undefined Issue-provider spelling is gone.

Finding 3 — architecture.md and the factory specification now state the merged
and abandoned paths as separate ordered step lists, and the terminal record's
two shapes differ exactly as the paths do, so neither can be read as requiring
the other's effects.

Inventories, evidence tiers and the acceptance checklist are reconciled with all
of it. Documentation only; the ownership SVG is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Apply rulings A-K from the Architect's answers on 80884f9.

A. Keep the four-method `WorkflowHost` and add that its transition and request
types — `WorkflowExecutionTransitions`, `WorkflowBeginRequest`,
`WorkflowExecutionBegun`, `WorkflowForkRequest`, `WorkflowForkSelection`,
`WorkflowRunCreation` — are provider-neutral and become package-root public
types, with runtime-named entrypoints keeping implementations and retained
encodings. Recorded as authorized future export work, not performed here.

B. `Evidence.Run` becomes an ordered fail-fast pipeline binding the executed
prefix: `{ completion, authoredCommands, executed, runTimeout? }`, rows carrying
`limit` for whichever of the two host-owned ceilings fired, a `runTimeout`
record for a whole-run expiry between commands rather than a fabricated argv
row, timeouts as ordinary unsuccessful outcomes, and fixed precedence —
cancellation over everything, first infrastructure failure authoritative with a
later teardown failure as secondary evidence, teardown authoritative alone —
with bounded diagnostic evidence retained on a failed effect's Error.

C. A comment provider must support a stable opaque correlation marker or refuse
before its first mutation. The authored logical body is preserved byte for byte
and the correlation representation rides outside it. Absence is now judged
against attempt state: no marker after an attempted-but-uncommitted creation is
permanent ambiguity, not proven absence.

D. `PullRequest.Merged` splits still-open (temporary unavailability) from
closed-unmerged (conflict), and the factory gains §10.4 — a bounded
host-configured retry, then a durable machine wait with its own protocol record,
woken by intake or an explicit resume and carrying no verdict.

E. A configured total bijection between the nine stages and nine exact Project
status option IDs, validated against a complete reread and refusing before any
intake, token, run or projection.

F. `Git.Merge`'s `purpose` is validated authorization against the
provider-authenticated merge ceiling, not write-only provenance.

G. The `RunnerRequest` envelope is deleted. The runner and owner ship as one
release identity checked by build fingerprint at admission; the messages are
private, while the owner-side authority invariants stay public and exact.

H. `FactoryTerminal` keeps journal event references, with cross-path, kind,
completeness and agreement validation spelled out.

I. The factory protocol schemas stay in the factory specification §11.2; the
other four documents link rather than duplicate.

J/K. Mixed wrapping left alone, new prose unwrapped; the tenth construct is
reflected everywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
taras#710)

Ruling C asked for the comment contracts, the architecture, the WGE evidence and
the generic external-effect explanation to agree. The first three landed in
0d08dcc; §10.2 did not.

Split the two kinds of reconciled effect where the difference actually lives.
An effect that mutates a subject already there — Push, a numbered pull-request
update, ready, close, issue close, Project status — reads that subject, and a
complete observation is decisive whether or not the effect has attempted
anything. An effect that creates a new object the host names has nothing
pre-existing to read and no client-supplied idempotency key, so its completion
is visible only through a correlation value it wrote itself, and absence means a
different thing before and after a mutation has been attempted.

Only the second kind carries attempt state, and it narrows the decision in one
place: unattempted with nothing found performs once, while attempted with no
committed completion and nothing found is permanent ambiguity rather than
absence. A provider that cannot write, preserve and completely query such a
value refuses the effect from observation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Four narrow corrections from the Architect's verdict on b364b98.

1. The merged-observation wait is not a typed-answer suspension. A machine wait
is a second durable wait kind inside the existing lifecycle: it asks nobody
anything and ends because a later execution observed a provider again, so it
has no response schema, no `xmd workflow answer` route, no form and no bound
value, and it publishes no `suspension_request` and consumes no
`suspension_answer`. It shares only the atomic boundary — its `machine_wait`
event and the `suspended` status commit together, and the acquisition is
released after that commit. Its identity is a `waitId`, never a suspension ID;
`MergedObservationWait.suspensionId` becomes `waitId`, while
`ConflictSuspension.suspensionId` and the typed-answer protocol are untouched.
A new `MergedObservationWake` names the same `waitId` and a closed `source`,
with `intakeId` required exactly for a provider intake and absent exactly for
an operator resume. Waking is permission to look again: the intake retains a
bounded notification carrying no answer, verdict, stage, transition or
observation result, a later executor consumes it and appends the wake event in
one transaction, and a resume without a pending wake or operator authority
reports the same wait and settles suspended again.

2. The generic create-effect rule now distinguishes its two safe mechanisms. An
effect with a provider-native client key — an Issue upsert, a pull-request
upsert — reconciles on that key and carries no attempt state; creating an
object is not by itself what makes an effect attempt-stateful. A comment has
neither a native key nor a pre-existing subject, which is why it needs the
marker and the unattempted/attempted distinction. An effect with neither
mechanism refuses before its first mutation, as an intentional constraint.

3. Completed replay may reach and read the run's durable owner — lifecycle
storage access, not external-effect replay — while attaching no external-effect
provider, performing no effect again and starting no native operation. The
"no remote storage session" wording is gone from every document.

4. The authored comment body is preserved byte for byte as the authored portion
of the projection, with transport metadata outside it, rather than the provider
payload being claimed to equal the authored bytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
)

The first slice of F1: the seams a remote host needs, and the identity it is
addressed by. No Cloudflare code yet — this is the boundary work that has to be
true before a second host can exist.

`WorkflowExecutionTransitions`, `WorkflowBeginRequest`, `WorkflowExecutionBegun`,
`WorkflowForkRequest`, `WorkflowForkSelection` and `WorkflowRunCreation` now
export from the package root, which is where they mean what they mean. They were
already defined in the provider-neutral lifecycle module but published only
through `@executablemd/workflow/deno`, so the shared CLI imported its own return
type from an adapter and a second host would have had to load that adapter to
name it. The Deno entrypoint keeps its re-exports for source compatibility with
a comment saying what belongs behind it — implementations and retained
encodings, not the shape of a request — and the CLI and its tests now import
from the root.

`deriveFactoryRunId()` implements the settled derivation: lowercase unpadded
RFC 4648 Base32 over all 32 SHA-256 bytes of `github-issue-v1`, NUL, the
canonical GitHub authority, NUL, the exact issue node id. The authority folds
case and keeps a non-default port; a scheme, user information, path, query,
fragment, whitespace, malformed host or port, and a written-out default port
each refuse by name rather than being repaired, because two spellings that both
became one authority would be two runs quietly becoming one. The node id is held
to non-empty and NUL-free and otherwise passes through byte for byte.

Three tests. The identity suite checks fixed vectors computed outside this
implementation, the RFC 4648 §10 encoding vectors, that the NUL separators make
(authority, node id) unambiguous, and every refusal. A host-neutrality suite
walks the shared modules and fails on a host-owned specifier or a runtime
detection outside a runtime-named entrypoint, so a Cloudflare import cannot
leak into shared code unnoticed. A host-boundary suite pins `WorkflowHost` to
exactly four methods and proves it is satisfiable from root-exported types
alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
…aras#698)

`DLC13` in packages/workflow/tests/workspace-effect.test.ts scans
`packages/workflow/mod.ts` and `packages/workflow/src/**` (minus the Deno
adapter) and refuses a set of names that includes `GitHub`. Its reason is that
the shared external-effect boundary exists so any Git host can be adapted to it,
and "the first adapter naming itself in a shared contract is how a neutral
surface quietly becomes one provider's."

8f27b04 put `deriveFactoryRunId()` under `packages/workflow/src/factory/` and
exported it from the package root, which broke that test — correctly. The
derivation names GitHub in its scheme tag, its authority rule and its node id,
because the software factory is a GitHub product by definition.

Move it to `packages/cli/src/factory-run-id.ts`, beside the CLI's existing
`github-issues-config.ts` where naming GitHub is already legitimate, and take it
back out of `mod.ts`. The module and its tests are unchanged otherwise.

The host-neutrality scan added in 8f27b04 no longer exempts `cloudflare.ts` or
`src/cloudflare`: neither exists yet, and a scan that exempts a path nothing
occupies is a claim about a boundary nobody drew.

Where the derivation lives permanently is not settled by this commit. The
Durable Object owner needs it too, and F1 cannot place it until DLC13's scanned
set is reconciled with a Cloudflare adapter subtree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Applies the Architect's answers to Q1 and Q2 in .vscode/698/698-a-1.md.

Q1. `DLC13` now excludes `packages/workflow/src/cloudflare/**` and
`packages/workflow/src/software-factory/**`, each with its own reason. The two
implementation subtrees are runtime-owned — scanning an adapter for the
vocabulary of the runtime it adapts is a category error, and Code Rule 12 puts
host behavior behind exactly those names. The software factory is the other kind
of exception: not a runtime adapter, still held to the host-import and
runtime-detection rules by `host-neutrality.test.ts`, and allowed only the
product vocabulary, because §1.1 of the factory specification makes GitHub the
subject matter of that contract rather than one provider capturing a neutral
boundary. `packages/workflow/mod.ts` and every shared module stay scanned, and
no forbidden word gained an exception.

Beside the existing Deno assertion, `found` is now checked to contain no
`/src/cloudflare/` and no `/src/software-factory/` path, so an exclusion that
matched nothing or matched too little cannot pass quietly.

`host-neutrality.test.ts` gains `cloudflare.ts` and `src/cloudflare` in
`RUNTIME_OWNED` and asserts the Cloudflare subtree is absent from what it scans.
`software-factory.ts` is deliberately not runtime-owned there: it uses the
cross-runtime Web primitives and is checked like any shared module, which the
new assertion that `src/software-factory/run-id.ts` is among the scanned modules
now proves.

Q2. The derivation moves from `packages/cli/src/factory-run-id.ts` to
`packages/workflow/src/software-factory/run-id.ts`, published as
`@executablemd/workflow/software-factory` from both manifests. One
implementation, reachable by the provider host and by F2's GitHub intake without
either carrying a hash that has to agree byte for byte with the other's. The
package root does not re-export it — that neutrality is the point — and no
forwarding copy is left in the CLI. Its tests move to the workflow package and
import the published subpath, so the export map is what they exercise.

Q3 needs no change yet; it governs §8 and §9, which are unstarted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Plan §13. `@cloudflare/vitest-plugin@1.1.3` and `vitest@4.1.11` as root
development dependencies, which is what brings workerd in — plan §12 requires
the real Durable Object namespace, real SQLite storage and real WebSocket
admission, and an in-process fake is not evidence for any of them.

Both lock layouts are updated in the documented order: the pnpm add, then
`deno install --frozen=false`, then `deno task setup`. `pnpm-lock.yaml` gains
1231 lines and deletes none; `deno.lock` gains the npm graph for the same
packages.

Two things worth knowing for anyone reproducing this.

The lockfile is `lockfileVersion: '9.0'` and `package.json` declares
`pnpm@9.15.0`, but the pnpm on PATH here is 7.5.0, which cannot read a v9
lockfile — it warns "Ignoring broken lockfile" and rewrites it as
`lockfileVersion: 5.4`. That downgrade happened once and was reverted; this
commit was produced with the declared version through `corepack pnpm`. Anything
that shells out to a bare `pnpm`, `deno task setup` included, needs 9.15.0 ahead
of 7.5.0 on PATH or it will silently downgrade the lockfile again.

pnpm 9 also re-sorts `package.json` dependency keys, which is why `effection`,
`mdast-util-to-string` and `zod` move. That is the package manager's own
canonical ordering; hand-restoring it would only be undone by the next install.

`deno task setup` still exits 1 at its last step, `build:web`, with
`Module not found "file:///…/bundle"` from
`deno bundle --packages=bundle`. That is not this change: the identical argv
succeeds when run directly and under a preflight-shaped parent, it fails only
through `@effectionx/process`'s `exec()`, `packages/web/generated/` has never
existed in this checkout, and the browser bundle's module graph does not reach
vitest or workerd. `deno task check`, `deno task lint` and the focused suites
all pass on the new dependency state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
`@executablemd/workflow/software-factory` now publishes only
`deriveFactoryRunId`, `admitFactoryRunSubject`, `FactoryRunSubject`,
`FactoryRunSubjectFailure` and `FactoryRunSubjectError`. The scheme tag, the
Base32 alphabet, the authority rule, the preimage layout, the encoder and the
length constant are implementation: a caller that could reach them could also
reimplement the hash, and two implementations of an identity that must agree
byte for byte is the failure §1.1 exists to prevent.

`factoryRunIdPreimage()` and `base32Unpadded()` stay visible to their own module
so the encoding tests can prove an internal algorithm, and are absent from the
entrypoint. Everything the tests assert about public behavior — the fixed
vectors, the 52-character shape, case folding, the non-default port, and every
authority and node-id refusal — now goes through `admitFactoryRunSubject()` and
`deriveFactoryRunId()`.

The `as BufferSource` assertion is gone. `factoryRunIdPreimage()` returns an
`ArrayBuffer` rather than a view, which is what `crypto.subtle.digest()` accepts
with no assertion at the call site, so Code Rule 6 holds without one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
…aras#698)

Plan §12's harness, and the first thing it was built to answer.

`vitest.config.ts` at the repository root runs `*.vitest.ts` under
`@cloudflare/vitest-plugin` against real workerd, with a wrangler config
declaring `StorageProbeObject` as a `new_sqlite_classes` Durable Object. The
suffix keeps these files out of `deno task test`, `pnpm test:node` and
`bun run test:bun`: discovery matches `*.test.ts`, and
`scripts/tests/test-file-discovery.test.ts` still agrees with it, so nothing is
stranded and no runtime exclusion was needed. `pnpm test:cloudflare` and
`deno task test:cloudflare` run it.

Two things had to be settled to get it running. The plugin binds to `vitest`
through peer dependencies, and pnpm was resolving a second `vitest@4.1.11` copy
with a different peer set, so the pool was configured but never took over and
every test reported "failed to find the current suite" — adding the plugin's
declared `@vitest/runner` and `@vitest/snapshot` peers at the root collapses
that. The config lives at the repository root rather than in the package,
because a package-level config resolves `vitest` from the package's own
isolated `node_modules` and multiplies the copies again.

`StorageProbeObject` asks the runtime what it accepts rather than assuming.
Recorded on workerd 1.20260831.1:

- `PRAGMA application_id` and `PRAGMA user_version` — refused, read and write,
  with `not authorized: SQLITE_AUTH`.
- `sqlite_schema` introspection, ordinary DDL, and a plain metadata table — all
  fine.
- One outer `ctx.storage.transactionSync()` — fine.
- A reentrant `transactionSync`, and a direct `SAVEPOINT` — refused: the runtime
  requires its own transaction API instead of SQL transaction statements.
- DOFS schema initialization and DOFS filesystem writes outside a transaction —
  fine.
- A DOFS filesystem write *inside* one `transactionSync` — refused, because
  `writeFileSync` opens a `transactionSync` of its own and that nests.

The last two contradict plan §4, which has the owner reuse the pragma-based
recognition the Deno host uses and commit Workspace mutations inside one
`ctx.storage.transactionSync()`. Neither is possible as written. The probe is
committed so the finding is reproducible rather than reported.

An earlier version of the probe called the asynchronous `WorkspaceFilesystem`
wrapper and never awaited it, which reported success for work that had failed.
It now uses the same synchronous primitives the Deno provider imports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
`@cloudflare/vitest-plugin` binds to `vitest` through peer dependencies, and the
pool only takes over when the plugin and the CLI hold the same instance. pnpm
was producing two: `acpx` depends on `tsx@4.23.1` exactly, the root range
`^4.19.0` resolved to `4.21.0`, and `vite` peers on `tsx`, so `vitest@4.1.11`
existed twice under different peer contexts. The plugin bound to one and the
root link to the other, which is why the pool reported itself configured while
every test failed with "Vitest failed to find the current suite".

One `pnpm.overrides` entry pinning `tsx` collapses that. Both the root link and
the plugin now resolve the same `vitest`, and `pnpm test:cloudflare` passes.

Ordering note for anyone reproducing this: `deno install` prunes the links pnpm
placed — `scripts/deps.ts` says so in its own header — so `deno task setup`
leaves the pnpm-owned test tooling unlinked and `pnpm install` afterwards
restores it. Setup itself completes: exit 0, "ready", browser bundle generated,
lockfile still v9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
taras#698)

The storage correction from `.vscode/698/698-c.md`.

`packages/workflow/src/sqlite/workflow-schema.ts` now holds version 1 once: the
application identity, the logical version, every declared table and index, the
DDL, the expected inventory, the pre-release shapes, and
`declaredStructureFailure()`, which compares what a database declares against
what this build writes and answers with the disagreement instead of raising it.
Two adapters keeping their own copy of that DDL would be two schemas that happen
to look alike, and the first amendment either missed would be a run neither
could recognize. The module names no runtime, owns no connection, path,
transaction or lifecycle authority, and is published from no entrypoint.

`src/deno/schema.ts` loses 505 lines and keeps what is genuinely its own:
`DatabaseSync`, filesystem paths, SQLite error translation, and its existing
`PRAGMA application_id`/`PRAGMA user_version` carrier. It reports the shared
finding as its own failures, so the Deno host's behavior and its released
recognition are unchanged.

`src/cloudflare/marker.ts` is the other carrier. A Durable Object's SQLite
refuses both pragmas — `not authorized: SQLITE_AUTH`, on read as well as write —
so that adapter records the same two values in `_xmd_workflow_schema`, a
singleton row fixed at `id = 1` by a CHECK and a primary key so a second
identity row cannot exist. Same logical version, different physical carrier;
adapter-private recognition metadata, not a WorkflowRun record, journal value,
exported field or second schema. `src/cloudflare/storage.ts` reconciles the
runtime's concrete `SqlStorageValue` rows with the vendor's structural row type
in one place, and deliberately does not forward `transactionSync`.

`DLC13` excludes `src/sqlite/**` with its own recorded reason and asserts the
subtree is absent from what it scanned; `host-neutrality.test.ts` keeps scanning
it and now proves it is among the modules checked.

The exploratory probe moves out of production source into
`tests/cloudflare/support/`, and its console dump becomes three asserted tests
matching stable categories rather than platform wording: the pragmas are
refused as unauthorized; direct savepoints, reentrant transactions and a DOFS
filesystem write inside an owner transaction are all refused toward the storage
transaction API; and ordinary DDL, `sqlite_schema`, an outer transaction, DOFS
schema initialization and a strict metadata table are accepted.

`pnpm check:cloudflare` type-checks the Cloudflare production source and its
tests against `@cloudflare/workers-types`, which the root Deno check cannot do
because `cloudflare:` modules do not resolve there. Two `pnpm.overrides` pin
`tsx` and `@cloudflare/workers-types`: both are peers that were splintering
`vitest` and the plugin into several instances, and a single copy of each is
what makes the pool engage. `@cloudflare/workers-types` is pinned to
`5.20260831.1` rather than the newest, which Deno's minimum-dependency-age
policy blocks.

Ordering, unchanged from `670ad0c` and now deterministic in both directions:
`deno task setup` completes and leaves the workerd suite unable to run, and a
`pnpm install` afterwards restores it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The Cloudflare owner's storage paths, and the evidence that they hold.

`src/cloudflare/owner-transaction.ts` is the transaction every authoritative
commit runs inside. It enters `ctx.storage.transactionSync()` once and hands
DOFS a wrapper whose `transactionSync` runs its callback directly — inside the
real callback that is not a weaker promise, because the outer transaction is
already open, and it is the only way DOFS can participate at all: asked to
transact while it believes one is open, the vendor falls back to `SAVEPOINT`,
which the runtime refuses, and every DOFS filesystem primitive opens a
transaction on the way in. The enlistment is created for one callback, refuses
use outside it, and clears the DOFS resolve and blob caches on both sides so
nothing populated from rows a rollback discarded is read later. The vendored
snapshot is untouched and `deno task vendor:verify` still passes.

`src/cloudflare/recognition.ts` initializes pristine storage in one transaction
— schema, DOFS schema, run row, then the marker last, so the code says what the
marker means even though atomicity hides the ordering — and recognizes it again
through the same four conditions the Deno host distinguishes: foreign,
unsupported version, corrupt, or a version-1 run.

Fourteen assertions on real workerd, in two suites. Initialization writes the
marker and is recognized again; storage that already holds an object is refused
rather than written into; nothing at all, objects without a marker, another
application's identity, version 2, version 0, and a dropped table are each
refused as their own condition. A mixed commit publishes a DOFS filesystem
write and a WorkflowRun row together. A body that throws after changing both
leaves neither — the run row is unchanged, the file is absent, the object still
recognizes, and a later commit succeeds from the frontier the failure left.
That last test is the one the whole design turns on.

`pnpm check:cloudflare` type-checks all of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
`test-cloudflare` runs `pnpm check:cloudflare` and `pnpm test:cloudflare`, and
is in `green.needs`. It runs on every event and carries no condition, so `green`
requires success from it unconditionally.

The job owns evidence no other job can produce. A Durable Object's acquisition
lifetime, its eviction and its transaction atomicity are properties of that
runtime, and the `.vitest.ts` files proving them are invisible to the Deno, Node
and Bun corpora by design — so without this job the evidence would simply stop
running while everything else stayed green.

`ci-workflow.test.ts` already fails when a job is missing from the aggregate;
verified by removing the entry and watching "requires every other job and no
future job can be omitted" fail. Added a test naming this job's two commands as
well, so what it is *for* is legible rather than only that it is depended on.

`pnpm install` is the job's last preparation step: `deno install` prunes the
links pnpm placed, and the plugin only takes over the pool when it and the CLI
hold the same vitest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The executor plane: who may advance a run, and what they may say.

`release.ts` compares an exact build fingerprint. `admission.ts` holds verified
OIDC claims to a configured policy — issuer, audience, repository ID, owner ID,
event name, workflow ref, workflow SHA and the immutable workflow identity —
checking IDs rather than names, because a repository can be renamed and a check
on the name would admit whoever holds it today. It reads a closed claim set, so
a claim this contract does not name cannot be depended on, and it takes claims a
verifier already authenticated rather than a token: how a signature is checked
is not this module's business.

`acquisition.ts` makes the connection the acquisition. Hibernation is why it
cannot be a field — an evicted object has no memory of what it admitted — so
authority is the pair the runtime hands back: `getWebSockets()` says which
sockets are real and a bounded attachment says what one was admitted as. Copied
attachment bytes prove nothing, because the question is not whether a value
looks right but whether this socket is the one live socket holding an
acquisition. There is no lease, expiry, renewal, heartbeat, alarm or poll.

`commands.ts` reads the private transport strictly: unknown command, unknown
member, wrong kind, oversized message, too many chunks — each refuses whole,
nothing is partially adopted. The answer is a serialized record keyed on
`outcome` rather than an Effection `Result`, because an `Error` does not cross a
connection; Code Rule 13 governs in-process results and this is not one.

`owner.ts` is the Durable Object. Its admission order is the contract: build
before token, token before run, acquisition last, so a refusal at any step
leaves no acquisition and no state. A message proves its acquisition before it
is parsed. A closed connection releases ownership, rolls nothing back and
settles nothing — an executor that disappeared decided nothing.

`routing.ts` selects the owner arithmetically and admits the run ID first,
because `idFromName` answers for any string and a mistyped id would silently
address a fresh, empty owner.

Twenty-nine assertions on real workerd across three suites. A mismatched build
is refused while its claims are deliberately unusable, proving the order. Every
policy claim is refused one at a time. A second healthy executor is refused
rather than followed. A closed connection owns nothing and the next executor may
take it with no lease having expired. A stranger's socket is refused before its
command is read.

`./cloudflare` is an npm export only: JSR cannot typecheck a `cloudflare:`
entrypoint, and `deno task check:jsr` stays green because the Deno host is what
JSR serves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
`transact()` for a run whose storage is somewhere else, as `698-a-1.md` Q3
settles it.

A Durable Object commits synchronously and cannot hold a transaction open
across a network wait, so the obvious reading is unavailable. What makes it
tractable is that the body does not need the transaction open while it runs: it
needs the starting history, somewhere for its writes to go, and all of them
landing together. So the callback runs in a runner-owned scope against a
collector. The starting frontier is one bounded read that opens and closes its
own owner-side read. Appends go into a local buffer that `readAll()` reads back
after the starting prefix, so a body sees its own writes. Nothing is sent while
the body runs; when the body and everything it started have torn down, one
closed intent goes to the owner.

The callback is never serialized, interpreted or executed on the owner. That is
what makes arbitrary control flow safe — nothing tries to infer what the body
did, and only what it *enlisted* travels.

`src/remote/collector.ts` rather than `src/cloudflare/**`: this is the client
half and it runs on the runner, not in the Worker. It names no host, so the
ordinary Deno check and both boundary scans cover it.

Nine assertions with a deterministic fake link, because Cloudflare mechanics are
not the subject — what the owner does with an intent is proven on workerd, what
the client sends is proven here. One intent carries what was enlisted and the
frontier it was proposed against. `readAll()` gives read-your-writes. A body may
cross a suspension point with no owner transaction open. A body that throws
sends nothing and leaves the gate closed. An owner refusal is returned instead
of the body's value, which never crossed the connection. A nested transaction
and an ordinary same-handle operation inside a body each refuse. A handle used
after its body closed refuses. And an event mutated after it was appended
commits as it was handed over, because the collector cloned it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The runner half of the connection.

`src/remote/client.ts` holds one connection open for the calling scope and
correlates answers by the id they name rather than by arrival order — a socket
delivers what the owner sent whenever it sent it, and a client that assumed
order would attribute one command's refusal to another. Teardown fails every
request still waiting, because a caller blocked on an answer that can never
arrive would outlive the connection it asked through.

It decides nothing about the run. A refusal comes back as an answer rather than
a transport failure: what a command means is the owner's, and a client that
interpreted a refusal would be a second place deciding what a run may do.

Seven assertions against a socket a test drives by hand. The command reaches the
wire carrying its id; two answers returned in the opposite order to the asking
still reach the right callers; a refusal is handed back as an answer; an answer
that is not JSON and an answer naming nobody are both dropped without disturbing
the caller that was waiting; a connection that ends fails the request in flight
and refuses the next one; and a second request under an id already in flight is
refused rather than silently replacing it.

This is client code and it names no host, so it lives beside the collector in
`src/remote/` where the ordinary Deno check and both boundary scans cover it.

Not included, deliberately: `packages/cli/src/remote-workflow.ts`. The four-
method assembly composes client operations — a remote `WorkflowRunDatabase`,
lifecycle, delivery and inspection over this connection — and those do not exist
yet. A host whose four methods all raise would be the placeholder the plan says
not to add, so the assembly waits until there is something real to assemble.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Corrections 1 and 2 from `.vscode/698/698-c-2.md`. The first is a real hole I
opened, and the review is right about it: `admit()` took an
`AdmissionRequest.claims` and compared its values to the policy, so a caller
could assert every configured identity and obtain the acquisition without ever
holding a signed token. A comment saying a verifier had authenticated them was
not an authority boundary — `OwnerConfiguration` held no verifier, and the
claims arrived through the same request surface the owner distrusts.

`src/cloudflare/token.ts` verifies a compact JWS: one algorithm family by
allowlist rather than by reading `alg` and believing it, keys the deployment
configured, signature, then temporal validity — and only then is a payload
member read as a claim. The raw token, the key material, the header and the
claims the policy does not name stop there; none is retained, attached,
journaled or returned.

`admitClaims()` and `parseClaims()` are no longer exported. `admitToken()` is
the only way into that module, because an exported "check these claims" is
precisely the surface that made this forgeable. `AdmissionRequest` now carries
the raw token and has no member for a verified result, a claim set, an
acquisition identity or verification material — a request that could name any of
those would be a request choosing what it is allowed to be. Verification
material is closure state on the owner.

`admit()` is an Effection operation, since verification suspends; the test owner
drives it through one scope at the runtime callback boundary.

Correction 2: the acquisition correlation is minted on the owner after both
checks pass, from `crypto.getRandomValues`, and is no longer a caller argument.
It partitions acquisition-private staging and duplicate handling and is not a
bearer credential — the exact live socket is still what proves a message may
act.

Thirty-five assertions. Tokens are signed with an RSA key pair generated in the
test process, so these are real signatures rather than a stub that agreed. A
correctly signed token admits; an edited payload, a wrong key under a configured
key id, an unknown key id, `alg: "none"`, an absent token, a non-JWS, an expired
one and a not-yet-valid one each refuse before acquisition and before state. The
wrong-release test now presents an unusable token, so it still proves the build
is compared first. Two sequential acquisitions receive different correlations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
…ed (taras#698)

Corrections 3, 4 and 5 from `.vscode/698/698-c-2.md`. All three were real.

Correction 3. `transactRemotely()` checked `gate.open`, then suspended in
`frontier()`, and only took the gate afterwards — so two calls on one handle
could both pass the check and act from the same starting frontier. It also
released the gate when the body ended, before `commit()` returned, so later work
could run while which state won was still undecided. The gate is now taken
synchronously before the first suspension and released in one finalizer after
the commit answer, on success, refusal, body failure, transport failure and
cancellation alike. The transaction object still closes when the body does, so a
retained handle refuses while the handle-level gate is held.

Correction 4. `append()` cloned, but `readAll()` handed back references into the
collector's own arrays: a body could read an event and mutate what it received,
and the committed intent would differ from what `append()` admitted. Every
crossing is now a fresh copy — in, out, and into the intent — and the
collector's array never leaves. Events are admitted rather than assumed, with a
count bound and an aggregate serialized-byte bound; an invalid or oversized one
fails locally and sends nothing.

Correction 5. The connection dropped malformed answers and answers for unknown
ids, which meant a malformed reply to an in-flight commit left the caller
waiting forever while the owner may already have committed. All three of those
are evidence that the two sides disagree about which command completed, so the
channel now fails closed: an unreadable answer, an answer naming a request
nobody made, and a second answer to a request already settled each stop the
channel, close the socket and reject every waiter. Owner refusals remain typed
answers, and answers are bounded.

Nine assertions on the channel and fifteen on the transaction. Two of the
transaction ones are the concurrency cases the review asked for: a second
transaction refuses while the first is suspended in `frontier()`, and the handle
stays owned while a blocked `commit()` is undecided. Two more prove detachment
from both directions — a reader mutating what `readAll()` returned, and a caller
mutating what it appended.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
D1 item 1, and Ruling 1 from `.vscode/698/698-c-4.md`.

The owner has to parse a settle request with the shared
`parseDocumentExecutionCompletion()`, and could not: `storage/record.ts` imports
`canonicalize` from the `@executablemd/core` root, whose barrel reaches
`node:crypto`, `node:process` and `node:readline/promises`. None resolves in a
Workers typecheck, so a pure function was unreachable because of where it sat.

`canonicalize()` moves to `packages/core/src/canonicalize.ts`, importing only a
type. `canonicalFingerprint()` stays in the Node-capable module, importing the
pure function and `node:crypto`. Both keep their package-root exports and their
behavior, and `@executablemd/core/canonicalize` publishes the pure half.

Two more predicates blocked the same graph for the same reason, which the ruling
anticipates and authorizes handling the same way. `isComponentName` was
co-located with the registration machinery and `isCanonicalTarget` with the
document-target catalog and its Markdown parser; both are string arithmetic.
They move to `src/component-name.ts` and `src/document-target-spelling.ts`, with
`@executablemd/core/component-name` and `@executablemd/core/document-target`
selecting them. `storage/definition.ts` imports through those.

No copy of any of them exists — each original module re-exports the leaf, so
there is one implementation and the root surface is unchanged. This is not a
`portable` barrel: three narrowly named leaves, each holding what it is named
after.

Two adjustments fell out. `tsconfig.cloudflare.json` no longer sets
`exactOptionalPropertyTypes` or `noUncheckedIndexedAccess`: it was stricter than
the repository holds itself to, so it failed pre-existing shared code the Deno
check accepts, and a check that invents rules proves the wrong thing. And the
percent-decoder now states `ignoreBOM: false` — already its behavior everywhere,
and Cloudflare's own type declares both `TextDecoder` options required.

Evidence. `packages/core/tests/canonicalize.test.ts` proves root and subpath
answer identically, including for `__proto__`, and that the fingerprint still
composes over the same ordering. `settle-parser.vitest.ts` runs on real workerd,
which is the assertion that matters: it loads the shared parser where a Node
builtin is genuinely absent, so a test-only import could not have passed for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
D1 items 2 through 6 from `.vscode/698/698-c-4.md`.

**Token.** `verifyToken()` accepted a token with no `exp`, and a non-numeric
`exp` or `nbf`, because it checked those claims only when they happened to be
numbers — so omitting one was treated as satisfying it. All three of `exp`,
`iat` and `nbf` are now required finite integer NumericDates. The expiration
boundary is expired, per RFC 7519 §4.1.4's "before". An `iat` in the future
beyond tolerance is not-yet-valid rather than accepted. The header must say
`typ` is a JWT, and `kid` must name exactly one configured key: the old filter
fell back to an unkeyed candidate, so an unrecognized key id still got a
signature check against whatever else was configured. The token and its segments
are bounded before anything is decoded, and skew is bounded at both ends —
negative would reject tokens for being on time, and unbounded is
indistinguishable from not checking.

**Transaction ownership.** The module-global `let open` is gone. It was shared
by every Durable Object in an isolate, so one object's transaction would refuse
another's. The ruling suggested a `WeakSet` keyed by storage; this repository's
`local/no-module-scoped-registry` forbids that too, and for a related reason. So
the claim belongs to the object: `OwnerTransactions` is created and held by the
owner, its lifetime is the object's, and no other object can see it. Same
guarantee, no process-lifetime table.

**Settle.** `{ status: string }` becomes a closed request carrying
`completion: DocumentExecutionCompletion` and `expectedWorkspaceRootId`, with
the completion read by the shared `parseDocumentExecutionCompletion()` — a
failed parse becomes this transport's own `malformed-member` without carrying
the parser's message, which names members a request supplied.

**Answers.** `ask()` now takes the parser for its own success value and returns
`OwnerAnswer<T>`. `unknown` exists only at the JSON boundary; a value the
command's parser cannot read fails the channel closed like any other
disagreement about what completed, and a refusal is delivered without consulting
the parser at all. The generic stays inside the closure the request built, so
the reader settles an answer without asserting what it is.

**Removed claims.** `duplicate-conflict` is gone until the mechanism that
produces it exists. `perform()` stays abstract and the echo stays test-only.
Every checked-then-asserted `Record<string, unknown>` became `Object.entries`
narrowing, and the one double assertion in the tests became a helper that passes
`unknown` through. One documented assertion remains, in `storage.ts`, bridging
two type declarations of the same runtime rows.

Forty-eight workerd assertions and thirty portable ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Closing two gaps in D1 item 5 that I found auditing my own work against the
list, rather than leaving them for review.

The refusal was checked to be a string and nothing more, so an arbitrary remote
sentence became this side's public failure identity — and a refusal is something
a caller branches on. It is now held to the category shape the owner actually
produces, and the correlation id is bounded. Anything else is an answer this
build cannot read, which fails the channel closed like every other disagreement
about what completed.

Item 5 also asked for cancellation cleanup evidence and there was none. The test
asserts what is observable rather than what I first assumed: Effection halts a
cancelled task instead of raising into it, so the proof is that the scope
completes at all with a request in flight — a waiter nothing settled would hang
teardown — and that a late answer afterwards reaches nothing and raises nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The runner's socket is the executor acquisition, and D1 left it behind. The
resource installed listeners and never removed them, never closed the socket,
and never observed `error`, so a scope that ended — normally, by failure, or by
cancellation — left the owner looking at a healthy socket that no longer had
anybody on the other end. With no lease and no heartbeat by design, that made
the run unadvanceable by anybody, forever.

Teardown is now one idempotent operation with one owner. Scope exit,
cancellation, a remote close, a socket error, a protocol failure, a
command-specific parse failure and a failed send all reach it; it runs once,
removes the exact callbacks it installed, and closes the socket once. The
finalizer is registered before anything can suspend.

Answers are now read where they arrive rather than through a signal the reader
drained later. That ordering is the point: a close arriving in the same turn as
an unreadable answer used to reach teardown first and tell the caller `closed`
for something that was actually `malformed-answer`. What went wrong is decided
where it is observed.

The envelope is closed for real: each outcome declares its whole key set, so a
performed answer carrying a `refusal`, a refused one carrying a `value`, a
missing member and an unknown member are all refused. An outgoing correlation id
is held to the contract an incoming one is held to, and refusal text gets its
own small bound. The regular expression proves spelling, and the comment now
says so — narrowing to the declared union stays the adapter's job.

A retained Workspace root is a content identity, so every command that names one
parses it as 64 lowercase hexadecimal characters rather than as any non-empty
text.

The transaction machinery is no longer exported from the Cloudflare subpath. It
stays private to `src/cloudflare/**`, owned by the object.

The old teardown test asserted nothing: its fake retained the message callback,
so the connection could leak both listeners and the socket and still pass. The
fake now counts closes and live listeners, and the evidence is what those
counters say.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
An admitted connection could prove who it was and ask nothing. This gives it
the reads a runner actually needs — the committed frontier, one retained
Workspace root, and the content that root names — and the private mechanics
those reads depend on.

Reads are bounded and coherent. One frontier request returns the parsed run
record, the current canonical root and the last journal event that existed at
that moment; the journal itself comes back in anchored pages, so a journal
larger than one message is still one snapshot and later appends cannot enter an
earlier one. Each page carries its own predecessor, and the runner reassembles
them by checking rather than trusting: a page that skips, repeats, reorders or
ends in the wrong place closes the connection before one event reaches a
caller, because half a journal that looks whole is worse than none.

A root comes back as its canonical manifest, then one referenced piece at a
time. The owner proves the manifest is canonically encoded, that its identity
is the digest of its own bytes, that the retained references are exactly the
ones the entries name, and that every piece it sends is referenced by that root
and hashes to the digest it is asked for. The runner proves it all again on
arrival. The owner being honest is not evidence about the wire, and content
that is not what it is named must never become a materialization.

A runner that hears no answer cannot tell a lost question from a lost answer,
so it asks again. That is only safe if asking twice is asking once. Every
command is now decided once per acquisition and its decision retained beside
it. Two requests are the same request when their parsed commands are equal, so
member order does not make a retry into a new command and a changed value does:
reusing an id for something else is refused as a conflict rather than answered.
Reads whose answers are fixed by immutable state and an anchor the request
already carries are remembered as a decision to read again; the frontier is
kept whole, because it is the one read whose answer would otherwise move, and a
retry that returned a later frontier would hand back a snapshot nobody asked
for. The ledger never evicts while the acquisition lives — dropping an id would
make a retry look new, which for a mutation is the difference between doing
something once and twice — so a full ledger refuses and fails closed.

Content a runner offers is staged, and staging is not publication. It is
digest-checked, bounded per piece and in aggregate, stored detached, keyed by
the acquisition that offered it, and visible to no retained read. Adopting it
is a later checkpoint's transaction. Both private tables live in SQLite because
an evicted object remembers nothing and the attachment is 16 KiB of identity,
not somewhere to grow a ledger; a replacement acquisition discards its
predecessor's scratch before it accepts, and touches no retained history doing
it.

The rules a root is held to now live in one place instead of two. Both hosts
retain the same roots and must name them identically, so the manifest format,
its canonical encoding and the stored-row parsers moved beside the schema they
belong to, and the Deno host reads through the same implementation it always
described. The digest is arithmetic in the language: a shared module cannot
import a host's crypto, and the one place this is needed most is inside a
synchronous transaction, where there is nothing to await into.

`commit` and `settle` parse strictly and refuse. Applying them is D3 and D4,
and a placeholder that reported success would be the one answer a runner cannot
recover from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
A root the owner returns is where the run stands. The runner materializes it,
works inside it, and proposes against it. So a root whose content cannot all be
found is not a frontier that happens to be incomplete — it is not a frontier,
and saying otherwise is the one answer that cannot be taken back.

The reads checked the root manifest and the manifests its entries named, and
stopped. Everything past that — whether those manifests were still there, what
their bytes were, whether the blobs they named existed, whether the root
retained references to exactly those blobs — was checked only when a runner
later asked for that particular piece. A root missing half its content
therefore answered `frontier` and `root` performed, and failed afterwards, one
piece at a time, once the run had already been told where it stood.

Now the graph is walked before either answers. The manifests the entries name
must be exactly the manifests the root retains; each must exist, be bounded,
decode canonically, hash to its identity, and agree with its recorded size and
with every file naming it. The blobs those manifests name must be exactly the
blobs the root retains; each must exist, be bounded, hash to its identity, and
agree with its recorded size and with every chunk naming it. Both directions of
each reference set are checked: a missing row is content nothing is keeping
alive, an extra row is content no manifest accounts for, and neither describes
a root anybody should start from.

The bytes are read and dropped. What survives the walk is the proof, and a
content request still re-reads the single piece it sends — a validated root is
not permission to answer with all of it at once.

The schema already refuses to let content vanish from under a root that
references it, so the states worth reproducing are the ones that restriction
cannot prevent: a reference collected together with its content, bytes that no
longer hash to the identity they are stored under, a recorded size that
disagrees with what it describes, and a reference to content no manifest names.
Each of those now refuses before a root is returned, and says only that storage
is damaged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The owner holds the run and can run nothing. Git, an Agent and an evidence
command need real files, so a root has to become a directory somewhere the
runner owns, and whatever happens there has to come back as a root again.

The equality that matters is that those two operations compose to nothing: a
materialization nobody touched must capture to the exact root it came from. If
it did not, every Workspace operation that changed nothing would still propose
a new root, and the owner could not tell a real change from an artefact of how
the runner unpacked the tree. Directories, an empty file, a symbolic link, a
hardlink group, modes and modification times all have to survive for that to be
true, which is why the evidence is a real temporary filesystem rather than a
map that would only prove it kept what it was given.

Ordering, hardlink numbering, manifest encoding and chunk size now live beside
the format instead of inside whoever happened to walk the tree. The local host
walks SQLite rows and the runner walks a directory; those walks cannot be
shared and their meaning must not diverge, because a root identity is a digest
of the encoding and two encodings would be two names for one Workspace.

The materialization knows no runtime and no path. Native operations arrive
injected, adapted from the runtime's asynchronous primitives with `until`
where `@effectionx/fs` has no equivalent; the logical root stays `/`, and the
temporary directory the invocation happens to use reaches no manifest, event,
proposal or error. A run that recorded where it was unpacked could not be
resumed anywhere else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Materialization indexed a hardlink group's first path by the content digest of
its bytes. Two groups holding identical bytes legally share one DOFS manifest
and are still two inodes, so the second group linked to the first and recapture
saw one group of four names where the root said two of two. The Workspace that
came back was a different Workspace, arriving under the identity of the one
that was asked for. Group membership comes from the root's own `hardlink`
value now, which is the only thing that ever said what a group was.

Modes and times were left to whatever creation happened to produce. A creation
mode is narrowed by the process umask, so a root retaining a group-writable
file materialized without that bit; a symbolic link's own mode and time were
never restored at all, and `utimes` could not have done it without following
the link — which may point outside the tree deliberately. Permissions are now
set explicitly after creation, deepest-first so a mode that forbids writing is
not applied while children are still arriving, and a link's own metadata is set
through the operations that do not follow it, where the platform has them.

Where a platform has none, materialization refuses. Every entry is read back
and compared with what the root declared before anything executes against the
tree: a host that cannot represent a legal retained mode or time says so, once,
before native work begins. Quietly normalizing it would hand the run a
Workspace whose durable identity differs from the history it accepted, and the
run would have no way to notice.

The round trip now carries two hardlink groups with identical bytes, two
independent files with identical bytes that must stay independent, modes the
test's own umask would narrow, and a symbolic link with an old time of its own.
Reverting the grouping key alone fails it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The shared coordination boundary is scanned for host vocabulary, and the root
manifest module had been failing that scan since D2: its refusals named DOFS in
string literals, which the scan reads as code because they are. D2's evidence
list did not include the scan, so nothing said so until this checkpoint's wider
verification ran it.

The names were a symptom of the wrong seam. A Workspace root names a file's
content by one identity and says nothing about how those bytes are kept; how
they are kept is a separate format, and it was sitting inside the module that
describes roots. It now has its own, which decides only whether a sequence of
bytes is a canonically encoded manifest and produces the bytes one ought to be.

Its refusals describe the format rather than the store implementing it. That is
not a rename to satisfy a scan: every host keeps content this way, the vendored
layer is one implementation, and a neutral module naming that implementation
would be the Workspace surface learning where it happened to be stored. No
message this reworded is asserted anywhere, so nothing observable moved.

The obvious home — beside the schema that declares the content tables — is
closed. A neutral module importing from there names the storage engine in its
own import specifier, which is the same crossing by a different route.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
`commit` carried a proposed root identity and nothing that could justify it. An
identity with no manifest and no content closure is a name, and an owner
adopting one would be taking the runner's word for what a root contains. The
command now carries the whole thing — what the runner started from, what it
proposes, the canonical manifest that identity is the digest of, the exact
content that manifest closes over, the retained mappings the same operation
produced, and the filtered events to append — and the owner recomputes all of
it before anything is written.

The frontier is re-read inside the transaction rather than before it, and
compared with what the runner said it started from, root and terminal event
both, `null` included exactly. A frontier read outside the transaction is a
frontier that can move before the write.

The inventory has to be exactly the closure of the proposed manifest: every
manifest its file entries name, every blob those manifests name, once each, and
nothing else. A missing piece is a root that cannot be materialized; an extra
one is content the root does not account for. Each piece resolves from content
already authoritative or from bytes this exact acquisition staged — staging
supplies bytes and grants nothing, so another acquisition's scratch is
unreachable and a digest already retained under different bytes is a
disagreement rather than an overwrite.

Everything lands together or not at all: content, the immutable root and its
exact references, the mappings, the current pointer moved by compare-and-set
from the expected root, the journal rows, and the retry decision. Journal rows
name the root this commit selected — the proposed one when there is a
publication, the unchanged one when there is not — which is what makes history
readable against the Workspace it happened in. A journal-only transaction and
an empty one are both ordinary, and neither invents a Workspace change to look
uniform.

Retained mappings go through the parsers the local host holds its own rows to,
and creation identity is immutable: a second proposal naming the same
Repository must describe the same Repository. An Agent-session mapping carries
the canonical assertion and the derived key and nothing of the conversation;
the owner never contacts a provider.

The evidence runs on real workerd because none of it is provable otherwise. A
forced failure after every category has been written rolls all of them back,
the retry decision with them, leaving the id free. A lost response retried
across eviction publishes once. A moved root, a moved anchor, an identity that
is not its manifest's digest, an inventory missing or inventing a piece,
unstaged content, a mapping that would rewrite an established identity, and a
foreign socket each change nothing at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
minkimcello and others added 17 commits September 3, 2026 19:18
Three ways the owner could publish something nobody proposed.

A journal record was admitted for being a non-empty string, and the database
proved only that it was JSON. `{}` could therefore be retained atomically and
reported performed, and a later read — which parses events properly — would
find history it cannot understand. The run would become unreplayable at the
moment it was told it had committed. Every record is now parsed with the
authoritative parser and serialized again, and must be the same bytes that
arrived, terminating newline included. A nearly-right record is refused rather
than normalized, because retaining a corrected one would retain something the
runner never proposed.

A repository mapping carried only its journal-safe record, so the fingerprint
was written where the locator belongs and the run could not reattach to the
repository it claimed to have retained. The mapping now carries the admitted
locator beside the record, and the fingerprint must follow from it. Existing
rows are compared on every field that establishes creation identity rather than
a convenient subset — a partial comparison reported performed for proposals
that disagreed with what an earlier execution established.

A Repository or Worktree row names a checkout, and the row is only true if the
Workspace this commit selects contains it. A mapping-only commit could retain a
claim about a directory nothing put there, and the next execution would find the
claim and not the files. New checkout mappings must now accompany the
publication that creates them, a Worktree must name a Repository that exists or
arrives with it, and one proposal naming one mapping twice is refused.

And the starting root was taken from the pointer without being proved. A
journal-only commit could append history against a current root whose content
graph cannot be materialized — accepting a frontier nothing can restore. The
same validator the reads use now proves it inside the commit transaction, and
authoritative content is confirmed against its companion metadata rather than
trusted and then preserved by an `ON CONFLICT DO NOTHING`. A proposal is not a
licence to repair damage.

Two things surfaced while proving it. Refusals that are answers about the
request — a moved frontier, a duplicate id, a mapping conflict, a command this
release does not implement — no longer close the connection; only a broken
channel or damaged store does, because closing on an ordinary disagreement made
the runner reconnect to be told the same thing. And the workerd tests that
walked several object round trips were using sockets made inside the object,
which do not survive it being reset; they now use real accepted connections,
which is what the claims were always about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
…#698)

Three ways the owner could retain something it had not actually agreed to.

A repository locator was admitted for having a fingerprint that matched itself.
That says the two values agree with each other and nothing about whether the
locator is one this system would ever hand to Git — so an authenticated
proposal could retain a credential-bearing URL, an executable transport form, a
query carrying a token, or a relative path, each with a perfectly consistent
fingerprint. The closed allowlist the local host applies before Git sees a
locator is now shared and applied by the owner before the fingerprint is
compared at all. It is an allowlist rather than a search for bad shapes because
Git's locator grammar reaches well past URLs, and a second copy of an allowlist
is the copy that ends up longer.

A Worktree's parent was looked for while its own row was being written, so
`[repository, worktree]` committed and `[worktree, repository]` did not — the
same transaction, refused for the order its mappings happened to arrive in.
Identity, duplication, parent relationships and checkout placement are
properties of the proposal rather than of one mapping, so they are all settled
against the whole collection before anything is written, and application then
runs parents before children. Which one a proposal lists first is the owner's
problem, not something a runner should have to arrange to suit a schema.

And a blob's metadata row with no bytes beside it was treated as absent rather
than as damaged, so staging would supply the missing bytes and a proposal would
quietly complete a half-written identity. Which durable state won then depended
on the write path instead of on what the store actually holds. A partial
identity is now storage corruption, refused before staging is consulted.

The evidence is on real workerd: five locator forms that must never be retained,
a positive round trip proving the row holds the repository rather than a digest
of it, a Repository and its Worktree accepted in both orders with the same
result, and a metadata row without its bytes refusing without repairing
anything or recording a decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The owner could decide a proposal and nothing could make one. `CommitIntent`
carried the frontier and the events; the link refused every commit by
construction; and the trees a mutation happens in were owned by a test helper.
That is the half of D3a that makes the other half reachable.

A transaction now carries what it decided: the expected frontier, the events,
optionally one complete Workspace publication, and the retained mappings the
same operation produced. Enlisting is a private capability handed to the body
rather than something reachable from the database, so work that never received
it cannot publish a Workspace by accident, and a second enlistment is refused —
two Workspaces proposed for one commit is a choice nobody may make on the run's
behalf. What is enlisted is detached on the way in: the caller still holds its
arrays and may go on editing them, and the proposal the identity was computed
over must not change underneath it.

The Cloudflare link stages the pieces the owner does not already hold, encodes
one closed command, and reads the decision. Content is addressed by what it is,
so a Workspace the owner never lost is not sent again. The command identity is
minted once per intent and reused verbatim if the answer is lost — the owner
recognizes a retry by that identity, and regenerating one would ask a second
question rather than the same question again. A lost answer returns undecided
rather than failed, because whether the owner committed is exactly what cannot
be known from the runner.

Two trees, and the difference is the point. The materialization is the accepted
root: what the owner last confirmed, restored so tools can work in it. The
attempt is where a mutation happens, and it is disposable by construction. A
documented failure throws the attempt away and leaves the accepted tree exactly
as the owner confirmed it; working directly in the accepted tree would mean a
failed effect had already changed the only local copy of the run's Workspace.
Both are resources, so a raised failure, a cancellation and a refused commit all
leave nothing behind, and only a performed owner answer promotes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The production-path test carried a dead helper, two unused bindings, two
redundant scopes and a chain of assertions that told the compiler what the
request was rather than checking it. What is being proved is the shape of what
the runner sent, so it is read through narrowing helpers that fail with the
member that disagreed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
…#698)

The local host walks the content tables inside its own SQLite file; the runner
walks a real directory. Neither walk can be shared, and a root identity is a
digest of what the walk produced — so if the two ever disagreed, a run would
change its Workspace by moving between hosts and every no-op remote effect
would propose a root the local host had never seen. Until now the round trip
proved only that the runner agreed with itself.

The fixture is now the local provider's own capture. A real run database is
opened, the discriminating tree is written through the authoritative Workspace
transaction, and `capture({ publish: true })` retains the root exactly as a run
does. That root and the content rows it closes over are served over the remote
read boundary, materialized by the production runner adapter, and captured
again by the runner's implementation. The two identities have to be one string.

It found the disagreement it was written to find. The retained format carries
modification times in milliseconds, because the retaining host's clock is
`Date.now`; the runner adapter reported seconds. Every retained tree would have
looked like a different one, and writing a millisecond value through an
interface that speaks seconds would have put each file tens of thousands of
years from now, where the filesystem cannot keep it. The adapter now preserves
the unit the format uses and converts only where the host primitive requires it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
…#698)

Completing the discriminating regressions `698-e-6.md` asked for surfaced two
defects in what had just been built.

The command identity was counted rather than derived. The owner recognizes a
retry by that identity and returns the decision it already made — so an
identity that changed between attempts would ask a second question, and the
owner would apply the proposal twice. It is now a digest of the exact bytes
being sent: two attempts at one proposal share an identity, two different
proposals cannot, and a retry is byte-equivalent. That retry necessarily
happens on a new connection, because the one that lost the answer is gone and a
connection refuses to reuse a correlation id of its own; the identity is stable
across connections, which is where it has to be.

And the body ran through `call()`, which let a resource whose teardown failed
surface its failure after the commit had already gone out. The comment above it
claimed the opposite. The body now runs in a scope closed before the intent is
built, so "no commit was sent" and "the body did not finish" really are one
statement — the one ordering here that cannot be taken back.

The rest of the list is now covered: a journal-only commit proposing nothing and
staging nothing, all three retained mapping kinds encoded with the locator only
where one belongs, a refused answer and a lost answer each promoting nothing and
leaving no attempt tree, and a transaction past its local bound sending nothing.
The materialization suite uses the production trees resource rather than a
helper of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Three ways a committed run could fail to converge.

A mutation's decision lived in the acquisition-scoped ledger, and a replacement
acquisition discards its predecessor's scratch. That is right for staged bytes
and read decisions, which belong to the connection that produced them, and
wrong for the fact that a mutation was applied — which is the one thing the next
connection needs to tell "this already happened" from "this never happened". So
the exact case the retry exists for was the case it could not answer: if the
commit landed, the same request met a moved frontier and was refused as stale;
if it did not land, the same request performed; and the runner could not tell
those histories apart, so it could not know whether to promote or discard.
Mutation decisions are now keyed by the run, cleanup never touches them, and a
reused identity with different bytes is still a conflict. A decision is not a
lease and does not expire because a socket did.

A performed answer was read for syntax and thrown away. The adapter accepted an
answer naming any root — including the root the run was already at while the
proposal published a different one — and returned success. The answer is now
checked against what was asked: the root the proposal selected, and one event
identity for each event sent. An owner agreeing to something else is not an
owner this runner can go on talking to.

And promotion took no evidence and moved no bytes. Any holder could call it
before a commit, after a refusal or after an ambiguous loss, and it relabelled
the accepted tree with the new root while the promoted tree stayed unreachable —
so the invocation would read the Workspace it used to be at under the name of
the one it is now at. Promotion now requires the owner's performed decision, and
that decision has to name the root this attempt actually captured. The attempt's
own tree becomes the accepted materialization, the accepted path answers with
the promoted bytes, and the tree the run used to be at is removed.

Sealing also reached further than it looked. A shallow copy left an Agent
session's provider assertion shared with the caller, and the content bytes were
fetched through a callback at staging time rather than captured at enlistment —
so a caller could change the retained session identity, or the bytes staged
under a digest, after the transaction had sealed. Mappings are now copied all
the way down and the bytes travel with the intent, checked against the identity
they were proposed under before any stage request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
…aras#698)

Promotion took a value shaped like the owner's decision, and any caller could
write one down. Comparing it against the attempt's own capture caught a wrong
root and nothing else: a fabricated decision naming the right root promoted
just as well as the real thing. The accepted materialization also carried a
public `replace()`, so anything that could create an attempt could move the run
to a Workspace the owner had never selected. And the one operation that did
receive the authoritative answer — the transaction itself — discarded it, so
completing an owner-authorized promotion meant leaving the supported path and
repeating the protocol by hand.

The authority is no longer data. An attempt offers a proposal, and that
proposal carries a way back to the attempt that nothing else can construct;
the transaction sends it, validates the answer against what it sent, and then
transfers the tree itself, before it reports success. A caller may still build
an enlistment by hand — it simply carries no attempt, so there is nothing to
transfer, which is the right outcome rather than a missing step.

`promote()` and `replace()` are gone from every surface a caller holds. What is
left on the materialization is where a path is and which root it is; what is
left on an attempt is where it is, what it captured, and how to offer it. The
capability that moves the accepted tree is a symbol only this module can write,
so a value that did not come from `useMaterialization()` cannot be mistaken for
one.

The evidence follows the production path end to end: mutate an attempt, enlist
its capture, return from the body, and find that by the time the transaction
reported success the accepted path had become the attempt's tree and read the
attempted bytes, while the old tree was gone. The supported surfaces are
enumerated and shown to expose no replacement or promotion, and a value naming
the exact proposed root is handed to the only thing that takes an enlistment
and moves nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The capability moved again instead of going away. `propose()` handed back an
object with a callable `transfer`, so the same fabricated decision that used to
promote through `attempt.promote(decision)` now promoted through
`proposal.transfer(decision)` — before any commit, comparing only a
caller-supplied root against a caller-held capture. The regression that was
supposed to catch this built a *different* enlistment without that member, so
it never touched the bypass at all.

Two more things followed from taking a capture as an argument. The transfer
moved the live directory while the identity came from whatever capture was
passed, so a body could enlist, keep writing, and have the owner commit one
root while the runner labelled different bytes with it. And a hand-built
publication with no attempt behind it committed happily: the owner advanced and
the invocation stayed on its old tree.

So `enlist` takes the attempt. Not a proposal, not a capture — the attempt
itself. The transaction seals it after the body and everything it started have
finished, which means the proposal is the tree as it finally is rather than as
it was at some earlier moment; the same seal produces the transfer, and the
transaction calls it after validating the owner's answer. A publication that no
live attempt owns is now unrepresentable rather than merely refused.

What a caller holds is a place to work and a way to read what it did. Sealing
and transfer hang off a symbol in a module no entrypoint reaches, so they are
not on the declared surface and cannot be written down by anything that does
not already have them.

The evidence follows: enlist an attempt, keep writing to it, and the committed
root, the transferred tree and the recaptured accepted root are one root and
one set of bytes — the final ones. The supported surfaces are enumerated and
carry no promote, transfer, replace or accept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The interface a run's storage answers is the same wherever the run lives. This
is that handle, backed by a connection: snapshots that stay snapshots, a
journal that reads fresh and appends through the one commit path, a caller-owned
transaction, retrieval replacement, and an anchored execution list.

Two mechanisms keep one handle in order because they answer different
questions. A turn stops two operations interleaving, so unrelated work waits and
then proceeds. A scope-local marker records that this scope is *inside* a
transaction on this handle, so a nested transaction or an ordinary operation
called from the body is refused at once rather than queued behind a turn its own
caller is holding and will not release. A queue alone deadlocks that; a flag
alone mistakes unrelated work for nested work.

Retrieval replacement is its own mutation, not a degenerate commit. It appends
nothing and publishes nothing, and its revision is the owner's arithmetic over
what is stored — two handles that both read revision one before either wrote
would otherwise both write two and the second would lose the first. Clearing
removes the row, so the next replacement starts counting again. The identity is
minted per invocation rather than derived from the request, because two calls
carrying identical metadata are two replacements and must not collapse into one;
and because a decision now names its command kind, one textual id used for a
commit and for a replacement is two requests rather than a recognized retry.

Executions read as one anchored snapshot for the same reason the journal does: a
caller assembling a list across several requests must see one moment rather than
whatever the table held at each of them. The first page fixes the terminal row
and every later page is held to it, with the run identity and the sequence
travelling privately so the runner can refuse another run's page and prove
adjacency — neither becomes part of the record it returns.

Failures cross as the categories the local host reports for the same condition.
Command names, refusal spellings, rows and cursors stay below the adapter; they
describe a protocol nobody above it is party to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
…ras#698)

The runner had no way to learn which snapshot it was reading. It sent the first
page with no anchor, and the owner treated that as a claim the run had no
executions — true only for an empty run, and storage damage for every other,
so a run with any history could not read its own list at all.

The owner chooses the anchor now. A first request carries none, the owner fixes
the terminal row at that moment and answers with it, and every later page is
held to what the first one chose. An empty run answers with an explicit empty
anchor, which is a different answer from a terminal page and is read as one.

The evidence is the case that exposed it: 129 executions, a first page that
returns 128 rows and the anchor it selected, another execution begun while the
read is in flight, and a final page that ends exactly at the anchor without it.

Also proves the retrieval mutation on real storage: revisions counted by the
owner, byte-identical metadata under two identities counted as two
replacements, clearing removing the row so the next one starts again, one
invocation retried across a lost answer and eviction applying once, the same
identity with different content refused, and a replacement against a root the
run has left refused without touching the row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
…aras#698)

Three ways the remote database could accept or report something it should not.

An execution page was believed on very little. The run identity was required to
be present and never compared, so another run's retained history passed. Rows
had only to increase, so a gap, a first row that was not the first, or a row
past the anchor all passed — and a page could call itself terminal while
stopping short of the anchor, which turned an incomplete history into a
complete-looking answer. There is a state machine now: the first page fixes the
snapshot, every later page continues from the exact cursor it was asked for,
rows begin at the next sequence and stay adjacent and within the anchor, an
empty snapshot is terminal and carries nothing, and terminal means exactly at
the anchor. A record must be one of its two legal shapes, so a stop status
without a stop, or a member the shape does not declare, is refused rather than
ignored. The page's own byte bound is enforced on both sides, including a
single row too large to page at all.

The public boundary leaked in both directions. Members that return `Result`
raised when the link raised; a body that failed propagated instead of returning
the failed `Result` the same condition returns from the local host; and the
publication link's own errors reached callers as themselves, so a private
refusal class and its spelling crossed a provider-neutral interface. Each is
translated once now, at the adapter, and the storage distinctions are kept: a
store belonging to something else is not damage, a version this build does not
implement is not damage, and a record it cannot read is not an unreachable
owner. Those are different facts and a host acts on them differently.

Retrieval accepted any well-formed answer. A performed reply naming different
metadata would have installed where the definition is fetched from — so the
answer is now required to be canonically the value that was asked for, and a
contradiction fails closed with the snapshot untouched. Input is parsed by the
rules a stored value is held to and bounded before anything is sent, because a
value this build will not keep is not a request to make.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
…read

The owner validated each execution row and then sent the physical row anyway,
so the composed owner-to-client path could not return a single execution. Both
halves passed against a hand-built counterpart, which is exactly how the
disagreement survived; the new workerd regression composes the real pages
through the real client and fails against the previous commit.

The page bound is now one function both ends call, measuring the exact `rows`
member as it crosses. The unsupported-version refusal carries the version the
owner actually read instead of a placeholder. A retrieval answer describing
another replacement is settled where the answer arrives, closing the channel.
The message bound is enforced against the complete serialized request,
correlation id included, before it is registered or sent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
A parser failure was discarded in favour of the channel's, so a contradictory
retrieval answer reached the public boundary as an unreachable owner. The
request whose answer could not be read now keeps that failure and every other
waiter still learns the channel ended, which lets the adapter report a
malformed record. A commit answer this build cannot read is undecided for the
same reason a lost connection is.

An oversized request is classified once, at the adapter boundary, so the
database returns a request failure rather than a transaction failure; the
duplicate mapping that could never run is gone.

Schema versions now have one domain, taken from the carrier they live in and
used at marker recognition, refusal encoding, refusal parsing and the public
error. Version zero is a partial initialization and so damage, and a value the
carrier could never hold is damaged retained data — neither is a version this
build is behind.

The composed execution regression now writes its later row between the two
page requests of one read, and both ends are held to the shared page bound.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
The Deno coordinator opens a transaction, hands a mutation the authoritative
filesystem and the retained metadata, and commits both together. This is that
shape with the storage somewhere else: the Workspace is a real directory
materialized from the exact admitted root, the metadata is a detached snapshot
of that same admitted state, and the commit is one intent the owner performs
atomically or not at all.

Root, journal anchor and every retained mapping are admitted in one owner-side
read, because they are one state. It is complete rather than paged: the mapping
tables are insert-only, so a cursor over sorted names cannot be made safe by
root and journal equality — a later insert can sort before it and never be
seen. A count and byte ceiling refuses instead, and refuses whole.

The attempt is created outside the transaction, because the collector seals it
after the body tears down. Inside the exact callback the route proves it starts
from the admitted state before the document runs, and drift refuses there.
A documented Workspace failure journals against the unchanged root; everything
else raises and publishes nothing.

The Filesystem contract, the Repository/Worktree metadata shapes, the
journaled-failure base class and Agent-session reconciliation move to neutral
modules so both hosts implement one contract rather than two. The Deno adapter
keeps its rows, its savepoint and its synchronous-SQLite exception.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Lexical admission compared the spelling of a path with the attempt root and
then handed it to a syscall that follows symbolic links. An in-tree link read
and overwrote a file outside the attempt, and `..notes.md` was refused for
beginning with two dots. Every operation now resolves before it acts, on the
rule `packages/runtime/host-files.ts` states: only a complete `..` segment
leaves, the existing prefix is walked so a path that does not exist yet is
still judged, an operation about a link does not follow it, and both ends of a
rename or hardlink are admitted at the time of use. A link's target is a
Workspace path, so an absolute one names the Workspace root rather than the
machine's — resolution is done here rather than by the kernel.

The runtime, database, owner link, journal and provenance were supplied
separately and could be recombined: pair run B's handle with run A's link and
journal, and if both began at the same root and anchor, one effect journalled
in A and published its Workspace in B. `useRemoteRun()` now constructs them
together and hands back one opaque binding, so there is nothing to recombine
and nothing structural to forge.

Wiring the production coordinator to a real Durable Object found a third
defect the scripted owner could not: a sealed proposal listed its content
manifests before its blobs, while the owner requires one sequence ordered by
kind and digest. Two owners agreeing about a proposal is what that test is for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
`useRemoteRun()` took the database/commit link and the Workspace read link as
two unrelated members. A caller could pass B's link and A's reads and get a
legitimate, module-created binding: the invocation would be admitted from A's
retained mappings and content, and if both owners began at the same root and
anchor the start-anchor check passed and the result, the Files proposal and the
typed deltas committed to B.

There is one member now. `cloudflareRunLink()` builds its own read link from
the same connection and returns one value satisfying both halves, so the reads
an invocation is admitted from and the commits it publishes are the same
authority by construction rather than by a check. The runtime's read view is
derived from that object — `frontier` names two different reads on the two
contracts, and materialization wants the coherent one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP9dEis51dcwBnwdaXZQAg
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant