From 91acce9b32ec5bd4600e2fb0015144f1eace9559 Mon Sep 17 00:00:00 2001 From: zhi Date: Wed, 29 Apr 2026 08:19:59 +0000 Subject: [PATCH] fix(bridge): skip OpenClaw runtime-context envelope when picking prompt OpenClaw emits its runtime-context block as a separate custom_message; the openai-completions adapter folds that into the request as an extra role=user message after the real user input. extractLatestUserMessage was taking the last user message unconditionally, so Claude received only the metadata envelope and replied "your message came through empty". Walk user messages backward, skip ones starting with the runtime-context marker, and return the most recent real user message instead. Co-Authored-By: Claude Opus 4.7 (1M context) --- plugin/web/input-filter.ts | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/plugin/web/input-filter.ts b/plugin/web/input-filter.ts index a84503e..af42b02 100644 --- a/plugin/web/input-filter.ts +++ b/plugin/web/input-filter.ts @@ -16,7 +16,22 @@ function stripOpenClawTimestampPrefix(raw: string): string { } /** - * Extract the latest user message from the OpenClaw request. + * 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". + */ +const RUNTIME_CONTEXT_MARKER = + "OpenClaw runtime context for the immediately preceding user message"; + +function isRuntimeContextMessage(text: string): boolean { + return text.trimStart().startsWith(RUNTIME_CONTEXT_MARKER); +} + +/** + * Extract the latest user-authored message from the OpenClaw request. * * OpenClaw accumulates all user messages and sends the full array every turn, * but assistant messages may be missing if the previous response wasn't streamed @@ -26,15 +41,22 @@ function stripOpenClawTimestampPrefix(raw: string): string { * OpenClaw prefixes user messages with a timestamp: "[Day YYYY-MM-DD HH:MM TZ] text" * We strip the timestamp prefix before forwarding. * - * Returns "" if no user messages exist or the latest user message is empty - * (e.g. a bare /new turn — see also extractRequestContext.bareSessionReset). + * 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. + * + * Returns "" if no user-authored messages exist (e.g. a bare /new turn — see + * also extractRequestContext.bareSessionReset). */ export function extractLatestUserMessage(req: BridgeInboundRequest): string { const userMessages = req.messages.filter((m) => m.role === "user"); - if (userMessages.length === 0) return ""; - - const raw = messageText(userMessages[userMessages.length - 1]); - return stripOpenClawTimestampPrefix(raw); + for (let i = userMessages.length - 1; i >= 0; i -= 1) { + const raw = messageText(userMessages[i]); + if (!raw) continue; + if (isRuntimeContextMessage(raw)) continue; + return stripOpenClawTimestampPrefix(raw); + } + return ""; } export type RequestContext = { -- 2.49.1