Skip to content

fix(chat): honor to: end-to-end — derive dm:<sorted> channel + preserve through cloud sync#1322

Merged
itskai-dev merged 2 commits into
mainfrom
claude/dm-routing-seam
May 3, 2026
Merged

fix(chat): honor to: end-to-end — derive dm:<sorted> channel + preserve through cloud sync#1322
itskai-dev merged 2 commits into
mainfrom
claude/dm-routing-seam

Conversation

@itskai-dev

Copy link
Copy Markdown
Collaborator

Summary

Wires the chat send to: field to a real routing destination so DMs stop falling through to #general.

  • chat.ts — when to: is set and channel is absent, derive a stable dm:<sorted endpoints> channel (a→b and b→a hash to the same thread, lowercased + trimmed)
  • cloud.ts — preserve to: over the cloud sync wire alongside the dm channel (belt-and-suspenders; cloud schema unchanged, handler ignores to for now)
  • new tests/chat-dm-routing.test.ts (7 tests)

No schema change · no inbox change · no UI in this PR · no auth/model rewrite · no backfill.

This is PR1 of the DM lane — pairs with the openclaw plugin fix (reflectt-channel-openclaw#TBD) where outbound.sendText was hardcoding "general" and dropping ctx.to. Together those two cuts close the routing lie at the source. PR2 will be the minimal canvas/UI subscribe seam for current-user dm:* threads.

Routing truth proven

  • Host-agent DM (openclaw outbound.sendText({ to: "kai", ... })) → lands in dm:<sorted>, not #general (after openclaw PR)
  • Meta-team DM (MCP send_message({ from, to, content })) → lands in dm:<sorted>, not #general
  • Cloud sync preserves to: on the wire (cloud handleChatSync silently drops the field for now — future schema add can pick it up additively)
  • Inbox routing already DM-aware at inbox.ts:320 (if (message.to === agent) return { priority: 'high', reason: 'dm' }); no change needed

Test plan

  • npx vitest run tests/chat-dm-routing.test.ts — 7/7 pass
  • Full suite — 244 files / 2638 passed (1 unrelated background-timer error in chat-approval-detector, pre-existing)
  • npx tsc --noEmit — clean
  • Pair with openclaw PR, then staging proof on canonical (e4e35463-…): two agents DM each other, observe dm:<sorted> in chat_messages, observe absence in #general

🤖 Generated with Claude Code

…erve through cloud sync

The `to:` field was accepted at the chat send boundary but never consulted
for routing — every DM fell through to #general:

  // before
  const channel = message.channel || 'general'

This wires `to:` to a stable `dm:<sorted endpoints>` channel when no
explicit channel is provided, so a→b and b→a hash to the same thread.
Cloud sync now preserves `to:` over the wire alongside the dm channel
(belt-and-suspenders; cloud schema unchanged, handler ignores `to` for
now). Inbox routing already DM-aware, no changes there.

Pairs with the openclaw plugin fix (sendText was hardcoding "general"
and dropping ctx.to). Together those two cuts close the routing lie
without schema/inbox/UI/auth/backfill changes.

Tests: 7 new in chat-dm-routing.test.ts cover sorted-endpoint derivation,
case/whitespace normalization, explicit-channel precedence, and that
non-DM messages still default to #general.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Internal-names guard caught the example name in the comment block.
Same intent, generic phrasing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@itskai-dev
itskai-dev merged commit 9dfde8a into main May 3, 2026
11 checks passed
@itskai-dev
itskai-dev deleted the claude/dm-routing-seam branch May 3, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant