Inbound `message.created` already carries `xType` (dm / triage / group /
broadcast / etc.) — record it in a per-channel cache so other plugins
can answer "is this channel a DM?" without poking the Center API.
New module src/channel-meta.ts:
- in-memory Map<channelId, xType>
- lazily loaded from ~/.openclaw/fabric-channel-meta.json on first
access (so first-ever DM after a fresh gateway start still hits
cache from the previous run)
- debounced 250ms flush on dirty; force-flush on gateway_stop
- recordChannelType(channelId, xType): called from inbound
- getChannelType(channelId): null if unknown — caller MUST treat null
as "don't know", NOT as "assume DM" (would re-introduce the false-
positive on group channels we're trying to eliminate)
Wiring:
- inbound.ts socket.on('message.created'): records xType BEFORE the
self-author / dedup gates (channel type is observer-agnostic)
- index.ts: installs globalThis.__fabric = { getChannelType } on
registerFull(); flushes on gateway_stop
Consumer: ClawPrompts' fabric-chat-injector will start gating its prompt
injection on getChannelType(channelId) === 'dm' (companion PR on
ClawPrompts). Removes the phase-1 "any fabric channel" false-positive.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two agent-facing tools that close the discoverability loop:
- fabric-guild-list — enumerates guilds the agent belongs to with
name + purpose + status (no api calls beyond the existing agentLogin
response). Optional nameFilter/purposeFilter for narrowing.
- fabric-channel-set-purpose — PATCH /api/channels/:id { purpose }
so agents can backfill or update an existing channel's purpose.
Extends existing tools:
- fabric-channel-list now returns purpose on each row.
- create-{chat,work,report,discussion}-channel accept optional purpose.
FabricClient + FabricSession type changes carry the new field through.
Manifest contracts.tools updated (jiti loader needs both manifest entry
and onStartup activation to register).
Lets workflows that previously needed hardcoded channel ids instead say
'find a guild whose purpose mentions debate, then a channel of x_type
announce whose purpose covers public debate broadcasts.'
OpenClaw delivers an agent turn whose blocks are text -> thinking/tool
-> text via multiple inbound deliver() calls (a non-text block is a
delivery boundary), so one turn became N Fabric messages.
Fix: buffer deliver() segments per channel (src/coalesce.ts) and flush
them as ONE postMessage at a deterministic boundary — the finally after
dispatchInboundReplyWithBase() resolves, which provably runs only after
every deliver() of the turn (verified: deliver,deliver -> dispatch
returned -> flush). No hooks, no timers, no idle guessing. The
agent_end hook was rejected: it fires BEFORE deliver(). gateway_stop
flushes any leftover; a long safety timeout is a leak-guard only.
channels.fabric.coalesce=false restores raw per-segment posting.
Verified on local openclaw + Fabric with a fake text/thinking/text
model: single trigger -> exactly one merged message.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- command-sync.ts: buildFabricCommandSpecs(cfg) reads OpenClaw native
command specs via openclaw/plugin-sdk/native-command-registry
(listNativeCommandSpecsForConfig + findCommandByNativeName), resolves
dynamic arg choices to a static snapshot (resolveCommandArgChoices) —
same data Discord registers as slash commands.
- syncFabricCommands(): on gateway_start, after inbound starts, PUT the
catalog to each connected guild (FabricClient.syncCommands ->
PUT /api/commands; idempotent, one per guild).
- Fabric stays a TEXT-command surface (no nativeCommands capability):
execution still flows as a /<cmd> message into OpenClaw's command
system; this catalog only drives frontend autocomplete.
Verified: 41 specs built (args/choices incl. dynamic), synced to
test-guild1, GET /api/commands round-trips count=41.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real channel-turn dispatch (resolveAgentRoute + finalizeInboundContext +
dispatchInboundReplyWithBase), wakeup->drop/dispatch, messaging target
grammar (fabric:<id>) + outbound.sendText, tools use execute/parameters.
Verified live: human msg in Fabric -> wakeup -> openclaw agent runs ->
reply posted back into the Fabric channel as the agent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>