Repository navigation
Description
Summary
Provide a durable job profile for work that must outlive an HTTP request: enqueue, lease, execute, retry, compensate, schedule, cancel, and inspect with exact idempotency and uncertainty semantics.
Why this matters competitively
NTNT and orchestration systems expose jobs and workflows. Semaprax can differentiate through typed payloads, stable job/handler identities, explicit capabilities, consumed leases, checkpoint/evidence roots, and honest uncertain-delivery handling.
Current state at the audit baseline
- Agent runtimes already model durable checkpoints, uncertain effects, retries, and migration in bounded form.
- No general application job queue/scheduler profile is supported.
- Database and application profiles can provide persistence and execution hosts.
- Agent checkpoints must not be copied blindly into application jobs; shared primitives should be factored explicitly.
Audit baseline: ae25c6a49dc09ec4613c7a6f52a27daa69dcd3f3 (2026-09-11). Recheck main, this issue, and the owning specifications before changing code; later commits may have superseded individual details.
Required outcome
Define typed durable jobs with stable handler and payload schema identities, explicit enqueue/execution capabilities, bounded retry/backoff, leases, idempotency keys, scheduled time, cancellation, dead-letter outcomes, and replayable evidence.
In scope
- Job definition and payload schema.
- Enqueue and unique/idempotency semantics.
- Lease/claim/heartbeat/expiry.
- At-least-once baseline with explicit idempotent/transactional patterns.
- Retry/backoff and dead-letter state.
- Scheduled and recurring jobs.
- Cancellation and compensation hooks.
- Database-backed reference adapter and deterministic fixture.
Explicitly out of scope
- Claiming exactly-once external effects.
- Automatic retry of uncertain non-idempotent operations.
- Unbounded retries or schedules.
- Embedding arbitrary source code in job payloads.
- Letting a job receipt authorize publication or payment.
Starting points in the repository
- Agent checkpoint/migration/retry implementations
- future database profile
- std/agent policy helpers
- structured task model
- new application job package/runtime
Use rg to find the current owners of the named schema, diagnostic, operation, or test before editing. Do not assume a path is still authoritative merely because it is listed here.
Implementation sequence
- Specify job states and legal transitions: pending, scheduled, leased, running, succeeded, retryable failure, permanent failure, cancelled, uncertain, dead-lettered.
- Derive payload codec/schema from checked Semaprax types and bind every record to job definition ID, handler ID, schema version, enqueue revision, and deployment policy.
- Implement enqueue with explicit idempotency key semantics and transaction integration. Define whether duplicate enqueue returns existing job or a closed conflict.
- Implement leases as affine grants bound to worker, job generation, attempt, deadline, and handler revision. Expired/stale leases cannot complete jobs.
- Define retry policy as pure bounded data; classify failures and uncertain effects. Reserve attempt/cost budgets before execution.
- Implement scheduled/recurring jobs with explicit clock/time-zone semantics and catch-up policy.
- Reuse generic checkpoint/evidence primitives where semantics match, but create separate application-job schemas and authority boundaries.
- Build deterministic in-memory and database-backed adapters and test worker crash/recovery, concurrent claims, network loss, schema migration, and handler revision changes.
Failure and security cases that must be considered
- Lease expiry while a worker continues can produce concurrent execution.
- Idempotency keys scoped too broadly/narrowly can merge unrelated work or duplicate work.
- Cron/time-zone/DST semantics can skip or duplicate schedules.
- Retry after uncertain external effect can duplicate side effects.
- Payload schema changes can make queued jobs undecodable.
Required tests and evidence
- Concurrent enqueue and idempotency races.
- Concurrent lease, expiry, stale completion, heartbeat, worker crash, and recovery.
- Retryable/permanent/uncertain/cancelled failures and exact attempt ceilings.
- Scheduled jobs across clock jumps, DST, missed windows, and recurrence bounds.
- Payload/handler revision migration and refusal.
- Database transaction integration for enqueue plus application state change.
- No execution occurs without a current valid lease and required capabilities.
Acceptance criteria
- A reference service can enqueue and execute durable typed jobs.
- Delivery semantics and uncertainty are honest and documented.
- Retries, schedules, and leases are bounded and deterministic.
- Job evidence is replayable but grants no execution authority.
- Queued jobs remain migration-safe or fail closed across code changes.
Dependencies and sequencing
- [P1] Add typed SQLite and PostgreSQL access with transactions and migrations
Rules for the implementing agent
- Read
AGENTS.md,docs/COMPLETION-MATRIX.md,docs/ROADMAP.md,docs/QUALITY-GATES.md, and every owning versioned specification named above before editing. - Prefer extending the existing semantic object, operation table, diagnostic catalogue, profile dispatcher, or evidence harness. Do not create a parallel source of truth.
- Preserve canonical source, persistent identities, revision binding, fail-closed replay, deterministic rendering, and the separation between evidence and authority.
- A local test is local evidence. A hosted run is hosted evidence. A generated artifact is not a support or publication decision. Record exactly what ran and do not widen claims.
- Any new wire format, ABI, descriptor, carrier, evidence document, or compatibility surface requires an explicit versioned schema, independent decoder/replay, hostile-input tests, bounds, and nonclaims.
- Never grant ambient filesystem, process, network, model, signing, payment, commit, publication, or registry authority merely because a declaration, receipt, test, or proof exists.
- Keep changes reviewable. Split implementation into commits that each preserve the existing test suite, and explain any known-answer update field by field.
- Do not weaken or delete a failing regression to obtain green CI. Fix the cause or narrow the admitted feature with a stable diagnostic.
Definition of done
The implementation, tests, documentation, status wording, diagnostics, and evidence agree on one exact scope. Targeted tests pass, the applicable repository quality gate passes, hostile cases fail with the documented stable reason, and no unrelated public surface or authority is widened.
Activity
feat(jobs): add durable-job lifecycle, lease, retry, and uncertainty …
chore(std): register std/jobs in the standard-library catalog (#192)
wsdt commentedon Sep 12, 2026
The decision procedures landed and execute. The half this issue is named for — surfacing delivery uncertainty to a SEMAPRAX caller — is blocked on #228, with the probe recorded. Landed in
42ae4c4e+a7a57e11.The #228 boundary, stated precisely
Delivered: the full decision procedure for what an
UNCERTAINoutcome means and how it resolves, given that akind == uncertainwas already observed — auto-retry only when the job is idempotent and under its ceiling, otherwise it staysUncertainpending explicit reconciliation.Blocked: a checked SEMAPRAX-authored job handler cannot itself produce that observation. Every fallible host op aborts its invocation on failure rather than returning a value — confirmed directly against
docs/BOUNDED-LANGUAGE-NETWORK-IO-V1.md: "a nonzero status aborts the enclosing function … exactly like any other fallible host-command operation." Only a Rust-side runner can supplykind == uncertaintoday, mirroringDatabaseFixture::connection_lost's existing precedent.No sentinel was invented to paper over this, and the verifier was not touched. The exact follow-on probe is written into
docs/DURABLE-JOBS-V1.md#the-228-boundary-what-blocks-a-checked-semaprax-caller.What shipped
std/jobs— 29 pure functions underuseful-data.v1: a ten-state lifecycle (including a non-terminalUNCERTAIN), affine lease legality (claim / begin-execution / heartbeat / completion), bounded retry classification with capped exponential backoff, idempotent enqueue (fresh / duplicate / conflict), tick-clock scheduling with bounded skip-missed catch-up and bounded recurrence, cancellation and compensation, handler/schema-revision refusal, and attempt/cost budget legality.src/job_fixture.rs— a Rust-only store composing those with a realDatabaseFixturetransaction for enqueue, so the ledger row and idempotency check commit atomically. That is the database-transaction integration the issue asks for.Criterion-by-criterion
revision.is_known/requires_refusal, exercised in the fixtureLimitations, stated rather than implied
The concurrency (claim-race) test is a deterministic single-threaded simulation, exactly as
database_fixture.rsalready does — not proof about a real multi-threaded runner. Scheduling is over an explicitusizetick clock, not wall-clock or calendar time; a real adapter must convert. No broker, durable transport or background scheduler thread exists or should be inferred — this is proof data a runner must obey, not a runner.JobStoreis an explicit value a caller must hold, so there is no ambient access, but that is thinner than the first-class capability grant the issue's phrasing suggests.One composition limit worth recording:
std.jobscannot cross-importstd.dbfrom its own.spxsource, becausestd.dbis not yet in the compiler's bundled dependency registry (src/project/standard_dependencies.rs). It composes withstd.bytesinstead and restates two small bounds locally.Catalog registration is its own commit (
a7a57e11), regenerated with the repo's own tool. I verified at integration that it is purely additive — zero deletions — and thatstd.db's entry from #190 survives intact, so the two package additions from this tranche do not collide.Gates on the merged tree:
job_fixture13 passed;semaprax check/test/run std/jobsall clean on the interpreter;committed_catalogs_match_the_sourcesand the std-identity contract test 2 passed;documentation29;module_size1;fmt --checkand clippy--lib/--binsclean. The full multi-backendstandard_library::gate was deliberately not run — it is one of the >10GB runs.test(std): execute std.db and std.jobs conformance on native C11 and …
feat(jobs): add a replayable job evidence log, closing #192's authori…
style: apply cargo fmt to the job evidence log
wsdt commentedon Sep 12, 2026
Update: the "job evidence replayable but grants no execution authority" gap noted Not met in the prior comment is closed on
main(35b608c8+4bd32942+ca6ad292).src/job_evidence.rsaddsJobEvidenceLog: an append-only, domain-separated SHA-256 hash chain over typed entries mirroring everyJobStoretransition.JobEvidenceLog::replayindependently recomputes a job's final lifecycle state from the recorded entries alone (reusingjob_fixture::decisions, not a second parallel state machine) and returnsFinalStateMismatchwhen a recorded input's recomputed outcome disagrees with the log's own claimed final state. Appending, hashing, and replaying are inert data operations — no socket, thread, lease claim, or execution — matching AGENTS.md's "a settlement or concurrency model is proof data, not permission to perform a physical finalizer, spawn runtime work, or publish an artifact."Explicitly documented, not swept under the rug:
Claimed'sis_dueandCompleted'soutcome_kindare asserted facts this format does not itself check against a clock, so replay only proves internal legality and agreement with the log's own claim, not that the asserted facts were true. A terminal state (SUCCEEDED/PERMANENT_FAILURE/CANCELLED/DEAD_LETTERED) can never be reopened by a later entry, mirroring this repository's sticky-failure-selection invariant.Updated criterion table
src/job_evidence.rs, aboveVerification this session (local evidence only — nothing here is hosted/CI-verified)
Re-ran the full gate set against the current merged tree (HEAD
288b4a77, same asorigin/main):cargo test --locked -p semaprax --lib job_fixture::— 13 passed, exit 0cargo test --locked -p semaprax --lib job_evidence::— 10 passed, exit 0cargo test --locked --test documentation— 29 passed / 1 ignored, exit 0cargo test --locked --test module_size— 1 passed, exit 0cargo fmt --all --check— exit 0(
cargo test --test project -- standard_library::was attempted but externally SIGTERM'd mid-run on a resource-saturated shared host after every visible sub-test had passed and none had failed; not re-run given host load, and not one of this session's required gates.)No further code change from this session — an independent pass at this same gap (working blind, before discovering it was already merged) converged on a byte-identical
src/job_evidence.rsto what's already onmain, which is itself a form of cross-check. Leaving this issue open given the one remaining, architecturally-intentional Partial.test(jobs): reject an evidence-log claim asserted before a scheduled …
wsdt commentedon Sep 12, 2026
Audited against
main.issue-192-durable-jobsis an ancestor oforigin/main— it ships asstd/jobs,src/job_fixture.rs,src/job_evidence.rs+docs/DURABLE-JOBS-V1.md. Not closing: one acceptance criterion is blocked on a language gap.JobStorecomposes enqueue/claim/heartbeat/complete/cancel overDatabaseFixtureUNCERTAIN(state 8) is a sticky resting state — never silently retried or dead-letteredretry.backoff_ticksensures result <= max_ticks;schedule.catch_up_next_runbounded bymax_catch_up; leases are affine(lease_generation, job_generation, deadline_tick)grantsJobEvidenceLog::replayindependently recomputes final state from inert datarevision.is_known/requires_refusal, viarevision_check_refuses_an_unknown_bound_revisionAdded this pass (merge
cce695a2):a_scheduled_job_claimed_before_it_is_due_is_rejectedinsrc/job_evidence.rs.replay()already recomputedclaim_is_legalfor everyClaimedentry, but no test proved that aSCHEDULEDjob asserted-claimed withis_due: falseis rejected at the evidence-replay layer — that case existed only in the pure.spxdecision procedure (std.jobs.tests.claim_legality). The replay layer is the one a real deployment verifies submitted job histories against, so the gap mattered. It assertsIllegalTransition { entry_index: 1 }specifically. No behaviour changed; this closes coverage, it does not fix a bug.Test-name honesty check — clean. Every test name in
job_fixture.rs,job_evidence.rsandstd/jobs/src/tests.spxwas checked against what it exercises. The concurrency simulation is labelled as a simulation rather than as proof of concurrency, which is the honest form.Caller check:
job_evidence/job_fixtureare called bysrc/lib.rs, each other, and their own tests.std.jobsis called bydb_jobs_backend_audit.rsandexecution_matrix.rs. No runtime caller outside tests.Evidence class: local re-runnable. Nothing hosted.
Remaining for closure: #228 — until a checked handler can distinguish "definitely did not happen" from "may have happened and I cannot tell", the honest-uncertainty criterion cannot be met. A lane is working #228 now.
fix(jobs): close the completion-durability gap in the #192 job fixture
wsdt commentedon Sep 12, 2026
Progress in
eec9b187. Not closing — one criterion remains genuinely blocked.The gap I found and closed.
complete()only ever mutated the in-memory job record. The ledger'sstatecolumn, written once as a placeholder byenqueue, was never rewritten by anything — so nothing in the system distinguished "the handler ran and the outcome is durably recorded" from "the handler ran and the worker crashed before that record committed." That is precisely the second of the three idempotency cases that are usually faked, and it was open here.complete_durablecommits the ledger write before advancing in-memory state, and when the commit cannot be confirmed it forces the existingUncertainresting state rather than the handler's reported outcome.complete()itself is unchanged. Supporting primitive:DatabaseFixture::update_column, added alongside the existinginsert/select_eq.Proved by breaking it, not by assertion:
commit_confirmedgate so the method unconditionally applies the outcome →complete_durable_forces_uncertain_rather_than_the_handlers_outcome_when_the_ledger_commit_is_never_confirmedfailed withleft: Succeeded, right: Uncertain, every other test still passing. Restored.update_column's shape check to accept any type →update_column_rewrites_matching_rows_and_refuses_a_shape_mismatchfailed withleft: Ok(1), right: Err(RowShapeMismatch). Restored.Which idempotency cases the tests genuinely reach, stated honestly:
attempt/backoff already model.Reachability, unchanged and stated plainly:
job_fixture/job_evidenceare Rust-only and called bylib.rs, each other, and their own tests.std.jobsis called from the backend audits and the scaffold template. There is no runtime caller outside tests — no broker, no scheduler thread, nothing claiming jobs on a timer. That is deliberate, per the invariant that a settlement or concurrency model is proof data, not permission to spawn runtime work; but it means "durable jobs" here means a checked decision procedure plus a fixture, not a running system.Still NOT met, and deliberately not worked around: surfacing an uncertain outcome to a checked SEMAPRAX handler. Every fallible host op aborts the invocation rather than returning a value, so a handler cannot observe the
UNCERTAINstate that the decision procedure models. That is #228, whose ABI change sits behind the SPX-AI-019..025 review checkpoint. Four independent sessions have now reached this same conclusion; inventing a status vocabulary here to close the criterion would be the wrong kind of progress.Evidence:
--lib fixture::→ 39 passed, 0 failed (14database_fixture::, 15job_fixture::, 10 unrelated pre-existing matches). Local, offline, re-runnable; not hosted.Separately noted:
--test module_sizeis red onmainforsrc/cli_driver.rs,src/hir/validation.rsandtests/project/standard_library.rs, none of which this touches. Being fixed by extraction rather than by raising the recorded budgets.fix(jobs): close the completion-durability gap in the #192 job fixture
feat(jobs): commit a job's completion to the ledger before advancing …
wsdt commentedon Sep 13, 2026
Pushed to main in
4103e23f1471fd0689c9dca01d147e42d3a53e85.Added an explicit host-driven single-job checkpoint runtime over JobStore and JobEvidence. It admits payloads using a compiled interaction schema, persists original attempt times for replay, uses CAS generations with native advisory file locking and same-directory replacement, recovers Running as Uncertain, and poisons runtime instances after failed checkpoint writes.
The host handler seam is not proof of a compiled callable binding. DatabaseFixture remains an in-memory decision mirror; this is not a physical database transaction with application state. Source-handler/deployment identity binding, general queue/service integration, runtime recurrence/heartbeat/cancellation, and physical database integration remain open. The file store makes no power-loss or hostile-root guarantee. Keeping this issue open.
Local evidence: 34 job fixture/evidence/runtime tests, including compiled-schema retry/recovery and checkpoint-failure poisoning. The combined library/binary compile passed. No full quality profile or hosted CI run was performed for this batch, per the requested implementation-first workflow.
wsdt commentedon Sep 13, 2026
Implemented persistent cancellation and recurring execution lifecycle controls (commit cbce6ed):
Focused cancellation/recovery, recurring/recovery, overflow/no-effects and hostile evidence tests passed. No full or hosted CI gates run.
Issue remains open for source handler bindings, broader queue/heartbeat execution, production database/transaction integration and full reference-app acceptance. These changes extend the real host-driven single-job runtime, without claiming autonomous execution or physical database atomicity.
5 remaining items
wsdt commentedon Sep 18, 2026
Closure audit @
e9ab9242— NOT CLOSEABLE.The lease-fencing defect found here is fixed (
52fa3980): lease generations now derive from a monotoniclease_epochrather than the outstanding lease, so a reclaimed worker's stale completion can never land over the new holder's run.Blocker is this issue's own listed dependency: no physical SQLite/Postgres adapter exists — only an in-memory
DatabaseFixture. True multi-process concurrency is therefore unproven, which is the property this issue exists to establish.wsdt commentedon Sep 18, 2026
Correction: a claim I made about this issue today was wrong.
An audit I ran earlier reported that it had grepped this repository for any durable-job or lease primitive and found none, and I passed that on. It is false. I verified against the tree at
81031817:src/job_fixture.rs(1448 lines) —JobStoreoverDatabaseFixture: ledger-transactional enqueue, lease claim/heartbeat/expiry with epoch-fenced generations, retry/backoff/dead-letter, idempotent completion.src/job_evidence.rs(809 lines) —JobEvidenceLog: append-only domain-separated SHA-256 hash chain with an independentreplay()that recomputes final state.src/job_runtime.rsandsrc/job_runtime/{source_handler,source_handler_tests,tests}.rs— host-driven checkpoint runtime, versioned checkpoint wire format with CAS-generation file locking, heartbeat and lease extension, source-handler binding with deployment digest.std/jobs/src/jobs.spx— 29 pure functions: ten-state lifecycle, affine lease legality, bounded retry classification, idempotent enqueue, tick-clock scheduling with bounded catch-up.docs/DURABLE-JOBS-V1.md(519 lines).Registered at
src/lib.rs:210-212. Gate, run locally just now:That count already includes the hostile coverage a greenfield build would have been chasing:
checkpoint_decoder_refuses_trailing_or_noncanonical_boolean_bytes,recover_refuses_a_checkpoint_whose_schema_digest_no_longer_matches,physical_store_refuses_stale_cas_and_retains_current_generation,a_recovered_succeeded_job_never_redispatches_its_handler,retryable_failures_back_off_and_dead_letter_at_the_ceiling,scheduled_job_claims_only_once_due_and_recurs_with_bounded_catch_up, anda_reclaimed_workers_stale_completion_can_never_land_over_the_new_holders_run— the last covering a real lease-fencing bug this codebase already found and fixed in52fa3980.No code was written. The agent I dispatched checked the premise before building, found the existing implementation, and refused the task rather than standing up a second, less mature set of primitives beside it. That was the correct call and I'm recording it here so the mistake doesn't propagate further.
The residual recorded in this issue's own last closure audit stands unchanged and is accurate: a real SQL adapter behind
DatabaseFixture(still in-memory), true multi-process concurrency (still a disclosed single-threaded simulation), and the acceptance criterion blocked on #228, where a checked.spxhandler cannot observe anUNCERTAINhost outcome because every fallible host op aborts rather than returning a value.Worth noting for anyone auditing this backlog: today's errors have run in both directions. Four issues understated what was built; this one claimed nothing was built when a great deal was. Verify against the tree, not against the record — including mine.
test(jobs): race the claim and CAS paths on real OS threads
wsdt commentedon Sep 19, 2026
A correction to how this issue has been classified, because it changes what unblocks it.
An audit sweep listed #192 as offline-actionable with two remaining gaps: "replace the in-memory
DatabaseFixturewith a real SQL adapter" and "implement true multi-process concurrency (currently a disclosed single-threaded simulation)". Checked against the code, both are one gap, and it is not offline-actionable.On the concurrency half — the existing disclosure is already accurate, and better than the sweep implied.
src/job_fixture.rs:10-26describes exactly what it has: "a deterministic single-threaded scripted concurrent-claim simulation, a second concurrent-claim race driven by genuine OS threads racing under aBarrier(proving the same fencing invariant under real nondeterministic scheduling rather than a call order this module's own test chose)".d6a12309landed that OS-thread race. Nothing here is passing off a simulation as real concurrency.Multi-process is not an increment on that; it is blocked by the storage question.
DatabaseFixtureisBTreeMap<String, Table>in process memory (src/database_fixture.rs:166-171). Separate processes share no address space, so there is nothing for a second process to contend over — a multi-process claim race is only meaningful once the store is a shared durable medium. The two "gaps" are therefore the same gap.And that gap needs a decision, not an afternoon. This repository has no SQL dependency at all — no
rusqlite, nolibsqlite3-sys, nopostgresinCargo.toml. Adding one means a new C dependency, aCargo.lockchange, and passage through thesupply-chainrelease-blocker job. AGENTS.md also forbids build-time network access, so any such dependency has to be vendored or pinned accordingly. That is a maintainer call about the project's dependency surface, not something an implementation pass should decide unilaterally — which is presumably why every prior pass stopped where it did.Suggested reclassification: blocked on a storage-dependency decision. Two things would unblock it, and they are quite different in cost:
docs/HOST-OPERATION-OUTCOME-V1.md) — in which case multi-process contention can be built on that without any new dependency, and the "real SQL adapter" requirement should be rewritten to say so.Option 2 may well be the smaller and more honest path, since it keeps the capability surface unchanged. Either way the choice belongs to a maintainer. Nothing in this comment changes code.
test(jobs): race enqueue idempotency under genuine OS-thread concurrency
wsdt commentedon Sep 19, 2026
Re-audit at
b9540a2e— one genuine gap found and closed; the rest was already shipped.Audited
src/job_fixture.rs,src/job_runtime.rs(+source_handler),src/job_evidence.rsandstd/jobs/src/jobs.spxagainst every acceptance criterion and every entry on the required-tests list. Nearly all of it was already in place:JobRuntime::enqueue/drive_once, withsource_handler.rsdriving a real checked SEMAPRAX callable to completion.UNCERTAINstate,uncertain.retry_is_permitted, andcomplete_durableforcingUncertainwhen a ledger commit is unconfirmed.retry_backoff_ticks,schedule_catch_up_next_runbounded bymax_catch_up, andlease_epochfencing.revision_checkplus a checkpoint refusal on schema-digest drift.The one real gap: "concurrent enqueue and idempotency races" is its own line item on the required-tests list, and it had no concurrent test. Every existing idempotency test drove
JobStore::enqueue's find-then-insert sequence sequentially. Onlyclaimhad a genuine race test. A find-then-insert pair is exactly the shape that passes every sequential test and still admits two winners under contention.Closed in
486a14eewith two tests using real OS threads, barrier-synchronised, 16 workers × 25 iterations, following the existing claim-race test's pattern:real_os_thread_concurrent_enqueue_race_creates_exactly_one_job_for_the_same_key— same key, same descriptor. Exactly oneCreated, every other call aDuplicateof it, one ledger row. Catches a find-then-insert race outside the lock, or any bug letting two threads both pass the uniqueness check.real_os_thread_concurrent_enqueue_race_with_differing_descriptors_conflicts_the_losers— same key, half the workers using a different descriptor. Whichever wins, matching calls must beDuplicateand non-matching calls must beConflict. Catches a silent merge onto the winning row, or a spurious secondCreated.Evidence:
--lib job_fixture::→ 20 passed,--lib job_runtime::→ 22 passed,--lib job_evidence::→ 13 passed, 0 failed throughout. All local.Residual scope, honestly:
DatabaseFixture. The transaction integration for enqueue is proven against the in-memory fixture only; a real Postgres/SQLite driver remains separate work.usizetick, and the language has no calendar library or numeric cast to build a wall-clock adapter on. This is a documented scope boundary, not a code gap, and was not attempted.Refs, notCloses— those three non-claims mean not every criterion is unconditionally met, though the large majority were already shipped before this pass.Gotcha worth recording for gate scripts:
cargo test <filter_a> <filter_b>with two positional filters is rejected by cargo itself ("unexpected argument"); the two suites must be separate invocations.feat(job-service): reference project that enqueues and executes a rea…
wsdt commentedon Sep 19, 2026
A reference service that genuinely executes durable jobs from
.spxsource (fb5f1de0) — this issue's first acceptance criterion.examples/job-service-project.job_service.corecomposes bundledstd.jobsinto a claim/idempotency/retry/dead-letter decision walk, gated on interpreter, native C11-O0/-O2, and Core Wasm under Node.job_service.appdeclares a real checked handler,handle_report_job, with itsReportJobpayload record.It executes, rather than models.
tests/useful_data/job_service_project.rsbindsjob_service.app.handle_report_job— compiled from the project's own retained.spxsource, not a Rust stand-in — throughSourceJobHandlerBinding/drive_checked_source_job, enqueues a realJobRuntimejob, and drives it to completion through the retained-call interpreter seam.JobRuntimeowns every checkpoint, lease, retry, backoff and dead-letter transition; the.spxhandler owns only effect-free classification of an already-admitted payload. That division is the point: the handler gains no authority by being callable.Four previously undocumented constraints surfaced, now written up in the README — each of which silently breaks a binding that otherwise looks correct:
SourceJobHandlerBindingre-verifies its bound file standalone, so that file must have zero cross-moduleuseand its ownfn main.entryor its singletestsmodule may declaremainat all (SPX-G172).graph::revisionhashes comment-dropped canonical text while the project's per-filesource_revisionhashes raw bytes including comments, so a single//causes aSourceRevisionMismatchon a file that passessemaprax fmt --check. This is the same comment-versus-canonical-bytes seam rename_display_name preview is unsatisfiable for any project with commented bundled dependencies (SPX-G525) #274 found behindSPX-G525, surfacing in a second place.SPX-F102), which is whymain()stays a trivial0.Budget: measured with
builder_bytes_breakdownat 12,616,649 bytes — 67% of the 18,874,368 cap, roughly a third headroom — so a service with a minimal dependency closure fits comfortably. That is a useful counterweight to the 92.7% figure measured ontask-service-project: the ceiling is reachable, but not inherent to a reference service.Four fault-injection cycles, each confirmed red then reverted: a wrong idempotency expectation breaks the test aggregator; a constant-returning handler turns
SucceededintoPermanentFailure;max_attempts=1collapsesScheduled → DeadLetteredinto an immediate dead-letter; a mismatched payload descriptor turnsDuplicateintoConflict.Disclosure. A concurrent lane's
yieldrollout left the crate non-compiling for this lane's entire session, so its verification ran via the pre-builttarget/debug/semapraxbinary andrustc --testagainst a pre-built rlib — 6 passed, run four times with consistent results. The crate compiles again as of24c1d166, and I re-ran the suites centrally before pushing.Residual:
docs/COMPLETION-MATRIX.mdnot updated (shared file, other lanes active). No granular per-modulebuilder_bytesbreakdown of the final three-file layout —semaprax checksucceeding is authoritative that it fits, but the per-module numbers are for the earlier four-file shape. Nostd/source changed.Refs, notCloses— the DST/wall-clock and multi-process non-claims stand.wsdt commentedon Sep 19, 2026
Re-audit at
12373af8— still NOT closeable; independently re-verified this pass rather than quoted from this thread's history.Fresh runs:
Five acceptance criteria are MET with executed evidence: durable typed enqueue/execute (
entry_and_conformance_return_zero_on_interpreter_native_and_wasm), honest delivery-uncertainty documentation (uncertain_outcome_stays_uncertain_for_a_non_idempotent_handler_until_reconciled), bounded retries/leases (retryable_failures_back_off_and_dead_letter_at_the_ceiling,heartbeat_extends_the_deadline_and_is_refused_once_stale), replayable evidence with no execution authority (10 replay/tamper tests), and fail-closed migration (revision_check_refuses_an_unknown_bound_revision).Two required tests remain genuinely unmet, and neither is a small fix:
src/database_fixture.rs:166-168is still an in-memoryBTreeMap;Cargo.tomlhas norusqlite/libsqlite3/postgres/sqlx— re-confirmed this pass. This issue's own dependency [P1] Add typed SQLite and PostgreSQL access with transactions and migrations #190 is closed, but [P1] Add typed SQLite and PostgreSQL access with transactions and migrations #190's final comment records the driver decision as stillHUMAN_BLOCKED, so it closed without shipping a physical driver. Adding one is a new C dependency plus a supply-chain and vendoring decision reserved for the maintainer. Multi-process concurrency is the same root cause — there is no shared durable medium for separate processes to contend over.scheduled_job_claims_only_once_due_and_recurs_with_bounded_catch_up;schedule_catch_up_next_run(999,100,10,2) == 130), but SEMAPRAX has no calendar/timezone library, so DST semantics cannot be built. This is a disclosed non-claim, not a defect.One detail worth recording, since #228 closing could be read as having settled it:
src/job_runtime/source_handler.rs:336-348(map_outcome) maps a checked.spxhandler's0/1/2toSucceeded/RetryableFailure/PermanentFailure, and has no branch producingHostJobOutcome::Uncertain— only the Rust host adapter can reach that state. This is deliberate and documented in place: "The checked callable is effect-free. A language or capacity failure therefore cannot report an external delivery result; classify it as terminal rather than inventing a source-visible uncertainty value." So the profile's uncertainty documentation is accurate, but the "surfaceUNCERTAINto a checked handler" ambition is still undelivered even though its language blocker (#228) is gone, and that residual is not currently restated in this issue's criteria table.No code changed this pass. Forcing either gap would mean adding an undeclared dependency or weakening the effect-free handler boundary — both prohibited here. Remaining work needs a maintainer decision on the storage dependency: vendor a SQL driver, or repurpose the existing atomic-write provider as the durable medium.
docs(decisions): record the signing and durable-job storage decisions
wsdt commentedon Sep 19, 2026
Decision recorded —
docs/decisions/0005-durable-job-storage-medium.md(commit94924c98). The storage dependency this issue was blocked on is now chosen.Durable jobs run on this repository's own atomic-generation substrate, behind a sealed
JobStoreadapter seam. No SQL driver in the default build. A vendored SQLite adapter stays possible and additive behind that seam, off by default, and is not required to close this issue.Why not a bundled SQL driver:
rusqlitewithbundledcompiles vendored C and needsunsafeFFI, in a workspace where eight crates declareunsafe_code = "forbid". PostgreSQL is worse here — a running server and network authority the compiler is forbidden to acquire ambiently.synchronous=NORMALin WAL mode permits losing recently acknowledged writes after OS or power failure. Taking the dependency and still having to reason carefully about fsync buys less than it looks like.Why the existing substrate is right: ADR 0002 already publishes one complete immutable generation through
ACTIVE(.semaprax-workspace/ACTIVE) — staged content pivoted by a single atomic rename. That is the canonical durable-commit pattern, already crash-safe, already deterministic, already load-bearing here. Durable jobs built on it inherit deterministic replay and "failed transactions leave authoritative state unchanged" for free. It also supplies the shared medium that multi-process concurrency testing currently lacks — so that second gap closes with the same work, since both had one root cause.The honest caveat, recorded in the ADR so it isn't rediscovered: append-only plus atomic rename is crash-friendly but not automatically durable. Acknowledging a job as durably accepted requires write → flush → fsync the file → rename on the same filesystem → sync the directory entry, and only then acknowledge; group-commit keeps that affordable. Whichever durability level is chosen must be stated in the profile documentation, not implied — a store that has not fsynced has not committed and must not report that it has.
The competitive argument: "durable background jobs, scheduling, retries and idempotency, with no database to run" is genuinely differentiating — the same move SQLite made one level down. Every comparable runtime makes you provision Postgres or Redis before a job can survive a restart. It also keeps the
--locked --offlineCI story intact: no service, no container, no credentials.One scoping consequence that needs your eye: this issue's required test says "database transaction integration for enqueue + app state." Read against the
JobStoreseam, that criterion is satisfiable without SQL. If the intent was specifically a SQL engine, this ADR re-scopes the criterion rather than satisfying it — flagging that explicitly rather than quietly assuming the looser reading.Still genuinely out of scope regardless: DST/wall-clock scheduling, since the language has no calendar library — a disclosed non-claim, not a defect.
docs(summary): list ADR 0004 and ADR 0005
feat(jobs): add a file-backed JobStore seam on the atomic-generation …
wsdt commentedon Sep 19, 2026
Closing — with one criterion explicitly re-scoped, not silently checked off
Most of #192 shipped before this session (
std/jobs,src/job_fixture.rs,src/job_evidence.rs,src/job_runtime.rs,examples/job-service-project,docs/DURABLE-JOBS-V1.md). What was missing was a durable-job persistence seam.da68ff46adds it:src/durable_jobs/—model,retry,codec,durable_fs,store— 1994 lines, every file under the size cap, one line insrc/lib.rs, nothing else touched. No newSPX-code, so nothing to regenerate.The one criterion that does not close as written
"Database transaction integration for enqueue plus application state change."
enqueue_with_side_record_commits_both_or_neither_on_successanda_fault_during_the_joint_commit_leaves_neither_the_job_nor_the_side_record_visible, the latter fault-injected at three points in the durable-write sequence.unsafe, and a Postgres client needs ambient network authority, which AGENTS.md prohibits outright. No amount of work unblocks that; it would take a decision to relax a capability invariant.ADR 0005 records that re-scope and the reasoning, and was written precisely because this criterion was ambiguous. Closing on the first reading, with the second named here rather than quietly dropped. If the literal SQL reading was intended, reopen and it becomes a capability-invariant question rather than an implementation one.
The rest
racing_enqueue_calls_with_the_same_idempotency_key_create_exactly_one_job(16 OS threads,Barrier-synchronized), plusa_conflicting_payload_under_the_same_key_is_a_closed_refusal_not_a_mergesrc/job_fixture/tests.rs, extended here with generation fencing (completing_with_a_stale_lease_generation_is_refused_and_never_mutates_the_job)a_retryable_job_that_never_succeeds_is_dead_lettered_at_the_ceiling_not_retried_forever, so the retry loop has a terminal statedocs/DURABLE-JOBS-V1.mdsays soa_forged_lease_token_naming_the_wrong_worker_is_refusedCrash durability is proved by dropping the store and reopening from disk (
an_acknowledged_job_survives_a_simulated_crash_and_reopen). The durability discipline is the one that is usually got wrong and is right here:durable_fs.rsfsyncs the staging file (:91) and its parent directory (:118), because a file-only fsync can lose the directory entry that names it on crash; the non-portability of directory fsync outside Unix is documented rather than ignored.Determinism holds with no wall clock anywhere in the model —
identical_operation_sequences_produce_byte_identical_generationsandjob_ids_are_assigned_in_strictly_increasing_enqueue_order, withBTreeMap-ordered encoding so no unordered iteration reaches the bytes on disk. A source-scan regression (module_source_never_reaches_for_network_process_or_env_authority) keeps the module free of ambient authority.cargo test --locked -p semaprax --lib durable_jobs→ 40/40, with-- --listrun first to rule out the zero-match false pass.cargo check --locked -p semaprax --all-targetsclean.