fix(cli-session): only hash static extraSystemPrompt for session reuse

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
This commit is contained in:
Zijun Lin
2026-04-22 03:23:27 -07:00
committed by Peter Steinberger
parent d48763caf9
commit d1c414305b
3 changed files with 20 additions and 1 deletions

View File

@@ -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}`;

View File

@@ -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;

View File

@@ -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, {