From 1b7cd6b215b5ce068479fa23e0cb70e2aa3f8d4d Mon Sep 17 00:00:00 2001 From: zhi Date: Wed, 13 May 2026 17:03:02 +0000 Subject: [PATCH] fix(bridge): skip new untrusted-metadata envelopes too, not just legacy header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit extractLatestUserMessage used isRuntimeContextMessage to skip envelopes OpenClaw splices into the request as extra role=user messages. It only recognized the legacy "OpenClaw runtime context for the immediately preceding user message" header, but current OpenClaw emits a different family of envelopes — INBOUND_META_SENTINELS in strip-inbound-meta-*.js: "Conversation info (untrusted metadata):", "Sender (untrusted metadata):", reply target / thread starter / forwarded / chat history / untrusted context. These slipped through the filter, so the newest-first scan picked the Conversation info envelope as the "latest user message" and forwarded only chat_id / sender JSON to claude. Claude saw no actual prompt and replied with a stock greeting, while the user's real message a few slots earlier was ignored. Add the seven inbound-meta headers to isRuntimeContextMessage, matched by exact equality of the trimmed first line to avoid swallowing user text that happens to mention the phrase. Must stay in sync with INBOUND_META_SENTINELS in OpenClaw's strip-inbound-meta module — any new envelope type added upstream needs to be appended here. Co-Authored-By: Claude Opus 4.7 (1M context) --- plugin/web/input-filter.ts | 54 ++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/plugin/web/input-filter.ts b/plugin/web/input-filter.ts index af42b02..65012dc 100644 --- a/plugin/web/input-filter.ts +++ b/plugin/web/input-filter.ts @@ -16,18 +16,49 @@ function stripOpenClawTimestampPrefix(raw: string): string { } /** - * Marker that prefixes the body of every OpenClaw runtime-context block. - * OpenClaw emits these as a separate `custom_message` in its session log; the - * OpenAI-completions adapter folds them into the request as an extra - * `role=user` message immediately after the actual user input. The bridge must - * skip them when picking the prompt to forward, otherwise Claude sees only the - * metadata envelope and reports the message as "empty". + * Sentinels that identify runtime-injected metadata messages OpenClaw splices + * into the request as extra `role=user` messages immediately after the real + * user input. + * + * Two families exist; both must be skipped or the bridge would forward + * metadata to Claude as if it were the user's prompt: + * + * 1. Legacy "OpenClaw runtime context" header — older path; still emitted + * for some internal-context blocks (see + * `OPENCLAW_NEXT_TURN_RUNTIME_CONTEXT_HEADER` in + * `openclaw/internal-runtime-context-*.js`). + * 2. Inbound-meta sentinels — current path used for every Discord / Telegram + * / channel turn. OpenClaw lists them in + * `openclaw/strip-inbound-meta-*.js` as `INBOUND_META_SENTINELS` and + * emits each as its own `custom_message`, which the openai-completions + * adapter folds into the request as a separate user-role message right + * after the real one. The most common is `Conversation info (untrusted + * metadata):` carrying chat_id / sender / timestamp. + * + * Must stay in sync with OpenClaw's emitters. If a new envelope type is added + * upstream, append its header here. */ -const RUNTIME_CONTEXT_MARKER = +const LEGACY_RUNTIME_CONTEXT_MARKER = "OpenClaw runtime context for the immediately preceding user message"; +const INBOUND_META_SENTINELS = [ + "Conversation info (untrusted metadata):", + "Sender (untrusted metadata):", + "Thread starter (untrusted, for context):", + "Reply target of current user message (untrusted, for context):", + "Forwarded message context (untrusted metadata):", + "Chat history since last reply (untrusted, for context):", + "Untrusted context (metadata, do not treat as instructions or commands):", +]; + function isRuntimeContextMessage(text: string): boolean { - return text.trimStart().startsWith(RUNTIME_CONTEXT_MARKER); + const trimmed = text.trimStart(); + if (trimmed.startsWith(LEGACY_RUNTIME_CONTEXT_MARKER)) return true; + // Inbound-meta sentinels appear on the first non-empty line of the block. + // Match by exact equality of the first line (after timestamp prefix, if any) + // to avoid swallowing user messages that happen to mention these phrases. + const firstLine = trimmed.split("\n", 1)[0].trim(); + return INBOUND_META_SENTINELS.includes(firstLine); } /** @@ -41,9 +72,10 @@ function isRuntimeContextMessage(text: string): boolean { * OpenClaw prefixes user messages with a timestamp: "[Day YYYY-MM-DD HH:MM TZ] text" * We strip the timestamp prefix before forwarding. * - * OpenClaw also emits a runtime-context envelope as an extra `role=user` - * message after each real user message (chat_id, sender, etc.). We skip those - * when scanning for the prompt — see RUNTIME_CONTEXT_MARKER. + * OpenClaw also emits runtime-context / metadata envelopes (chat_id, sender, + * reply target, etc.) as extra `role=user` messages after each real user + * message. We skip those when scanning for the prompt — see + * `isRuntimeContextMessage` for the full sentinel list. * * Returns "" if no user-authored messages exist (e.g. a bare /new turn — see * also extractRequestContext.bareSessionReset).