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).