fix: resolve issue #75452

This commit is contained in:
zhang-guiping
2026-05-01 17:38:36 +08:00
parent 7e25003868
commit 09c467241d
3 changed files with 59 additions and 1 deletions

View File

@@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai
- Control UI/chat: show inline feedback when local slash-command dispatch is unavailable or fails unexpectedly instead of clearing the composer silently. Fixes #52105. Thanks @MooreQiao.
- Memory/markdown: replace CRLF managed blocks in place and collapse duplicate marker blocks without rewriting unmanaged markdown, so Dreaming and Memory Wiki files self-heal from repeated generated sections. Fixes #75491; supersedes #75495, #75810, and #76008. Thanks @asaenokkostya-coder, @ottodeng, @everettjf, and @lrg913427-dot.
- Agents/tools: return critical tool-loop circuit-breaker stops as blocked tool results instead of thrown tool failures, so models see the guardrail and stop retrying the same call. Thanks @rayraiser.
- Agents/sessions: preserve pre-existing runtime model and context window after heartbeat turns so a per-run heartbeat model override does not bleed into shared-session status. Fixes #75452. Thanks @zhang-guiping.
- Model commands: clarify direct and inline `/model` acknowledgements for non-default selections as session-scoped. Thanks @addu2612.
- Doctor/gateway: stop warning that non-existent, unconfigured user-bin directories are required in the Gateway service PATH. Fixes #76017. Thanks @xiphis.
- TUI/chat: skip full provider model normalization during context-window warmup while preserving provider-owned context metadata, avoiding cold-start stalls with large model registries. Thanks @547895019.

View File

@@ -931,6 +931,55 @@ describe("updateSessionStoreAfterAgentRun", () => {
});
});
it("leaves contextTokens unset when entry has prior model but no contextTokens (heartbeat bleed guard)", async () => {
await withTempSessionStore(async ({ storePath }) => {
const cfg = {} as OpenClawConfig;
const sessionKey = "agent:main:explicit:test-heartbeat-no-context-tokens";
const sessionId = "test-heartbeat-no-context-tokens-session";
const sessionStore: Record<string, SessionEntry> = {
[sessionKey]: {
sessionId,
updatedAt: 1,
modelProvider: "anthropic",
model: "claude-opus-4-6",
// contextTokens intentionally missing — older session without cached context
},
};
await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2));
// Heartbeat turn uses a different, smaller model
const result: EmbeddedPiRunResult = {
meta: {
durationMs: 500,
agentMeta: {
sessionId,
provider: "ollama",
model: "llama3.2:1b",
contextTokens: 128_000,
},
},
};
await updateSessionStoreAfterAgentRun({
cfg,
sessionId,
sessionKey,
storePath,
sessionStore,
defaultProvider: "anthropic",
defaultModel: "claude-opus-4-6",
result,
preserveRuntimeModel: true,
});
// Runtime model should be preserved
expect(sessionStore[sessionKey]?.model).toBe("claude-opus-4-6");
expect(sessionStore[sessionKey]?.modelProvider).toBe("anthropic");
// contextTokens should NOT bleed from the heartbeat run's smaller window
expect(sessionStore[sessionKey]?.contextTokens).toBeUndefined();
});
});
it("falls back to run model when preserveRuntimeModel is true but entry has no prior runtime model", async () => {
await withTempSessionStore(async ({ storePath }) => {
const cfg = {} as OpenClawConfig;

View File

@@ -120,7 +120,15 @@ export async function updateSessionStoreAfterAgentRun(params: {
// Keep the pre-existing runtime model and context window so a background
// heartbeat turn using a different model does not bleed into the main
// session's perceived state.
next.contextTokens = entry.contextTokens ?? contextTokens;
if (entry.model) {
// Prior runtime model exists: preserve its contextTokens. When missing,
// leave contextTokens unset rather than falling back to the heartbeat
// run's context window; status derives it from the preserved model.
next.contextTokens = entry.contextTokens;
} else {
// No prior runtime model: heartbeat establishes initial state.
next.contextTokens = entry.contextTokens ?? contextTokens;
}
setSessionRuntimeModel(next, {
provider: entry.modelProvider ?? providerUsed,
model: entry.model ?? modelUsed,