You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #555 filed after running the M9 diagnostic patch against the trader soak on 2026-06-15.
TL;DR
M9 (EARLY_MESSAGE_CAP=32 silent drop) is RULED OUT as a §4 failure mechanism. The cap was nowhere near being hit on any ACP DM call across an entire soak (peak buffered=14/32, drops=0/0 across 8 measured calls).
A new anomaly surfaced that wasn't in Nostr relay degradation across consecutive trader-soak runs — retry-loop flooding hypothesis + amplification of #473 #555's M1–M7: every fresh CLI invocation sees 3–14 NIP-17 gift-wrap events arrive on its chat subscription within 140–900 ms of subscribe, on a brand-new wallet whose #p filter should match nothing in the relay's index. The decoded DMs sit in the early-message buffer until sendDM resolves resolvedPubkey. This isn't the §4 cause but it's relay-side noise that needs explaining.
Diagnostic patch used
Temporary instrumentation in sphere-cli/src/trader/acp-transport.ts (not landed). On every acp.command send:
[acp-m9] resolvedPubkey set t+Nms, buffered=K/32 dropCount=0 command=X — fires when sendDM resolves, recording the resolve window and how many backlog DMs were buffered during it.
[acp-m9] early-message cap reached t+Nms, ... — fires when earlyMessages.length === EARLY_MESSAGE_CAP (silent-drop path).
Patch lives at the head of AcpDmTransportImpl.handleIncoming and inside the comms.sendDM(...).then((sent) => ...) callback. Easy to re-apply if needed — see #558 for the SDK side.
Data from one full soak (8 ACP DM calls)
Section
Caller
Command
resolve t+ms
buffered/32
dropCount
§3 spawn
alice
SET_STRATEGY
142
4
0
§3 spawn
bob
SET_STRATEGY
198
3
0
§4 probe
alice
GET_PORTFOLIO
886
11
0
§4 probe
bob
GET_PORTFOLIO
147
4
0
§5 pre
alice
GET_PORTFOLIO
136
12
0
§5 pre
bob
GET_PORTFOLIO
150
5
0
§6 sell
alice
CREATE_INTENT
144
14
0
§7 buy
bob
CREATE_INTENT
145
7
0
(Snapshot dir preserved at /tmp/trader-roundtrip-2967836/ via KEEP=1 for reference. Raw [acp-m9] lines are in each snapshots/*-acp-probe.log and the per-command snapshots.)
What ruled out
M9 silent-drop. Cap=32, observed peak=14/32 (44 %). Even if the run-1 failure had been the failing condition, the cap was untouched. Drop this from the active hypothesis list.
What remains open
A) Fresh-wallet backlog anomaly (NEW)
Every fresh CLI process — fresh wallet, fresh #p filter — finds 3–14 NIP-17 gift-wrap events arriving on the chat subscription within ~200 ms of subscribe. The decoded events make it through the receiver's NIP-17 unwrap (otherwise they wouldn't reach handleIncoming), so each successfully decrypted to something targeting this controller's transport pubkey.
For a brand-new pubkey there should be nothing matching on the relay. Possibilities:
Relay filter looseness. The testnet relay may be returning kind=1059 events that don't actually match #p:[fresh_pubkey] — for example, by widening the filter under load, or because the #p index isn't applied before the since window. Cheap to verify with nostr-tool sub --kinds 1059 --p <fresh_pubkey> against the same relay from a clean Node script — should return 0 events; if it returns N, that's the bug.
Tenant or escrow side-channel publishes. Something in the spawned stack (HM-tenant ACK, hello, escrow ping, market-API DMs) is publishing kind-1059 events targeting the fresh controller pubkey before the controller even sends its first DM. Plausible since the controller's pubkey is in trader spawn metadata.
HD-derivation collision. The wallet generates multiple addresses and adds all transport pubkeys to allPubkeys for the chat subscription. If any of these derivations collide with previously-used pubkeys on the relay (BIP-32 path reuse, tenant key derivation overlap, etc.), the filter would match historical events. Less likely but worth checking by counting how many distinct senderPubkeys appear in the buffered messages.
The anomaly doesn't break §4 — the buffer drains cleanly when resolvedPubkey resolves and the sender filter throws out anything that isn't the tenant — but it does mean the buffer is starting each session at 30–40 % full of something, which materially shifts the headroom for the EARLY_MESSAGE_CAP discussion above.
B) The 886 ms resolve outlier
Alice's first §4 ACP probe took 886 ms to resolve sendDM, vs 136–198 ms for every other call in the same soak. All other calls (including bob's same-section probe) clustered at 142–198 ms. Hypotheses:
Nametag resolution cache cold-start.sendDM resolves @tenant_nt → tenant pubkey via NostrTransportProvider.resolveNametagInfo which issues a separate REQ on the same socket. First-ever @tenant resolution on a fresh wallet should cost one extra round-trip; cache hits should be ms-fast. Confirm with a debug log around queryBindingByNametag.
REQ multiplexing contention on the shared socket. The chat subscription opens first and immediately starts streaming the (mystery) backlog. The nametag REQ goes out on the same socket — depending on the relay's REQ-serialization behavior, the nametag response can wait behind the chat backlog stream. The 886 ms outlier correlates with the bigger backlog (11 messages) on the same call.
(B) and (A) interact — if the relay is dumping unexpected backlog on the chat subscription, the nametag REQ on the same socket pays for it. The fix surface is shared.
C) M10b (per-IP rate-limit) — still untested
Strongest remaining hypothesis for the cross-run monotonic creep in #555. Cheapest discriminator: run §4 from a different egress IP after a slow day on the dev box. Hasn't been attempted.
Suggested next actions
Verify (A) with a clean Nostr query — directly query nostr-relay.testnet.unicity.network for kind=1059, #p=<fresh_pubkey> from a clean Node script. Expected: 0 events. Observed during this soak (indirectly): 3–14. If the direct query reproduces, file as a relay-side issue with concrete numbers.
Run the M10b discriminator (different-IP §4 test).
Follow-up to #555 filed after running the M9 diagnostic patch against the trader soak on 2026-06-15.
TL;DR
#pfilter should match nothing in the relay's index. The decoded DMs sit in the early-message buffer untilsendDMresolvesresolvedPubkey. This isn't the §4 cause but it's relay-side noise that needs explaining.Diagnostic patch used
Temporary instrumentation in
sphere-cli/src/trader/acp-transport.ts(not landed). On everyacp.commandsend:[acp-m9] resolvedPubkey set t+Nms, buffered=K/32 dropCount=0 command=X— fires whensendDMresolves, recording the resolve window and how many backlog DMs were buffered during it.[acp-m9] early-message cap reached t+Nms, ...— fires whenearlyMessages.length === EARLY_MESSAGE_CAP(silent-drop path).Patch lives at the head of
AcpDmTransportImpl.handleIncomingand inside thecomms.sendDM(...).then((sent) => ...)callback. Easy to re-apply if needed — see #558 for the SDK side.Data from one full soak (8 ACP DM calls)
(Snapshot dir preserved at
/tmp/trader-roundtrip-2967836/viaKEEP=1for reference. Raw[acp-m9]lines are in eachsnapshots/*-acp-probe.logand the per-command snapshots.)What ruled out
What remains open
A) Fresh-wallet backlog anomaly (NEW)
Every fresh CLI process — fresh wallet, fresh
#pfilter — finds 3–14 NIP-17 gift-wrap events arriving on the chat subscription within ~200 ms of subscribe. The decoded events make it through the receiver's NIP-17 unwrap (otherwise they wouldn't reachhandleIncoming), so each successfully decrypted to something targeting this controller's transport pubkey.For a brand-new pubkey there should be nothing matching on the relay. Possibilities:
#p:[fresh_pubkey]— for example, by widening the filter under load, or because the#pindex isn't applied before thesincewindow. Cheap to verify withnostr-tool sub --kinds 1059 --p <fresh_pubkey>against the same relay from a clean Node script — should return 0 events; if it returns N, that's the bug.trader spawnmetadata.allPubkeysfor the chat subscription. If any of these derivations collide with previously-used pubkeys on the relay (BIP-32 path reuse, tenant key derivation overlap, etc.), the filter would match historical events. Less likely but worth checking by counting how many distinctsenderPubkeys appear in the buffered messages.The anomaly doesn't break §4 — the buffer drains cleanly when
resolvedPubkeyresolves and the sender filter throws out anything that isn't the tenant — but it does mean the buffer is starting each session at 30–40 % full of something, which materially shifts the headroom for the EARLY_MESSAGE_CAP discussion above.B) The 886 ms resolve outlier
Alice's first §4 ACP probe took 886 ms to resolve
sendDM, vs 136–198 ms for every other call in the same soak. All other calls (including bob's same-section probe) clustered at 142–198 ms. Hypotheses:sendDMresolves@tenant_nt → tenant pubkeyviaNostrTransportProvider.resolveNametagInfowhich issues a separate REQ on the same socket. First-ever@tenantresolution on a fresh wallet should cost one extra round-trip; cache hits should be ms-fast. Confirm with a debug log aroundqueryBindingByNametag.(B) and (A) interact — if the relay is dumping unexpected backlog on the chat subscription, the nametag REQ on the same socket pays for it. The fix surface is shared.
C) M10b (per-IP rate-limit) — still untested
Strongest remaining hypothesis for the cross-run monotonic creep in #555. Cheapest discriminator: run §4 from a different egress IP after a slow day on the dev box. Hasn't been attempted.
Suggested next actions
nostr-relay.testnet.unicity.networkforkind=1059, #p=<fresh_pubkey>from a clean Node script. Expected: 0 events. Observed during this soak (indirectly): 3–14. If the direct query reproduces, file as a relay-side issue with concrete numbers.EARLY_MESSAGE_CAPfrom 32 to ~128 insphere-cli/src/trader/acp-transport.ts. Risk is low (in-memory buffer growth), upside is robustness against the (A) anomaly should it ever scale up. Not urgent.Related
selfWrap: falseAPI surface)