Skip to content

Commit 40e58f9

Browse files
fix(agents): persist compaction rotation on preserved-state runs too
ClawSweeper review of #88106 flagged that the preserveUserFacingSessionModelState (heartbeat) path builds metadataPatch from only updatedAt/lastInteractionAt, so a rotated sessionId/sessionFile assigned on `next` never reached mergeSessionEntry. A heartbeat turn runs through the embedded runner and can hit a compaction rotation, so that path would reintroduce the #88040 deadlock. Carry the rotated identity (sessionId + sessionFile + sessionStartedAt) into the preserve-path patch too: a physical transcript rotation is filesystem identity, not user-facing model state. Add a preserved-state rotation regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5c93584 commit 40e58f9

2 files changed

Lines changed: 72 additions & 0 deletions

File tree

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,67 @@ describe("updateSessionStoreAfterAgentRun", () => {
309309
});
310310
});
311311

312+
it("persists a transcript rotation even on a preserved-state heartbeat run", async () => {
313+
await withTempSessionStore(async ({ storePath }) => {
314+
const cfg = {} as OpenClawConfig;
315+
const sessionKey = "agent:main:explicit:test-rotation-preserve";
316+
const sessionId = "pre-rotation-heartbeat";
317+
const rotatedSessionId = "post-rotation-heartbeat";
318+
const rotatedSessionFile = "/tmp/post-rotation-heartbeat.jsonl";
319+
const sessionStore: Record<string, SessionEntry> = {
320+
[sessionKey]: {
321+
sessionId,
322+
sessionFile: "/tmp/pre-rotation-heartbeat.jsonl",
323+
updatedAt: 1,
324+
sessionStartedAt: 1,
325+
modelProvider: "anthropic",
326+
model: "claude-opus-4-6",
327+
contextTokens: 400_000,
328+
},
329+
};
330+
await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2));
331+
332+
const result: EmbeddedAgentRunResult = {
333+
meta: {
334+
durationMs: 1,
335+
agentMeta: {
336+
// A heartbeat turn can still hit a compaction rotation; the embedded
337+
// runner reports the post-rotation openclaw identity here.
338+
sessionId: rotatedSessionId,
339+
sessionFile: rotatedSessionFile,
340+
provider: "ollama",
341+
model: "llama3.2:1b",
342+
},
343+
},
344+
};
345+
346+
await updateSessionStoreAfterAgentRun({
347+
cfg,
348+
sessionId,
349+
sessionKey,
350+
storePath,
351+
contextTokensOverride: 200_000,
352+
sessionStore,
353+
defaultProvider: "ollama",
354+
defaultModel: "llama3.2:1b",
355+
result,
356+
preserveUserFacingSessionModelState: true,
357+
});
358+
359+
const persisted = loadSessionStore(storePath);
360+
// The physical transcript rotation must reach disk even in preserve mode,
361+
// or the next turn reopens the rotated-away file and deadlocks (#88040).
362+
expect(persisted[sessionKey]?.sessionId).toBe(rotatedSessionId);
363+
expect(persisted[sessionKey]?.sessionFile).toBe(rotatedSessionFile);
364+
// A rotated identity is a new session, so the start timestamp resets.
365+
expect(persisted[sessionKey]?.sessionStartedAt).not.toBe(1);
366+
// Preserve mode still keeps the prior user-facing runtime model rather than
367+
// adopting the heartbeat model — only the transcript identity is updated.
368+
expect(persisted[sessionKey]?.model).toBe("claude-opus-4-6");
369+
expect(persisted[sessionKey]?.modelProvider).toBe("anthropic");
370+
});
371+
});
372+
312373
it("uses the runtime context budget from agent metadata instead of cold fallback", async () => {
313374
await withTempSessionStore(async ({ storePath }) => {
314375
const cfg = {} as OpenClawConfig;

src/agents/command/session-store.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,17 @@ export async function updateSessionStoreAfterAgentRun(params: {
282282
? {
283283
updatedAt: next.updatedAt,
284284
...(touchInteraction ? { lastInteractionAt: next.lastInteractionAt } : {}),
285+
// A physical transcript rotation must still reach sessions.json on a
286+
// preserved-state (heartbeat) turn: it is filesystem identity, not
287+
// user-facing model state. Skipping it would leave the stale pointer
288+
// so the next turn reopens the rotated-away file and deadlocks (#88040).
289+
...(sessionRotated && rotatedSessionFile
290+
? {
291+
sessionId: effectiveSessionId,
292+
sessionFile: rotatedSessionFile,
293+
sessionStartedAt: next.sessionStartedAt,
294+
}
295+
: {}),
285296
}
286297
: removeLifecycleStateFromMetadataPatch(next);
287298
const persisted = await updateSessionStore(

0 commit comments

Comments
 (0)