Files
Fabric.OpenclawPlugin/openclaw.plugin.json
hzhang 8774cfd7cc feat(fabric): coalesce a split agent turn into ONE message (deterministic)
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>
2026-05-16 22:15:46 +01:00

81 lines
2.8 KiB
JSON

{
"id": "fabric",
"kind": "channel",
"channels": ["fabric"],
"name": "Fabric",
"description": "Fabric channel plugin — OpenClaw agents speak in Fabric guilds",
"activation": {
"onStartup": true
},
"contracts": {
"tools": [
"fabric-register",
"create-chat-channel",
"create-work-channel",
"create-report-channel",
"create-discussion-channel",
"discussion-complete"
]
},
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"identityFilePath": {
"type": "string",
"description": "Path to the agent identity registry JSON (agentId -> Fabric API key). Default ~/.openclaw/fabric-identity.json"
},
"debugMode": { "type": "boolean", "default": false }
},
"required": []
},
"channelConfigs": {
"fabric": {
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"centerApiBase": {
"type": "string",
"description": "Fabric Center API base, e.g. http://localhost:7001/api"
},
"commandsSyncKey": {
"type": "string",
"minLength": 1,
"description": "Shared secret that must equal the guild's FABRIC_BACKEND_GUILD_COMMANDS_SYNC_KEY. Required to register the slash-command catalog (Guild C-2). Read it from the guild via: docker exec fabric-backend-guild node dist/cli/print-commands-sync-key.js"
},
"coalesce": {
"type": "boolean",
"description": "Merge a split agent turn (text → thinking/tool → text) into ONE Fabric message. Flushed deterministically on the agent_end hook. Default true; false = raw per-segment posting."
},
"dmSecurity": { "type": "string" },
"dmPolicy": { "type": "string" },
"enabled": { "type": "boolean" },
"allowFrom": { "type": "array", "items": { "type": "string" } },
"defaultAccount": { "type": "string" },
"accounts": {
"type": "object",
"description": "agent = account; key is the openclaw agentId",
"additionalProperties": {
"type": "object",
"additionalProperties": false,
"properties": {
"fabricApiKey": { "type": "string" },
"centerApiBase": { "type": "string" },
"enabled": { "type": "boolean" },
"dmPolicy": { "type": "string" },
"allowFrom": { "type": "array", "items": { "type": "string" } }
}
}
}
},
"required": ["commandsSyncKey"]
},
"uiHints": {
"centerApiBase": { "label": "Center API base" },
"commandsSyncKey": { "label": "Commands sync key" }
}
}
}
}