Split out of #926 as an accepted, deferred regression rather than left as a bullet in a PR body.
What changes
The OpenClaw harness cutover deletes MoltZapService.sendToAgent and the agentConversationCacheRef it maintained. That cache is what made repeated proactive DMs to the same agent land in one conversation.
Outbound on the harness path is HarnessClient.startConversation(names, initialContent), and moltzapd.ts → startConversationForHarness calls agentConversationCreate unconditionally. So after the cutover, every proactive DM to the same peer creates a new conversation.
Why it is being taken rather than fixed inline
Three options were considered:
- (a) Drop
conv:<id> outbound and accept the regression. ← current state
- (b) Drop
conv:<id> outbound but add a reuse-existing-1:1 lookup inside start_conversation.
- (c) Widen the reply tool to accept an explicit
conversationId.
(c) is off the table: 20260728-model-surface-is-start-reply-listen states there is no generic send, and a conversationId-bearing reply is generic send by another name.
(b) is the right end state and is what this issue tracks. It was deferred so the cutover could land as one atomic shape change.
Fix recipe
Inside startConversationForHarness (packages/client/src/moltzapd.ts), before calling agentConversationCreate: when the participant set is exactly [self, one-other], search existing conversations for a matching two-member conversation and return it instead of creating one.
conversationSearch is already wired into the daemon's MCP surface by the read plane, so the lookup needs no new protocol method. Note the existing two-member read semantics — client already treats a two-member conversation as direct (see 5af93a09 fix(client): read a two-member conversation as direct).
Acceptance
A test that calls startConversation([peer], text) twice for the same peer and asserts both messages land in the same ConversationId.
Provenance
Maintainer decision, 2026-08-04: "Proactive DMs; that's fine. lets deal with that later."
Split out of #926 as an accepted, deferred regression rather than left as a bullet in a PR body.
What changes
The OpenClaw harness cutover deletes
MoltZapService.sendToAgentand theagentConversationCacheRefit maintained. That cache is what made repeated proactive DMs to the same agent land in one conversation.Outbound on the harness path is
HarnessClient.startConversation(names, initialContent), andmoltzapd.ts → startConversationForHarnesscallsagentConversationCreateunconditionally. So after the cutover, every proactive DM to the same peer creates a new conversation.Why it is being taken rather than fixed inline
Three options were considered:
conv:<id>outbound and accept the regression. ← current stateconv:<id>outbound but add a reuse-existing-1:1 lookup insidestart_conversation.conversationId.(c) is off the table:
20260728-model-surface-is-start-reply-listenstates there is no generic send, and a conversationId-bearing reply is generic send by another name.(b) is the right end state and is what this issue tracks. It was deferred so the cutover could land as one atomic shape change.
Fix recipe
Inside
startConversationForHarness(packages/client/src/moltzapd.ts), before callingagentConversationCreate: when the participant set is exactly[self, one-other], search existing conversations for a matching two-member conversation and return it instead of creating one.conversationSearchis already wired into the daemon's MCP surface by the read plane, so the lookup needs no new protocol method. Note the existing two-member read semantics —clientalready treats a two-member conversation as direct (see5af93a09 fix(client): read a two-member conversation as direct).Acceptance
A test that calls
startConversation([peer], text)twice for the same peer and asserts both messages land in the sameConversationId.Provenance
Maintainer decision, 2026-08-04: "Proactive DMs; that's fine. lets deal with that later."