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;