Symptom

Not a bug — phase B of swarm-llm-workloads (openspec design D1–D7): the contract-net dispatch machinery. The design challenge was carrying an auction (claims, windows, leases, reclaim) on Krill’s observer spine without adding a broker process, a job database, or a second dispatch path.

Root cause

Three mismatches between the auction model and the existing machinery had to be resolved without SDK or protocol changes:

  1. The invoke wire (InvokeRequest) carries only identity + verb — no claim payload. Resolved by having the arbiter read the claimant’s LLM node (its advert block) over the existing authenticated read at claim time; a claim is just an identity.
  2. Nothing observes the assignee for the result. Resolved by writing the winner into the work node’s sources at assignment — the standard NodeObservationRegistry + peer mirror then deliver the assignee’s publication back to the host, which copies the fresh snapshot into result.
  3. Heartbeats needed a transport. Resolved by defining CLAIM-from-the- current-assignee on non-OPEN work as lease renewal; claims from anyone else on non-OPEN work remain no-ops, exactly as the spec requires.

A fourth trap surfaced during design: folding busy/window-closed into the stored costScore would destroy a manually-set static cost after one busy spell. Acceptance (“do I take work right now?”) is therefore evaluated on the claimant’s server immediately before claiming (SwarmAdvert.isAccepting), while the stored advert keeps the cost value.

Fix

New server/.../krillapp/swarm/ package: pure SwarmArbitration (eligibility, lowest-cost/first-arrival winner, payload budgeting) and SwarmAdvert (fold + acceptance), ServerSwarmWorkProcessor (state machine, claim window, lease/reclaim, deadline, result return), ServerSwarmBatchProcessor (fan-out/aggregation), SwarmWatchTask (mirror-driven + sweep claiming), BlobPuller; ServerLLMProcessor gained the ADVERTISE path and the swarm execution bridge (work-payload prompt, lazy FileRef pull, lease heartbeat); PeerObservationClient exposes a mirroredPublications stream; ConflatingGate gained an advisory busy().

Prevention

Timer-bearing processors take an injected scope and clock: () -> Long so every window/lease/deadline test runs on runTest virtual time — 32 tests, zero wall-clock waits, suite passes under HOME=/nonexistent. When a new cross-server interaction needs a payload, first check whether an existing authenticated read can supply it at the receiver — extending the wire format is a last resort, not a default.