Skip to content

🌳 Retain and restore immutable Workspace roots - #393

Merged
taras merged 3 commits into
mainfrom
agent/issue-365-3-retained-roots
Aug 8, 2026
Merged

🌳 Retain and restore immutable Workspace roots#393
taras merged 3 commits into
mainfrom
agent/issue-365-3-retained-roots

Conversation

@taras

@taras taras commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Depends on #391. This is stack layer 3 for #365 and replaces the retained-root/restoration portion of draft #387. Its base is agent/issue-365-2-connection-schema; it does not close #365.

Why

A WorkflowRun database already owns one authoritative SQLite/DOFS connection and the complete version-1 schema, but it only recognizes the canonical empty Workspace. A retained root must name the complete filesystem deterministically and remain sufficient to rebuild it after the live frontier changes.

What changes

Before:

Complete schema v1 stores the empty root and refuses any nonempty live Workspace.

After:

The Deno adapter captures arbitrary live DOFS frontiers as immutable, content-addressed Workspace roots, retains their exact manifest/blob reachability, validates arbitrary current and historical roots without repair, and privately restores a selected root through a nested savepoint.

Opening an existing run reads structure, retained content, the live/current comparison, and the run row through one SQLite snapshot. Retained-root integer reads opt into bigint conversion so corrupt 64-bit values reach redacting parsers instead of escaping through node:sqlite. Private Workspace transaction bodies finish child teardown before final live/current validation.

How it works

caller-owned WorkflowRun transaction
  → authoritative DOFS frontier traversal and validation
  → canonical root-format-1 JSON + domain-separated SHA-256
  → immutable root and exact retained references
  → optional current-root publication

selected retained root
  → nested savepoint
  → rebuild authoritative DOFS frontier
  → clear resolution/blob caches
  → resnapshot and require the selected identity

Files remain solely in DOFS blobs. Root manifests record paths, topology, kind, mode, observable mtime, file size and DOFS manifest identity, symlink targets, and canonical hardlink groups; they exclude mutable inode/revision/cache bookkeeping.

Review guide

Start with: packages/workflow/src/deno/workspace/manifest.ts

Then review:

  1. packages/workflow/src/deno/workspace/root.ts — live traversal, bigint-safe DOFS reads, content validation, retention, and read-only recognition
  2. packages/workflow/src/deno/workspace/restore.ts — savepoint materialization, cache coherence, and identity proof
  3. packages/workflow/src/deno/workspace/private.ts — adapter-private transaction boundary and child-teardown ordering
  4. packages/workflow/src/deno/provider.ts and reading.ts — single-snapshot recognition and redacted integer reads
  5. packages/workflow/tests/workspace-root*.test.ts — identity, retention, restoration, corruption, teardown, and rollback proof
  6. architecture.md and specs/workflow-spec.md — the present-tense storage contract

Look carefully at:

  • canonical path and hardlink invariants;
  • exact root-to-manifest/blob set equality;
  • read-only validation versus explicit manifest materialization;
  • one-snapshot recognition and bigint-safe corruption handling;
  • child teardown before final root validation;
  • cache clearing before and after topology replacement; and
  • restoration rollback when rebuilding the frontier fails.

What must stay true

  • Root identity is sha256("xmd-workspace-root\0v1\0" || canonical_manifest_utf8) — enforced by fixed-order encoding and checked by independent-state and reopen tests.
  • Historical file bytes are not copied into XMD tables — roots reference validated DOFS manifests/blobs, protected by existing restrictive foreign keys.
  • Recognition never repairs an existing database — arbitrary-root validation snapshots read-only and corruption tests compare database bytes before and after lookup.
  • Corrupt 64-bit SQLite integers are refused as redacted WorkflowDatabaseCorruptError values, never raw RangeError values containing stored data.
  • Restoration is all-or-nothing — it uses the caller-owned transaction's unique nested savepoint and resnapshots before release.
  • Final live/current validation runs after the supplied private Workspace scope and its cleanup finish.
  • DOFS garbage collection remains absent — no GC source is in the vendored production snapshot and a closure test rejects imports or calls.

