Orchestrata Ultimate › Plans › volk-comms-heartbeat-design

Volk↔Volk Messaging + Orchestrator↔Volk Heartbeat/Status Awareness — Design

Source: plans/volk-comms-heartbeat-design.md at 8a2cb42ad · last changed 2026-08-19 · first committed 2026-08-05 · 443 lines.

Prose pending.

Plan text

Volk↔Volk Messaging + Orchestrator↔Volk Heartbeat/Status Awareness — Design

Status: DESIGN ONLY (RUL-022 — no implementation in this artifact) Branch: feat/cxp-direct-transport · Date: 2026-08-05 · Author: analyst (design phase) Epic: Communication: volk<->volk messaging + orchestrator<->volk with real-time status/heartbeat awareness

Every as-built claim below was grounded via the AST toolkit (scripts/serialize_codebase_ast.py {symbols,refs,source}), never by hand-reading .py files. Specs are authoritative (RUL-013). Rulings consulted: RUL-002 (no mocks), RUL-003 (env from profile, injected seams, no endpoint defaults), RUL-011 (ports from env), RUL-020 (async/sync contract), RUL-022 (design before build), RUL-023 (feature means wired).


1. AS-BUILT INVENTORY

1.1 Volk↔volk messaging path (message_volk → Postman → handler)

