fix(routing): map Fabric DM to a per-channel group session

A Fabric DM (xType='dm') was routed as peerKind='direct', which openclaw
resolves through session.dmScope (default "main") — collapsing EVERY DM
into the agent's single agent:<id>:main session. Fabric supports many
independent DM channels for the same user pair, so each must open its own
openclaw session; otherwise they pile into :main and share context (a
recruiter DM'd on channel A sees channel B's history and never opens a
fresh linear journal per DM).

Route 'dm' as peerKind='group' (session key agent:<id>:fabric:group:<chan>,
one per DM channel) while keeping chatType='direct' for 1:1 semantics. The
no-wakeup-gating DM exception keys off xType==='dm', not chatType, so it's
unaffected. Inbound + outbound (incl. cold-cache fallback) both resolve to
'group', so sessions never split.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
h z
2026-06-08 12:40:25 +01:00
parent f8c8c21727
commit a0b280541c

View File

@@ -27,27 +27,31 @@ import { getChannelType } from './channel-meta.js';
* Map a Fabric channel xType to an openclaw routing peer.kind / ChatType. * Map a Fabric channel xType to an openclaw routing peer.kind / ChatType.
* *
* Fabric distinguishes channels by xType ('dm' | 'triage' | 'group' | * Fabric distinguishes channels by xType ('dm' | 'triage' | 'group' |
* 'broadcast' | 'announce' | ...). Openclaw's session router only knows * 'broadcast' | 'announce' | ...). This maps each to:
* 'direct' | 'group' | 'channel'. We collapse: * - peerKind — openclaw's SESSION router key ('direct' | 'group')
* - 'dm' → 'direct' (1:1 conversation; agent always speaks) * - chatType — ctx.ChatType the agent sees (drives isDirectMessage etc.)
* - rest → 'group' (multi-party; turn-engine gates speech)
* *
* Sessions are keyed by peer.kind, so inbound and outbound MUST agree — * CRITICAL: peerKind for 'dm' is 'group', NOT 'direct'. Openclaw routes
* otherwise the agent's outbound message lands in a different session * peer.kind==='direct' through `session.dmScope` (default "main"), which
* than the inbound that triggered it and conversation state splits. * collapses EVERY direct conversation into the agent's single `:main`
* session. Fabric, however, supports many independent DM channels for the
* same pair of users — each must get its own openclaw session, or they all
* pile into `:main` and share context (a recruiter DM'd on channel A would
* see channel B's history, and never opens a fresh linear journal per DM).
* Routing 'dm' as peerKind='group' keys the session by channelId
* (`agent:<id>:fabric:group:<chan>`) — one session per DM channel — while
* chatType='direct' preserves 1:1 semantics (agent always speaks; the
* inbound no-wakeup-gating exception keys off xType==='dm', not chatType).
* *
* Outbound has no live xType (the agent target is just a channelId), so * Sessions are keyed by peerKind, so inbound and outbound MUST agree.
* it consults the channel-meta cache populated by inbound. Cache miss * Both now resolve 'dm' (and the cold-cache fallback) to 'group', so they
* (channel never observed) falls back to 'group' — same as the pre-fix * never split.
* behavior, no regression on cold cache. The proactive-DM-first-message
* edge case (agent DMs a channel before any inbound) still lands as
* 'group' on that one outbound; the next inbound + outbound pair will
* agree on 'direct'.
*/ */
export type FabricPeerRouting = { peerKind: 'direct' | 'group'; chatType: 'direct' | 'group' }; export type FabricPeerRouting = { peerKind: 'direct' | 'group'; chatType: 'direct' | 'group' };
export function fabricPeerRoutingForXType(xType: string | null | undefined): FabricPeerRouting { export function fabricPeerRoutingForXType(xType: string | null | undefined): FabricPeerRouting {
if (xType === 'dm') return { peerKind: 'direct', chatType: 'direct' }; // dm → group session routing (per-channel), but direct chat semantics.
if (xType === 'dm') return { peerKind: 'group', chatType: 'direct' };
return { peerKind: 'group', chatType: 'group' }; return { peerKind: 'group', chatType: 'group' };
} }