fix: reset sessions after role ordering conflicts

This commit is contained in:
Peter Steinberger
2026-01-16 09:03:54 +00:00
parent 6c6bc6ff1c
commit 9838a2850f
7 changed files with 173 additions and 1 deletions

View File

@@ -63,6 +63,7 @@ export async function runAgentTurnWithFallback(params: {
shouldEmitToolResult: () => boolean;
pendingToolTasks: Set<Promise<void>>;
resetSessionAfterCompactionFailure: (reason: string) => Promise<boolean>;
resetSessionAfterRoleOrderingConflict: (reason: string) => Promise<boolean>;
isHeartbeat: boolean;
sessionKey?: string;
getActiveSessionEntry: () => SessionEntry | undefined;
@@ -376,6 +377,17 @@ export async function runAgentTurnWithFallback(params: {
didResetAfterCompactionFailure = true;
continue;
}
if (embeddedError?.kind === "role_ordering") {
const didReset = await params.resetSessionAfterRoleOrderingConflict(embeddedError.message);
if (didReset) {
return {
kind: "final",
payload: {
text: "⚠️ Message ordering conflict. I've reset the conversation - please try again.",
},
};
}
}
break;
} catch (err) {
@@ -395,6 +407,17 @@ export async function runAgentTurnWithFallback(params: {
didResetAfterCompactionFailure = true;
continue;
}
if (isRoleOrderingError) {
const didReset = await params.resetSessionAfterRoleOrderingConflict(message);
if (didReset) {
return {
kind: "final",
payload: {
text: "⚠️ Message ordering conflict. I've reset the conversation - please try again.",
},
};
}
}
// Auto-recover from Gemini session corruption by resetting the session
if (

View File

@@ -212,6 +212,55 @@ describe("runReplyAgent typing (heartbeat)", () => {
expect(payload).toMatchObject({ text: "ok" });
expect(sessionStore.main.sessionId).not.toBe(sessionId);
const persisted = JSON.parse(await fs.readFile(storePath, "utf-8"));
expect(persisted.main.sessionId).toBe(sessionStore.main.sessionId);
} finally {
if (prevStateDir) {
process.env.CLAWDBOT_STATE_DIR = prevStateDir;
} else {
delete process.env.CLAWDBOT_STATE_DIR;
}
}
});
it("resets the session after role ordering payloads", async () => {
const prevStateDir = process.env.CLAWDBOT_STATE_DIR;
const stateDir = await fs.mkdtemp(path.join(tmpdir(), "clawdbot-session-role-ordering-"));
process.env.CLAWDBOT_STATE_DIR = stateDir;
try {
const sessionId = "session";
const storePath = path.join(stateDir, "sessions", "sessions.json");
const sessionEntry = { sessionId, updatedAt: Date.now() };
const sessionStore = { main: sessionEntry };
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(storePath, JSON.stringify(sessionStore), "utf-8");
runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({
payloads: [{ text: "Message ordering conflict - please try again.", isError: true }],
meta: {
durationMs: 1,
error: {
kind: "role_ordering",
message: 'messages: roles must alternate between "user" and "assistant"',
},
},
}));
const { run } = createMinimalRun({
sessionEntry,
sessionStore,
sessionKey: "main",
storePath,
});
const res = await run();
const payload = Array.isArray(res) ? res[0] : res;
expect(payload).toMatchObject({
text: expect.stringContaining("Message ordering conflict"),
});
expect(payload.text?.toLowerCase()).toContain("reset");
expect(sessionStore.main.sessionId).not.toBe(sessionId);
const persisted = JSON.parse(await fs.readFile(storePath, "utf-8"));
expect(persisted.main.sessionId).toBe(sessionStore.main.sessionId);
} finally {

View File

@@ -245,6 +245,42 @@ export async function runReplyAgent(params: {
);
return true;
};
const resetSessionAfterRoleOrderingConflict = async (reason: string): Promise<boolean> => {
if (!sessionKey || !activeSessionStore || !storePath) return false;
const nextSessionId = crypto.randomUUID();
const nextEntry: SessionEntry = {
...(activeSessionStore[sessionKey] ?? activeSessionEntry),
sessionId: nextSessionId,
updatedAt: Date.now(),
systemSent: false,
abortedLastRun: false,
};
const agentId = resolveAgentIdFromSessionKey(sessionKey);
const nextSessionFile = resolveSessionTranscriptPath(
nextSessionId,
agentId,
sessionCtx.MessageThreadId,
);
nextEntry.sessionFile = nextSessionFile;
activeSessionStore[sessionKey] = nextEntry;
try {
await updateSessionStore(storePath, (store) => {
store[sessionKey] = nextEntry;
});
} catch (err) {
defaultRuntime.error(
`Failed to persist session reset after role ordering conflict (${sessionKey}): ${String(err)}`,
);
}
followupRun.run.sessionId = nextSessionId;
followupRun.run.sessionFile = nextSessionFile;
activeSessionEntry = nextEntry;
activeIsNewSession = true;
defaultRuntime.error(
`Role ordering conflict (${reason}). Restarting session ${sessionKey} -> ${nextSessionId}.`,
);
return true;
};
try {
const runOutcome = await runAgentTurnWithFallback({
commandBody,
@@ -260,6 +296,7 @@ export async function runReplyAgent(params: {
shouldEmitToolResult,
pendingToolTasks,
resetSessionAfterCompactionFailure,
resetSessionAfterRoleOrderingConflict,
isHeartbeat,
sessionKey,
getActiveSessionEntry: () => activeSessionEntry,