Component Location Facts (AST-verified)
message_volk tool agents/tool_registry.py:1171-1225 (ToolExecutor._message_volk) Builds a real CXPPacket (from_agent/to_agent/subject/payload/thread_id, WorkStatus.IN_PROGRESS), pre-checks recipient health via self._volk_registry.get_volk_health(to_volk) (line 1191 — the ONLY production caller of get_volk_health per refs), appends a [WARNING: … unhealthy] suffix but still attempts delivery, then await self._postman.aroute(packet). A None reply is reported as success=True, "Message delivered … (no reply)" (lines 1213-1219).
Postman.aroute postman/postman.py:331-351 In-process handler-table delivery: destination in self._handlers → call handler (await if coroutine), persist packets. Returns None for ALL of: no handler registered, handler raised, handler returned None. @traces(spec="001", story="FR-002", reqs=["REQ-001-US-002-001"]).
Handler registration postman/postman.py:202 (Postman.register) ← orchestrator/pipeline_runner.py:764 (PipelineRunner._register_handlers) Stage volks' handlers are registered per-alias; the same handler table serves both orchestrator dispatch and peer message_volk traffic. _register_handlers also calls volk.set_volk_registry(...) at pipeline_runner.py:786 — the ONLY caller of set_volk_registry in the tree (per refs).
Orchestrator dispatch orchestrator/intent_router.py:259-342 (IntentDispatcher.dispatch) Postman.route_sync via asyncio.to_thread (postman/sync_router.py:55); None response = "delivery failed: no route" → DispatchResult(success=False) (honest, unlike message_volk).
Tool wiring seam agents/volks/base.py:351+ (BaseAgent._call_llm_with_tools) constructs ToolExecutor(workspace, postman=self._postman, from_alias=…, thread_id=…, volk_registry=self._volk_registry, …) at base.py:~423 _volk_registry is set only by BaseAgent.set_volk_registry (base.py:119).
CXP transport factory cxp/transport.py:79-87 (cxp_transport(env)), create_cxp_server:90, create_cxp_client:110 Env-injectable seam (`env: Mapping[str,str]
Existing tests tests/agents/test_tool_messaging.py (message_volk routes a real CXP packet between volks); tests/cxp/test_direct_transport.py, tests/cxp/test_transport.py Cover happy-path delivery + transport selection.

1.2 Existing heartbeat (activity-based, dispatch-time)

refs record_heartbeat — exactly two production write sites, both activity-based:

  1. orchestrator/intent_router.py:296-312 (inside IntentDispatcher.dispatch): after a successful route_sync delivery, getattr(self._registry, "record_heartbeat", None) → call if callable, swallow all failures. In-code comment states verbatim: "IVolkRegistry does not declare record_heartbeat, so guard for its presence" and "A periodic push-heartbeat from volks themselves is a future extension." @traces(spec="001", reqs=["REQ-001-018"]) + @traces(spec="003", story="US-004", reqs=["REQ-003-US-004-010"]).
  2. orchestrator/pipeline_runner.py:1004 (inside PipelineRunner._run_stage): direct self.volk_registry.record_heartbeat(stage_spec.alias) after a successful stage dispatch/response, in a try/except debug-log guard.

Registry side (kernel/volk_registry.py):

Interface landscape (the duck-typing root cause):

Health-aware routing consumer: IntentRouter._is_volk_healthy (intent_router.py:82-97)

Existing test: tests/orchestrator/test_dispatch_heartbeat.py — 3 tests, all @traces(spec="001", story="US-001", reqs=["REQ-001-018"]): test_dispatch_records_heartbeat_for_registered_volk, test_dispatch_to_unhandled_target_fails_without_heartbeat, test_dispatch_heartbeat_reflects_recent_activity.

1.3 Status surfaces (two DISJOINT ones)

Surface A — heartbeat health (VolkRegistry): VolkHealth via get_volk_health; consumed by _is_volk_healthy (routing) and the two tools:

Surface B — roster status (kernel PipelineRunner): kernel/pipeline.py:440-461 PipelineRunner.status_report() (@traces spec="000" story="US-002" reqs=["REQ-000-023"]) reads volk.status.value + current_thread-derived busy off the in-process BaseAgent instances — it never consults VolkRegistry heartbeat health. Flow: gateway VolkHandler._live_status (gateway/handlers/volk.py:219-232, @traces spec="014" reqs=["REQ-014-016","REQ-014-017"]) → kernel client get_status (both transports) → cxp/grpc_server.py:237-276 → kernel packet handler _status_reply (agents/kernel_handler.py:119-142) → status_report().

Push-side surfaces that exist but are not fed by registry transitions:

Production registry constructors (refs VolkRegistry, non-test): agents/runtime.py:71 (VolkRuntime), kernel/cli_main.py:252, 428 (kernel boot), kernel/router.py:188, orchestrator/pipeline_runner.py:190/207, plugin/sdk.py:60.

Unrelated heartbeats (do not conflate): kernel/performance/cluster.py:372 _heartbeat_loop (perf-cluster nodes), gateway/session.py:111 update_heartbeat (WS connection liveness).


2. SPEC CONTRACT REVIEW (specs are authoritative — RUL-013)

SPEC-001 (kernel) — specs/001-kernel/checklists/requirements.md

US-001 "Volk Registration and Discovery":

US-002 "Message Routing and Dispatch" (NOTE: SPEC-001's checklist numbers this story's requirements REQ-002-NNN; they are defined in specs/001-kernel/checklists/requirements.md lines 80-130, distinct from the postman checklist's REQ-002-USn/FRn-NNN series):

SPEC-002 (postman) — specs/002-postman/checklists/requirements.md

SPEC-005 (multi-agent) — specs/005-multi-agent/checklists/requirements.md

Neither spec.md nor the checklists of 001/002/005 mandate a specific heartbeat mechanism (no "push every N seconds" requirement exists — grep -i heartbeat over all three specs returns only REQ-001-018's health-status line). The mechanism is therefore a design choice; the CONTRACT is: health status per volk must be available (REQ-001-018), registry updates must be subscribable (REQ-001-014), senders must learn delivery status (REQ-001-US-002-033/035/038, REQ-002-US-001-026), and unknown/offline recipients must be handled (REQ-001-US-002-008/009).


3. GAP LIST (as-built vs spec, each with evidence)

# Gap Evidence Spec anchor
G1 No periodic liveness heartbeat — liveness is conflated with dispatch recency. Only 2 heartbeat writers exist, both fire on successful dispatch (intent_router.py:305-307, pipeline_runner.py:1004). An idle-but-alive volk decays to INACTIVE after heartbeat_timeout_seconds=60 (volk_registry.py:364, 968-1019), is then deprioritized by routing (intent_router.py:100) and flagged "unhealthy" by message_volk (tool_registry.py:1189-1198) — i.e. health status reports FALSE data for healthy idle volks. The in-code comment at intent_router.py:298 explicitly defers the periodic heartbeat. refs record_heartbeat = 2 use-sites; symbols --pattern heartbeat shows no volk-side loop REQ-001-018 (health status must be true), REQ-001-017
G2 Interface gap: the routing/tool seams are typed against the wrong (narrow) protocol and duck-type the rest. IntentRouter/IntentDispatcher annotate IVolkRegistry (plugin/interfaces.py:21 — only register_volk/unregister), then getattr-probe record_heartbeat (intent_router.py:305) and get_volk_health (intent_router.py:90); ToolExecutor._get_volk_status getattr-probes get_volk_health/is_registered (tool_registry.py:1242-1245). Meanwhile core/protocols/decoupling.py:91,94 (VolkRegistryProtocol) already declares both — unused at these seams. refs IVolkRegistry, refs VolkRegistryProtocol, sources above REQ-001-018 (typed health surface), project composition rule
G3 No real-time status propagation: registry transitions are invisible. (a) add_notification_handler has ZERO consumers (refs: 0 imports, 0 uses) — REQ-001-014's subscription surface is unwired. (b) ACTIVE↔INACTIVE transitions in record_heartbeat/get_volk_health only log (volk_registry.py:1041-1043, 999-1004). (c) The status the gateway/UI polls (volk.list → kernel get_status → kernel/pipeline.py:440-461 status_report) reads BaseAgent.status/current_thread and NEVER includes heartbeat health — the two status surfaces are disjoint; heartbeat health never reaches the orchestrator's roster view or the UI. (d) publish_lifecycle_event fires only on gateway-initiated register/deregister (volk.py:364,403), never on kernel-side transitions. sources cited; refs add_notification_handler, refs publish_lifecycle_event REQ-001-014, REQ-001-009, REQ-001-022, REQ-005-US-004-001/AC6-001
G4 False-delivered defect in volk↔volk messaging. Postman.aroute returns None indistinguishably for no-handler / handler-raised / handler-returned-None (postman.py:331-351); message_volk maps None to success=True "Message delivered … (no reply)" (tool_registry.py:1213-1219). An unroutable peer message is reported to the sending volk as delivered. Contrast: IntentDispatcher.dispatch treats the same None as "delivery failed: no route" (intent_router.py:283-294). sources cited REQ-001-US-002-009, REQ-001-US-002-033/035, REQ-001-US-002-038, REQ-002-US-001-026
G5 Peer-status tools are unwired in the kernel-runtime path. set_volk_registry has exactly ONE caller — orchestrator/pipeline_runner.py:786. The kernel-side PipelineRunner (kernel/pipeline.py:98) never binds a registry into its volks (refs _volk_registry: no kernel/ site), so under the kernel packet-handler path (agents/kernel_handler.py) ToolExecutor._volk_registry is None → get_volk_status returns "no registry bound" and message_volk skips the health pre-check. refs set_volk_registry, refs _volk_registry REQ-001-018, RUL-023 (feature means wired)
G6 (minor) Delivered volk→volk messages do not count as recipient liveness. Only orchestrator dispatch records heartbeats; a successful message_volk delivery (recipient's handler ran) records nothing. refs record_heartbeat REQ-001-018

Candidate (b) from the epic brief is confirmed with a correction: the gap is not "no protocol declares record_heartbeat" (VolkRegistryProtocol does, decoupling.py:91) — it is that the seams are typed against the plugin IVolkRegistry and duck-type around it.


4. CONTRACT DESIGN

4.1 Key decisions

  1. One narrow Protocol, composition-first (closes G2). Introduce VolkHealthProbe — a thin <<Protocol>> with exactly the three members the routing/tool seams actually use (record_heartbeat, get_volk_health, is_registered). kernel.volk_registry.VolkRegistry already satisfies it structurally (methods exist at 1022, 969, 703) — zero implementation change, no inheritance; VolkRegistryProtocol remains the wide boundary and is a structural superset. Retype IntentDispatcher._registry, IntentRouter health path, and ToolExecutor._volk_registry as VolkHealthProbe | None; delete every getattr probe. (Twin check performed: no existing narrow health protocol exists to reuse; the wide VolkRegistryProtocol stays untouched.)
  2. Roster heartbeat pump, threads not asyncio (closes G1). A HeartbeatPump composed of (probe: VolkHealthProbe, roster: Callable[[], list[str]], tracer) beats for every alias in the live roster on a fixed interval using threading.Thread + threading.Event — matching the codebase's existing thread posture (route_sync via asyncio.to_thread, free-threaded 3.14t). Rationale: volks are in-process BaseAgent instances; "the hosting runtime is up AND the handler is registered" IS the correct liveness signal for them — there is no separate volk process to push from. Busy/failed remains BaseAgent.status's job (surface B). Interval from an injected env mapping, env.get("VOLK_HEARTBEAT_INTERVAL_S", "15") — a tunable, NOT an infra endpoint key, so a literal default is permitted under RUL-003's inverse rule. Remote/plugin volks (gRPC-registered) are OUT of MVP scope (recorded below).
  3. Status-change observer on the registry (closes G3a/b). VolkRegistry gains an on_status_change: Callable[[str, VolkStatus, VolkStatus], None] | None constructor seam — the exact precedent is the existing on_registration seam (volk_registry.py:364). Both transition sites (record_heartbeat ACTIVE flip, get_volk_health lazy INACTIVE flip) fire it under the same lock discipline, best-effort (REQ-001-017). No new event framework: the pattern already exists in this class.
  4. Enrich the EXISTING polled roster instead of a new RPC (closes G3c). kernel/pipeline.py PipelineRunner.status_report() gains an optional injected health_probe: VolkHealthProbe | None; when present each row adds healthy, heartbeat_age_s, last_heartbeat. The enrichment then flows through the entire existing chain with ZERO new wiring: _status_reply → gRPC/cxpt get_status → gateway VolkHandler._live_status → volk.list. Both transports already carry the payload (dict-shaped volks list).
  5. Real-time push via the EXISTING kernel-events forwarder (closes G3d). A tiny VolkStatusEventBridge adapter subscribes to decision-3's callback and emits a volk.status_changed kernel event through the already-tested kernel-events → gateway broadcast seam (gateway/server.py:637 _init_kernel_events_forwarder; broadcast, not pubsub.publish, is the delivery mechanism that works — session-193 finding). UI work itself is out of scope; the event on the wire satisfies REQ-001-014's subscription contract.
  6. Honest delivery outcome (closes G4). DeliveryOutcome <<dataclass>> + DeliveryFailure <<enum>> (OK / NO_ROUTE / HANDLER_ERROR); new Postman.aroute_outcome(packet) -> DeliveryOutcome; existing aroute becomes a thin delegate (outcome.response if outcome.delivered else None) so all current callers keep exact behavior. message_volk switches to aroute_outcome and reports success=False on NO_ROUTE/HANDLER_ERROR (REQ-001-US-002-009/035/038); on delivered it records the RECIPIENT's heartbeat via the probe (G6, REQ-001-018) — delivery through the recipient's handler is activity evidence, symmetric with the dispatch-time heartbeat.
  7. Wire the kernel path (closes G5). Kernel-side PipelineRunner binds the registry into every volk it rosters (same set_volk_registry seam the orchestrator runner already uses at pipeline_runner.py:786), at its volk construction/registration site; kernel boot (kernel/cli_main.py VolkRegistry construction sites at 252/428) owns the HeartbeatPump lifecycle and the VolkStatusEventBridge.

Non-goals (MVP discipline — explicitly deferred): push heartbeats from remote/ plugin volks over CXP; postman offline-queue/health integration (REQ-001-US-002-018 already served by queue manager); VizInt UI rendering of the new event; multicast status digests; any change to IVolkRegistry (plugin SDK surface, SPEC-015-owned).

4.2 Contract diagram

Diagram projects-orchestrata-ultimate-plans-volk-comms-heartbeat-design-m0eff85e9e6

patternRole map for gen-skeleton: VolkHealthProbe=protocol · HeartbeatPump.beat_once=leaf, lifecycle=boilerplate · VolkStatusEventBridge= observer-adapter (leaf body one method) · DeliveryOutcome/DeliveryFailure= dataclass/enum (fully generatable) · Postman.aroute_outcome=leaf · edits to existing classes = builder tasks, not generated.

4.3 Wiring plan (RUL-023 / Hard Rule 8 — named production seams, found via refs, not guessed)

New element Production importer / seam
VolkHealthProbe orchestrator/intent_router.py:17 (replaces IVolkRegistry import for the dispatcher seam), agents/tool_registry.py (ToolExecutor.__init__:1053 param type), agents/volks/base.py:119 (set_volk_registry param type), kernel/pipeline.py (probe param)
HeartbeatPump kernel boot: kernel/cli_main.py at the VolkRegistry construction sites (252 / 428) — constructed with probe=registry, roster=lambda: list(runner.volks), started with kernel lifecycle, stopped on shutdown. Orchestrator path: orchestrator/pipeline_runner.py lifecycle (same class that already owns volk_registry at 190/207 and _register_handlers at 764).
VolkStatusEventBridge kernel/cli_main.py boot: VolkRegistry(..., on_status_change=bridge.on_status_change) with emit= the existing _emit_kernel_event seam (proven end-to-end by tests/kernel/test_cli_main_kernel_events.py); events reach the UI via gateway/server.py:637 _init_kernel_events_forwarder → broadcast.
DeliveryOutcome / aroute_outcome agents/tool_registry.py:1212 (_message_volk switches from aroute to aroute_outcome); postman/postman.py:332 (aroute delegates).
status_report enrichment Already-wired chain: agents/kernel_handler.py:126 → cxp/grpc_server.py:237 + cxp/direct_cxp_adapter.py:497 → gateway/handlers/volk.py:220 — no new importer needed; the kernel boot passes the probe into kernel/pipeline.py PipelineRunner.
G5 binding kernel/pipeline.py volk-roster site calls volk.set_volk_registry(registry) (mirror of orchestrator/pipeline_runner.py:786).

Transport constraint respected: all kernel↔gateway status traffic rides the cxp.transport factory (CXP_TRANSPORT=cxpt in the test profile); both transports already implement get_status, and the enrichment is payload-only — no proto change, no new RPC.


5. TDD TEST PLAN (tests-first; real infra only — Redis 10.93.53.11, graph 10.93.53.10, kernel 10.93.53.13:50053, env profile test; no mocks/monkeypatch per RUL-002; env via injected mappings per RUL-003)

Test module Test Requirement Behavior proven
tests/kernel/test_heartbeat_pump.py test_beat_once_records_all_rostered_volks REQ-001-018 Real VolkRegistry + 2 registered volks; beat_once() returns 2; both last_heartbeat fresh, status ACTIVE.
test_idle_volk_stays_healthy_under_pump REQ-001-018 Registry with heartbeat_timeout_seconds=2, pump env={"VOLK_HEARTBEAT_INTERVAL_S":"1"}; after 3s idle, get_volk_health(...).is_healthy is True (kills the G1 decay).
test_partial_failure_tolerated REQ-001-017 Roster contains an unregistered alias; beat_once() still records the registered ones and returns their count.
test_stop_joins_thread REQ-001-017 start() then stop(timeout=5); thread not alive; no further beats recorded.
tests/kernel/test_volk_registry_status_events.py test_heartbeat_transition_fires_on_status_change REQ-001-014 Registry constructed with a recording callback; INACTIVE volk + record_heartbeat → exactly one (alias, INACTIVE, ACTIVE) event.
test_lazy_timeout_transition_fires_on_status_change REQ-001-014, REQ-001-018 Stale heartbeat + get_volk_health → (alias, ACTIVE, INACTIVE) event.
test_no_event_when_status_unchanged REQ-001-014 Second consecutive heartbeat on an ACTIVE volk fires nothing.
test_callback_error_never_breaks_heartbeat REQ-001-017 Raising callback; record_heartbeat still returns True and updates state.
tests/kernel/test_status_report_health.py test_status_report_includes_heartbeat_health REQ-001-018, REQ-001-015 Kernel PipelineRunner + probe: rows carry healthy/heartbeat_age_s/last_heartbeat consistent with the registry.
test_status_report_without_probe_backcompat REQ-001-015 No probe injected → rows byte-compatible with today's shape.
tests/postman/test_aroute_outcome.py test_no_route_outcome REQ-001-US-002-009 Unregistered destination → delivered=False, reason=NO_ROUTE.
test_handler_error_outcome REQ-001-US-002-035 Registered raising handler → delivered=False, reason=HANDLER_ERROR, error text captured.
test_delivered_with_and_without_reply REQ-001-US-002-033 Echo handler → delivered=True, reason=OK, response set; None-returning handler → delivered=True, response=None (distinguishable from NO_ROUTE at last).
test_aroute_backcompat_delegates REQ-001-US-002-019 aroute returns exactly outcome.response if delivered else None for all three cases above.
tests/agents/test_message_volk_delivery_status.py test_message_volk_reports_no_route_as_failure REQ-001-US-002-038, REQ-001-US-002-009 Real Postman, no recipient handler → ToolResult.success is False with a no-route message (kills G4).
test_message_volk_reports_handler_error_as_failure REQ-001-US-002-035, REQ-002-US-001-026 Raising recipient handler → failure result naming the error.
test_message_volk_records_recipient_heartbeat REQ-001-018 Echo handler + real VolkRegistry bound as probe → after delivery, recipient last_heartbeat fresh (G6).
test_message_volk_health_warning_preserved REQ-001-018 Unhealthy-but-registered recipient still gets the [WARNING: …] suffix and delivery is attempted (existing behavior locked).
tests/agents/test_get_volk_status_probe_typing.py test_get_volk_status_via_typed_probe REQ-001-018 ToolExecutor with a real VolkRegistry typed as VolkHealthProbe; status text includes status/healthy/heartbeat-age; the code path contains no getattr fallback branches (structural: probe methods called directly).
tests/kernel/test_kernel_pipeline_registry_binding.py test_kernel_pipeline_binds_registry_into_volks REQ-001-018, RUL-023 Kernel PipelineRunner built through the production construction path with a registry → every rostered volk's _volk_registry is that registry (kills G5).
tests/gateway/test_volk_status_event_broadcast.py test_status_change_event_reaches_gateway_broadcast REQ-001-014, REQ-001-009 VolkStatusEventBridge wired to the kernel event emitter; a registry transition produces a volk.status_changed broadcast observable through the existing forwarder fixtures (real Redis).
existing tests/orchestrator/test_dispatch_heartbeat.py 3 tests unchanged REQ-001-018 Regression: probe retyping must keep all three green with zero edits.

Runner discipline: scripts/run_tests.py --affected / targeted modules only, live-observed (observe_pytest attach), 5-min cap per run; kernel required.


6. BUILD PACKETS (each <5 min for a builder agent; MVP order; tests-first inside each)

# Packet Files Depends on
P1 VolkHealthProbe protocol + export core/protocols/volk_health.py (new), core/protocols/__init__.py —
P2 Retype seams, delete getattr duck-typing orchestrator/intent_router.py (83-97, 255, 305-307), agents/tool_registry.py (1053, 1242-1245), agents/volks/base.py:119 + regression run of test_dispatch_heartbeat.py P1
P3 DeliveryOutcome/DeliveryFailure + Postman.aroute_outcome (+aroute delegate) + tests/postman/test_aroute_outcome.py postman/delivery_outcome.py (new), postman/postman.py —
P4 message_volk honest delivery + recipient heartbeat + tests/agents/test_message_volk_delivery_status.py agents/tool_registry.py (1171-1225) P2, P3
P5 VolkRegistry.on_status_change seam + _fire_status_change + tests/kernel/test_volk_registry_status_events.py kernel/volk_registry.py (364, 968-1019, 1021-1053) —
P6 HeartbeatPump + tests/kernel/test_heartbeat_pump.py kernel/heartbeat_pump.py (new) P1
P7 status_report health enrichment + tests/kernel/test_status_report_health.py kernel/pipeline.py (98, 440-461) P1
P8 Kernel wiring: pump lifecycle + probe into kernel runner + set_volk_registry binding (G5) + VolkStatusEventBridge + wiring tests kernel/cli_main.py (252/428 region), kernel/pipeline.py, kernel/volk_status_bridge.py (new), tests/kernel/test_kernel_pipeline_registry_binding.py, tests/gateway/test_volk_status_event_broadcast.py P5, P6, P7

P1/P3/P5/P6 are gen-skeleton-friendly (protocol/dataclass/enum/observer shells + traced test stubs); P2/P4/P7/P8 are builder/wirer edits to existing classes.