How to verify it

  • Focused root/restoration/storage/journal suites — 14 suites / 90 steps pass.
  • deno task lint — passes with 0 errors.
  • deno task check — passes.
  • deno task test — 403 tests / 2,755 steps pass in 4m57s.
  • deno task check:jsr — passes with Success Dry run complete.
  • pnpm exec tsc --project tsconfig.node.json --noEmit — passes.
  • pnpm test:node under the repository's Node 22 toolchain — 2,543 tests pass with 0 failures.
  • deno task verify from the committed, prepared, clean worktree — all 9 applicable commands pass concurrently: vendor 2.1s, lint 2s, check 0.8s, test 446.8s, check:jsr 0.9s, tsc 18.3s, test:node 208.8s, test:bun 288.8s, docs 12.7s. The tracked tree remains unchanged.
  • git diff --check origin/agent/issue-365-2-connection-schema...HEAD — passes.

The earlier apparent 20-minute Deno timeout was caused by the restricted local sandbox denying the verifier's /bin/ps child-process cleanup. Running the unchanged battery in its required host environment completed and exposed two test portability errors; those were corrected before the successful final run. The deadline, concurrency, and clean/composability checks are unchanged.

Scope

Included

  • Canonical root-format-1 manifests and idempotent identities
  • Complete live DOFS traversal and content validation
  • Immutable root and exact manifest/blob reference retention
  • Arbitrary-root current-state recognition
  • Adapter-private capture and savepoint restoration
  • Authoritative resolution/blob cache invalidation
  • Consistent-snapshot opening and bigint-safe corruption reads
  • Child teardown before final root validation
  • Present-tense architecture and workflow-storage specifications

Intentionally unchanged

  • Public Workspace effects and filesystem APIs
  • Atomic effect/root/journal publication and transaction-route fencing
  • Public history/fork commands, <File>, or workflow start/resume
  • Worker Shell, Git/Repository effects, FUSE, subprocesses, workerd, GC, migrations, or schema version 2

New abstractions

  • The private root manifest/capture/restore modules separate immutable identity, live-frontier validation, and materialization so later transaction coordination and Add versioned workflow history checkpoints and forks #368 can consume the primitives without exposing a public history API.
  • The adapter-private read helper centralizes setReadBigInts(true) and consistent SQLite read transactions without crossing a shared production boundary.
  • Each new abstraction has multiple concrete uses or a clear justification.
  • No speculative functionality is included.

Risks and limitations

  • Capture and restoration are deliberately adapter-private. This layer does not coordinate live Workspace effects with durable event publication; that remains a dependent stack layer.
  • All roots are retained indefinitely and DOFS GC remains disabled. Root-aware deletion/GC is outside this foundation.
  • Recovery or rollback: any private restoration failure rolls back its nested savepoint, preserving the prior live frontier and current-root pointer.

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 test results.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

PR #393: 🌳 Retain and restore immutable Workspace roots

19 files, +2834 / -190

Scope

🔴 PR has 3024 lines changed. Split into focused PRs.

🟡 3024 lines changed. PRs under 400 receive more thorough review.

Structural

✅ No structural bloat detected.

Slop

  • packages/workflow/src/deno/workspace/filesystem.ts:96// deno-lint-ignore require-yield
  • packages/workflow/src/deno/workspace/filesystem.ts:109// deno-lint-ignore require-yield
  • packages/workflow/src/deno/workspace/private.ts:30// deno-lint-ignore require-yield
  • packages/workflow/src/deno/workspace/private.ts:35// deno-lint-ignore require-yield
  • packages/workflow/src/deno/workspace/private.ts:40// deno-lint-ignore require-yield

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras force-pushed the agent/issue-365-2-connection-schema branch from fe00594 to 02a7363 Compare August 8, 2026 20:51
@taras
taras marked this pull request as ready for review August 8, 2026 20:52
Base automatically changed from agent/issue-365-2-connection-schema to main August 8, 2026 21:01
@taras
taras force-pushed the agent/issue-365-3-retained-roots branch from 84998e3 to cbe78a1 Compare August 8, 2026 21:09
@taras

taras commented Aug 8, 2026

Copy link
Copy Markdown
Owner Author

System/software architecture review

Verdict: REQUEST CHANGES. The root model is coherent, but two correctness defects prevent this layer from satisfying its own durability contract.

The architecture direction is right:

  • root identity is provider-independent and excludes mutable DOFS bookkeeping;
  • capture retains canonical bytes plus exact manifest/blob reachability instead of copying file bytes;
  • recognition uses one explicit SQLite snapshot and does not materialize missing manifests;
  • restoration uses the authoritative wrapper, a nested savepoint, cache clearing, and a post-materialization identity proof;
  • hardlink topology, path ordering, retained-content foreign keys, and the absence of GC are represented at the right layer.

