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 = {