fix(inbound): route fabric DM channels as peer.kind='direct' / ChatType='direct'
Inbound was hardcoding `peer: { kind: 'group' }` and `ChatType: 'group'`
for every fabric channel regardless of xType. As a result:
- sessionKey for a DM was `agent:<id>:fabric:group:<chan>` instead of
`agent:<id>:fabric:direct:<chan>`
- ctx.ChatType='group' caused user-prompt metadata to render
`is_group_chat: true` on a DM
- openclaw's `isDirectMessage()` check (ChatType==='direct') returned
false, so DM-specific prompt and turn behavior never engaged
Caught by recruiter test in session 40c51de2: the model's thinking trace
acknowledged "fabric DM channel" (from the ClawPrompts chat-injector
hook) but the surrounding user-prompt metadata contradicted it with
`is_group_chat: true`, and the model reasoned its way out of running
`workflow_start`.
Fix factors a small helper `fabricPeerRoutingForXType` (and a cache-
backed `fabricPeerRoutingForChannel` for outbound) in channel.ts that
maps:
- 'dm' → { peerKind: 'direct', chatType: 'direct' }
- rest → { peerKind: 'group', chatType: 'group' } (no change)
Inbound uses m.xType directly (live, authoritative). Outbound has no
xType in its call signature, so it consults the channel-meta cache
populated by inbound (same `getChannelType` already exposed via
__fabric). Cache miss falls back to 'group' — the pre-fix default, no
regression. The proactive-DM-without-prior-inbound edge case still
routes that one outbound as 'group'; the next round agrees on 'direct'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,39 @@ import {
|
||||
resolveDefaultFabricAccountId,
|
||||
type ResolvedFabricAccount,
|
||||
} from './accounts.js';
|
||||
import { getChannelType } from './channel-meta.js';
|
||||
|
||||
/**
|
||||
* Map a Fabric channel xType to an openclaw routing peer.kind / ChatType.
|
||||
*
|
||||
* Fabric distinguishes channels by xType ('dm' | 'triage' | 'group' |
|
||||
* 'broadcast' | 'announce' | ...). Openclaw's session router only knows
|
||||
* 'direct' | 'group' | 'channel'. We collapse:
|
||||
* - 'dm' → 'direct' (1:1 conversation; agent always speaks)
|
||||
* - rest → 'group' (multi-party; turn-engine gates speech)
|
||||
*
|
||||
* Sessions are keyed by peer.kind, so inbound and outbound MUST agree —
|
||||
* otherwise the agent's outbound message lands in a different session
|
||||
* than the inbound that triggered it and conversation state splits.
|
||||
*
|
||||
* Outbound has no live xType (the agent target is just a channelId), so
|
||||
* it consults the channel-meta cache populated by inbound. Cache miss
|
||||
* (channel never observed) falls back to 'group' — same as the pre-fix
|
||||
* 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 function fabricPeerRoutingForXType(xType: string | null | undefined): FabricPeerRouting {
|
||||
if (xType === 'dm') return { peerKind: 'direct', chatType: 'direct' };
|
||||
return { peerKind: 'group', chatType: 'group' };
|
||||
}
|
||||
|
||||
export function fabricPeerRoutingForChannel(channelId: string): FabricPeerRouting {
|
||||
return fabricPeerRoutingForXType(getChannelType(channelId));
|
||||
}
|
||||
|
||||
type AnyCfg = { channels?: { fabric?: unknown }; [k: string]: unknown };
|
||||
|
||||
@@ -45,13 +78,18 @@ export function looksLikeFabricTargetId(raw: string): boolean {
|
||||
export function resolveFabricOutboundSessionRoute(params: ChannelOutboundSessionRouteParams) {
|
||||
const id = stripFabricTargetPrefix(params.target);
|
||||
if (!id) return null;
|
||||
// Consult the channel-meta cache populated by inbound — DM channels
|
||||
// need peer.kind='direct' so the outbound session key matches the
|
||||
// inbound one. Cache miss falls back to 'group' (the pre-fix default,
|
||||
// no regression on cold cache).
|
||||
const { peerKind, chatType } = fabricPeerRoutingForChannel(id);
|
||||
return buildChannelOutboundSessionRoute({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
channel: 'fabric',
|
||||
accountId: params.accountId,
|
||||
peer: { kind: 'group', id },
|
||||
chatType: 'group',
|
||||
peer: { kind: peerKind, id },
|
||||
chatType,
|
||||
from: `fabric:channel:${id}`,
|
||||
to: `fabric:${id}`,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user