Update — revised mechanism. I originally filed this as "the relay stops delivering DM messages after a websocket reconnect." Further investigation suggests that framing is probably wrong, or at least describes a symptom rather than the cause. A more likely mechanism is duplicate agent identities: one display name registered under two distinct pubkeys, which makes mention/DM resolution by name land on a dead identity. The original observations are preserved below because they are reproducible and may still be independently useful; the revised analysis is at the top.
Revised hypothesis: duplicate identities break name resolution
What is confirmed (directly observed)
1. Identity is split across three stores with nothing reconciling them.
- The
pubkey recorded in the local agent registry at provisioning time.
- The private key, in a password store under a hardcoded entry name.
- The registration held by the relay once the agent first announces.
Nothing verifies these agree, and nothing deregisters a stale relay registration when an identity is replaced.
2. Agents were provisioned with a pubkey but no private key. Three agents were created within 4 seconds of each other with pubkeys recorded, but no private key was ever written to the password store. That gap sat latent for 2.5 days, until the supervisor tried to start them and failed with identity key unavailable for exactly those three. New key material appeared 8 minutes later.
For comparison, agents provisioned later had their key written 22–57 seconds after the record was created. The three earliest had a 2.5-day gap, and a fourth had an 11.5-hour gap.
If the key written later is a fresh keypair rather than a recovery of the original, the agent now announces under a new pubkey while the original pubkey remains registered on the relay indefinitely.
3. Nothing validates the key against the record. There is no step anywhere in the startup path that derives the pubkey from the loaded private key and compares it to the recorded pubkey. An agent whose key does not match its record will connect and announce under a second identity, silently.
4. The password-store entry name is a hardcoded map with three different naming conventions across eight agents (<name>, <lowercase-name>, <lowercase-name>-key). Any rename breaks the lookup, and a failed lookup is what leads to replacement key material being generated.
5. The lookup can wedge. pass spins at 100% CPU indefinitely when gpg cannot prompt for a passphrase (e.g. a non-interactive session). Reproduced accidentally during this investigation — one such process consumed a full core for 10 minutes. A comment in the operator's supervisor script records a prior incident where three of these pinned three cores. A wedged lookup presents as identity key unavailable, which is the same signal that precedes replacement-key generation.
What is reported but not independently verified
An agent with a working identity queried the community roster and reported that five display names each map to two distinct pubkeys, while three are singletons — 13 bot registrations for 8 running agents. It named a specific second pubkey for one of the duplicated display names.
I could not verify this myself: querying the roster requires an authenticated relay connection, and the credential is behind a gpg passphrase prompt unavailable in a non-interactive session. Treat this as a strong lead rather than established fact.
Why this explains the original symptom better
If a display name resolves to two pubkeys and only one has a live process behind it, then mention-by-name and DM addressing can resolve to the dead identity and fail silently — no error on either side. That accounts for the group-works / DM-fails split in the original report: group-channel delivery is by channel membership and does not depend on resolving a name to a pubkey, whereas DM addressing does.
It also explains why a full cold restart with verifiably correct subscriptions changed nothing, which never fit a "relay stopped delivering" explanation.
Suggested hardening
- Make provisioning atomic — write the key and the recorded pubkey together, or neither. A record with no key is the origin of this whole failure.
- Assert identity at startup — derive the pubkey from the loaded key, compare to the record, refuse to start on mismatch rather than announcing a second identity.
- Never generate replacement key material on lookup failure. A missing key should be loud and fatal for that agent only.
- Deregister on re-key — if an identity is legitimately replaced, remove the prior registration.
- Reject duplicate display names at registration, or require disambiguation, so name resolution cannot become ambiguous.
- Bound the credential lookup — a passphrase prompt with no tty should fail fast, not spin.
Original report (preserved)
Summary
After a websocket reconnect, a hosted community relay stopped delivering DM-channel messages to headless ACP agents, while continuing to deliver group-channel messages to those same agents over the same websocket, for over two hours. It survived agent reconnect, full agent restart, and creation of an entirely new DM channel.
Timeline (UTC, single day)
DM delivery working normally — 7 turns across 6 agents on 6 distinct DM channels:
09:22:59 agent A turn starting for channel 4026f65c… (DM)
09:41:45 agent B turn starting for channel 4252c972… (DM)
09:42:10 agent C turn starting for channel 551a61db… (DM)
09:42:33 agent D turn starting for channel b5b744e2… (DM)
09:43:31 agent E turn starting for channel 108ee621… (DM)
09:47:28 agent F turn starting for channel 327df1c1… (DM)
09:48:35 agent A turn starting for channel 4026f65c… (DM) <-- last DM ever delivered
Then the host running the agents came under heavy resource pressure and the websockets flapped:
10:13:31 WARN no pong received within 10s — connection dead, reconnecting
10:14-10:30 reconnect attempts fail: "Connection closed", "Connection reset by peer"
10:32:0x all 8 agents reconnect and resubscribe
The host-side resource problem was fully resolved shortly after. From 09:48:35 onward, zero DM-channel messages were delivered to any of the 8 agents, while group-channel messages continued flowing the entire time.
Steps taken to recover (none worked)
- Agent-driven reconnect —
resubscribing to N channel(s) after reconnect, counts matched the pre-disconnect subscription sets including DM channels. Group messages resumed; DMs did not.
- Full cold restart of 7 of 8 agents. Fresh processes, fresh discovery:
discovered 5 channel(s) followed by explicit subscribed to channel <dm-channel-id>. Group messages delivered; DMs did not.
- Brand-new DM channel. An agent received
membership notification: subscribing to new channel and subscribed. A message was sent to that channel by the human user. 22+ minutes later the agent had received nothing on it.
Agent-side state during the failure
Every observable client-side signal reported healthy: 8/8 established websockets; each agent logging subscribed to channel <its own DM channel>; each logging subscribed to membership notifications; each logging presence set to online; balanced turn starting/turn complete counts; and group-channel messages from the same human sender delivered and processed normally on the same connection throughout.
Note on observability
subscribed to channel X is logged on the agent side without any indication of whether the relay acknowledged and registered the subscription. A subscription the relay never honoured is indistinguishable in the logs from one that works. Surfacing a relay-side ack — or a periodic subscription reconciliation — would make this class of failure diagnosable.
Revised hypothesis: duplicate identities break name resolution
What is confirmed (directly observed)
1. Identity is split across three stores with nothing reconciling them.
pubkeyrecorded in the local agent registry at provisioning time.Nothing verifies these agree, and nothing deregisters a stale relay registration when an identity is replaced.
2. Agents were provisioned with a pubkey but no private key. Three agents were created within 4 seconds of each other with pubkeys recorded, but no private key was ever written to the password store. That gap sat latent for 2.5 days, until the supervisor tried to start them and failed with
identity key unavailablefor exactly those three. New key material appeared 8 minutes later.For comparison, agents provisioned later had their key written 22–57 seconds after the record was created. The three earliest had a 2.5-day gap, and a fourth had an 11.5-hour gap.
If the key written later is a fresh keypair rather than a recovery of the original, the agent now announces under a new pubkey while the original pubkey remains registered on the relay indefinitely.
3. Nothing validates the key against the record. There is no step anywhere in the startup path that derives the pubkey from the loaded private key and compares it to the recorded pubkey. An agent whose key does not match its record will connect and announce under a second identity, silently.
4. The password-store entry name is a hardcoded map with three different naming conventions across eight agents (
<name>,<lowercase-name>,<lowercase-name>-key). Any rename breaks the lookup, and a failed lookup is what leads to replacement key material being generated.5. The lookup can wedge.
passspins at 100% CPU indefinitely when gpg cannot prompt for a passphrase (e.g. a non-interactive session). Reproduced accidentally during this investigation — one such process consumed a full core for 10 minutes. A comment in the operator's supervisor script records a prior incident where three of these pinned three cores. A wedged lookup presents asidentity key unavailable, which is the same signal that precedes replacement-key generation.What is reported but not independently verified
An agent with a working identity queried the community roster and reported that five display names each map to two distinct pubkeys, while three are singletons — 13 bot registrations for 8 running agents. It named a specific second pubkey for one of the duplicated display names.
I could not verify this myself: querying the roster requires an authenticated relay connection, and the credential is behind a gpg passphrase prompt unavailable in a non-interactive session. Treat this as a strong lead rather than established fact.
Why this explains the original symptom better
If a display name resolves to two pubkeys and only one has a live process behind it, then mention-by-name and DM addressing can resolve to the dead identity and fail silently — no error on either side. That accounts for the group-works / DM-fails split in the original report: group-channel delivery is by channel membership and does not depend on resolving a name to a pubkey, whereas DM addressing does.
It also explains why a full cold restart with verifiably correct subscriptions changed nothing, which never fit a "relay stopped delivering" explanation.
Suggested hardening
Original report (preserved)
Summary
After a websocket reconnect, a hosted community relay stopped delivering DM-channel messages to headless ACP agents, while continuing to deliver group-channel messages to those same agents over the same websocket, for over two hours. It survived agent reconnect, full agent restart, and creation of an entirely new DM channel.
Timeline (UTC, single day)
DM delivery working normally — 7 turns across 6 agents on 6 distinct DM channels:
Then the host running the agents came under heavy resource pressure and the websockets flapped:
The host-side resource problem was fully resolved shortly after. From 09:48:35 onward, zero DM-channel messages were delivered to any of the 8 agents, while group-channel messages continued flowing the entire time.
Steps taken to recover (none worked)
resubscribing to N channel(s) after reconnect, counts matched the pre-disconnect subscription sets including DM channels. Group messages resumed; DMs did not.discovered 5 channel(s)followed by explicitsubscribed to channel <dm-channel-id>. Group messages delivered; DMs did not.membership notification: subscribing to new channeland subscribed. A message was sent to that channel by the human user. 22+ minutes later the agent had received nothing on it.Agent-side state during the failure
Every observable client-side signal reported healthy: 8/8 established websockets; each agent logging
subscribed to channel <its own DM channel>; each loggingsubscribed to membership notifications; each loggingpresence set to online; balancedturn starting/turn completecounts; and group-channel messages from the same human sender delivered and processed normally on the same connection throughout.Note on observability
subscribed to channel Xis logged on the agent side without any indication of whether the relay acknowledged and registered the subscription. A subscription the relay never honoured is indistinguishable in the logs from one that works. Surfacing a relay-side ack — or a periodic subscription reconciliation — would make this class of failure diagnosable.