The queue.jsonl Primitive: A Pull-Based Task-Distribution Spec for RDCO pipeline-* Fan-Out
The question
What queue.jsonl / task-distribution primitive should RDCO standardize for pipeline-* fan-out — the per-run scratch dir + task-queue file that lets workers pull tasks instead of being explicitly invoked, plus per-task cost-ceiling enforcement? (Follow-up from the 2026-05-20 fan-out brief: Glasswing has a public 8-stage shape but no public queue.jsonl spec, so RDCO must spec its own. This is a DESIGN deliverable — a concrete spec, plus a candidate /deep-research adoption path.)
What we already know (from the vault)
- [[2026-05-20-multi-agent-fanout-architectural-patterns]] already committed the direction: the 30-day recommendation is literally "add queue.jsonl scratch-file pattern to one existing pipeline (start with
/deep-researchsince it already has the dequeue shape), add per-task scratch dir, document the primitive in an SOP." It also fixes the hard constraint: don't scale fan-out without per-agent cost ceilings — Glasswing has per-task scratch dirs for isolation but no public per-task budget, so we must add explicit token/tool caps in the dispatch prompt. - The station skills already establish the per-run scratch-dir convention:
~/rdco-vault/01-projects/skill-pipelines/runs/<domain>-<timestamp>/, wherestation-spec-author→station-criticwritespec.md, tests, code, andcritic-feedback-iter-N.mdartifacts. The scratch dir exists; what is missing is a queue file and a pull protocol so workers self-assign instead of being one-sub-agent-per-explicit-dispatch. - [[2026-06-10-multi-agent-resumability-checkpointing-caf]] gives the durable-state discipline to reuse: a single authoritative on-disk record (
manifest.yaml) as the "what's done / where to resume" store, the write-artifact-before-updating-status invariant (a phase iscompleteonly when its manifest entry AND its artifact file both exist), and a startup integrity check that repairs status vs artifact drift. That invariant is directly portable to a queue. /deep-researchtoday is the closest existing shape: it dequeues N approved questions from Notion, marks each In Progress, spawns one sub-agent per question (hardcoded 3-concurrent cap), and enforces a crude budget via per-sub-agent tool caps (5 QMD + 3 WebSearch + 3 WebFetch, ~$2-5/question). It is explicit-invocation, not pull, and has no mid-run crash recovery (a failed question just stays In Progress for the next night).
What the web says
- Single-file CAS queue with heartbeat leases is a solved pattern. A distributed queue can live in one
queue.jsonon object storage: workers claim by compare-and-set (read file + version → mark first unclaimed job in-progress → write only if the file hasn't changed since read), and hold the claim via a heartbeat timestamp written back to the record; if the last heartbeat exceeds a timeout the job is assumed abandoned and the next worker takes over. (turbopuffer) - File-based agent coordination is durable by default and needs no broker. The
hive/pattern uses an append-onlylog.jsonlevent log plus one-file-per-message inboxes; atomicity comes from create-then-rename (no explicit locks), and a per-agentcursor.jsontracks the last-processed marker. "A file in an inbox is the message — it can't be lost to a process restart because it was never in a process." Tradeoff: seconds-not-instant latency, and you write the semantics the broker would ship. (Munder Difflin) - Lease / visibility-timeout is the standard crash-recovery primitive. A worker "claims" a job by writing a future-timestamp lease on the record; if it crashes before completing, the lease expires and the job becomes claimable again — at-least-once delivery without a coordinator. (SoloPHP Job-Queue, Netdata: FOR UPDATE SKIP LOCKED)
- Concurrent LLM agents already claim tasks in JSONL. A Feb-2026 event store shows Claude Opus claiming five tasks simultaneously, each claim recorded as a JSONL line with timestamp, action type,
task_id, andagent_id— the append-only audit-log shape mapped onto agent fan-out. (LogRocket) - Budget enforcement is a 5-layer stack, and it must live outside the agent's own reasoning. Layers: (1) per-request
max_tokensceiling, (2) per-session rolling budget that returns 429/402 at a $/token cap, (3) per-key monthly cap, (4) model-tier routing, (5) circuit-breaker on rate-of-spend. Key principle: "budget enforcement must live outside the agent code" so buggy agents can't bypass it. Hierarchical budget inheritance — parent tasks allocate fixed budgets to subtasks — is the multi-agent shape. (AI Security Gateway) - The cost-explosion failure mode is real and expensive. Four agents looped for 11 days in Nov 2025 for a $47,000 bill; the general estimate is that 50 concurrent runaway agents accumulate $1,200+ in a single night. A budget guard checks cumulative spend before each call and terminates when the ceiling is hit — not a billing alert after the fact. (Nexgismo, SupraWall)
Convergences and contradictions
- Vault + web converge on "don't build a broker." RDCO's filesystem-per-run scratch dir is the right substrate; the file-based-coordination and single-file-CAS patterns both say a durable queue on disk with lease/heartbeat semantics is sufficient at RDCO's scale. The lift is adding a claim protocol, not new infrastructure.
- One real contradiction to resolve: JSONL is append-only, but claims mutate state. Editing a
statusfield in place inside a shared JSONL line is racy (line rewrites are not atomic across concurrent writers). The fix the web implies but doesn't state cleanly: keepqueue.jsonlimmutable (the task manifest), and hold claim/lease state in a separate atomic structure (a lock dir or a rename), with terminal outcomes in an append-onlyevents.jsonl. This is the reconciliation the CAF write-before-mark invariant already anticipates. - Honest limit on cost enforcement. The web's strongest enforcement lives at an API gateway (429 before the call reaches the provider). RDCO dispatches sub-agents through the Agent/Task tool with no metering proxy, so a per-task dollar ceiling is currently only self-metered (advisory). The actually-hard gate RDCO can enforce today is per-task tool-call caps (the
/deep-researchpattern) plus wall-clock. State this plainly rather than pretending theusdfield is a hard stop.
Synthesis for RDCO
Design principle: queue.jsonl is the immutable task manifest; claim state and outcomes live beside it. The founder's naming ("queue.jsonl") is kept, but with correct semantics — the JSONL file is written once by the planner (one line per task) and only ever appended to (a Gapfill pass may add re-queued tasks). Task status is never mutated inside the JSONL; it is derived from filesystem state. This gives a clean answer to the "workers pull instead of being invoked" requirement: an orchestrator writes the queue and spawns a fixed pool of K generic worker sub-agents pointed only at run_dir — not one sub-agent per task. Each worker loops: find the highest-priority claimable task, atomically claim it, do the work under its cost ceiling, write its result, mark done, repeat until the queue drains. Worker count is now decoupled from task count (--count 12 becomes 12 tasks drained by 3 pullers, not 12 simultaneous sub-agents), which is the concrete scaling win.
(a) queue.jsonl record schema — one JSON object per line, immutable after write:
task_id(primary key;<run_id>-<seq>),run_id,task_class(the Glasswing attack-class analog:research-question,critic-axis,spec, …),scope_hint(the narrow scope this worker owns).payload(task inputs) and/orinput_paths(files underrun_dir/inputs/to read).depends_on(array oftask_ids that must be DONE before this task is claimable — encodes spec→test→code→critic ordering while still allowing fan-out within a stage; empty = embarrassingly parallel).priority(int, lower first),cost_ceiling(object, below),result_path(pre-assigned relative path the worker MUST write to, so the orchestrator knows where to collect),attempt/max_attempts(dead-letter after N),created_at.cost_ceiling={ usd, max_tokens, max_tool_calls: {qmd, websearch, webfetch, …}, wall_clock_s }— per-task budget carried on the row.
(b) per-run scratch-dir layout (extends the existing runs/<domain>-<timestamp>/ station convention):
~/rdco-vault/01-projects/skill-pipelines/runs/<domain>-<YYYYMMDDThhmmssZ>/
run.json # run manifest: run_id, domain, created_at, run_budget_usd, pool_size, status
queue.jsonl # immutable task list (planner writes; Gapfill may append)
events.jsonl # append-only audit log: claim / heartbeat / done / failed / killed-over-budget
inputs/ # shared read-only inputs the planner drops for workers
claims/<task_id>/lease.json # atomic claim dir (mkdir = the lock) + lease + heartbeat
results/<task_id>.<ext> # per-task worker output (write BEFORE marking done)
dead-letter/ # tasks that exhausted max_attempts
(c) pull / claim protocol (worker self-assigns):
- Read
queue.jsonl; filter to claimable = noclaims/<task_id>/dir AND noresults/<task_id>file AND alldepends_onare DONE. Sort bypriority, thenseq. - Attempt atomic claim on the first candidate:
mkdir claims/<task_id>—mkdirfailsEEXISTif another worker already claimed it (this is the atomic primitive; the mkdir-lock is chosen over CAS because it needs no version-read round-trip and is single-host-atomic on APFS). On failure, move to the next candidate. - On success, write
claims/<task_id>/lease.json={worker_id, claimed_at, expires_at = now + lease_ttl, heartbeat_at}and append aclaimevent toevents.jsonl. - Do the work, touching
heartbeat_at/ extendingexpires_atperiodically (long tasks) so a live worker's task is never reaped. - On completion, write
result_pathfirst, then append thedoneevent (CAF write-before-mark invariant — a crash between the two leaves an orphaned result the integrity check detects). Loop back to step 1. Exit when nothing is claimable. - Lease reaper (orchestrator between dispatch waves, or a sweep): for any
claims/<task_id>whose lease expired with noresults/<task_id>, delete the claim dir,attempt += 1(append a re-queued line ifattempt < max_attempts, else move todead-letter/), append alease-expiredevent. This is work-stealing crash recovery — a died worker's task returns to the pool for another puller, which the current/deep-research"leave it In Progress for tomorrow" cannot do mid-run.
(d) where per-task cost-ceiling is enforced — three points, defense in depth: (1) Dispatch-time / static — the worker prompt injects cost_ceiling.max_tool_calls as hard caps (the enforceable gate today, extending the /deep-research 5+3+3 pattern) plus max_tokens and wall_clock_s; this is the parent→subtask hierarchical inheritance the web describes. (2) Runtime / self-metering — the worker tracks a running tally and, before any expensive call that would breach usd/max_tokens, stops, writes a partial result + killed-over-budget event, and exits gracefully (advisory until a metering proxy exists; say so). (3) Run-aggregate — run.json.run_budget_usd caps the whole run; the orchestrator sums events.jsonl spend before dispatching each wave and stops dispatching if the run cap is breached (the $47k/11-day and $1,200/night incidents are the reason this exists).
How /deep-research adopts it first. Only Step 4 changes. Instead of spawning one sub-agent per question, the orchestrator writes queue.jsonl to a fresh runs/deep-research-<ts>/ (one line per approved question, cost_ceiling = {usd:5, max_tool_calls:{qmd:5, websearch:3, webfetch:3}}, result_path = the brief file), then spawns K=3 generic worker sub-agents pointed only at run_dir with the pull loop above. Notion stays the approval/backlog system of record; queue.jsonl is the per-run execution substrate. The orchestrator reflects each DONE back to Notion (Status → Done, Brief Path). Net gains over today: bounded concurrency independent of question count, graceful over-budget stop per question, and mid-run crash recovery. The SOP for this belongs at ~/rdco-vault/02-sops/, with the pull-loop worker logic factored into a shared helper the other pipeline-* / station skills import rather than copy-paste.
Example queue.jsonl record:
{"task_id":"deep-research-20260702T0100Z-003","run_id":"deep-research-20260702T0100Z","task_class":"research-question","scope_hint":"queue.jsonl primitive for pipeline-* fan-out","payload":{"question":"What queue.jsonl primitive should RDCO standardize for pipeline-* fan-out?","category":"Trends","notion_page_id":"366f7d49-36d1-8129-b428-cd1f50fb6377","notes":"follow-up from 2026-05-20 fan-out brief"},"depends_on":[],"priority":20,"cost_ceiling":{"usd":5.00,"max_tokens":400000,"max_tool_calls":{"qmd":5,"websearch":3,"webfetch":3},"wall_clock_s":1200},"result_path":"results/deep-research-20260702T0100Z-003.md","attempt":0,"max_attempts":2,"created_at":"2026-07-02T01:00:04Z"}
Open follow-ups
- Validate
mkdir-as-atomic-claim on the actual scratch filesystem (APFS local = atomic; a network/iCloud-synced path breaks the guarantee). Decide the canonicalruns/location accordingly. - Should
cost_ceiling.usdbecome a hard gate? That needs a local metering proxy (LiteLLM-style) in front of the Anthropic API so per-sub-agent spend is observable; without it, tool-call caps + wall-clock are the only hard limits. Is the proxy worth standing up? - Default
lease_ttl+ heartbeat interval tuning: research workers (minutes) vs code/critic workers (longer). One global default or per-task_class? - Gapfill re-queue policy: concrete rule for when the planner appends under-covered-scope tasks (the model-drift gap from the 2026-05-20 brief).
- Package the pull-loop as a shared
/queue-libhelper skill vs inlining per pipeline — the skill-composition decision (and how the station-* skills migrate to it). - At what pool size K does the orchestrator itself become the bottleneck? The 2026-05-20 brief flagged ~50 as Glasswing's implied ceiling; RDCO's Agent-tool concurrency limit is the real constraint to measure.
Related
- [[2026-05-20-multi-agent-fanout-architectural-patterns]] — the parent brief that committed the queue.jsonl + per-task scratch-dir +
/deep-research-first direction and the per-task-cost-ceiling constraint this spec fills in. - [[2026-06-10-multi-agent-resumability-checkpointing-caf]] — the durable-state discipline (single authoritative record, write-artifact-before-mark invariant, startup integrity check) ported into the claim/lease protocol.
- [[2026-06-28-agent-brigade-rough-edges-editor-pass]] — the station/brigade vocabulary and the per-run scratch-dir this primitive extends; also the "lock one canonical exit set" discipline that maps to task terminal states (done/failed/killed/dead-letter).
- [[2026-05-18-implementation-notes-pattern-for-sub-agent-dispatches]] — the sub-agent dispatch discipline the worker pull-prompt must satisfy.
Sources
Vault:
rdco-vault/06-reference/research/2026-05-20-multi-agent-fanout-architectural-patterns.mdrdco-vault/06-reference/research/2026-06-10-multi-agent-resumability-checkpointing-caf.mdrdco-vault/08-tooling/2026-06-28-agent-brigade-rough-edges-editor-pass.mdrdco-vault/02-sops/2026-05-18-implementation-notes-pattern-for-sub-agent-dispatches.md~/.claude/skills/deep-research/SKILL.md,~/.claude/skills/station-spec-author/SKILL.md(current per-run scratch-dir + dequeue shapes)
Web:
- turbopuffer — a distributed queue in a single JSON file on object storage
- Munder Difflin — File-Based Coordination vs Message Queues for AI Agents
- AI Security Gateway — LLM Token Budget Strategies for Agents (5 layers)
- Nexgismo — AI Agent Budget Guards: Stop Runaway API Costs
- SupraWall — Stop Runaway AI Agent Costs: Hard Budget Caps
- LogRocket — Why your AI agent needs a task queue (and how to build one)
- SoloPHP Job-Queue — atomic claim, visibility timeout, backoff
- Netdata — Using FOR UPDATE SKIP LOCKED for queue workflows