checkpoint
Two distinct uses, same tool:
- Gated barrier — buffer stdin to disk, release to stdout only after every gate in
wait_for_gates[]reportspredicate_met: true. Pair withgateto hold a downstream branch until an upstream phase finishes. - Drain barrier — set
wait_for_gates: [](or omit it). Buffers everything to disk, then releases as one burst the moment upstream EOFs. Useful as a barrier-then-replay primitive on its own — nogatestage needed.
Lives in tools/checkpoint/ in the combycode/dpe monorepo (Rust). Tool name: checkpoint.
Three phases, strictly sequential:
- Ingestion — read stdin line-by-line, append verbatim to
<spool_dir>/<name>/buf.ndjson. No stdout output during this phase. - Wait — after stdin EOF, poll every
<poll_ms>milliseconds until every gate listed inwait_for_gatesshowspredicate_met: true. - Release — stream the spool file to stdout, then delete the spool.
This makes the downstream pipeline see envelopes only after the barrier is crossed.
Input / output
Section titled “Input / output”- Input: any NDJSON stream.
- Output: same NDJSON stream, verbatim, after release.
Settings
Section titled “Settings”hold: tool: checkpoint settings: name: wait-for-src # used for spool subdir wait_for_gates: [src-done] # list of gate names gates_dir: "$session/gates" # optional; default = DPE_SESSION/gates spool_dir: "$temp/checkpoint" # optional; default = DPE_TEMP/checkpoint poll_ms: 100 input: upstreamwait_for_gates— all gates must showpredicate_met: truefor release. An empty list (or the field omitted entirely) releases the spool immediately after upstream EOF — see Drain barrier below.poll_ms— how often to re-read each gate file. Low = snappier release but more FS reads; 100 ms is a good default. Ignored whenwait_for_gatesis empty.
Spool files
Section titled “Spool files”<spool_dir>/<name>/buf.ndjson — append-only, crash-safe. After release, deleted.
For crash recovery during the wait phase: the spool persists. On a restart, resume would need explicit invocation (not yet implemented). For MVP, a killed run loses the spool → re-run from scratch.
Examples
Section titled “Examples”Basic barrier
Section titled “Basic barrier”stages: producer: tool: ingest-x input: $input gate: tool: gate settings: { name: produce-done, expect_count: 5000 } input: producer hold: tool: checkpoint settings: name: wait wait_for_gates: [produce-done] input: gate consumer: tool: analyze-y input: holdanalyze-y sees zero envelopes until ingest-x has produced 5000. Then it receives all 5000 at once (streamed from disk) and runs normally from there.
Wait on multiple gates
Section titled “Wait on multiple gates”hold: tool: checkpoint settings: name: double-barrier wait_for_gates: [rates-ready, registry-ready] input: mergedRelease happens only once BOTH gates report done. Useful when two independent prepare-stages must finish before a joint step.
Drain barrier (no gates)
Section titled “Drain barrier (no gates)”drain-and-flow: tool: checkpoint settings: name: drain # spool subdir; required even without gates # wait_for_gates: [] # absent or empty — drain mode input: upstream-chainWith no gates configured, the wait phase returns immediately after stdin EOF. The pipeline therefore behaves as:
- Ingest every envelope from
upstream-chainto disk while it streams. - The moment upstream EOFs (its whole chain has finished), flush the spool to stdout in a single burst.
- Downstream of
drain-and-flowruns only AFTER all upstream work is done.
Use cases:
- Writes-before-read coordination — a
spreadfans the same stream into awrite-file-streamsink AND a downstream reader; the reader is wired through a drain checkpoint. Reader starts only after every write has flushed, no race. - Sort-before-emit — pipe through a stage that sorts in memory once it sees EOF; the checkpoint guarantees the sorter receives the complete stream before emitting.
- Sequencing dependent phases without gates — when the only signal you need is “phase 1 finished”, no need to author a
gatestage just to hold phase 2.
Cost: every envelope round-trips through <spool_dir>/<name>/buf.ndjson. Memory bounded by disk, not RAM. Latency ≈ full upstream completion time. The spool is deleted after release.
If you need multiple drain checkpoints in one variant, give each a distinct name (the spool path is <spool_dir>/<name>/).
Behavior notes
Section titled “Behavior notes”- No output during ingestion. Don’t expect live progress; the stream is all-or-nothing.
- Release is a single pass. After release, the spool is deleted; re-running would re-ingest from upstream.
- Polling cost. Each poll is one fs read per gate file. With 100 ms poll and 10 gates, that’s 100 small reads/sec — trivial.
- Backpressure applies. While the checkpoint is in the “wait” phase, its stdin is EOF’d already (upstream finished). While it’s ingesting, normal pipe backpressure applies to upstream.
- DPE_TEMP convention. Spool lives in
$temp/checkpoint/<name>/by default.temp/persists across runs but individual spools clean themselves up on successful release.
Exit codes
Section titled “Exit codes”0— ingestion + wait + release all complete.2— invalid settings.3— fatal IO during ingestion (can’t create spool dir / file).