✅ Prove Workspace effects survive a real host crash (#365 slice 7) - #419
Draft
taras wants to merge 2 commits into
Draft
✅ Prove Workspace effects survive a real host crash (#365 slice 7)#419taras wants to merge 2 commits into
taras wants to merge 2 commits into
Conversation
Every other atomic-Workspace proof ends its transaction inside the test process, where Effection tears the scope down. A crash does none of that: it leaves an open SQLite transaction with nobody to roll it back, and what the database holds afterwards is SQLite's recovery rather than anything the adapter runs. So this is made of real processes. One resumes a committed run, performs a real Workspace effect, and stops at the accepted construction-time routed-append hook with the mutation, the immutable root, the current-root pointer and the routed journal row all written and none of them committed. It says so on standard output, a second connection is shown seeing only the baseline, and then it is killed with SIGKILL. A different process, through a newly installed production provider, finds the baseline filesystem, root, retained counts and journal exactly. Two more processes commit a Workspace history and reconstruct it cold: the second observes the committed filesystem, current root, ordered events, event identities and event-to-root associations without performing a recorded effect again, then selects the older event's root through the adapter-private materializer and rebuilds its topology, bytes, modes, hardlinks and symbolic links from the DOFS content that root retains. The crash child assembles the adapter's own modules rather than calling `useWorkflowRunStorage`, because the routed-append hook is installed when the connection registry is constructed and the provider constructs its own. The inspector and both restart processes use the provider. No production behavior changes.
The boundary check read two files and searched them for eight names, which answered a smaller question than the one it was named after: whether any shared module of the coordination surface names a host at all. It now globs that surface — the workflow package outside its Deno adapter, and the durable-stream coordination modules — and refuses storage and runtime implementation types, the adapter's private connection, savepoint and transaction-token identities, runtime detection, process globals, and imports that reach an adapter, a vendored source or a host process. Two things make the sweep answerable. It reads code rather than the file: these modules explain in their own prose that they name no host, and a substring search finds the explanation. And it proves it can fail before it runs, on a crossing and on a comment that only looks like one, because a glob that matched nothing would report the cleanest boundary of all. The list of what it must have found is written down, so a module that stops being matched fails instead of quietly leaving the surface.
PR #419: ✅ Prove Workspace effects survive a real host crash (#365 slice 7)10 files, +1096 / -18 Scope🔴 PR has 1114 lines changed. Split into focused PRs. 🟡 1114 lines changed. PRs under 400 receive more thorough review. 🟡 Changes span 6 directories. Structural🟡 1 console statements. Slop✅ Slop indicators look low. Static AnalysisOxlint: 2 diagnostics across 2 files (1 rule) no-floating-promises (2): packages/workflow/tests/support/workspace-crash-child.ts, packages/workflow/tests/support/workspace-restart-child.ts CorrectnessNo extraneous code patterns detected. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Issue #365 claims that one Workspace expansion produces one effect and one
caller-owned SQLite transaction, and that host interruption publishes none of
it. Every proof of that claim so far ends its transaction inside the test
process, where Effection tears the scope down — a cancellation, a failure or a
closed scope, never a crash. A crash runs no cleanup at all: it leaves an open
SQLite transaction with nobody to roll it back, and what the database holds
afterwards is SQLite's recovery rather than anything the adapter executes.
This is #365 slice 7, the final proof layer. It adds no product behavior.
What changes
Before:
(WAC6, WAC13, WAC14). Restart was proven for the ordinary journal only
(WJ25). Historical restoration was proven inside one process (WRR4).
After:
SIGKILLduring an uncommitted effect, a cold restart of a committed Workspace, and a
cold reconstruction of an older event's root.
prose, and proves it can fail before it runs.
How it works
The crash timeline
baselineExecutions: 0)BEGIN IMMEDIATE; mutation savepoint writes/crash.txtthrough DOFScurrent_rootpublished, secret gate runs (gateCalls: 1)transaction.journalafterRoutedJournalAppendreports and suspends; the transaction never reachesCOMMITprocess.kill(pid, "SIGKILL");join()reportssignal: "SIGKILL", no exit codeThe child's readings at step 6 and the parent's at step 7 are of one database at
one moment, through the connection that made the writes and through a
connection that did not:
/crash.txtcurrent_rootReview guide
Start with:
packages/workflow/tests/workspace-crash-recovery.test.tsThen review:
tests/support/workspace-crash-child.ts— the killed process and the inspectortests/support/workspace-restart-child.ts— commit, cold read, cold restorationtests/workspace-effect.test.tsDLC13 — the boundary sweepscripts/runtime-test-exclusions.ts, specs,architecture.mdLook carefully at:
useWorkflowRunStorage. The routed-append hook is installed when theconnection registry is constructed, and the provider constructs its own; the
child needs both that hook and the authoritative connection to read
uncommitted rows from. The inspector and both restart processes use the
provider itself.
durableRun. A journal holding a Close is a run with nothing left toexecute, so the crash child's effect would never run at all.
empties, and a suspended Effection task is not on it.
What must stay true
slice-6 coordinator, checked across a process boundary by WAC19.
short-lived raw read-only
DatabaseSyncthat distinguishes committed fromuncommitted visibility.
structure, checked by DLC13 over the globbed surface.
How to verify it
SIGKILLbetween the routed append and the commitpublishes nothing, and fails if the transaction were committed before the
parent looked. Verified by mutation: moving the child's announcement to after
withWorkspaceEffectsreturns makesexpect(during.outside).toEqual(before)fail with 5 blob refs against 2, 3 blobs against 2, and a changed current
root.
root, ordered events, event identities and event-to-root associations, and
fails if a recorded effect ran again — each effect appends its name to a
marker file, and the first process's two lines must still be two.
lookup for
/tree/file.txthappens before restoration and is answered fromthe live frontier, so the successful read afterwards is cache invalidation
rather than a cache never consulted. The hardlink relationship is proven by
identity rather than by inspection:
resnapshotRoot === historicalcan onlyhold if the canonical manifest's hardlink group was reproduced. The manifests
the historical root references and the later root does not are asserted
non-empty, so restoration cannot have borrowed live content.
comment that only looks like one, and names the modules it must have matched
so a glob that matched nothing cannot report a clean boundary. It caught a
real collision while being written (
WorkflowRunTransactioncontainsRunTransaction).deno task verifyon this head: 12:42.91, all nine commands ok, tracked treeunchanged — vendor 6.6s, lint 5.4s, check 2.5s, test 762.2s, check:jsr 2.6s,
tsc 38.4s, test:node 436.3s, test:bun 627.8s, docs 35.9s. The new suite is
Deno-only and registered for Node and Bun in
scripts/runtime-test-exclusions.tsagainst this issue; both helpers stilltypecheck under
tsconfig.node.json.This PR is stacked, so its CI does not run the jobs that target
main. Thebattery above is the local evidence for that gap.
Scope
Included
architecture.md,specs/workflow-spec.md§9.6 andspecs/workflow-workspace-spec.md§10.1/§13, and the WAC19–WAC21 conformance rows
Intentionally unchanged
xmdworkflow start/resume;API.Filesand<File>integrationadds only the process-level composition they do not cover
New abstractions
tests/support/workspace-process.tsexists because the constants and thefilesystem reader are shared by two child processes and the suite that
launches them, and importing them from a child would run that child's
main()inside the test.Risks and limitations
for the reason above. If a future slice exposes the connection hooks through
a provider-level seam, the child should use it.
code()in DLC13 does not model regular-expression literals. No module onthe scanned surface contains one that could hide a forbidden name, and the
scanner's own failure cases are asserted.
Scope confirmation
Closes #365
Stacked on
agent/issue-365-6-atomic-workspace(#415).