The sprint list going into this session was eleven issues — #213 through a batch of S and XS items that had been accumulating while the connector backend work ran. I brought Claude in to work through them sequentially on a single branch.

Extracting ObligorTrustPolicy (#213)

The main item was an M-scale SPI extraction. MessageService.dispatch() had an inline if block calling TrustGateService.meetsThreshold() directly — no extension point, no way for Claudony to plug in capability-scoped trust evaluation. The fix is a @FunctionalInterface ObligorTrustPolicy in api/spi/, with a default impl that wraps the old gate logic.

The interesting design choice was the context record. The issue described a simple (String obligorId, double minTrust) signature, but that leaks config into the SPI and leaves Claudony without the channel context it needs for per-capability routing. We went with ObligorTrustContext(String obligorId, UUID channelId, String channelName) — both channel fields, because Claudony’s trust routing maps from channel names to capability dimensions. I nearly wrote long channelId before the compiler flagged it. Channel.id is UUID, which required reading the entity before writing the record.

Claude’s code review afterwards caught a dead QhorusConfig injection in ReactiveMessageService — the threshold check had moved into the default impl, but the field stayed behind. Easy miss that would have been an unused import at best, a misleading config.commitment().minObligorTrust() call that no longer existed at worst.

Three Issues Already Done

When we got to #150, #148, and #146 — the A2A cleanup items — all three were already resolved. They’d been waiting behind #135 and when that landed, the fixes came with it. Closed all three without a line of code.

The issue queue had inflated. Worth a periodic pass to check which “blocked by” items resolved when their blocker merged.

The CDI Observer That Wouldn’t

For #215 (fire ChannelInitialisedEvent on binding update), I wanted to verify the event fires via a CDI observer in the integration test. Tried an @ApplicationScoped static inner class inside the @QuarkusTest class — observer method never called. Moved to a separate top-level class in the test package — same result.

The event was firing; the production code was correct. But the CDI observer wasn’t receiving it in the @TestTransaction context. We couldn’t find a clean explanation. The working approach: drop CDI observation and mock Event<ChannelInitialisedEvent> directly in a pure Mockito unit test, then verify the call with the correct arguments. Simpler, and it actually tests what matters.

The failure mode is now in the garden (GE-20260530-c13942).

JTA ROLLBACK_ONLY and Narayana’s “State 1”

The most interesting problem came from #166 — deferring MessageObserver calls to post-commit via TransactionSynchronizationRegistry. The implementation was straightforward. Then nine unrelated tests started failing with:

IllegalStateException: Syncs are not allowed to be registered when the
transaction is in state 1

“State 1” is not a JTA spec constant. The spec says STATUS_ACTIVE = 0, STATUS_MARKED_ROLLBACK = 1. Narayana’s internal state machine uses different numbering. When a @Transactional method throws inside a @TestTransaction outer transaction — as happens in pause/resume tests where one sendMessage is expected to fail — Narayana marks the outer TX as ROLLBACK_ONLY. Any subsequent registerInterposedSynchronization() call then throws, because the TX isn’t active.

The fix:

if (tsr == null || tsr.getTransactionStatus() != Status.STATUS_ACTIVE) {
    dispatchSynchronously(event, observers);
    return;
}
tsr.registerInterposedSynchronization(synchronization);

There’s a neat testing consequence: mock(TransactionSynchronizationRegistry.class) returns 0 for int methods by default — which is STATUS_ACTIVE — so the deferred path activates in tests without explicit stubbing.

This became two garden entries and a protocol (PP-20260530-332d70).

Fifty Test Files and a Python Parser

When create_channel gained four connector binding params, every test calling the old 9-arg version needed four trailing nulls. Fifty files, mix of single-line and multi-line calls. Regex wasn’t sufficient — calls that spread across lines with non-null trailing args don’t match a simple suffix pattern.

We wrote a Python script that walks balanced parentheses character by character, counts top-level args, and injects the nulls before the closing paren. Handled all fifty files in one pass, including the multi-line cases. The technique is in the garden now (GE-20260530-319607) — it comes up any time a public Java method gains new parameters with dozens of existing call sites.

The SPI Placement Discovery

During brainstorming for #213, I checked whether CommitmentAttestationPolicy and InstanceActorIdProvider were correctly placed — CLAUDE.md described them as being in runtime/ledger/, which would violate the consumer-spi-placement protocol we’d just drafted. Checked the actual source: they’re already in api/spi/. The CLAUDE.md description was about the default implementations, not the interfaces.

Filed and closed issue #223 within the same session. The protocol still stands — api/spi/ is the right place, and now there’s a written rule for it.


<
Previous Post
Humans in the mesh
>
Next Post
Layer 6: when trust scores route reviewers