The following must be corrected before merge.

1. Clear authoritative DOFS caches when an outer Workspace transaction rolls back

transactWorkspaceRoots() can leave a negative path-cache entry describing uncommitted state after SQLite has rolled that state back.

The failing sequence is:

  1. A committed root contains /kept.txt.
  2. A private Workspace transaction removes it.
  3. The transaction reads /kept.txt and observes absence.
  4. That read stores a negative entry because the vendored Database.inTransaction flag sees only transactionSync(); it cannot see XMD's raw outer BEGIN IMMEDIATE.
  5. The body fails, is cancelled, or final live/current validation fails.
  6. SQLite restores /kept.txt, but the provider-owned wrapper survives with the negative cache entry and can continue reporting the restored file as absent.

Restoration itself clears caches, but ordinary private Workspace rollback does not. This is exactly the raw-outer-transaction hazard called out in the #391 architecture review.

Make rollback of any caller-owned transaction that may have reached DOFS clear both the resolution and blob caches on the authoritative wrapper. The cleanup must run for thrown failure, returned validation failure, cancellation/teardown, and commit failure; it cannot rely on code after a yield* resuming. Putting the behavior on the provider-owned connection/transaction cleanup is preferable to making individual filesystem effects remember it.

Add a discriminating test:

  • commit and read a file;
  • inside transactWorkspaceRoots, remove it and read it to populate the negative cache;
  • force outer rollback;
  • through the same provider-owned connection, prove the file is readable with its original bytes;
  • prove the current root and journal are unchanged.

Cover cancellation as well as ordinary failure if the common cleanup path is not already directly proven.

2. Validate every historical file entry's declared size against its DOFS manifest

rootFromManifest() validates that each referenced DOFS manifest exists and that its encoded chunks sum to the DOFS manifest row's size, but it ignores the size stored in the Workspace root's file entry.

The live/current comparison catches this only for the current root. A historical root can therefore be changed coherently at the XMD-table level—canonical manifest bytes, recomputed root ID, exact root reference rows—so that a file entry declares a different size from the referenced DOFS manifest. Lookup accepts that historical root, but restoreWorkspaceRoot() later rejects it in materializeNode(). Storage has then recognized a retained checkpoint that is not restorable, contrary to the architecture and the PR description.

When deriving/validating a root, require every file entry's size to equal readDofsManifest(...).size. This belongs in semantic recognition, not only restoration.

Add a read-only corruption test where:

  • the current root remains valid;
  • only a historical root's file size is changed;
  • its canonical bytes and root ID are recomputed and all XMD references are updated consistently;
  • lookup returns WorkflowDatabaseCorruptError;
  • the database bytes remain unchanged.

3. Add the new WRR conformance tier to the executable specification

The tests introduce Tier WRR and cases WRR1WRR9, but specs/executable-mdx-spec.md contains no WRR tier. The workflow specification describes the behavior, but the repository's conformance inventory does not name or map these tests.

Add a Tier WRR table covering canonical identity, mutation-derived roots, restoration, rollback/cache coherence, read-only corruption recognition, bigint redaction, child teardown, and no-GC closure. Include the two missing cases above in that mapping. This keeps specification, mechanics, and the executable test inventory moving together.

After these changes, rerun the focused root/restoration suites and the four repository gates. The currently green CI demonstrates the existing test set passes; it does not exercise the two counterexamples above.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 5 redundant comments. Inline suggestions to remove them below.

yield* until(filesystem.rm(path, options));
},

// deno-lint-ignore require-yield

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// deno-lint-ignore require-yield

yield* until(filesystem.symlink(target, path));
},

// deno-lint-ignore require-yield

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// deno-lint-ignore require-yield

const workspace: PrivateWorkspaceTransaction = {
filesystem: createDenoWorkspaceFilesystem(connection),

// deno-lint-ignore require-yield

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// deno-lint-ignore require-yield

return currentWorkspaceRoot(connection.database, connection.path);
},

// deno-lint-ignore require-yield

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// deno-lint-ignore require-yield

return captureWorkspaceRoot(connection, options);
},

// deno-lint-ignore require-yield

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// deno-lint-ignore require-yield

@taras
taras merged commit 5094aed into main Aug 8, 2026
11 checks passed
@taras
taras deleted the agent/issue-365-3-retained-roots branch August 8, 2026 21:56
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.

Commit Workspace mutations and journal results atomically

1 participant