Skip to content

Commit 13e9461

Browse files
fix: resolve issue #75452
1 parent 5d33846 commit 13e9461

3 files changed

Lines changed: 59 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
1616

1717
### Fixes
1818

19+
- 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.
1920
- Plugins/runtime-deps: prune legacy version-scoped plugin runtime-deps roots during bundled dependency repair and cover the path in Package Acceptance's upgrade-survivor matrix, so upgrades from 2026.4.x no longer leave stale per-plugin runtime trees after doctor runs. Thanks @vincentkoc.
2021
- Plugins/runtime-deps: keep Gateway startup plugin imports and runtime plugin fallback loads verify-only after startup/config repair planning, so packaged installs no longer spawn package-manager repair from hot paths after readiness. Refs #75283 and #75069. Thanks @brokemac79 and @xiaohuaxi.
2122
- Voice Call/realtime: add default-off fast memory/session context for `openclaw_agent_consult`, giving live calls a bounded answer-or-miss path before the full agent consult. Fixes #71849. Thanks @amzzzzzzz.

src/agents/command/session-store.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,55 @@ describe("updateSessionStoreAfterAgentRun", () => {
931931
});
932932
});
933933

934+
it("leaves contextTokens unset when entry has prior model but no contextTokens (heartbeat bleed guard)", async () => {
935+
await withTempSessionStore(async ({ storePath }) => {
936+
const cfg = {} as OpenClawConfig;
937+
const sessionKey = "agent:main:explicit:test-heartbeat-no-context-tokens";
938+
const sessionId = "test-heartbeat-no-context-tokens-session";
939+
const sessionStore: Record<string, SessionEntry> = {
940+
[sessionKey]: {
941+
sessionId,
942+
updatedAt: 1,
943+
modelProvider: "anthropic",
944+
model: "claude-opus-4-6",
945+
// contextTokens intentionally missing — older session without cached context
946+
},
947+
};
948+
await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2));
949+
950+
// Heartbeat turn uses a different, smaller model
951+
const result: EmbeddedPiRunResult = {
952+
meta: {
953+
durationMs: 500,
954+
agentMeta: {
955+
sessionId,
956+
provider: "ollama",
957+
model: "llama3.2:1b",
958+
contextTokens: 128_000,
959+
},
960+
},
961+
};
962+
963+
await updateSessionStoreAfterAgentRun({
964+
cfg,
965+
sessionId,
966+
sessionKey,
967+
storePath,
968+
sessionStore,
969+
defaultProvider: "anthropic",
970+
defaultModel: "claude-opus-4-6",
971+
result,
972+
preserveRuntimeModel: true,
973+
});
974+
975+
// Runtime model should be preserved
976+
expect(sessionStore[sessionKey]?.model).toBe("claude-opus-4-6");
977+
expect(sessionStore[sessionKey]?.modelProvider).toBe("anthropic");
978+
// contextTokens should NOT bleed from the heartbeat run's smaller window
979+
expect(sessionStore[sessionKey]?.contextTokens).toBeUndefined();
980+
});
981+
});
982+
934983
it("falls back to run model when preserveRuntimeModel is true but entry has no prior runtime model", async () => {
935984
await withTempSessionStore(async ({ storePath }) => {
936985
const cfg = {} as OpenClawConfig;

src/agents/command/session-store.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,15 @@ export async function updateSessionStoreAfterAgentRun(params: {
121121
// Keep the pre-existing runtime model and context window so a background
122122
// heartbeat turn using a different model does not bleed into the main
123123
// session's perceived state.
124-
next.contextTokens = entry.contextTokens ?? contextTokens;
124+
if (entry.model) {
125+
// Prior runtime model exists: preserve its contextTokens. When missing,
126+
// leave contextTokens unset rather than falling back to the heartbeat
127+
// run's context window; status derives it from the preserved model.
128+
next.contextTokens = entry.contextTokens;
129+
} else {
130+
// No prior runtime model: heartbeat establishes initial state.
131+
next.contextTokens = entry.contextTokens ?? contextTokens;
132+
}
125133
setSessionRuntimeModel(next, {
126134
provider: entry.modelProvider ?? providerUsed,
127135
model: entry.model ?? modelUsed,

0 commit comments

Comments
 (0)