From 932cf0f8d6211937c2d3f29b68c049d51f9d8937 Mon Sep 17 00:00:00 2001 From: Vladimir Rogojin Date: Sun, 21 Jun 2026 09:34:03 +0200 Subject: [PATCH] fix(transport)(sphere-sdk#559): opt out HMCP + ACP sendDM self-wraps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F3 from sphere-sdk#559. Wires `{ selfWrap: false }` into the two short-lived RPC paths the F1 soak identified as the biggest contributors to §3/§4 relay-index pollution: - src/transport/dm-transport.ts (HMCP — CLI → host manager) - src/trader/acp-transport.ts (ACP — CLI → trader tenant) Both are short-lived RPC: the `sphere host …` / `sphere trader …` process publishes a single request and exits as soon as the reply correlates. The default NIP-17 self-wrap publishes a SECOND gift-wrap targeting the SENDER's own pubkey so a returning client can replay outbound history — useless work for one-shot CLI, and exactly the noise the F1 soak measured at ~7 new events per spawn process in §3 of `manual-test-trader-roundtrip.sh`. Both files also widen their local `SphereComms.sendDM` interface to forward an optional `SendMessageOptions` to the SDK call. `sphere message send` in src/legacy/legacy-cli.ts:3961 is NOT touched — that command is user-initiated and the user likely wants their sent DM in their own outbound history. Default behavior preserved. ## Validation - npx tsc --noEmit → no new errors (3 pre-existing in legacy-cli unrelated to F3, confirmed by stashing F3 and re-running) - npx vitest run → 187/187 pass ## Related - sphere-sdk#555 — original M8 mitigation framing - sphere-sdk#559 — fresh-wallet backlog + F1 soak verdict - sphere-sdk PR #558 — selfWrap: false SDK API (merged into uxf) - sphere-sdk PR #582 — F1 instrumentation (open, awaiting follow-up soak) - sphere-sdk PR #614 — re-export SendMessageOptions from index.ts (merged) --- src/trader/acp-transport.ts | 15 ++++++++++++--- src/transport/dm-transport.ts | 15 ++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/trader/acp-transport.ts b/src/trader/acp-transport.ts index 40a0135..d65215a 100644 --- a/src/trader/acp-transport.ts +++ b/src/trader/acp-transport.ts @@ -10,7 +10,7 @@ * Mirror of trader-service/src/cli/dm-transport.ts. */ -import type { DirectMessage } from '@unicitylabs/sphere-sdk'; +import type { DirectMessage, SendMessageOptions } from '@unicitylabs/sphere-sdk'; import { createAcpMessage, @@ -30,7 +30,11 @@ export type { TimeoutError, TransportError }; export { MIN_TIMEOUT_MS }; export interface SphereComms { - sendDM(recipient: string, content: string): Promise<{ recipientPubkey: string }>; + sendDM( + recipient: string, + content: string, + options?: SendMessageOptions, + ): Promise<{ recipientPubkey: string }>; onDirectMessage(handler: (message: DirectMessage) => void): () => void; } @@ -165,7 +169,12 @@ class AcpDmTransportImpl implements AcpDmTransport { return; } - this.comms.sendDM(this.tenantAddress, payload).then((sent) => { + // sphere-sdk#559 / PR #558 — ACP RPC is short-lived (the trader + // CLI's `sphere trader ` exits as soon as the result + // lands). Skip the self-wrap so we don't leave a copy of every + // outgoing ACP command in the relay's `#p:controller` index for + // the next short-lived CLI process to pull down on subscribe. + this.comms.sendDM(this.tenantAddress, payload, { selfWrap: false }).then((sent) => { if (!this.resolvedPubkey) { this.resolvedPubkey = normalizeKey(sent.recipientPubkey); // Drain pre-resolution buffer. diff --git a/src/transport/dm-transport.ts b/src/transport/dm-transport.ts index 79bc99f..0d97711 100644 --- a/src/transport/dm-transport.ts +++ b/src/transport/dm-transport.ts @@ -13,7 +13,7 @@ * }); */ -import type { DirectMessage } from '@unicitylabs/sphere-sdk'; +import type { DirectMessage, SendMessageOptions } from '@unicitylabs/sphere-sdk'; import type { HmcpRequest, HmcpResponse } from './hmcp-types.js'; import { parseHmcpResponse, byteLength, MAX_MESSAGE_SIZE } from './hmcp-types.js'; import { TimeoutError, TransportError } from './errors.js'; @@ -47,7 +47,11 @@ export interface DmTransportConfig { /** Narrow slice of Sphere.communications needed by DmTransport — injectable for testing. */ export interface SphereComms { - sendDM(recipient: string, content: string): Promise<{ recipientPubkey: string }>; + sendDM( + recipient: string, + content: string, + options?: SendMessageOptions, + ): Promise<{ recipientPubkey: string }>; onDirectMessage(handler: (message: DirectMessage) => void): () => void; } @@ -286,7 +290,12 @@ class DmTransportImpl implements DmTransport { `Request too large: ${byteLength(serialized)} bytes exceeds MAX_MESSAGE_SIZE (${MAX_MESSAGE_SIZE})`, ); } - const sent = await this.comms.sendDM(this.managerAddress, serialized); + // sphere-sdk#559 / PR #558 — HMCP RPC is short-lived (CLI exits + // shortly after the manager's reply). Suppressing the self-wrap + // halves the per-process relay-index pollution targeting the + // controller pubkey, which directly reduces the §3/§4 buffered- + // event noise the F1 soak measured at ~7 new events per spawn. + const sent = await this.comms.sendDM(this.managerAddress, serialized, { selfWrap: false }); // Cache the resolved pubkey on first send. Subsequent sends for the same // manager address produce the same pubkey, so concurrent writes are safe. if (!this.resolvedPubkey) {