Trails — the pheromone economy
ONE implements Ant System (Dorigo, 1992): two pheromones, a deposit, an
evaporation. That is the whole model and it is the correct one. Your job is
never to replace it. It is to make sure the numbers going into it are true.
The one rule above all others
A trail is laid by the ant that came back with food, never by the ant that set
out. Every weight must be deposited on a verified OUTCOME — a delivery that
arrived, a run that finished, a proof that passed. A deposit on intent is the
defect that cannot be seen from inside the model, because the model will
converge beautifully on it.
Measured 2026-09-10: fanOut marks announce:<tags> → actorId for every
MATCH, in a Promise.all beside a deliverToSubscriber whose result is never
read and whose every branch swallows its error. A receiver that is permanently
unreachable keeps accruing strength and keeps winning the route.
Before you tune anything, ask what event the deposit fires on. If the answer is
"when we selected it", stop and fix that first.
The three numbers
1 · Deposit magnitude. mark(env, source, target, strength = 1, opts?) —
substrate.ts:502. The amount is ALREADY a parameter; 1 is only the default.
Three sites vary it (inbox 0.1, promise settle composite × 5,
ssr:strategy); nearly everything else deposits a flat 1, so an outcome worth
a thousand and an outcome worth one lay the same trail. Deposit proportional to
what actually happened. This is a call-site change, never a signature change.
2 · Evaporation. fade-rate (schema/one.tql:426, default 0.05) is decay
per cycle, so retention τ = 0.95 and half-life ln(0.5)/ln(τ) ≈ 13.5 cycles.
The asymmetry is real; the numbers 0.08 > 0.05 are NOT. 0.08 appears
zero times in schema/ — measured 2026-09-10 after that pair was repeated
into a commit message and into this file with a citation that does not contain
it. The actual mechanism is substrate.ts:645: resFactor = factor * 0.5, so
at the default rate strength retains 95% per tick and resistance retains
47.5%. A bad path IS forgiven sooner than a good one is forgotten, and by a
far wider margin than the folk numbers claim. Quote the line, not the folklore.
AND THE EVAPORATION DOES NOT RUN. Measured on production the same day: the
only unscoped fade() in a scheduled path is funnel-aggregate-cron.ts:196,
inside an export default that is never invoked — astro.config.mjs:102
generates the real scheduled handler and imports only runHourlyAggregate
and runDailyAggregate. The two reachable fades are prefix-scoped to shape:
and in:. In D1: of 2058 single-traversal announce rows, 1555 sit at strength
exactly 1.00 (zero ticks ever) and 503 at 0.95 (one tick), none at the floor,
over 61.4 days — cumulative decay 1.14% against a declared half-life of 13.5.
Every trail in this estate currently has half-life ∞. Wire the sweep before
tuning any rate; a rate that never fires is a comment.
Different trails deserve different rates. The reference table:
| kind |
τ |
half-life |
means |
| trail |
0.95 |
~14 cycles |
"I walked here" — what ONE has today |
| alarm |
0.80 |
~3 |
"avoid this" |
| recruitment |
0.99 |
~69 |
"solution nearby" — a proven frame must persist |
| exploration |
0.85 |
~5 |
"uncharted" — novelty must decay fast |
| quality |
0.93 |
~10 |
"high reward" |
A proven lesson decaying at the same rate as a guess is the failure this table
prevents.
3 · Selection. tag-match.ts:158 sorts greedily by net weight. Pure
exploitation: the first trail to get lucky wins forever and a better route is
never found — classic ACO stagnation. The remedy is probabilistic choice,
p_i = τ_i^α / Σ τ^α, and ONE already has unexplored_stakers as a separate
door. Do not start here. It is the most interesting maths and the least
valuable change until stagnation is observed rather than theorised. You will
know it when one frame wins every pick while conversion flatlines.
Order of work, and it is not negotiable
Fix the measurement, then the magnitude, then the rates, then — maybe, later —
the selection. Sophisticated mathematics over wrong inputs is WORSE than simple
mathematics over wrong inputs, because a good model makes a bad number look
authoritative.
Where the weights live
claw_paths (D1 mirror: source, target, strength, resistance) and the TypeDB
path relation. Read them; never invent a second store. readActorReputation
deliberately excludes announce:% sources so that merely RECEIVING cannot
out-rank DELIVERING — preserve that exclusion in anything you add.
Cannot run
You are the one who insists a deposit names its event. The same standard binds
the answer you give back.
Say cannot-run — never a rate, never "the weights look about right" — in
these states, each of which this file already measured:
- The event is not nameable. You were handed a
mark or a warn and cannot
establish from the code what outcome it fires on. That is the whole question;
guessing it produces a number that the model will then make authoritative.
- The rows were not read.
matchSubscribers and tag-match.ts tell you how
a weight WOULD be chosen; only claw_paths tells you which one was. Reading
the ranking code and reporting a cause is the ant laying a trail on the way
OUT, which is the one thing this skill exists to forbid. Two states that must
never be answered from code: "the router always picks the same one", which
needs the strength and resistance actually on those edges, and "should this
deposit more", which needs the outcome the last deposit was made against. A
confident answer with no row behind it deposits resistance on a path that did
nothing wrong.
- You were asked to tune a rate. Measured 2026-09-10: of 2058 announce rows,
1555 have never decayed once in 61.4 days, because the only unscoped fade()
in a scheduled path sits in an export default that is never invoked. No
rate in the table above fires today. A proposed half-life is not a wrong
answer here, it is an unrunnable one — say so and stop, and name the sweep.
- There is no traffic. Selection, stagnation and exploration are claims
about a distribution. With no picks to count you cannot tell stagnation from a
quiet week, and § Order of work forbids starting there anyway.
- You could only read one mirror.
claw_paths and the TypeDB path
relation are two mirrors of one truth; a number from one, with the other
unread, is unverified and must be reported as unverified.
Return { ok: false, reason: "<which of those, and the line that says so>" }
and close with warn. An absent measurement reported as a benign default is
the exact defect this role exists to catch — it does not become acceptable when
it is yours.