Handoff: UrlProtocol peer address propagation ? a newly-registered peer's direct address never reaches the resolving peer's registry (two hermetic reproducers ready; prior fix attempt was broken and caused two production outages)
UPDATE 2026-07-26 02:20 UTC — reconciliation: a review-complete fix pair ALREADY EXISTS; do not re-derive from scratch
This handoff was written about the OLDER broken fix attempt (UrlResolver #758 + HCF pin bumps). It does not mention that a later, independent, review-complete implementation of exactly the prescribed shape (protocol-half in UrlProtocol, consumer-half in UrlResolver) already exists:
- https://github.com/CodexCoder21Organization/UrlProtocol/pull/400 — bounded peer exchange may return the authenticated responder itself (protocol half).
- https://github.com/CodexCoder21Organization/UrlResolver/pull/827 — propagate authenticated peer addresses during inbound RPC (resolver half; includes 4 hermetic tests incl. rejection cases).
Both passed supervisory final review on 2026-07-22 (tracker: url://handoff/handoffs/hf-2026-07-22-hcf-peer-scoped-propagation-fix-ci-blocked). As of 2026-07-26 they are being rebased onto moved mains and driven to merge by Opus lanes with user-granted merge authority; #827's one CI-only test failure is being root-caused with a reliable-reproduction requirement before any fix. The next reader should treat this pair as the fix of record and drive/review it rather than re-deriving. The hermetic reproducers described below remain valuable as acceptance tests for the pair. The no-production-deploy guardrail below still stands: nothing deploys to prod without explicit user authorization.
Written: 2026-07-26 (~00:30 UTC). Re-homed out of
url://handoff/handoffs/hf-2026-07-11-hcf-onboarding-address-propagation-fix-and-prod-incident,
which was scoped to HardwareControlFabric onboarding. The owning layer is almost certainly
UrlProtocol (with UrlResolver as the consumer), so the work gets its own record here.
RE-VERIFY BEFORE ACTING
Every state claim below is a write-time snapshot and goes stale. Branches get force-pushed,
CI re-runs, artifacts get published, and hosts restart. Before acting on anything here, re-verify:
- Branches:
git ls-remote https://github.com/CodexCoder21Organization/<repo>.git refs/heads/<branch>
- PRs:
gh pr view <n> --repo CodexCoder21Organization/<repo> --json state,mergeStateStatus,statusCheckRollup,mergedAt
- Check runs on a head SHA:
gh api repos/CodexCoder21Organization/<repo>/commits/<sha>/check-runs
- Production HCF:
curl -sS -o /dev/null -w "%{http_code}\n" https://hardware-control-fabric-wui.wasmserver.com/
A fresh query always wins over this document.
Mission summary
Fix the address-propagation defect in the UrlProtocol/UrlResolver peer stack:
When peer A resolves a peer-scoped alias belonging to a freshly-registered peer B, A's
candidate address set for B contains only /p2p-circuit/ relay routes. B's own reachable
direct listen address is never a candidate, because it never propagates into A's registry.
Since the relays hold no circuit-v2 reservation for B, every dial fails and the resolution fails.
This is a propagation defect, not a dial-ordering defect: you cannot reorder onto a direct
address that was never learned. Five prior dial-side changes (UrlResolver 0.0.622 / 0.0.625 /
0.0.628 / 0.0.630 plus an on-disk dial diff) all failed for this reason.
The first observed victim was HardwareControlFabric new-daemon onboarding via --register-with
(the completeRegistration ping), which fails with
completeRegistration ? 'url://hardware.control.fabric.daemon.<peerId>/' did not respond to ping.
That is a symptom, not the scope ? the scope is the UrlProtocol layer that is supposed to make
B's direct/observed address known to A.
What was found (verified evidence, not speculation)
-
Root cause proven by a live PEER-DIAL-TRACE on the production droplet. Every candidate A held
for B logged containsDropletDirect=false; each dial threw RelayException ? not reachable /
io.libp2p.etc.types.NothingToCompleteException. B's direct address
(/ip4/198.199.106.165/tcp/<p2pPort>/p2p/<peerId>) was open and directly connectable from A on
the same host, yet was never in the candidate set.
-
Where the missing half most likely belongs (hypothesis worth pursuing first). UrlProtocol
main already carries adjacent routing work: #348 Prefer direct mesh paths before relay fallback, #343 link-state relay routing, #349 relay ACK routing race. #348 is a no-op when
the direct path was never learned. The missing half is one (or more) of:
- propagating B's authenticated direct/observed listen addresses into A's registry from the
inbound RPC B makes to A (identify observed-addr, or an explicit announce at registration),
- propagating them via peer exchange (PEX),
- or making a NATed B actually hold a real circuit-v2 reservation so the relay routes work.
-
Peer-scoped aliases can also resolve to the wrong provider. A second reproducer shows a client
that knows the legacy parent service hardware.control.fabric.daemon will satisfy a
peer-scoped child alias hardware.control.fabric.daemon.<peerId> from that parent. That is a
separate correctness bug in peer-scoped resolution and it masks/complicates the propagation bug.
The two hermetic reproducers (carried over ? this is the most valuable asset here)
They live on the surviving UrlResolver branch reproduce-hcf-circuit-only-onboarding, remote
head 839843074a47a8f00d81aac475619d6ae483ae58 (verified present 2026-07-26):
https://github.com/CodexCoder21Organization/UrlResolver/tree/reproduce-hcf-circuit-only-onboarding
| File | What it demonstrates |
|---|---|
| tests/testInboundRpcPropagatesDirectAddressesForPeerScopedRegistration.kts | The primary reproducer. Builds three real in-process UrlProtocol2 peers ? relay R, manager A, daemon B. A is seeded so it knows B only through an unreserved relay-circuit address via R (exactly the production precondition: no direct candidate), while B is a real same-host peer with a bound direct listen address. B then calls A's registration service. The assertion is that the ping inside completeRegistration must see at least one non-circuit direct candidate ? i.e. A must have learned B's authenticated direct listen address from that inbound RPC. On unfixed code A only has the unreserved circuit through R and the in-handler ping fails. @Timeout(90). |
| tests/testPeerScopedResolutionIgnoresLegacyParentService.kts | Registers the legacy parent service url://hardware.control.fabric.daemon/ on one peer, makes a client discover it, then resolves the peer-scoped child alias hardware.control.fabric.daemon.<peerId>. Asserts the child alias is not satisfied by the parent provider. @Timeout(45). |
A third, related reproducer is already on UrlResolver main and is a good starting point for
anyone building on this:
tests/testPeerScopedAliasCircuitOnlyUnreservedRelayReproducesNothingToComplete.kts.
How to run them (hermetic; no droplet, no network services, no deployment):
gh repo clone CodexCoder21Organization/UrlResolver workspace/UrlResolver
cd workspace/UrlResolver
git fetch origin reproduce-hcf-circuit-only-onboarding
git checkout reproduce-hcf-circuit-only-onboarding
scripts/test.bash tests/testInboundRpcPropagatesDirectAddressesForPeerScopedRegistration.kts
scripts/test.bash tests/testPeerScopedResolutionIgnoresLegacyParentService.kts
These tests are in-process UrlProtocol2 instances with real libp2p transports and no mocks. They
are the reason this handoff is worth picking up: the hard part (a hermetic reproduction of a defect
previously only observed in production) is already done.
Note the branch's @file:WithArtifact pins are old (foundation.url:protocol:0.0.337,
jvm-libp2p:1.3.0-codexcoder21-snapshot-10). Rebase onto current main and re-pin before drawing
conclusions from a red run ? a failure caused by a stale pin is not the defect.
History of the prior fix attempt ? it was broken, and it took production down twice
This is recorded in full because the next person must not repeat it.
-
The fix did not work: CI was red, verified. codex (gpt-5.5) wrote an "inbound registration RPC
seeds the daemon's direct addresses into the resolver" fix and pushed it across five repos. Three
of the five had failing CI. Re-verified 2026-07-26 against the recorded head SHAs:
| Repo / PR | Branch @ head | Check-run conclusions on that head |
|---|---|---|
| https://github.com/CodexCoder21Organization/UrlResolver/pull/758 | reproduce-hcf-circuit-only-onboarding @ 83984307 | bld-build: failure, kotlin.build (remote): failure |
| https://github.com/CodexCoder21Organization/HardwareControlFabricDaemon/pull/113 | bump-urlresolver-0.0.630 @ 831e2c97 | kotlin.build (remote): failure |
| https://github.com/CodexCoder21Organization/HardwareControlFabricServiceServer/pull/7 | bump-urlresolver-0.0.630 @ 6c454c5b | kotlin.build (remote): failure |
| https://github.com/CodexCoder21Organization/HardwareControlFabricApi/pull/38 | bump-urlresolver-0.0.630 @ 5aefbeee | passed |
| https://github.com/CodexCoder21Organization/HardwareControlFabricEmbedded/pull/6 | bump-urlresolver-0.0.630 @ 0ab02a0e | passed |
All five PRs are now CLOSED and none was ever merged (verified 2026-07-26; UrlResolver #758
closed 2026-07-17, the four HCF PRs closed 2026-07-22). All five branches still exist at exactly
those head SHAs, so the diff is fully recoverable ? treat it as a broken starting point, not a
near-miss.
-
Production outage #1 (codex run 9). A prod deploy of unverified code.
-
Production outage #2 (~40 minutes, 2026-07-12). codex ran an unconstrained live end-to-end
test: it deployed its broken ServiceServer into ContainerNursery (route
url:hardware-control-fabric:) plus a fix daemon onto the shared droplet, and fired all five
repos' CI simultaneously. The 8-vCPU droplet hit load average 119 and the WUI went down with
500 ? All 0 peers for service 'hardware-control-fabric'. Recovery required restoring the
known-good ServiceServer jar; restoring the daemon alone does not clear "0 peers" ? the
ServiceServer jar is what fixes it.
Hard guardrail for this workstream
Code plus hermetic tests only. No production deploys as part of this work ? none.
- Do not deploy any jar to ContainerNursery or to the droplet at
198.199.106.165 as part of
developing or validating this fix.
- Do not run an unconstrained live end-to-end test against the shared droplet. It is shared
infrastructure and it has been taken down twice by exactly that.
- Do not fire multiple repos' CI simultaneously ? it overloads the shared buildtest droplet.
One heavy build at a time.
- A human drives any production touch, after CI is genuinely green and the diff has been reviewed.
Landing in
main is the end of an agent's scope on this.
- Never merge any of the five recorded PR branches without explicit user approval.
Any future fix must ship with comprehensive end-to-end tests
This is a requirement, not a preference. This code path has taken production down twice and has been
repeatedly under-tested ? five prior "fixes" shipped against it without a test that actually
exercised the failing path. A fix is not acceptable without tests that demonstrably cover:
- The propagation path itself ? A learns B's direct address from B's inbound RPC / identify /
PEX, asserted as "A's candidate set for B contains at least one non-circuit direct address",
not merely "the ping succeeded".
- The failing-before / passing-after property ? show the new test failing on unfixed code and
passing on fixed code. A test that passes on both proves nothing.
- Peer-scoped alias resolution ? a child alias
<service>.<peerId> must never be satisfied by
the parent service <service>, and must resolve to the correct peer when several peers advertise
sibling aliases.
- Relay-only preconditions ? B reachable only via an unreserved circuit, via a reserved
circuit, and via a direct address; and the transitions between those states.
- Re-registration and restart ? B restarting with a new listen port must not leave A pinned to a
stale direct address.
- Concurrency ? several peers registering against A at once; propagation must not depend on
ordering luck.
Testing rules that apply (per the org's testing guidance): real in-process instances, no mocks and
never mockito; fakes only where a real instance is genuinely impossible; assert the full text of
error and warning messages, not fragments; warnings surface as algebraic effects (toss), not
println; anything that measures or waits on time takes a Clock from
community.kotlin.clocks.simple (default SystemClock, tests drive ManualClock). Note the known
trap: Clock.schedule takes an absolute deadline, not an interval ? passing an interval spins
the loop, and ManualClock(0) hides it, so construct ManualClock with a realistic epoch.
Relevant PRs / refs
Surviving branches (all verified present 2026-07-26 at these exact remote SHAs; nothing exists only
on a local disk):
| Repo | Branch | Remote head | What is on it |
|---|---|---|---|
| UrlResolver | reproduce-hcf-circuit-only-onboarding | 839843074a47a8f00d81aac475619d6ae483ae58 | The two reproducers + the broken propagation fix. CI red on this head. |
| HardwareControlFabricApi | bump-urlresolver-0.0.630 | 5aefbeeebabc861ad2b13fdfade1c4af5a6539ff | Pin bump. CI passed. |
| HardwareControlFabricDaemon | bump-urlresolver-0.0.630 | 831e2c97ce3bbfaf4345ac3723cfc964cd81f8f9 | Pin bump. CI red. |
| HardwareControlFabricEmbedded | bump-urlresolver-0.0.630 | 0ab02a0ecff6bc1c1af3d866de71796ea61481c5 | Pin bump. CI passed. |
| HardwareControlFabricServiceServer | bump-urlresolver-0.0.630 | 6c454c5b2db3f65ea4e9475f953c97cb07587318 | Pin bump. CI red. |
Closed PRs (none merged): UrlResolver
https://github.com/CodexCoder21Organization/UrlResolver/pull/758 ? HCFApi
https://github.com/CodexCoder21Organization/HardwareControlFabricApi/pull/38 ? HCFDaemon
https://github.com/CodexCoder21Organization/HardwareControlFabricDaemon/pull/113 ? HCFEmbedded
https://github.com/CodexCoder21Organization/HardwareControlFabricEmbedded/pull/6 ? HCFServiceServer
https://github.com/CodexCoder21Organization/HardwareControlFabricServiceServer/pull/7
Adjacent UrlProtocol work to read before starting: #348 prefer-direct-before-relay, #343
link-state relay routing, #349 relay ACK routing race.
Nothing is unpushed. No local-only work exists for this effort; the working clones from the
originating session were verified clean against the pushed heads above.
Next steps (ordered)
- Re-verify the branches and PR states in the table above before touching anything.
- Rebase the reproducer branch onto current
UrlResolver main and refresh its stale
@file:WithArtifact pins (foundation.url:protocol, jvm-libp2p) to whatever main uses.
Confirm the two reproducers still fail for the right reason ? read the failure message, do
not accept a red run caused by a stale pin as evidence of the defect.
- Discard the broken fix diff and re-derive it in UrlProtocol. Decide explicitly which mechanism
carries B's direct address to A (inbound-RPC seeding, identify observed-addr, announce at
registration, PEX, or a real circuit-v2 reservation) and write the mechanism down in one or two
sentences before writing code.
- Write the comprehensive test suite described above alongside the fix, with failing-before /
passing-after demonstrated for each new test.
- Drive CI genuinely green, one repo at a time, entirely off production. Diagnose the recorded
red runs first ? start from the run logs on the three red head SHAs.
- Stop at merged-to-main. Publishing artifacts, bumping downstream pins, and any droplet or
ContainerNursery deploy are a separate, human-driven step.
Reusable / operational knowledge
All 0 peers for service 'hardware-control-fabric' (HTTP 500 on the WUI) means the deployed
ServiceServer jar is bad, or the daemon is not running. Restarting the daemon alone does not
clear it when the ServiceServer is the bad artifact.
- The HCF daemon has no watchdog ? no systemd unit, no auto-restart. If it dies, the WUI 500s
until a human restarts it.
- Daemon restart, canonical form: kill by filtering
pgrep -x java plus /proc/$p/cmdline for
/root/.HardwareControlFabric/daemon.jar ? never pkill -f daemon.jar, which matches the ssh
argv and kills your own session ? then relaunch with -Xmx128m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/root/.HardwareControlFabric/logs/ under
setsid nohup ... </dev/null.
- Droplet: 8 vCPU / 16 GB at
198.199.106.165 port 23. It is shared with buildtest CI; heavy
parallel builds starve it.
gh pr edit is broken in this org (Projects-classic GraphQL deprecation) ? use
gh api -X PATCH repos/<owner>/<repo>/pulls/<n> -F body=@file.
gh pr checks --json is unsupported here. Use
gh pr view <n> --json statusCheckRollup or gh api repos/<o>/<r>/commits/<sha>/check-runs.
/usr/local/bin/coursier is the wrong architecture on these hosts (Exec format error);
launch the JVM jar instead: java -jar ~/bin/coursier launch ....
- kompile gotchas: bump the
build.kts version literal after source edits or tests silently
resolve the previously published jar; kotlin.directory artifacts are immutable (a republish 409s
even with --force, so bump the patch); purge
~/.cache/coursier/v1/bldbinary/... after same-version source edits.
- The PEX no-backoff busy-loop that used to accompany this investigation is fixed. The daemon
log spam
[PEX-EMPTY] ... re-pull of configured bootstrap ... dial-failed repeating every few
milliseconds was an unthrottled re-pull of an unreachable configured bootstrap in UrlResolver
(src/foundation/url/resolver/UrlResolver.kt). UrlResolver main now has both an inner bounded
exponential backoff (CONFIGURED_BOOTSTRAP_REPULL_BASE_BACKOFF_MS 250 ms ?
CONFIGURED_BOOTSTRAP_REPULL_MAX_BACKOFF_MS 2 s, within a
CONFIGURED_BOOTSTRAP_REPULL_BUDGET_MS 15 s budget) and a shared per-peer schedule across all
service-resync jobs (CONFIGURED_BOOTSTRAP_PEER_BACKOFF_INITIAL_MS 250 ms ?
CONFIGURED_BOOTSTRAP_PEER_BACKOFF_MAX_MS 30 s), driven by the injected Clock and reset on a
received response. Hermetic coverage:
tests/testDeadConfiguredBootstrapRepullsUseSharedPerPeerBackoff.kts (8 services against a dead
bootstrap; asserts at most 3 completed re-pulls in an 18 s window). Landed in
https://github.com/CodexCoder21Organization/UrlResolver/pull/835 (resolver 0.0.661, merged
2026-07-25). Anything still emitting that spam is running a resolver older than 0.0.661 ? the
remedy is a pin bump and redeploy, which is a human-driven production step, not a code fix.