From d1c414305b6ddb017e8365b85b0b0c84e427e14a Mon Sep 17 00:00:00 2001 From: Zijun Lin Date: Wed, 22 Apr 2026 03:23:27 -0700 Subject: [PATCH] fix(cli-session): only hash static extraSystemPrompt for session reuse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The extraSystemPrompt includes per-message dynamic content from buildInboundMetaSystemPrompt() (timestamps, message IDs, sender metadata) that changes on every inbound message. This causes the extraSystemPromptHash to differ every turn, triggering a session reset with reason='system-prompt' and discarding all CLI session context. Fix: split extraSystemPrompt into dynamic (inbound meta) and static (group context, group intro, group system prompt, exec override hints) portions. Only hash the static portion for session reuse validation. The full extraSystemPrompt (dynamic + static) is still sent to the CLI as before — only the session stability hash uses the static subset. Fixes #70100 --- src/agents/cli-runner/prepare.ts | 6 +++++- src/agents/cli-runner/types.ts | 2 ++ src/auto-reply/reply/get-reply-run.ts | 13 +++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/agents/cli-runner/prepare.ts b/src/agents/cli-runner/prepare.ts index 4901fbf34e5..8389aee398f 100644 --- a/src/agents/cli-runner/prepare.ts +++ b/src/agents/cli-runner/prepare.ts @@ -107,7 +107,11 @@ export async function prepareCliRunContext( authCredential = authStore.profiles[effectiveAuthProfileId]; } const extraSystemPrompt = params.extraSystemPrompt?.trim() ?? ""; - const extraSystemPromptHash = hashCliSessionText(extraSystemPrompt); + // Use the static portion (excluding per-message inbound metadata) for session reuse hashing. + // Per-message metadata (timestamps, message IDs) changes every turn and must not trigger session resets. + const extraSystemPromptHash = hashCliSessionText(params.extraSystemPromptStatic?.trim() || undefined) ?? hashCliSessionText(extraSystemPrompt); + + const modelId = (params.model ?? "default").trim() || "default"; const normalizedModel = normalizeCliModel(modelId, backendResolved.config); const modelDisplay = `${params.provider}/${modelId}`; diff --git a/src/agents/cli-runner/types.ts b/src/agents/cli-runner/types.ts index ecae4de5e3e..aa3bfa13322 100644 --- a/src/agents/cli-runner/types.ts +++ b/src/agents/cli-runner/types.ts @@ -23,6 +23,8 @@ export type RunCliAgentParams = { timeoutMs: number; runId: string; extraSystemPrompt?: string; + /** Static portion of extraSystemPrompt (excluding per-message inbound metadata) for session reuse hashing. */ + extraSystemPromptStatic?: string; streamParams?: import("../command/types.js").AgentStreamParams; ownerNumbers?: string[]; cliSessionId?: string; diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index f2cc3554f5b..24cc9887a4f 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -305,6 +305,18 @@ export async function runPreparedReply( fullAccessBlockedReason: fullAccessState.blockedReason, }), ].filter(Boolean); + // Static parts only (no per-message inbound metadata) for CLI session reuse hashing. + const extraSystemPromptStaticParts = [ + groupChatContext, + groupIntro, + groupSystemPrompt, + buildExecOverridePromptHint({ + execOverrides, + elevatedLevel: resolvedElevatedLevel, + fullAccessAvailable: fullAccessState.available, + fullAccessBlockedReason: fullAccessState.blockedReason, + }), + ].filter(Boolean); const baseBody = sessionCtx.BodyStripped ?? sessionCtx.Body ?? ""; // Use CommandBody/RawBody for bare reset detection (clean message without structural context). const rawBodyTrimmed = (ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? "").trim(); @@ -734,6 +746,7 @@ export async function runPreparedReply( ownerNumbers: command.ownerList.length > 0 ? command.ownerList : undefined, inputProvenance: ctx.InputProvenance ?? sessionCtx.InputProvenance, extraSystemPrompt: extraSystemPromptParts.join("\n\n") || undefined, + extraSystemPromptStatic: extraSystemPromptStaticParts.join("\n\n") || undefined, skipProviderRuntimeHints: useFastReplyRuntime, ...(!useFastReplyRuntime && isReasoningTagProvider(provider, {