Merge pull request 'fix(bridge): recover inline-prefixed metadata in user message body' (#4) from fix/inline-metadata-strip into main

This commit was merged in pull request #4.
This commit is contained in:
h z
2026-05-31 19:52:39 +00:00

View File

@@ -61,6 +61,51 @@ function isRuntimeContextMessage(text: string): boolean {
return INBOUND_META_SENTINELS.includes(firstLine);
}
/**
* Strip *prefixed* metadata blocks from a single user message body.
*
* Background: OpenClaw's older / canonical convention is to emit metadata
* envelopes (chat_id, sender, reply target, …) as SEPARATE user-role
* messages folded into the openai-completions request right after the real
* one. `extractLatestUserMessage` skips those separate messages whole.
*
* The Fabric channel plugin (Fabric.OpenclawPlugin) does not split: it
* passes the metadata blocks and the actual user content as ONE merged
* user-message body, separated by blank lines. Result: a Fabric inbound
* turn has a single user message whose first line matches a sentinel, so
* `isRuntimeContextMessage` returned true and the whole turn was dropped
* with "no user message found" (HTTP 400). Symptom: every contractor agent
* subscribed to a Fabric channel silently fails to reply.
*
* Strip leading sentinel-prefixed blocks (sentinel header + optional code
* fence + blank-line separator) until we hit a block whose first line is
* NOT a sentinel — that's the real prompt. Returns "" if the entire body
* is metadata-only (still a no-op turn, same as before).
*/
function stripPrefixedMetadataBlocks(raw: string): string {
// Split on blank-line block boundaries. Within a metadata block the JSON
// code fence has only single newlines, so it stays in the same chunk as
// its sentinel header.
const blocks = raw.split(/\n\n+/);
const kept: string[] = [];
let stillStripping = true;
for (const block of blocks) {
if (stillStripping) {
const trimmed = block.trimStart();
const firstLine = trimmed.split("\n", 1)[0].trim();
if (
INBOUND_META_SENTINELS.includes(firstLine) ||
trimmed.startsWith(LEGACY_RUNTIME_CONTEXT_MARKER)
) {
continue; // drop this prefix block
}
stillStripping = false;
}
kept.push(block);
}
return kept.join("\n\n").trim();
}
/**
* Extract the latest user-authored message from the OpenClaw request.
*
@@ -85,7 +130,18 @@ export function extractLatestUserMessage(req: BridgeInboundRequest): string {
for (let i = userMessages.length - 1; i >= 0; i -= 1) {
const raw = messageText(userMessages[i]);
if (!raw) continue;
if (isRuntimeContextMessage(raw)) continue;
// First try the separate-message convention (Discord/Telegram path): a
// user message whose entire body is one metadata envelope — skip whole.
if (isRuntimeContextMessage(raw)) {
// ...but also try inline-prefixed-metadata recovery (Fabric path):
// some channels splice metadata + real content into one body. Walk
// past leading sentinel blocks; if any non-metadata block follows,
// that's the prompt. If nothing follows, this turn really is pure
// metadata and we keep skipping.
const stripped = stripPrefixedMetadataBlocks(raw);
if (stripped) return stripOpenClawTimestampPrefix(stripped);
continue;
}
return stripOpenClawTimestampPrefix(raw);
}
return "";