Continues from Shared Vocabulary for Overload, which delivered the platform SPI. This entry covers the qhorus signal source and redistribution executor — where the vocabulary meets reality.

The previous entry ended with a promise: “Next is the signal source that reads existing CONTEXT_PRESSURE watchdog data and the redistribution executor that does the actual HANDOFF delegation.” That turned out to be the easy part. The hard part was proving the system converges.

The before picture

Three systems detect overload. None of them talk to each other. None of them can do anything about it after the assignment is made.

Qhorus context_window_pct: 92 fires alert, can't act Engine tasks: 8/10 blocks new, can't move Agent Gate sessions: 5/5 throws, can't shed Agent (overloaded)

The dashed lines are the problem. Each system can observe overload in its own metric. None can reach across and redistribute the agent’s actual obligations.

The feedback loop

The redistribution executor closes the loop. The platform’s CapacityPressureMonitor sweeps every 60 seconds, aggregates all signal sources into a single pressure value per actor, and fires a CDI event for each actor above the threshold. The qhorus executor observes those events, queries the actor’s open commitments, and decides what to do.

Sweep (60s) getOverloaded(0.7) CDI Event per overloaded actor Policy evaluate(context) Hold below threshold Compress 0.7 – 0.85 Redistribute ≥ 0.85, HANDOFF Escalate stuck or inactive pressure drops → next sweep sees lower value each HANDOFF reduces obligation count by 1 finite obligations → finite iterations → converges

The red dashed arrow is the convergence mechanism. Each HANDOFF moves one commitment to another agent — the overloaded actor’s obligation count drops by one. The next sweep measures the new pressure. If it’s still above threshold, another HANDOFF fires. Finite obligations, finite iterations, guaranteed termination.

Why compression almost didn’t make the cut

The first design had four decision levels. I nearly dropped compression entirely — it’s indirect and unreliable. Channel summaries reduce context tokens, but only if the agent re-reads the channel. The context window is the agent’s internal state; the summary is a qhorus-side artifact.

What saved it: multi-channel agents. An agent with open commitments across ten channels carries all ten histories in context. A single summary can replace hundreds of messages with a paragraph. For the common case — agents with many shallow channel interactions — compression is the cheapest effective intervention.

The executor treats it as fire-and-forget. Trigger channel summaries on stale channels, exit. The sweep cycle re-evaluates on the next tick. If compression worked, pressure dropped below threshold — done. If not, the policy escalates to redistribution naturally as pressure rises. No blocking, no state, no waiting for LLM-driven summary generation to complete.

The guard that matters most

I enumerated ten failure modes. Nine are harmless or wasteful — the commitment state machine prevents double HANDOFFs, the watchdog catches circular delegation chains, crash recovery comes for free because the executor is stateless.

One was genuinely stuck: when the policy says Redistribute but no routing target exists for any commitment. The executor tries to HANDOFF, RoutingBridge rejects every candidate, obligation count stays the same, next sweep fires the same event, infinite loop.

The fix is two lines of code. After redistribution, check if any HANDOFFs succeeded. If zero succeeded and obligations exist, fire Escalate instead of waiting for the next sweep to try the same thing. The feedback loop needs an exit when the loop itself can’t make progress.

What shaped the executor

CDI shaped everything. @ObservesAsync runs on a managed executor thread — no request scope, no CurrentPrincipal, no @Transactional. Three garden entries saved real time here: the @RequestScoped constraint on async observers, the unreliable @ObservesAsync + @Transactional combination, and the separate delegate pattern that makes transactional work reachable from an async context.

The result is two classes. QhorusRedistributionExecutor is the observer — stateless, reads ground truth on each invocation, dispatches to the delegate. RedistributionDelegate is @Transactional with @ActivateRequestContext — it establishes a request scope, sets the tenant context per-commitment from the commitment’s own tenancyId, and dispatches the HANDOFF through the full MessageService.dispatch() pipeline. The tenant context bridging was the subtlest part — each commitment may belong to a different tenant, and the HANDOFF dispatch needs the right tenant context for the downstream integrity checks.

The executor carries no state between sweeps. Obligations live in the commitment store. Pressure lives in the capacity view. Signal freshness lives in the ledger. Every invocation reads current state, acts, and exits. If the executor crashes mid-HANDOFF, the next sweep picks up exactly where things stand.

I added capabilityTag to the Commitment record — “what capability was this obligation assigned for?” — so the executor knows which role:X target to use when HANDOFFing. Pre-release, so the schema change costs nothing. But the data model was genuinely incomplete without it — commitments had who, where, and what type, but not for what capability. That’s the redistribution target resolution, but it’s also obligation analytics and routing diagnostics.

The vocabulary now meets reality. Platform defines the shared language. Qhorus reads the signals and acts on them. The loop converges because obligations are finite and each iteration makes measurable progress. When it can’t make progress, it escalates instead of spinning.


<
Previous Post
Slot 169 and the absolute path problem
>
Next Post
Inheritance Without the Inheritance