Agent Dispatch
The Agent Dispatch system tracks Claude account capacity across multiple subscription accounts, identifies work that should be done when capacity is available, and fires that work as Stepwise flows in response to a daily digest that Zack approves via Telegram.
Overview
Vita runs multiple Claude Max accounts concurrently. Each account has token rate limits measured over rolling 5-hour and 7-day windows. The dispatch system answers: "How much slack exists right now, and what should we do with it?"
The system operates in three phases:
- Capacity tracking β record token consumption per account; project remaining slack across rolling windows
- Workload catalog β daily ranking of audit, mining, and research jobs ordered by priority and staleness
- Dispatch β queue approved workloads and fire them as Stepwise flow jobs
Accounts
Accounts are registered in data/capacity/accounts.json, modeled by the Account dataclass in accounts.py. Each account has:
idβ short identifier (e.g.,acct-a)nameβ human label (e.g.,primary/secondary/tertiary)planβmax-5xormax-20x(maps to estimated token budgets)max_5h_tokensβ rolling 5-hour token ceilingmax_weekly_tokensβ rolling 7-day token ceilingstatusβactive,paused, orretired(onlyactiveaccounts are projected)notesβ free-text note
On first run with no accounts.json, load_accounts() seeds three accounts (one max-20x, two max-5x) and writes them to disk.
Default budgets (approximate, tunable):
| Plan | 5-hour budget | Weekly budget |
|---|---|---|
max-20x |
5,000,000 | 35,000,000 |
max-5x |
1,500,000 | 10,000,000 |
pro |
300,000 | 2,000,000 |
Anthropic does not publish exact token figures, so these are estimates. The system treats capacity as a "slack" judgment rather than a precise ceiling.
Note: token recording is not auto-wired.
record_run()appends(timestamp, tokens)entries to the per-accountrolling_5h/rolling_7darrays indata/capacity/state.json, andget_state()prunes them by window and computes used/remaining. But nothing in the pipeline callsrecord_run()automatically β the only writer is the manualcapacity record <acct> <toks>command. In practice the rolling arrays sit empty, so the digest's capacity table reads near-zero usage (full slack). Auto-recording consumption from Claude usage data is the missing leg; until it lands, the capacity numbers are a stub, not a live meter.
Workload Catalog
The catalog is regenerated daily by generate_catalog() from three sources. Each pool is filtered to items past their rotation cadence ("due"), sorted by priority then descending staleness, capped at the top 12 per kind (research at 6), tagged with short codes, and written to data/capacity/workloads/catalog.jsonl. The digest then surfaces a smaller slice of recommendations on top of that (see Daily Digest).
Audit workloads (prefix A)
Pull from data/capacity/subsystems.jsonl β a registry of vita subsystems (briefing, scheduling, executive loop, CGM sync, etc.). Each subsystem has an audit_priority (high/medium/low) and a last_audited timestamp.
Rotation cadence: - high priority: audit every 30 days - medium: every 60 days - low: every 120 days
Subsystems past their rotation date appear in the catalog. Each audit runs via flows/capacity-audit β a single-agent Stepwise flow that reads the subsystem's code and data, then produces a findings report at data/audits/.
Mining workloads (prefix M)
Pull from data/capacity/mine-targets.jsonl β a list of historical data archives (conversation logs, interview transcripts, Slack exports, etc.) that benefit from periodic deep re-reads to extract identity observations, commitments, and patterns.
Rotation cadence: - high: re-mine every 60 days - medium: every 120 days - low: every 240 days
Mining runs via flows/capacity-mine and writes extracted insights to data/mining/.
Research workloads (prefix R)
Generated from two sources by _heartbeat_research_candidates():
- Top 5 topics from data/heartbeat/topics.json (silicon-zack's curiosity list, nudged daily) β tagged kind_hint: "heartbeat", priority medium
- A small hardcoded list of systemic-issue research seeds (open design questions flagged in identity state / reflection) β tagged kind_hint: "systemic-issue", priority high
The systemic-issue seeds are literally inlined in workloads.py (a fixed set of open-design-question stubs), so they are always present in the research pool regardless of staleness. Research workloads carry no last_* timestamp and are never filtered for rotation; they are simply ranked alongside the heartbeat candidates.
Research runs via flows/capacity-research and writes reports to data/reports/. That flow is two-step: a deep_research agent (cost cap 5.0 USD / 30 min) followed by a propose_work step (3.0 USD / 15 min).
Daily Digest
The digest is the primary interface. It is built and sent daily (via the flows/capacity-digest Stepwise flow) and delivers:
- Capacity status table β 5-hour and weekly usage per account, aggregate slack, next window reset
- Recommendations β top 4 audits (A1βA4), 3 mines (M1βM3), 2 research (R1βR2), sorted by priority and staleness, with short codes for approval
- In-flight / queue β any currently running or queued jobs
- Recently completed β last 5 finished runs with output paths
The Telegram message includes a reply_handler of type capacity_approval. Replies are routed through scripts/telegram_bot.py β scripts/capacity/reply_handler.py.
Reply grammar
| Reply | Action |
|---|---|
run A1 M2 R1 |
Queue those workloads by short code and fire immediately |
run all high |
Queue all high-priority recommendations and fire |
run all audits |
Queue all audit recommendations |
run all |
Queue everything in today's recommendations |
skip all |
Acknowledge, no action |
show queue |
Return current queue status |
kick off / fire |
Fire all currently queued jobs |
Dispatch Flow
When a workload is approved:
queue_workload()appends an entry todata/capacity/workloads/queue.jsonlwith statusqueuedand a freshrun_id(run-<8hex>), deduplicating byworkload_idif one is already queued/runningrun_queued()picks up tomax_jobsqueued entries and fires each via asubprocess.runof:Inputs are flow-specific (stepwise run flows/capacity-<kind> --name <job-name> --input KEY=VALUE ... --async_stepwise_inputs): audits passsubsystem_id/primary_path/subsystem_name; mines passtarget_id/target_path/title; research passestopic/description. Background dispatch appends--async;background=Falseswaps in--wait.- The queue entry transitions to
running, recordingstarted_at,stepwise_job_id(last line of stepwise stdout), andstepwise_cmd; a run record is mirrored todata/capacity/runs/<run_id>.json. If stepwise returns nonzero, the entry goes tofailedwith the last stderr line. - On completion,
mark_run_completed()records actual token usage, stores output path, and callsmark_completed()to writelast_audited/last_minedback into the source registry file
Workload states
queued β running β completed
β failed
β cancelled
β dry-run (capacity fire --dry-run / run_queued(dry_run=True))
dry-run is a terminal preview state: run_queued(dry_run=True) writes the command it would have run into the entry's error field without invoking stepwise.
File Locations
| Path | Purpose |
|---|---|
scripts/capacity/__init__.py |
Public API (queue_workload, run_queued, build_digest, etc.) |
scripts/capacity/accounts.py |
Account registry, default budgets |
scripts/capacity/tracker.py |
Rolling window token tracking, capacity snapshots |
scripts/capacity/workloads.py |
Catalog generation, staleness calculation |
scripts/capacity/dispatch.py |
Queue management, Stepwise job launch |
scripts/capacity/digest.py |
Markdown + email + Telegram digest composer |
scripts/capacity/reply_handler.py |
Parse and execute Telegram approval replies |
scripts/capacity/cli.py |
CLI: python -m capacity <command> |
data/capacity/accounts.json |
Account registry |
data/capacity/state.json |
Rolling token usage per account |
data/capacity/subsystems.jsonl |
Subsystem registry (audit source) |
data/capacity/mine-targets.jsonl |
Mining target registry |
data/capacity/workloads/catalog.jsonl |
Today's generated workload catalog |
data/capacity/workloads/queue.jsonl |
Dispatch queue |
data/capacity/runs/<run_id>.json |
Per-run records |
data/capacity/digests/YYYY-MM-DD.md |
Saved digest markdown |
flows/capacity-audit/ |
Stepwise flow: subsystem audit |
flows/capacity-mine/ |
Stepwise flow: archive deep-read |
flows/capacity-research/ |
Stepwise flow: research task |
flows/capacity-digest/ |
Stepwise flow: build + send daily digest |
CLI
# Capacity snapshot across all accounts
python -m capacity status
# Regenerate and print today's workload catalog
python -m capacity catalog
# Build digest (print only)
python -m capacity digest
# Build and send digest (email + Telegram)
python -m capacity digest --send
# Queue workloads by code and fire
python -m capacity run A1 M2 R1
# Show current queue
python -m capacity queue
# Fire all queued jobs (up to 3 by default; --dry-run to preview only)
python -m capacity fire --max 3
# Record token consumption against an account (optional --workload-id)
python -m capacity record acct-a 75000
# Mark a run done (writes back last_audited/last_mined); --failed to mark failed
python -m capacity complete run-1a2b3c4d --tokens 90000 --output data/audits/foo.md
# Run the telegram reply parser from the CLI
python -m capacity reply "run A1 M2"
# Show account registry
python -m capacity accounts
The module entry point is python -m capacity <command> (PYTHONPATH must include scripts/, per the repo convention β import as capacity, not scripts.capacity).
Scheduling
The daily digest runs as a native Stepwise cron schedule, not through the VitaScheduler. The schedule lives in the schedules table of .stepwise/stepwise.db:
| Field | Value |
|---|---|
id |
sched-a3479a1c |
name |
capacity-digest-daily |
flow_path |
flows/capacity-digest |
cron_expr |
0 7 * * * (07:00 daily) |
timezone |
America/Los_Angeles |
status |
active |
You can recreate it with:
stepwise schedule create flows/capacity-digest --cron "0 7 * * *"
There are zero capacity entries in data/scheduler/executions-*.jsonl, confirming the VitaScheduler does not drive this digest β Stepwise's own cron does. The digest flow ultimately calls send_digest() (email + Telegram). The Telegram message carries a capacity_approval reply handler, so Zack's reply is parsed and routed by the Telegram bot without additional interaction.
Design Notes
- Telegram-driven approval loop β capacity is only consumed when Zack greenlit it. The system never auto-runs workloads without the digest approval step.
- Deduplication β
queue_workload()skips re-queuing a workload that is already queued or running for the same target. - Rotation tracking β
last_audited/last_minedare written back to the source JSONL files on completion. The next day's catalog generation reads these timestamps to determine what is due. - Cost limits β every workload flow caps cost and duration per step (set in each
FLOW.yaml):
| Flow | max_cost_usd |
max_duration_minutes |
|---|---|---|
capacity-audit |
3.0 | 20 |
capacity-mine |
5.0 | 30 |
capacity-research (deep_research) |
5.0 | 30 |
capacity-research (propose_work) |
3.0 | 15 |
This bounds the blast radius of an idle-capacity job: a runaway audit or research run is force-stopped at its cap rather than draining an account.
Where to go next
- Telegram β how the
capacity_approvalreply handler is registered and routed - Executive Loop β the always-on autonomous cycle that this idle-capacity work runs alongside
- Research β the deep-research harness invoked by
capacity-researchworkloads - Model Guidance β model routing for the agents these flows spawn