ADR-131: Claim affinity, completion-conferred and time-bounded soft-preference routing for context-warm agents
Status: Proposed
Date: 2026-05-04
Related: ADR-080 (warp as the durable trigger queue, the dispatcher this ADR amends), ADR-098 (epics with structured requirements and parent_id, the schema this ADR routes on), ADR-102 (agent fleet, the consumer that benefits from cheaper re-dispatch).
Context
Operator framing on 2026-05-04: when an agent ships a ticket inside an epic and asks the dispatcher for more work, the dispatcher today does not remember that the agent is already context-warm in that epic. It treats every warp-ready call as a cold poll. The next ticket goes to whichever agent reads first, not to the agent that just shipped its sibling.
Every fresh gyrum-start-work rebuilds the same context the just-shipped sub-agent already had. Reading the ADR cluster, sibling tickets, recent merges, area README, and file conventions is roughly five to ten minutes of redundant priming the operator pays for in tokens, every dispatch. Tonight's swarms make the cost visible. 2026-05-04 shipped warp#1145 (auto-rerun review), warp#1144 (branch-meta fallback), warp#1164 (warp-stale-unlock), warp#1199, warp#1221, warp#1227 across six sub-agents and six full context rebuilds. If any one of those agents had picked up a sibling of what they shipped instead of an unrelated ticket, the second ship would have been roughly thirty per cent cheaper.
Three forces converge:
- The cost the fleet keeps paying is context-rebuild. Verified empirically across the swarm runs: sub-agent token usage is dominated by initial-state reads (ADR cluster, README, sibling-ticket lookups). The cheapest token is the one not spent re-priming.
- Epic tickets have natural sequence dependencies. warp#1223 r0 → r1 → r2 → ... → r6 is ordered. The dispatcher (warp-ready) does not know that today; it offers all of them at once. Affinity makes "if r0 just shipped, prefer r1" the default routing decision without modelling an explicit dependency graph.
- Today's failure mode is the inverse, agents grabbing unrelated tickets. No structural pull toward continuity. Operators have to thread sub-agent dispatches through related work by hand, every dispatch.
ADR-098 already gave the dispatcher the structural field it needs: parent_id on warp items, with epics as the umbrella and child tickets pointing up. ADR-080 named warp as the durable trigger queue and the claim protocol that completion-conferred affinity binds to. ADR-102's agent fleet is the consumer; cheaper re-dispatch compounds with every additional concurrent worker. The piece missing is the routing primitive that ties them together.
Decision
Adopt claim affinity as a fleet-wide dispatcher primitive: completion-conferred, time-bounded, soft-preference, routed on parent_id, and cleared on claim-release.
The shape:
- Completion-conferred. Affinity is granted when an agent calls
warp-completeon a ticket whoseparent_idis non-null. Not on claim. Completion ties the routing benefit to demonstrated throughput, not to the speculative "I have started something". Mis-claimed-and-abandoned tickets do not produce affinity that biases future dispatches. - Time-bounded. TTL is ten minutes from
completed_at. The interval covers the typical ship-to-next-dispatch gap (thirty seconds to three minutes) with margin, and expires before context rots through other PRs merging or conventions shifting in the same area. - Soft-preference. The next-in-epic ticket appears at the top of the affinity-holder's
warp-readyoutput, but remains visible in the open pool to other agents. If the affinity-holder does not claim within roughly thirty seconds of seeing it, any other agent can. Hard-exclusive epic locks would serialise entire epics onto one worker and lose parallelism, which is worse than today. - Routed on
parent_id. The existing schema field (per ADR-098) is sufficient. No new dependency-graph modelling is required. Epic tickets have children; children of the same epic shareparent_id; affinity routes between them. Ordering inside the affinity tier uses the existing priority and age sort. - Cleared on claim-release. If the agent's heartbeat dies or they
warp-release, all of their active affinities clear. Affinity is a function of "I am actively shipping in this epic", not "I once shipped here". A stuck or abandoned agent does not hold the next ticket hostage. - Cleared on parent terminal-status change. When an epic parent is operator-cancelled (
warp-releaseon the parent, or status flipped todone/closed/archived), every child affinity pointing at it cascade-clears. The dispatcher does not route into a closed epic.
Naming
The primitive is affinity, not "loan". The K8s pod-affinity, sticky load-balancer, and request-affinity-to-session prior art all use "affinity" for soft routing preference. Reserving the word "loan" for any future exclusive-coordination primitive (where one agent holds an exclusive write reservation on a shared resource) keeps the two shapes distinct. Affinity is non-exclusive routing preference; a loan would be exclusive coordination on shared writes. Conflating them in one word would mislead readers into expecting exclusivity from affinity, or non-exclusivity from a loan.
Agent-identity choice
Two definitions of "agent identity" exist in the fleet today: claim-time claimer_agent (per warp#1163) and parent-loop session id. Affinity must pick one. Default is parent-loop id (Option A). Most sub-agents are ephemeral; the parent loop is the persistent process whose context warmth is worth preserving across dispatches. Per-sub-agent affinity (Option B) is opt-in for claim-and-iterate sub-agents (per warp#1170) where the sub-agent itself is the long-lived warm process.
Consequences
What becomes easier
- Sub-agent dispatches into related work skip roughly thirty per cent of priming. At today's swarm cadence the rough fleet-wide saving is ten to twenty per cent of total agent-call tokens; the implementation ticket (warp#1237) measures the actual ratio post-landing.
- A six-phase epic such as warp#1223 (r0 to r6) routes to the same agent across phases when timing aligns, producing more coherent commits and fewer integration surprises at epic close.
- Cross-session continuity gets sharper. If a parent loop dispatches sub-agent A, A ships, the parent loop dispatches sub-agent B before A's affinity expires, B inherits the affinity (Option A in implementation, see warp#1237).
- The dispatcher gains a deterministic ordering primitive on epics without modelling explicit dependency edges. Authors do not declare "after-r0" relationships; the timing of the previous completion is the edge.
What becomes harder
- The dispatcher's ranking now depends on dispatcher-clock plus a per-agent affinity table, not only on item-state. Debugging "why did agent X get this ticket and not agent Y" requires a CLI surface (
warp-affinity --agent <id>) the implementation ticket adds. - Affinity drift across operator interruption is a real edge case. The cascade-clear on parent terminal-status change covers the common case; an operator manually editing
parent_idon a child after a completion would orphan the affinity. Acceptable: affinity expires on TTL anyway, and manualparent_idedits are rare. - Two agent-identity definitions (parent-loop id and per-sub-agent claimer-agent) coexist. The validator must reject ambiguous routes; the implementation ticket lands the disambiguation rule alongside the schema change.
What we sign up to operate
- A schema change on warp items (or a sibling table) recording the affinity tuple
(agent_id, parent_id, expires_at). - A dispatcher amendment in
warp-readythat joins on the affinity table and sorts the affinity-tier first. - A CLI debug surface (
warp-affinitylisting, expiring, and clearing entries) for operator inspection. - A nightly drift-detection job that flags affinities outliving their parent's terminal-status change (cascade-clear is the contract; the cron catches gaps in the event hookup).
- A regression-corpus entry covering "ship in epic, request next, observe sibling at top of list" so dispatcher refactors do not silently break the routing.
Alternatives considered
- Hard-exclusive epic loans. Rejected. Serialises an entire epic onto one agent, losing the parallelism the fleet relies on for swarm work. Today's three-concurrent ai-research swarm could not happen under hard-exclusive locks. The epic primitive is the umbrella for coordinated parallelism, not a queue for one worker.
- Opt-in affinity, set a flag at claim time. Rejected. Dead weight. Affinity is a routing hint that improves cost; there is no scenario where the dispatcher should avoid sending the next-context-warm ticket to the warm agent. An opt-in flag adds an authoring decision to every claim with no upside.
- Dependency-graph modelling, explicit "after" links between r1 and r2 and so on. Rejected. More schema, more authoring burden, more failure modes (cycles, stale links, deletion semantics).
parent_idalready encodes the structural information the dispatcher needs; the existing priority and age sort orders the children inside the affinity tier. Explicit edges would re-derive what the timing of the previous completion already tells us. - Affinity granted on claim, not completion. Rejected after the operator framing settled the cut. The ship-reward model ties the routing benefit to verified context warmth, not to a speculative claim that might be released. Claims are cheap; completions are the signal that the context-rebuild cost was actually paid down.
- Persistent (no-TTL) affinity until explicit release. Rejected. Context rots. Other PRs merge into the area, conventions shift, the warm context goes stale within hours. A ten-minute window is the post-completion grace, not a long-lived ownership claim.
- Route on tag membership instead of
parent_id. Rejected. Tags are unstructured and overlapping; an agent that shipped a ticket taggedwarpcould end up with affinity on everywarp-tagged ticket in the queue.parent_idis the structurally narrow signal: same epic, same context window, same area read.
Implementation
The implementation lands in warp#1237 (filed alongside this ADR), in three phases:
- Schema. Add the affinity tuple
(agent_id, parent_id, expires_at), with a unique index on(agent_id, parent_id), and the cascade-clear trigger on parent status change. - Dispatcher. Amend
warp-readyto join on the affinity table and sort the affinity tier first, with the soft-preference window (other agents can still claim after roughly thirty seconds). - CLI debug surface.
warp-affinity --agent <id>lists active entries;warp-affinity --clear <agent-id> <parent-id>removes one for operator override; the regression-corpus entry locks the routing behaviour.
Phase 1 ships the parent-loop id (Option A) shape. Phase 2 adds per-sub-agent affinity (Option B) only if demand emerges from warp#1170's context-pack work; today's evidence is that parent-loop affinity covers the dominant case.
Supersedes: none Superseded by: