ADR-003: Checkpoint is a durable job; diary writes are a direct call¶
Status¶
Accepted, amended by a dated note at the end of this record (2026-10-02: the submission methods were renamed)
Context¶
pi-palace, the reference implementation this project departs from, routes every write through its daemon's job queue
specifically to dodge a multi-process file-lock race between independent CLI invocations.
MemCastle already has one process, one writer, one daemon per palace —
that race doesn't exist here, so "goes through a job" can't be justified by lock-avoidance alone;
each write path has to earn a place in the queue on its own terms.
Two durable-write paths exist in Phase 1: checkpoint (persisting an already-classified batch of memory items, sometimes under time pressure right before a crash) and diary (an agent journaling one free-text entry).
Decision¶
JobKind::Checkpoint is a durable, scheduler-dispatched job because it needs properties only the job queue provides:
- Priority-based preemption:
AppServices::emergency_checkpointsubmits atPriority::Critical, jumping every other queued job (includingBackground-priority mining) — the entire point of an emergency checkpoint is to save state before a crash, which requires the scheduler's priority-ordered claim (job_status_idx ON job FIELDS status, priority, created_at) to mean something. The non-emergency path (AppServices::checkpoint) submits atPriority::High. Both route through one shared internal submission helper — one job kind, two priorities, not two job kinds. - Resumability: a checkpoint payload can carry multiple items;
checkpoint::runmirrorsmining::run's per-item cooperative pause/cancel, persisting{"next_index": n}after each item so a paused or crash-recovered checkpoint job resumes from where it left off, not from zero. - Deliberately no artificial per-item delay (unlike
jobs::demo'sSTEP_DELAY): synthetic latency here would work against the very feature — saving state before a crash — that motivates emergency checkpoints existing. Resumability is instead proven deterministically in tests by callingruntwice against a forced pause, rather than racing wall-clock time against a live scheduler.
AppServices::diary_write/diary_read are direct, synchronous calls — not job-queued —
because none of the above forces apply:
a diary entry is one small, atomic drawer write with no multi-item resumability need and no preemption requirement.
They still go through the exact same require_write/require_read MemoryMode policy as every other memory operation;
the sync-vs-job split is only about scheduler necessity, never about bypassing consistency or mode enforcement.
Alternatives rejected¶
- Routing diary writes through the job queue,
pi-palace-style, for uniformity with checkpoint/mining — rejected because the reasonpi-palaceneeds that (multi-process lock contention) doesn't exist in MemCastle's single-daemon/single-writer design; adding queue latency and job bookkeeping to one small write would be pure cost. - Making checkpoint a synchronous direct call like diary — rejected because it would give up priority preemption (no way for an emergency checkpoint to jump ahead of queued mining work) and crash-resumability (an interrupted checkpoint batch would have no persisted resume point).
Consequences¶
- Checkpoint jobs get every job-queue guarantee for free:
crash recovery (
Scheduler::recoverre-queues an interrupted checkpoint job on restart, attempt-budget permitting), retry viaJobEvent::Retry, and visibility throughlist_jobs/get_job. - An emergency checkpoint can and will delay already-queued
Background-priority mining behind it — the intended trade-off, not a bug. - Diary writes have no queue-provided retry/attempt-budget:
a failed diary write returns
Errstraight to the caller, which must retry itself if it wants to. Acceptable because a diary write is never decomposed into resumable units in the first place.
Amendment (2026-10-02)¶
The decision stands; only the method names changed.
AppServices::emergency_checkpoint and AppServices::checkpoint are now submit_emergency_checkpoint and
submit_checkpoint, matching the other submit_* methods that enqueue a job.
The operation name a refused session sees in Error::ModeForbidden is still emergency_checkpoint (ADR-007).