diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index fbdad1be160..44eea21b655 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -484,6 +484,7 @@ export async function runReplyAgent(params: { contextTokensUsed, systemPromptReport: runResult.meta?.systemPromptReport, cliSessionId, + isHeartbeat, }); // Drain any late tool/block deliveries before deciding there's "nothing to send". diff --git a/src/auto-reply/reply/followup-runner.ts b/src/auto-reply/reply/followup-runner.ts index 2fd21607095..d0ff6b1b60c 100644 --- a/src/auto-reply/reply/followup-runner.ts +++ b/src/auto-reply/reply/followup-runner.ts @@ -280,6 +280,7 @@ export function createFollowupRunner(params: { contextTokensUsed, systemPromptReport: runResult.meta?.systemPromptReport, logLabel: "followup", + isHeartbeat: opts?.isHeartbeat === true, }); } diff --git a/src/auto-reply/reply/session-usage.ts b/src/auto-reply/reply/session-usage.ts index d3594fcdf42..498696a8f63 100644 --- a/src/auto-reply/reply/session-usage.ts +++ b/src/auto-reply/reply/session-usage.ts @@ -75,6 +75,13 @@ export async function persistSessionUsageUpdate(params: { systemPromptReport?: SessionSystemPromptReport; cliSessionId?: string; logLabel?: string; + /** + * When true, the model/provider/contextTokens fields are NOT persisted to + * the session entry. This prevents heartbeat model overrides from bleeding + * into the main session's stored state (model, context window, etc.). + * Token usage counters are still recorded. + */ + isHeartbeat?: boolean; }): Promise { const { storePath, sessionKey } = params; if (!storePath || !sessionKey) { @@ -117,9 +124,15 @@ export async function persistSessionUsageUpdate(params: { }); const existingEstimatedCostUsd = resolveNonNegativeNumber(entry.estimatedCostUsd) ?? 0; const patch: Partial = { - modelProvider: params.providerUsed ?? entry.modelProvider, - model: params.modelUsed ?? entry.model, - contextTokens: resolvedContextTokens, + // When isHeartbeat is true, preserve the session's existing model/provider/context + // so that a heartbeat model override does not bleed into the main session state. + ...(params.isHeartbeat + ? {} + : { + modelProvider: params.providerUsed ?? entry.modelProvider, + model: params.modelUsed ?? entry.model, + contextTokens: resolvedContextTokens, + }), systemPromptReport: params.systemPromptReport ?? entry.systemPromptReport, updatedAt: Date.now(), }; @@ -150,7 +163,9 @@ export async function persistSessionUsageUpdate(params: { return; } - if (params.modelUsed || params.contextTokensUsed) { + // When isHeartbeat is true, skip persisting model/context entirely — the heartbeat + // model override should not affect the session's stored model state. + if (!params.isHeartbeat && (params.modelUsed || params.contextTokensUsed)) { try { await updateSessionStoreEntry({ storePath,