Slice 75 - Run Queue Core
Date: 2026-07-01
Scope#
Restore the core queue machinery behind background jobs:
- durable run specs beside run records
- oldest queued run selection
- per-wiki worker lock
- stale lock recovery
- in-process worker drain that executes queued Ingest and Garden specs
This slice does not spawn a detached background process yet, and it does not
add public --background / --foreground lifecycle flags. It builds the
execution core that those surfaces will call.
Why Now#
Slice 74 restored jobs attach and jobs cancel, but cancellation only becomes
useful for background work when queued work has a durable spec and a worker can
claim it through one serialized path.
The archived implementation used this split:
spec.tspersisted executable operation specsqueue.tsowned the per-wiki worker lock and oldest queued lookupworker.tsdrained queued jobsexecutor.tsran one job and finalized the record
The Python shape should reuse that product split without porting TypeScript module names directly.
Decisions#
- Keep durable lifecycle records in
services/runs. - Add
RunSpecas the persisted executable request shape. It supportsingestandgardenfirst. - Keep
RunsServiceas the owner of queue storage, spec storage, and worker lock state. - Add
RunQueueWorkflowas the application workflow that drains queued specs by callingIngestWorkflow.run_with_run(...)orGardenWorkflow.run_with_run(...). - Do not let the CLI shell out to
codealmanacinternally. - Do not add a second
JobService. Public CLI noun remainsjobs; internal service noun remainsruns. - Keep the process-spawn owner for the next slice. This slice proves the in-process execution path and the lock semantics first.
Shape#
queued = app.workflows.queue.queue_ingest(
RunIngestRequest(cwd=repo, inputs=("note.md",), harness=HarnessKind.CODEX)
)
drained = app.workflows.queue.drain(
DrainRunQueueRequest(cwd=repo)
)The queue workflow owns operation dispatch:
if item.spec.operation == RunOperation.INGEST:
ingest.run_with_run(RunIngestWithRunRequest(..., run_id=item.run.run_id))
elif item.spec.operation == RunOperation.GARDEN:
garden.run_with_run(RunGardenWithRunRequest(..., run_id=item.run.run_id))The run store owns the atomic-ish filesystem details:
<almanac-root>/jobs/<run-id>.json
<almanac-root>/jobs/<run-id>.jsonl
<almanac-root>/jobs/<run-id>.spec.json
<almanac-root>/jobs/worker.lock/owner.jsonCosmic Python Transfer#
Chapter 6 frames Unit of Work as the abstraction over atomic operations and stable persistent state. The filesystem cannot rollback like a database, but the same lesson applies: queue mutation, spec persistence, lock acquisition, and terminal finalization need one owned boundary rather than scattered CLI file writes.
Chapter 4 describes the service layer as the application orchestration layer.
RunQueueWorkflow is that layer for queued lifecycle execution; it should call
existing lifecycle workflows instead of knowing prompt, source, or harness
details itself.
Chapter 13 keeps object wiring in the composition root. app.py should wire
the queue workflow with RunsService, IngestWorkflow, and GardenWorkflow.
Files#
src/codealmanac/services/runs/models.pysrc/codealmanac/services/runs/requests.pysrc/codealmanac/services/runs/service.pysrc/codealmanac/services/runs/store.pysrc/codealmanac/workflows/garden/requests.pysrc/codealmanac/workflows/garden/service.pysrc/codealmanac/workflows/run_queue/src/codealmanac/app.pytests/test_runs_service.pytests/test_run_queue_workflow.pytests/test_architecture.py- steering docs under
docs/python-port/
Verification#
Run focused checks:
uv run pytest tests/test_runs_service.py tests/test_run_queue_workflow.py tests/test_architecture.py
uv run ruff check src/codealmanac/services/runs src/codealmanac/workflows src/codealmanac/app.py tests/test_runs_service.py tests/test_run_queue_workflow.py tests/test_architecture.pyThen run broad checks:
uv run pytest
uv run ruff check .
git diff --check