# Multi-agent refine loop, 0.92 → 0.98 in 5 hops — and the silent default-deny that almost made it look like the agents weren't talking

> Source: <https://loomcycle.dev/blog/multi-agent-refine-loop-and-the-silent-default-deny.html>
> Published: 2026-06-07 16:00:00+00:00

Day three of the operator-via-MCP series. Three agents iterating "What is recursion?" over Channels + Memory + Evaluation — answerer, evaluator, aggregator. Five hops. Scores climbed 0.92 → 0.98. Winner: "A mirror facing a mirror — each reflection a smaller twin of the last, until there's nothing left to reflect." A later sonnet-driven re-run pushed it further to "Mirrors facing mirrors, until one touches bedrock" at 1.0 laconic + 0.98 metaphor. The convergence is the easy part. The interesting part is everything we had to fix to make the loop run cleanly. Five bugs the runtime should have warned about at boot: F21 — every Memory operation was silently refused because the agents lacked memory_scopes. Default-deny is correct (operators must explicitly grant scopes); default-silent-deny is a footgun (the runtime knew at config-load this combination was wrong, didn't say so). #389 ships boot-warns for the family: Memory in allowed_tools + empty memory_scopes, Evaluation + empty evaluation_scopes, Channel + empty channels, Interruption + missing interruption.enabled. F18 — spawn_run user_id was silently overridden to "default" under the legacy LOOMCYCLE_AUTH_TOKEN deployment path because RFC L's applyPrincipal mints a fixed placeholder principal (Subject:"default", Legacy:true) and the principal is authoritative over the wire. Channel and Memory ops scoped to "default" not "exp3"; the orchestrator's kickoff to scope_id=exp3 never reached the answerer. #388 ships: when the principal is Legacy, honor the wire user_id (fall back to placeholder only when omitted); real OperatorTokenDef principals keep the strict override. F20 — channeldef CRUD lived only in REST; MCP had publish/subscribe/peek/ack/list but no channeldef. A Claude Code session managing the loomcycle substrate through MCP couldn't create channels. Plus yaml-declared channels were immutable through REST (409 channel_yaml_immutable); buffered messages from a failed earlier run had to be cleared via raw DB delete. #395 adds the meta-tool with create/delete plus #401 adds a purge op that clears channel_messages without a restart. F22 — Channel.subscribe wait_ms was silently truncated by LOOMCYCLE_CHANNELS_LONGPOLL_CAP_MS (default 30s); agents expecting to long-poll for a minute burned their max_iterations budget re-subscribing every 30 seconds. #390 warns at load and at runtime when the cap truncates the request. F29 — runtime-substrate channels were administered correctly but couldn't be used for pub/sub: the per-run channel policy was built from cfg.Channels (yaml) only, never the runtime channel store. So in the fully-dynamic configuration (every entity at runtime, no yaml) the loop couldn't run. #404 merges the runtime channel store with the yaml channels: block in both the per-run policy and the admin publish/subscribe wire check. The dynamic re-run on v0.23.3 (three agents created at runtime via POST /v1/_agentdef, three channels via POST /v1/_channels, kickoff admin-published to a runtime channel) completes the loop cleanly — scores 0.80 → 0.95 across 5 hops, aggregator picks the true max, memory retired. One prompt-side caveat from the dynamic re-run worth naming: read ordering after the done-signal. The aggregator called Evaluation.list_for_run BEFORE the hop-5 submit landed (got 4 evals + null answer for hop 5), then after the done-signal reused that stale snapshot instead of re-reading. The substrate served the data correctly; the agent's prompt forgot to refresh after the gate. Documentation fix, not a runtime fix. Three disciplines fall out: boot-warn on every capability-tool/gate inconsistency, warn on every silent truncation, authoring surface parity across MCP / HTTP / gRPC / TS.
