Skip to content

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:

  1. Capacity tracking β€” record token consumption per account; project remaining slack across rolling windows
  2. Workload catalog β€” daily ranking of audit, mining, and research jobs ordered by priority and staleness
  3. 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-5x or max-20x (maps to estimated token budgets)
  • max_5h_tokens β€” rolling 5-hour token ceiling
  • max_weekly_tokens β€” rolling 7-day token ceiling
  • status β€” active, paused, or retired (only active accounts 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-account rolling_5h / rolling_7d arrays in data/capacity/state.json, and get_state() prunes them by window and computes used/remaining. But nothing in the pipeline calls record_run() automatically β€” the only writer is the manual capacity 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:

  1. Capacity status table β€” 5-hour and weekly usage per account, aggregate slack, next window reset
  2. Recommendations β€” top 4 audits (A1–A4), 3 mines (M1–M3), 2 research (R1–R2), sorted by priority and staleness, with short codes for approval
  3. In-flight / queue β€” any currently running or queued jobs
  4. 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:

  1. queue_workload() appends an entry to data/capacity/workloads/queue.jsonl with status queued and a fresh run_id (run-<8hex>), deduplicating by workload_id if one is already queued/running
  2. run_queued() picks up to max_jobs queued entries and fires each via a subprocess.run of:
    stepwise run flows/capacity-<kind> --name <job-name> --input KEY=VALUE ... --async
    
    Inputs are flow-specific (_stepwise_inputs): audits pass subsystem_id / primary_path / subsystem_name; mines pass target_id / target_path / title; research passes topic / description. Background dispatch appends --async; background=False swaps in --wait.
  3. The queue entry transitions to running, recording started_at, stepwise_job_id (last line of stepwise stdout), and stepwise_cmd; a run record is mirrored to data/capacity/runs/<run_id>.json. If stepwise returns nonzero, the entry goes to failed with the last stderr line.
  4. On completion, mark_run_completed() records actual token usage, stores output path, and calls mark_completed() to write last_audited / last_mined back 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_mined are 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_approval reply 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-research workloads
  • Model Guidance β€” model routing for the agents these flows spawn