From cce85a9be88ec41e7e6b3b8323570d99d0c657f0 Mon Sep 17 00:00:00 2001 From: zhi Date: Wed, 13 May 2026 16:40:16 +0000 Subject: [PATCH] fix(bridge): reset claude session when OpenClaw sends no assistant history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The May 7 fix made the bridge detect /new turns by scanning messages for the bare-reset marker ("A new session was started via /new or /reset"). That handles the case where /new is the body of the current user turn, but misses a very common path: the user types `/new` as a standalone slash command. OpenClaw processes those in a side lane (e.g. agent::discord:slash:) that doesn't go through the bridge — it just renames the old session file aside. The follow-up real message then lands on a brand-new OpenClaw session, but as a normal turn with `softResetTriggered=false`, non-empty body, not bare /new — so isBareSessionReset is false in OpenClaw (get-reply isBareSessionReset condition) and the marker is never injected. The bridge keeps resuming the long-stale claudeSessionId from before the reset. OpenClaw always sends the full conversation history each turn (system + user/assistant pairs + latest user). A request with zero assistant turns in messages[] is therefore a positive signal that the OpenClaw session is brand-new and any prior claudeSessionId we hold belongs to an abandoned OpenClaw session. Treat "no assistant history" as equivalent to bareSessionReset: removeSession + existingEntry = null, so dispatchToClaude is called without --resume and claude starts a fresh CLI session whose id we then store. Also covers any future OpenClaw reset path that resets the session without injecting the marker (idle timeout new-session, admin tooling, etc.). Co-Authored-By: Claude Opus 4.7 (1M context) --- plugin/web/server.ts | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/plugin/web/server.ts b/plugin/web/server.ts index 82aa2e8..efceb67 100644 --- a/plugin/web/server.ts +++ b/plugin/web/server.ts @@ -276,9 +276,37 @@ export function createBridgeServer(config: BridgeServerConfig): http.Server { // updated `claudeSessionId` and we want to resume into the latest // one rather than a stale snapshot from request-arrival time. existingEntry = sessionKey ? getSession(workspace, sessionKey) : null; - if (bareSessionReset && existingEntry && sessionKey) { + + // Detect a fresh OpenClaw session even when the bare-reset marker is + // absent. The marker only arrives when `/new` is the body of *this* + // user turn (see get-reply isBareSessionReset). When `/new` is sent + // as a separate slash command (e.g. via Discord's slash UI), OpenClaw + // processes the reset in a side lane that doesn't hit the bridge — + // it just renames the prior session file aside. The follow-up real + // message then arrives on a brand-new OpenClaw session, but as a + // normal turn with no marker. Without this check, the bridge happily + // resumes the long-stale claudeSessionId from before the reset. + // + // OpenClaw sends the full conversation history every turn (system + + // user/assistant pairs + latest user). A request with zero assistant + // turns is therefore a positive signal that the OpenClaw session is + // brand-new and any prior claudeSessionId we hold is from a previous + // OpenClaw session that the user already abandoned. + const hasAssistantHistory = body.messages.some((m) => { + if (m.role !== "assistant") return false; + if (typeof m.content === "string") return m.content.trim().length > 0; + return m.content.some( + (c) => c.type === "text" && (c.text ?? "").trim().length > 0, + ); + }); + const isFreshOpenClawSession = !hasAssistantHistory; + + if ((bareSessionReset || isFreshOpenClawSession) && existingEntry && sessionKey) { + const reason = bareSessionReset + ? "bare /new detected" + : "fresh OpenClaw session (no assistant history in messages[])"; logger.info( - `[contractor-bridge] bare /new detected — dropping prior CLI session sessionKey=${sessionKey} prevClaudeSessionId=${existingEntry.claudeSessionId}`, + `[contractor-bridge] ${reason} — dropping prior CLI session sessionKey=${sessionKey} prevClaudeSessionId=${existingEntry.claudeSessionId}`, ); removeSession(workspace, sessionKey); existingEntry = null;