Skip to main content
← Back to list
01Issue
FeatureShippedSwamp CLIPublic
Assigneesstack72

Relationships

#3053 Datastore rework Phase 2: root units can checkpoint, and the CLI token commands push mid-command through it (no behaviour change)

Opened by stack72 · 10/5/2026· Shipped 10/5/2026

Background (read this first)

swamp is reworking its datastore layer (tracking: swamp-club#2865). Every operation stages typed changes into a unit of work, and pushes happen when the unit ends. Phase 1 is complete. Phase 2 has shipped so far:

  • swamp-club#3025: use cases open units;
  • swamp-club#3032: a root unit per command or request pushes once when it ends, and use-case units nest inside it as children;
  • swamp-club#3033: CLI commands push through roots;
  • swamp-club#3034 and swamp-club#3035: serve handlers push through roots.

Phase 2 must change no behaviour.

On main (fcfe2326):

  • The root. runInRootUnitOfWork(repoContext, { flush, onFlushError }, async (root) => …) in src/infrastructure/persistence/repo_unit_of_work.ts opens a root over repoContext.markDirty itself.

    • fn gets a RootUnitOfWork: stage and staged only.
    • The root runs flush once when it ends, on commit or abandon.
    • A nested root given its own flush throws (the guard from #3032).
  • The CLI wrapper. runCommandInRootUnit (src/cli/command_root_unit.ts) wraps it for CLI commands: the push and pushWhen options, a release after the root ends, and an onCleanupError handler.

  • Three commands still push mid-command with a direct syncService.pushChanged({ namespace }), pinned in PINNED_CLI_PUSH_CALLS in integration/datastore_write_seams_rules_test.ts with the reason "a root cannot push mid-command":

    • src/cli/commands/access_token_mint.ts (around line 266);
    • src/cli/commands/worker_token_create.ts (around line 265);
    • src/cli/commands/worker_token_revoke.ts (around line 190).

    In each, the command:

    1. stages { kind: "bulk", reason: "<command>" } on the root;
    2. pushes directly, so the token is published before it is read back (revoked tokens before the lock push);
    3. carries on;
    4. lets the root's flush, the model-lock push (absent when no model lock is held), push again when the command ends.

    The mid-command push is not the root's flush. It is a separate raw push that bypasses the lock push.

  • The equivalence suites:

    • integration/usecase_sync_characterization_access_test.ts covers access token mint and worker token create and revoke through the CLI, with root-unit checks on;
    • the other characterization files, integration/usecase_sync_root_unit_harness_test.ts, integration/datastore_remote_failure_test.ts and the swamp-uat datastore suite.

Goal

Let a root push partway through an operation with a checkpoint, and move the three commands' mid-command pushes onto it, so that every CLI push goes through a root. This unblocks "one flush path". Marks and pushes stay identical: the same count, the same order relative to the stage, the read-back and the end-of-command push, and the same behaviour on failure.

Design

  1. checkpoint() on the root, for composition code.
    • Add an option to runInRootUnitOfWork: checkpoint?: () => Promise<void>, the mid-operation push the caller performs today.
    • Add checkpoint(): Promise<void> to RootUnitOfWork.
    • It waits for every mark still in flight in the root and its children, then awaits options.checkpoint(). The root stays open and still runs its flush when it ends.
    • Calling it when no checkpoint option was given throws a clear programming error.
    • A nested call that became a child has no checkpoint. Calling checkpoint() there throws too, so the outer root has to own it.
    • Errors propagate exactly as the direct call did: a failed checkpoint rejects inside fn, and the command handles it as before.
  2. Keep it off the domain port. checkpoint lives on RootUnitOfWork and the legacy adapter, not on UnitOfWork in src/domain/datastore/unit_of_work.ts. What a Phase 3 commit-log unit means by a partial commit is a Phase 3 decision. Say so in the JSDoc.
  3. Thread it through runCommandInRootUnit. Add checkpoint?: () => Promise<void>, passed through to the root.
  4. The three commands. Pass checkpoint: () => syncService.pushChanged({ namespace }), the same call, namespace and sync service as today, and replace the direct call with await root.checkpoint(). Leave the root.stage({ kind: "bulk", … }) before it, the read-back after it, and the end-of-command lock push as they are.
  5. Pins. The three commands' entries move out of the mid-command group of PINNED_CLI_PUSH_CALLS. Their push now lives inside the checkpoint lambda each command gives its root. If the scan still matches the lambda, re-label those entries under "the root's checkpoint", like the "root's push" group.
    • Add a pinned list of root checkpoint users: exactly these three.
    • Update the design doc's Phase 2 section to say a root can checkpoint.

Tests

  1. Unit tests in src/infrastructure/persistence/repo_unit_of_work_test.ts and src/cli/command_root_unit_test.ts:
    • checkpoint() waits for a mark in flight, including a child's, before it pushes;
    • it runs the checkpoint push once per call, and the root still flushes once at the end;
    • two checkpoints, then the end, push three times in order;
    • with no checkpoint option it throws;
    • from a nested child it throws;
    • a failing checkpoint rejects inside fn, the root then abandons, and the flush still runs, so an error thrown later doesn't mask it.
  2. Equivalence. The access characterization rows for access token mint and worker token create and revoke pass with no expectation changes. The marks, the mid-command push, the end push and their order are identical, with and without a model lock held.
  3. Failure path. For access token mint, a failing mid-command push gives the same error and exit code as before, and the end-of-command lock push and lock release behave as before. Record the expectation from the current code first, then convert.
  4. Everything else passes unchanged: every other characterization file, the root-unit harness, datastore_remote_failure_test.ts, src/cli, src/libswamp and src/serve.

Dependencies

Blocked by: nothing. swamp-club#3032 to #3035 are merged. Blocks: "one flush path" (Phase 2, not filed yet), which makes every push a root's flush or checkpoint.

Done when

  • Roots support checkpoint() through runInRootUnitOfWork and runCommandInRootUnit, with the JSDoc keeping it off the domain port.
  • The three commands push mid-command only through root.checkpoint().
  • PINNED_CLI_PUSH_CALLS no longer has a mid-command group.
  • The characterization suites pass unchanged, and verification passes.

Out of scope

  • "One flush path": the coordinator, deleting ModelLockResult.flush(), consolidating pushWhen.
  • Checkpoints in serve, where nothing needs one today.
  • Making the mid-command push go through the lock push.
  • The lockfile.
  • Tracking: swamp-club#2865 (Phase 2).
  • Built on: swamp-club#3032, #3033, #3034, #3035.
02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 10 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

10/5/2026, 11:25:46 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack7210/5/2026, 10:47:35 PM

Sign in to post a ripple.