Relationships
#3257 Datastore rework Phase 3: workflow run and vault config repositories stage typed changes on v3 (no v2 behaviour change)
Opened by stack72 · 10/11/2026· Shipped 10/12/2026
Background (read this first)
swamp is reworking its datastore layer (tracking: swamp-club#2865). In the target design, v3, every change is an explicit commit in commits.db, made through a commit-log unit of work from typed changes: an intent that says what the operation is, plus its content. Phase 3 builds this dormant: nothing reaches it until a user opts a new, empty datastore into v3.
What's on main (897b64a0), documented in design/enablers/datastores.md:
The shared typed staging route (swamp-club#3250,
src/infrastructure/persistence/unit_of_work_scope.ts):signalChange(markDirty, change, { typed: true }). It keeps today's path change, which is skipped only when a typed stager is ambient.stageTyped(markDirty, build). Called after the write, it runsbuildonly under a commit-log unit bound to the hook, and stages the result as one batch, which lands in one commit. On v2 it returns after one lookup and never runsbuild. On a v3 hook with no bound unit, it throwsUnscopedWriteRefusedError.typedStagingActive(markDirty). It gates any read a typed change needs before the write destroys it, for example the digest of a file being replaced, so v2 reads nothing extra.- The model.
unified_data_repository.tsandyaml_output_repository.tsalready stage this way, throughModelChanges(src/domain/datastore_v3/repository_changes.ts). Follow that pattern.
The domain model (swamp-club#3233,
src/domain/datastore_v3/):partitionOf(),createIntent,intentPartition, the ops andMAX_COMMIT_OPS(10,000 changes per partition per batch). Intents include:run.create,run.updateandrun.delete, plusrun-snapshot.saveandrun-snapshot.delete, all onrun--{workflowId}--{runId};definition.create,definition.editanddefinition.delete, plusdefinition.rename-toanddefinition.rename-from, andevaluated-workflow.saveandevaluated-workflow.delete, onconfig--{model|workflow|vault}--{name};evaluated-definition.saveandevaluated-definition.delete, on the model partition;auto-definition.save, ondefinition--{id}.
A definition rename spans two partitions, committed in order: a
definition.rename-toput withexpect: "absent"in the new name's partition, then adefinition.rename-fromtombstone withexpect: "present"in the old one.The commit-log unit (swamp-club#3240, #3249, #3250):
- one commit per partition per batch, with run partitions last;
- retries keep the
txnId; - a post-commit chunk check re-stores missing chunks from the content source;
staged()is[]on v3, so readstagedOps().
The read model (swamp-club#3239): applies commits to a second directory and to
_catalog_v3.db. #3250's end-to-end test compares the files it rebuilds with what the repositories wrote. Reuse it:integration/datastore_v3_repository_staging_test.ts.The Phase 3 decision: on v3, repositories keep writing local files exactly as today and also stage typed changes.
This issue is one of two repository staging groups that build on #3250's route. The other is swamp-club#3256 (definition, workflow and evaluated).
Order of work (agreed while #3256 was in review):
- Workflow runs first, now. They overlap with nothing in #3256.
- Vault config last, once #3256 has merged. Rebase onto main, then implement vault config with #3256's definition helper (
src/infrastructure/persistence/config_staging.ts,definitionSaveBatches), widening itskindfrom"model" | "workflow"to include"vault". Write no rename or edit logic of your own: #3256 already does the managed-config partition, theexpect: { sha256 }on edits, the two-partition rename and the cleanups.
Put your tests in their own files, for example integration/datastore_v3_run_staging_test.ts and integration/datastore_v3_vault_staging_test.ts, not in #3250's or #3256's staging test files, so the branches don't conflict.
Goal
Make the workflow run and vault config repositories (yaml_workflow_run_repository.ts, yaml_vault_config_repository.ts) stage typed changes on v3 for every write they make. v2 doesn't change at all.
Design
Workflow runs.
- The run record:
run.create, thenrun.update,run.delete. - Run snapshots:
run-snapshot.saveandrun-snapshot.delete. - Both live in
run--{workflowId}--{runId}. - Run GC and supersede: per-key tombstones, split into batches when one partition would pass
MAX_COMMIT_OPS.
- The run record:
Run outcomes (decision 5 in "v3 domain model"): a run's unit commits its partitions when the run ends
completed,failed,suspendedorcancelled, and discards on an error or early stop. Check the run paths (execution_service.ts, the resume and cancel paths, serve's detached runs) against that, and test each outcome on v3.A run record must never name data whose partition was refused. This was a follow-up from #3240. The commit-log unit commits run partitions last, but it doesn't hold a run back when a data partition in the same flush was refused. Close it here:
- Option one: when a data partition the run's changes depend on is refused, the run record's batch for that flush isn't committed, or is committed with that step recorded as failed.
- Option two: the run record's typed change only names versions that landed.
Pick one, record why in
datastores.md, and test it: a refused data commit (for example anexpect-failedorversion-liverefusal) followed by the run record.Vault config (after #3256 merges).
- Vault configs are datastore-tier only under managed config.
- Stage them with #3256's
definitionSaveBatchesandkind: "vault"(config--vault--{name}). That givesdefinition.*intents,expect: { sha256 }on edits read throughtypedStagingActive, and a rename as two partitions, all from #3256's code. - Secrets are never committed; they go to the vault provider. Make sure no typed change carries secret material, and test that.
- Main now has swamp-club#3259, which creates a default
local_encryptionvault atrepo initand stops persisting an absolutebase_dir. Check that the config written at init stages correctly on v3, and that no absolute path lands in a commit.
Today's
bulkchanges in these repositories become explicit per-key typed changes. List them, and record the mapping indatastores.md.
The gate
v2 doesn't change:
- every v2 suite (usecase characterization, root-unit, dirty-coverage, peer-propagation, the unscoped-write guard, the routing test) passes with no expectation edits;
- a whole-suite route recorder shows identical route-1 and route-2 writes before and after (same caller, change kind and test). #3240 and #3250 did this, and their PRs describe the approach;
- the builders never run on v2.
v3 is right end to end. For every write path:
- run the repository on v3 in a commit-log root;
- commit to a real
commits.dband filesystemstorage/1; - catch up a read model into a second directory;
- assert that its files (bytes) and
_catalog_v3.dbrows equal what the repository wrote.
Cover namespaced and unnamespaced datastores. Add mutation or "red" checks for the rules you add, as #3250 did: drop a staged change and show a test fails.
Out of scope
- The definition, workflow and evaluated repositories (swamp-club#3256), other than widening its helper's
kindfor vault. - Making repositories stop writing files.
- The opt-in.
Done when
- These two repositories stage typed changes on v3, with the gate's evidence in the PR.
- The run-outcome rules are tested on v3.
- The "never name refused data" rule is closed and recorded.
- All v2 suites pass unchanged.
Blocked by: nothing for the workflow-run part (#3250 has merged). The vault-config part waits for swamp-club#3256 to merge.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.