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>
The slash-command sync secret now comes from
channels.fabric.commandsSyncKey (configSchema marks it required) and
is no longer read from FABRIC_COMMANDS_SYNC_KEY env. command-sync
resolves it from config and threads it into client.syncCommands;
when absent, sync is skipped with a clear warning. README updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One tool, three actions backed by FabricClient channelMembers (GET
/channels/:id/members -> [{userId,bypass}]), joinChannel, and new
leaveChannel (POST /channels/:id/leave).
Verified: client-level smoke against the running guild — members
initial=[tester], after join echo2 present, after leave echo2 gone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- bin/fabric-register.mjs: only AGENT_ID is read from the environment;
--api-key is flag-only (no FABRIC_API_KEY); dropped FABRIC_CENTER_API_BASE
/ FABRIC_IDENTITY_FILE / OPENCLAW_PATH env fallbacks (flags + sensible
defaults; --center still falls back to openclaw.json).
- New fabric-canvas tool (one tool, four actions): read / share / update /
close the channel's single pinned canvas. Backed by FabricClient
get/share/update/removeCanvas (GET/PUT/PATCH/DELETE; empty 2xx body ->
null). update/close are sharer-only server-side.
- README updated.
Verified: client-level smoke against the running guild —
read(empty→null) → share(v1) → read → update(v2) → close(→null) all pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Binding an agent's Fabric API key was an OpenClaw tool; make it a
self-contained Node script installed to ~/.openclaw/bin/fabric-register
instead.
- bin/fabric-register.mjs: no plugin deps; AGENT_ID env wins, else
--agent-id required; --api-key validated via POST /auth/agent/login;
on success upserts ~/.openclaw/fabric-identity.json (format matches
IdentityRegistry). Flags/env for center, identity-file, openclaw-path.
- install.mjs: copy the script to ~/.openclaw/bin (chmod 0755) on
install, remove on uninstall; Next-steps updated.
- tools.ts: drop the fabric-register tool; ctxGuild error now points to
the script / static accounts config.
- README updated.
Verified: missing-id -> exit 2; --agent-id and AGENT_ID both bind and
write a valid identity file; bad key -> 401, no write.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
agent=account only routes correctly when openclaw cfg.bindings has a
{agentId, match:{channel:fabric, accountId}} entry; else falls back to
the default agent. Verified: with the binding, account echo -> agent
echo, reply posted back as the agent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OpenClaw channel plugin: defineChannelPluginEntry + createChatChannelPlugin.
Inbound via channel-turn kernel (wakeup -> admission: true=dispatch,
else drop+recordHistory). One Fabric socket per agent identity in the
plugin runtime (no sidecar). Center API-key agent auth. Tools:
fabric-register, create-{chat,work,report,discussion}-channel,
discussion-complete (post summary + close channel).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>