v0.2.2 — Wall-clock parallel missions
Highlights
Wall-clock parallel mission execution. Independent steps in a mission
plan now actually run concurrently rather than being serialised by the
dispatch path. With 3 workers and 3 independent steps × 200 ms each,
end-to-end wall-clock is ~220–370 ms vs ~600 ms sequential (best vs
worst shuffle luck), verified by test in CI.
This is the cumulative result of two commits since v0.2.1:
a063974— supervisor-side parallel dispatch viamission.Manager.SetPlanParallel98f8c16— competing-consumer bus mode under the dispatch topic
Added — Parallel mission execution
mission.Manager.SetPlanParallel(missionID, steps)
New sibling to SetPlan. Marks the plan for DAG dispatch in the
supervisor: the supervisor walks Step.DependsOn as an authoritative
dependency graph, dispatches every step whose dependencies are
completed, and blocks on the next worker result rather than serialising
at each step. Failure terminates the mission (already-in-flight peers
publish their results; no new dispatches issued after the failure is
observed). Dangling DependsOn references are detected as deadlocks
and surface a clear error rather than hanging.
mission.Plan struct + mission.PlanFromJSON helper
The plan serialised in Mission.PlanJSON now carries a Parallel bool
flag. omitempty keeps existing PlanJSON blobs parsing as
Parallel=false — no migration needed.
Competing-consumer bus mode
New bus.SubscribeCompeting / bus.PublishCompeting /
bus.UnsubscribeCompeting. Each PublishCompeting message lands on
EXACTLY ONE subscriber in the competing group; per-publish shuffle
gives fair load distribution; busy peers fall through to ready peers
via SendTimeout.
Subscriber channels are unbuffered — the send only completes when a
subscriber is actively waiting in its receive select. This gives
clean "busy peer skipped" semantics with no queueing behind an
already-busy worker (an early buffered version had a race where the
supervisor's send could land in a momentarily-empty buffer of a busy
worker, leaving idle peers untouched).
Independent of the existing broadcast PublishReliable /
SubscribeReliable. The same topic can have both kinds of
subscribers; a competing publish does NOT reach broadcast subscribers
and vice versa. Missions use competing for .dispatch and broadcast
for .events so dashboards and other observers can still tail the
event stream.
Worker + supervisor switched to competing-consumer for dispatch
The worker subscribes to mission.{id}.dispatch via
bus.SubscribeCompeting; the supervisor publishes via
bus.PublishCompeting in both the sequential and parallel paths.
Eliminates the duplicate-execution path that a broadcast subscription
would have produced once multiple workers exist.
Performance
3 independent steps × 200 ms each with 3 workers complete in
~220–370 ms wall-clock (best vs worst shuffle luck) vs ~600 ms
sequential. CI-safe assertion bound is < 500 ms.
Tests
Six new bus tests covering the competing-consumer surface: single-
subscriber happy path, exactly-one delivery across a pool of 3 with
fair distribution, ErrNoSubscribers, busy-subscriber fall-through to
ready peer, CompetingTopicCount, and broadcast/competing path
independence.
One new supervisor test
(TestRun_Parallel_ConcurrentWorkers_AchievesWallClockParallelism)
asserts the wall-clock parallelism claim end-to-end with 3 workers
and 3 independent 200 ms steps.
Full repo: 127 packages, go test -race -count=1 passing.
Notes / follow-ups
A single worker still handles one task at a time inside its own
goroutine. Long-tail tasks could be fanned out into goroutines within
a worker as a follow-up; today, scaling parallelism means running
more worker instances.
Replanning on step failure and mid-mission user-confirmation auto-pause
remain explicitly deferred — both are mentioned as "what's not in this
revision" in docs/agents/MISSIONS.md.
Compatibility
No breaking changes. SetPlan keeps sequential semantics. Existing
mission plans persisted under v0.2.1 load unchanged (the new Parallel
flag is omitempty). The competing-consumer bus API is purely
additive — broadcast publishers and subscribers behave exactly as
before.
Full changelog: v0.2.1...v0.2.2