FireTrident: a colony, not a queue
Scaling benchmark, 2026-10-12. Live runs with real Artifacts commits (the serial trunk writer on the production deployment; group commit and the scale runs on preview deployments) and a discrete-event simulator calibrated on them, comparing FireTrident with the common design: exclusive file locks plus one serialized merge queue. Every chart and tile says whether its points are MEASURED or SIMULATED, and every count of failed calls names its run.
Throughput on the live workloadMEASUREDSIMULATED
Synthetic agents on the production deployment with the serial trunk writer, 4 runs at 10 to 1,000 concurrent agents. Each change adds one file in one of 64 directories, and every landing is a real trunk commit pushed with compare-and-swap. The lines are the simulator replaying the same workload with costs fitted to these runs: it lands within about 10% of every measured point.
Trunk group commit, built and MEASURED (2026-10-12-g1, on a preview deployment, before the scale fixes). The trunk writer now takes every lane batch that queued while its previous cycle ran: it re-validates each proposal against trunk and against the batches ahead of it, builds one commit per batch chained on the one before (so lineage and why stay per landing), pushes trunk once and all lineage notes once. Same driver, same workload: 10 agents 216/min (serial writer 92/min), 200 agents 690/min (serial writer 126/min), 1,000 agents 515/min (serial writer 138/min), stopped early by the driver's error guard.
Scale: 1,000 to 5,000 agents on one colonyMEASURED
These are synthetic agents: no model calls, no agent-side git and no work time, with the review layer turned off on the colony (POST /api/colonies/:id/decide/policy {"enabled":false}), so the runs measure coordination and landing only. Same driver and workload as above: each change adds one file in one of 64 directories, and every landing is a real Artifacts commit pushed with compare-and-swap. 120 s per run with a 10 s ramp; outcomes reach the agents over the live WebSocket with a 20 s fallback poll; group commit on; one colony (g6-scale5k) on a preview deployment with its own Durable Objects and a pre-forked workspace pool. Raw runs: ../2026-10-12-scale/.
What changed since the group-commit run: takes share one read of each region's claims, at most 128 takes score at once and waiting agents are paced, outcomes are pushed to agents instead of polled, stopped agents release their claims at once, leasing a workspace is pure SQL with one token kept per slot, and lineage notes use git's two-character fanout (docs/ARCHITECTURE.md, Scale).
Scale runs (MEASURED). ttl: time to land. cycle, proposals/cycle, queue (waiting for the trunk writer), pull (proposal blobs from workspace forks) and push: trunk writer medians over the steady window
| agents | landed/min | commits/min | ttl p50/p90/p99 s | take p50/p99 ms | cycle s | proposals/cycle | queue s | pull s | push s | calls | failed |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1,000 | 2,924 | 473 | 13.0 / 17.0 / 20.9 | 580 / 5,251 | 3.5 | 196 | 5.7 | 1.8 | 1.1 | 31,308 | 0 |
| 2,000 | 2,714 | 451 | 14.8 / 19.5 / 23.0 | 570 / 1,979 | 3.9 | 195 | 5.9 | 2.2 | 1.2 | 45,034 | 0 |
| 5,000 | 2,778 | 468 | 12.5 / 17.4 / 27.2 | 653 / 3,960 | 3.4 | 196 | 6.0 | 2.2 | 1.0 | 50,368 | 78 |
Failed calls, per run. 1,000 agents: 0 of 31,308 calls failed. 2,000 agents: 0 of 45,034 calls failed. 5,000 agents: 78 of 50,368 calls failed (0.15%), all in the first 33 s. The error kinds the driver recorded (it keeps the first eight distinct messages) are connection failures on the bench client (ENOTFOUND, ECONNRESET, UND_ERR_CONNECT_TIMEOUT), while one bench machine held up to 3,597 open sockets around the end of the ramp, and the service answering 500 Durable Object reset because its code was updated. (a Durable Object restarting on newly deployed code).
Lineage. After the runs, stock git fetched trunk and refs/notes/firetrident from colony g6-scale5k's Artifacts repository: 5,894 of 5,894 landing commits carry a lineage note (0 without), 60 sampled git notes show name their landing (0 mismatches), and the notes tree is 256 two-character fanout directories with 0 flat notes.
Next limit. The next limit is TrunkDO, the single trunk writer. Each commit cycle took 3.4–3.9 s at the median and carried 195–196 proposals, against its cap of 200 (CYCLE_MAX_PROPOSALS), so throughput is flat past 1,000 agents: 2,714–2,924 landings/min from 1,000 to 5,000 agents. About 6 s of queueing for the writer (5.7–6.0 s p50) dominates time to land (12.5–14.8 s p50). Inside a cycle, reading proposal blobs out of workspace forks takes longest (1.8–2.2 s p50), then the trunk push (1.0–1.2 s).
Earlier runs on the same colony, not in the table: 3,485/min at 1,000 agents and 3,342/min at 2,000 agents with the build before the take gate; the trunk cycle grew from 2.9 s to 3.9 s p50 over the 5 completed runs as the bench's trunk tree grew (each intent adds a file). 2 attempts at 5,000 agents stopped early: build before the take gate (10:56Z): ColonyDO isolate exceeded its memory limit about 20 s in (thousands of takes in flight, each holding up to 200 scored candidates) (error rate 84% over 5 s at 20 s); and take gate build, started 1.5 min after its deploy: a burst of dropped connections (cause not recorded yet) and 'reset because its code was updated' tripped the guard at 15 s (error rate 35% over 5 s at 15 s). summary.json records all of them; their raw files were not kept.
Throughput from 10 to 100,000 agents on hono's historySIMULATED
Write sets drawn from 2285 real changes to honojs/hono, grown along co-change edges. Read sets are the written files' direct imports. The repo is a monorepo of 100 copies (regions at depth 3), and agents work a log-normal median of 60 s per change. Mean of 3 seeds; hover a point for the seed range.
Wasted work: rework % of all work time
Time to land, p99 (s, submit to landing)
Conflicts per 1,000 submitted changes
Coordination messages per landed change (log)
The seizure detector: one landing, 30 stale returnsMEASURED
Live, colony bench-w2-cascade. 30 agents in src/core read src/core/config.ts and kept working. A real commit to that file landed, and all 30 in-flight proposals came back stale, each attributed to that one landing. σ jumped to 30, so the lane's cadence stretched from 1 s to 8 s. The reworked proposals then landed in inhibited batches until σ fell below 1. σ points are the sigma events on /api/colonies/:id/live; the line is GET /field polled every second.
Tables
Live runs (MEASURED, laptop-side latencies; serial writer on production, group commit and scale fixes on previews)
| agents | trunk writer | landed/min | commits/min | batch | ttl p50 s | ttl p99 s | take p50 ms | take p99 ms | propose p50 ms | propose p99 ms | admission waits | returned | failed / calls |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 10 | serial | 92.4 | 92.4 | 1.00 | 4.1 | 6.1 | 354 | 688 | 115 | 324 | 0 | 0 | 0 / 868 |
| 10 | group commit | 215.6 | 211.6 | 1.02 | 0.8 | 8.0 | 474 | 1,514 | 117 | 425 | 0 | 0 | 0 / 732 |
| 50 | serial | 84.4 | 84.4 | 1.01 | 24.7 | 44.3 | 487 | 3,402 | 123 | 498 | 0 | 0 | 0 / 1,302 |
| 200 | serial | 125.9 | 71.8 | 1.47 | 54.3 | 108.8 | 1,086 | 7,141 | 127 | 648 | 49 | 0 | 0 / 4,192 |
| 200 | group commit | 690.4 | 589.9 | 1.22 | 2.9 | 9.3 | 5,985 | 27,469 | 220 | 840 | 233 | 0 | 22 / 7,429 |
| 1,000 | serial | 138.5 | 63.7 | 1.99 | 61.6 | 127.9 | 714 | 3,617 | 155 | 524 | 21,439 | 16 | 0 / 29,307 |
| 1,000 | group commit | 515.1 | 369.2 | 1.41 | 4.8 | 10.4 | 15,033 | 27,823 | 1,327 | 4,164 | 2,821 | 0 | 816 / 9,927 (stopped early) |
| 1,000 | group commit + scale fixes | 2923.6 | 473.1 | 5.45 | 13.0 | 20.9 | 580 | 5,251 | 191 | 954 | 10,751 | 0 | 0 / 31,308 |
| 2,000 | group commit + scale fixes | 2713.9 | 450.9 | 5.24 | 14.8 | 23.0 | 570 | 1,979 | 204 | 807 | 23,607 | 0 | 0 / 45,034 |
| 5,000 | group commit + scale fixes | 2777.7 | 468.0 | 5.30 | 12.5 | 27.2 | 653 | 3,960 | 255 | 2,054 | 30,651 | 0 | 78 / 50,368 |
Simulator check against the live runs
| agents | trunk writer | measured/min | simulated/min | error | ttl p50 measured s | ttl p50 sim s | batch measured | batch sim |
|---|---|---|---|---|---|---|---|---|
| 10 | serial writer | 92.4 | 85.7 | -7% | 4.1 | 4.3 | 1.00 | 1.00 |
| 50 | serial writer | 84.4 | 86.4 | +2% | 24.7 | 28.3 | 1.01 | 1.00 |
| 200 | serial writer | 125.9 | 138.0 | +10% | 54.3 | 55.2 | 1.47 | 1.72 |
| 1,000 | serial writer | 138.5 | 144.9 | +5% | 61.6 | 58.2 | 1.99 | 1.80 |
| 10 | group commit | 215.6 | 212.9 | -1% | 0.8 | 1.4 | 1.9 / cycle | 3.12 |
| 200 | group commit | 690.4 | 680.8 | -1% | 2.9 | 11.4 | 18.6 / cycle | 45.60 |
| 1,000 | group commit | 515.1 | 712.0 | +38% | 4.8 | 28.9 | 16.6 / cycle | 85.71 |
hono workload (SIMULATED, mean of seeds)
| agents | model | landed/min | batch | rework % | conflicts/1k | ttl p50 s | ttl p99 s | msgs/landed | msgs/agent action |
|---|---|---|---|---|---|---|---|---|---|
| 10 | Baseline | 6.6 | 1.00 | 0.1 | 3 | 1.0 | 2.2 | 8 | 1.66 |
| 10 | FireTrident, serial writer | 6.6 | 1.00 | 0.4 | 7 | 0.8 | 14.3 | 324 | 72.16 |
| 10 | FireTrident + group commit | 6.6 | 1.00 | 0.4 | 7 | 0.8 | 14.3 | 322 | 71.74 |
| 100 | Baseline | 56.2 | 1.00 | 5.7 | 72 | 14.3 | 142 | 12 | 1.37 |
| 100 | FireTrident, serial writer | 62.1 | 1.00 | 12.3 | 75 | 1.4 | 204 | 294 | 56.36 |
| 100 | FireTrident + group commit | 62.0 | 1.21 | 12.7 | 77 | 1.0 | 212 | 295 | 60.45 |
| 1,000 | Baseline | 51.9 | 1.00 | 21.7 | 318 | 477 | 1,018 | 223 | 1.02 |
| 1,000 | FireTrident, serial writer | 85.6 | 1.02 | 42.5 | 397 | 270 | 974 | 1,391 | 7.37 |
| 1,000 | FireTrident + group commit | 204.5 | 2.91 | 45.3 | 319 | 1.6 | 651 | 670 | 7.51 |
| 10,000 | Baseline | 51.9 | 1.00 | 21.7 | 318 | 477 | 1,018 | 3,425 | 1.00 |
| 10,000 | FireTrident, serial writer | 85.8 | 1.02 | 41.4 | 386 | 270 | 936 | 5,022 | 1.33 |
| 10,000 | FireTrident + group commit | 205.6 | 2.93 | 46.9 | 333 | 1.6 | 655 | 2,145 | 1.35 |
| 100,000 | Baseline | 51.9 | 1.00 | 21.7 | 315 | 475 | 1,021 | 35,419 | 1.00 |
| 100,000 | FireTrident, serial writer | 85.8 | 1.02 | 41.9 | 389 | 271 | 965 | 40,683 | 1.03 |
| 100,000 | FireTrident + group commit | 207.2 | 2.95 | 45.7 | 322 | 1.5 | 636 | 17,131 | 1.03 |
Sensitivity: checks take 30 s (SIMULATED, hono workload)
FireTrident agents run checks in their own workspace before proposing, so the checks run in parallel. The baseline also re-runs them in the queue after every rebase, so they run serially.
| agents | model | landed/min | ttl p50 s | rework % |
|---|---|---|---|---|
| 10 | Baseline | 1.9 | 152.8 | 0.0 |
| 10 | FireTrident | 4.9 | 0.8 | 0.0 |
| 100 | Baseline | 1.9 | 700.1 | 0.0 |
| 100 | FireTrident | 47.2 | 1.0 | 10.1 |
| 1,000 | Baseline | 1.9 | 675.9 | 0.4 |
| 1,000 | FireTrident | 86.1 | 274.5 | 41.0 |
| 10,000 | Baseline | 1.9 | 675.9 | 0.4 |
| 10,000 | FireTrident | 85.7 | 269.9 | 42.9 |
| 100,000 | Baseline | 1.9 | 708.2 | 0.0 |
| 100,000 | FireTrident | 85.7 | 269.0 | 42.4 |
Trunk writer cost per batch (MEASURED: k proposals in one region, landing time after the first proposal)
| k | commits: size @ time |
|---|---|
| 2 | 2 @ 1.12 s |
| 10 | 8 @ 1.51 s, 2 @ 3.97 s |
| 25 | 24 @ 3.16 s, 1 @ 4.19 s |
| 50 | 49 @ 4.42 s, 1 @ 6.10 s |
Method and assumptions
- Live (MEASURED). Synthetic REST agents take, propose, and poll their outcome on the in-platform schedule (1.5 s, ×1.6, capped at 15 s). Each proposal writes a new file whose blob already exists in every workspace fork. That makes the trunk writer do the real work (read the blob from the agent's fork, build the tree, push trunk with compare-and-swap, write the lineage note), while the agents need no git of their own. The review prong (reflex and LLM triage) was turned off on the two bench colonies, so the numbers isolate coordination and landing. Latencies are laptop-side and include about 100 ms of internet round trip.
- Calibration. In-platform take 250 ms and propose 30 ms; lane overhead 100 ms. Trunk writer per commit: 620 ms + 72 ms per proposal (fitted on the swarm runs and a single-region batch experiment). The baseline lands a change in 692 ms (the same writer with one proposal) plus 350 ms to rebase a stale change (fetch and replay, from the Artifacts spike). Checks cost 0 ms in the headline runs, the most favourable setting for the baseline.
- FireTrident model. Mirrors the deployed code: take scores the 200 oldest open intents with core
chooseIntent, using repulsion from claims andadmissionOpenper region. Each proposal joins the lane of its primary region, where corevalidateProposalandselectBatchrun. The tick interval islaneIntervalMs×inhibitionFactor(σ), and σ is computed with corebranchingRatioover recorded causes, where returns are never parents. One trunk writer per colony serves the lanes' commits one at a time and re-validates each batch. A returned intent reopens and is redone in full. - Baseline model. Exclusive locks on write sets, offered from the same 200 oldest tasks, with the same 3 s retry hint. One FIFO merge queue: a stale change is rebased and its checks re-run, and a change whose read or written files changed since its base fails (the same rule as read-set validation). It goes back to its agent, which keeps its locks, reworks half of the change, and resubmits.
- Scale runs (MEASURED). The same synthetic agents with no work time, the review layer off, 120 s per run, outcomes pushed over the live WebSocket (20 s fallback poll), and every run's agents stopped at its end so their claims never hold the next run's admission. Throughput is counted from 25 s to 120 s, after the ramp.
- Not modelled. The coordinator's own request capacity (one ColonyDO per colony) is assumed to scale in the simulator. Live, it held in the serial trunk writer run (1,000 agents: 0 of 29,307 calls failed, take p99 3,617 ms); in the group-commit run, which pushed 3.7× as many landings through it, it saturated at 1,000 agents (816 of 9,927 calls failed (8.22%); the driver stopped the run: error rate 38% over 5 s at 60 s); after the scale fixes: 1,000 agents, 0 of 31,308 calls failed; 2,000 agents, 0 of 45,034 calls failed; 5,000 agents, 78 of 50,368 calls failed (0.15%), all in the first 33 s. Fork creation is not modelled (the pool is assumed warm).
What changed in RegionDO for the demo
- σ treats returns as leaves. A returned proposal changed nothing on trunk, so it cannot make anything stale. Counting every event in the window as a parent pinned a burst of N returns from one landing at N/(N+1) < 1 and hid every cascade. σ is now stale returns per landed proposal, with core
branchingRatio's window semantics (60 s window, 5 s settle). - σ is re-evaluated after the lane empties. While σ > 1, or while a recent return may still raise it, an empty lane keeps a 2 s alarm. Previously σ was sent only after ticks that had proposals, so neither the spike (which arrives after the settle time) nor the recovery reached the UI. An enqueue into an empty lane replaces that alarm, so the inhibited cadence still holds.
- Inhibition is published. Every alarm sends
sigma, and while the factor is above 1 (and once when it lifts) it also sendsmetriceventsinhibition:<region>andlaneDelayMs:<region>. Deployed 2026-10-12 06:14 UTC; DO tests pass, including a new case where 4 returns from 1 landing give σ = 4.
Findings and open issues
- Trunk group commit lifts the one-commit-at-a-time ceiling (MEASURED). At 1,000 agents the colony landed 515 changes/min, 3.7× the serial writer's 138/min, with 0 returns across the group-commit runs. Failed calls in the group-commit runs: 10 agents, 0 of 732 calls failed; 200 agents, 22 of 7,429 calls failed (0.30%); 1,000 agents, 816 of 9,927 calls failed (8.22%). The 1,000-agent run stopped early (error rate 38% over 5 s at 60 s): the one ColonyDO that serves every take, poll and propose saturated, so that run's rate is measured over a window it only partly worked in. The writer ran 24 cycles/min, each landing 11.8 lane batches (16.6 proposals) as chained commits behind one trunk push and one notes push. A cycle took 890 ms at the median: pull 132 ms, build 43 ms, push 517 ms. Each lane batch is still its own commit with its own lineage note, so
whystays per landing. - The scale fixes remove the coordinator limit (MEASURED; synthetic agents, no model calls, review off). 1,000 agents 2,924/min, 2,000 agents 2,714/min, 5,000 agents 2,778/min, against 515/min for the group-commit run at 1,000 agents before them. Failed calls: 1,000 agents, 0 of 31,308 calls failed; 2,000 agents, 0 of 45,034 calls failed; 5,000 agents, 78 of 50,368 calls failed (0.15%), all in the first 33 s.
- Next limit: the trunk writer. The next limit is TrunkDO, the single trunk writer. Each commit cycle took 3.4–3.9 s at the median and carried 195–196 proposals, against its cap of 200 (
CYCLE_MAX_PROPOSALS), so throughput is flat past 1,000 agents: 2,714–2,924 landings/min from 1,000 to 5,000 agents. About 6 s of queueing for the writer (5.7–6.0 s p50) dominates time to land (12.5–14.8 s p50). Inside a cycle, reading proposal blobs out of workspace forks takes longest (1.8–2.2 s p50), then the trunk push (1.0–1.2 s). - Fixed: landed proposals reported as returned. A lane's
TrunkDO.landcall could fail on the lane's side after the trunk writer had landed the batch (the logs showDurable Object reset because its code was updatedduring deploys). The retry then reported those proposals asalready landed in lnd_…(13 times at 1,000 agents), and a lane re-validating them saw them stale against their own new blob (on the Hono run one landed proposal was returned and its intent taken and abandoned 19 times). Outcomes are now idempotent per proposal id: the trunk writer reports earlier landings inprior, and a lane asksTrunkDO.landingsOfbefore returning a stale-looking proposal, so a landed proposal is always settled as landed. - Admission ignores the shared trunk writer. The floor of 3 agents per region admits far more agents than one writer serves when a colony has many regions. With the 200-oldest candidate window and repulsion, contended intents clog the window and are eventually taken anyway. That is why rework is about 42% vs 22% for locks on hono's history.
- σ was per region (fixed since). A stale read caused by a landing in another region got no cause, so cross-region cascades stayed invisible to the governor. A stale return with no local cause now records the other region's landing as its parent (
RegionDO.landedFor). - The review prong triages every proposal (deployed 06:32 UTC). It returned 41 of 87 synthetic proposals and added seconds each, so it was turned off on the two bench colonies with
POST /api/colonies/:id/decide/policy {"enabled":false}. The 10-1,000 agent runs predate it. Two other deploys happened during the runs (06:24 and 06:32 UTC); the 200-agent run spans the first. - Account use. 286 workspace forks (bench-w2-load 249, bench-w2-cascade 37), 8 seeds, 2 trunk imports. Both colonies are kept for re-runs and video capture;
DELETE /api/colonies/:idremoves them. The group-commit runs used one more colony on a preview deployment (g1-bench: 253 workspace forks, 4 seeds, 1 trunk import). The scale runs used one colony on another preview (g6-scale5k, with a workspace pool pre-forked to a target of 6,000).
Reproduce
# live runs talk to a deployment: FT_URL and FT_TOKEN (its admin token) come from the environment or ~/.config/firetrident/env
node packages/bench/src/workload.ts --repo /tmp/ft-w2/hono --tree <checkout of hono HEAD> --out packages/bench/data/hono-workload.json
FT_URL=<your deployment> FT_TOKEN=<admin token> node packages/bench/src/live.ts --colony bench-w2-load --agents 200 --duration 90 --out results/live-n200.json
FT_URL=<your deployment> FT_TOKEN=<admin token> node packages/bench/src/scenarios/batchcost.ts --colony bench-w2-load --sizes 2,10,25,50 --out results/batchcost.json
FT_URL=<your deployment> FT_TOKEN=<admin token> node packages/bench/src/scenarios/cascade.ts --colony bench-w2-cascade --readers 30 --out results/cascade.json
node packages/bench/src/sim/sweep.ts --mode live --agents 10,50,200,1000 --seeds 1,2,3 --out results/sim-live.json
node packages/bench/src/sim/sweep.ts --mode history --agents 10,100,1000,10000,100000 --seeds 1,2,3 --duration 1200 --warmup 300 --out results/sim-history.json
node packages/bench/src/sim/sweep.ts --mode history --agents 10,100,1000,10000,100000 --test-ms 30000 --out results/sim-history-test30s.json
# group commit: same driver, intents topped up so the supply never runs out
FT_URL=<your deployment> FT_TOKEN=<admin token> node packages/bench/src/live.ts --colony g1-bench --agents 200 --duration 90 --intents 3000 --out results/2026-10-12-g1/live-n200.json
# scale: synthetic agents on a colony with review off (POST /api/colonies/<colony>/decide/policy {"enabled":false}) and a pre-forked pool
FT_URL=<your deployment> FT_TOKEN=<admin token> node packages/bench/src/live.ts --colony <colony> --agents 5000 --duration 120 --out results/2026-10-12-scale/live-n5000.json
node packages/bench/src/report.ts packages/bench/results/2026-10-12 packages/bench/results/2026-10-12-g1 packages/bench/results/2026-10-12-scaleData: live-n*.json, sim-live.json, sim-history.json, sim-history-test30s.json, cascade.json, batchcost.json, results.json next to this page; ../2026-10-12-g1/ (group commit); ../2026-10-12-scale/ (scale runs, notes-check.json, summary.json). The production runs behind the README's results (Hono, a new project from a spec, an external agent over MCP, the vaccination meter) are in ../2026-10-12-prod/. Code: packages/bench. Generated 2026-10-12T20:57:41.536Z.