mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-14 05:36:03 +00:00
fix(auto-reply): preserve sessions after compaction failures (#70479)
Summary: - The PR removes the auto-reply compaction-failure session reset hook, adds preserved-session recovery guidance for overflow/compaction failure paths, and updates focused tests, docs, and the changelog. - Reproducibility: yes. at source level with high confidence. Current main routes both embedded overflow paylo ... resetSessionAfterCompactionFailure, and the PR body includes before/after terminal proof of those branches. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(auto-reply): drop dead compaction reset hook - PR branch already contained follow-up commit before automerge: fix(auto-reply): preserve sessions after compaction failures Validation: - ClawSweeper review passed for head193d3c0fdd. - Required merge gates passed before the squash merge. Prepared head SHA:193d3c0fddReview: https://github.com/openclaw/openclaw/pull/70479#issuecomment-4325128777 Co-authored-by: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -25,6 +25,10 @@ const state = vi.hoisted(() => ({
|
||||
isCliProviderMock: vi.fn((_: unknown) => false),
|
||||
isInternalMessageChannelMock: vi.fn((_: unknown) => false),
|
||||
createBlockReplyDeliveryHandlerMock: vi.fn(),
|
||||
isCompactionFailureErrorMock: vi.fn((_: string | undefined) => false),
|
||||
isContextOverflowErrorMock: vi.fn((_: string | undefined) => false),
|
||||
isLikelyContextOverflowErrorMock: vi.fn((_: string | undefined) => false),
|
||||
updateSessionStoreMock: vi.fn(),
|
||||
}));
|
||||
|
||||
const GENERIC_RUN_FAILURE_TEXT =
|
||||
@@ -87,10 +91,11 @@ vi.mock("../../agents/pi-embedded-helpers.js", () => ({
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
isCompactionFailureError: () => false,
|
||||
isContextOverflowError: () => false,
|
||||
isCompactionFailureError: (message?: string) => state.isCompactionFailureErrorMock(message),
|
||||
isContextOverflowError: (message?: string) => state.isContextOverflowErrorMock(message),
|
||||
isBillingErrorMessage: () => false,
|
||||
isLikelyContextOverflowError: () => false,
|
||||
isLikelyContextOverflowError: (message?: string) =>
|
||||
state.isLikelyContextOverflowErrorMock(message),
|
||||
isOverloadedErrorMessage: (message: string) => /overloaded|capacity/i.test(message),
|
||||
isRateLimitErrorMessage: (message: string) =>
|
||||
/rate.limit|too many requests|429|usage limit/i.test(message),
|
||||
@@ -101,7 +106,7 @@ vi.mock("../../agents/pi-embedded-helpers.js", () => ({
|
||||
vi.mock("../../config/sessions.js", () => ({
|
||||
resolveGroupSessionKey: vi.fn(() => null),
|
||||
resolveSessionTranscriptPath: vi.fn(),
|
||||
updateSessionStore: vi.fn(),
|
||||
updateSessionStore: state.updateSessionStoreMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../globals.js", () => ({
|
||||
@@ -270,10 +275,13 @@ function createFollowupRun(): FollowupRun {
|
||||
function createMockReplyOperation(): {
|
||||
replyOperation: ReplyOperation;
|
||||
failMock: ReturnType<typeof vi.fn>;
|
||||
updateSessionIdMock: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
const failMock = vi.fn();
|
||||
const updateSessionIdMock = vi.fn();
|
||||
return {
|
||||
failMock,
|
||||
updateSessionIdMock,
|
||||
replyOperation: {
|
||||
key: "main",
|
||||
sessionId: "session",
|
||||
@@ -282,7 +290,7 @@ function createMockReplyOperation(): {
|
||||
phase: "running",
|
||||
result: null,
|
||||
setPhase: vi.fn(),
|
||||
updateSessionId: vi.fn(),
|
||||
updateSessionId: updateSessionIdMock,
|
||||
attachBackend: vi.fn(),
|
||||
detachBackend: vi.fn(),
|
||||
complete: vi.fn(),
|
||||
@@ -389,7 +397,6 @@ function createMinimalRunAgentTurnParams(overrides?: {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set<Promise<void>>(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -410,6 +417,19 @@ describe("buildContextOverflowRecoveryText", () => {
|
||||
expect(text).not.toContain("heartbeat model bleed");
|
||||
});
|
||||
|
||||
it("keeps the preserved-session copy with the existing overflow hint", () => {
|
||||
const text = buildContextOverflowRecoveryText({
|
||||
preserveSessionMapping: true,
|
||||
cfg: {},
|
||||
primaryProvider: "openrouter",
|
||||
primaryModel: "qwen3.6-plus",
|
||||
});
|
||||
|
||||
expect(text).toContain("kept this conversation mapped to the current session");
|
||||
expect(text).toContain("reserveTokensFloor");
|
||||
expect(text).not.toContain("reset our conversation");
|
||||
});
|
||||
|
||||
it("points to heartbeat model bleed when the last runtime model matches configured heartbeat.model", () => {
|
||||
const text = buildContextOverflowRecoveryText({
|
||||
cfg: {
|
||||
@@ -593,6 +613,13 @@ describe("runAgentTurnWithFallback", () => {
|
||||
state.isInternalMessageChannelMock.mockReturnValue(false);
|
||||
state.createBlockReplyDeliveryHandlerMock.mockReset();
|
||||
state.createBlockReplyDeliveryHandlerMock.mockReturnValue(undefined);
|
||||
state.isCompactionFailureErrorMock.mockReset();
|
||||
state.isCompactionFailureErrorMock.mockReturnValue(false);
|
||||
state.isContextOverflowErrorMock.mockReset();
|
||||
state.isContextOverflowErrorMock.mockReturnValue(false);
|
||||
state.isLikelyContextOverflowErrorMock.mockReset();
|
||||
state.isLikelyContextOverflowErrorMock.mockReturnValue(false);
|
||||
state.updateSessionStoreMock.mockReset();
|
||||
state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => ({
|
||||
result: await params.run("anthropic", "claude"),
|
||||
provider: "anthropic",
|
||||
@@ -1250,7 +1277,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -1367,7 +1393,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -1443,7 +1468,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -1508,7 +1532,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -1569,7 +1592,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -1628,7 +1650,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -1681,7 +1702,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -1738,7 +1758,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -1785,7 +1804,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -1952,7 +1970,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks,
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -2004,7 +2021,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -2049,7 +2065,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -2338,7 +2353,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks,
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -2385,7 +2399,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks,
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -2431,7 +2444,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks,
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -2484,7 +2496,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks,
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -2715,7 +2726,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -2761,7 +2771,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -2826,7 +2835,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -2883,7 +2891,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -2944,7 +2951,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3032,7 +3038,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks,
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3167,7 +3172,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3251,7 +3255,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3298,7 +3301,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3344,7 +3346,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3393,7 +3394,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3450,7 +3450,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3515,7 +3514,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3579,7 +3577,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3637,7 +3634,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3690,7 +3686,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3742,7 +3737,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3782,7 +3776,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3843,7 +3836,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3892,7 +3884,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3946,7 +3937,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -3993,7 +3983,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -4032,7 +4021,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -4137,7 +4125,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -4367,7 +4354,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -4383,6 +4369,95 @@ describe("runAgentTurnWithFallback", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves the active session when embedded overflow recovery fails", async () => {
|
||||
state.isContextOverflowErrorMock.mockReturnValue(true);
|
||||
state.runEmbeddedPiAgentMock.mockResolvedValueOnce({
|
||||
payloads: [],
|
||||
meta: {
|
||||
error: {
|
||||
message: "400 The prompt is too long: 203557, model maximum context length: 196607",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const activeSessionEntry = { sessionId: "session", updatedAt: 1 } as SessionEntry;
|
||||
const activeSessionStore = { "agent:main:main": activeSessionEntry };
|
||||
const { replyOperation, failMock, updateSessionIdMock } = createMockReplyOperation();
|
||||
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
|
||||
const result = await runAgentTurnWithFallback({
|
||||
...createMinimalRunAgentTurnParams({
|
||||
sessionCtx: {
|
||||
Provider: "webchat",
|
||||
MessageSid: "msg",
|
||||
} as unknown as TemplateContext,
|
||||
}),
|
||||
replyOperation,
|
||||
sessionKey: "agent:main:main",
|
||||
getActiveSessionEntry: () => activeSessionEntry,
|
||||
activeSessionStore,
|
||||
storePath: "/tmp/sessions.json",
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("final");
|
||||
if (result.kind === "final") {
|
||||
expect(result.payload.text).toContain("kept this conversation mapped to the current session");
|
||||
expect(result.payload.text).toContain("reserveTokensFloor");
|
||||
expectRecordFields(requireRecord(getReplyPayloadMetadata(result.payload), "reply metadata"), {
|
||||
deliverDespiteSourceReplySuppression: true,
|
||||
});
|
||||
}
|
||||
expect(failMock).toHaveBeenCalledWith(
|
||||
"run_failed",
|
||||
expect.objectContaining({
|
||||
message: "400 The prompt is too long: 203557, model maximum context length: 196607",
|
||||
}),
|
||||
);
|
||||
expect(activeSessionStore["agent:main:main"]?.sessionId).toBe("session");
|
||||
expect(updateSessionIdMock).not.toHaveBeenCalled();
|
||||
expect(state.updateSessionStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves the active session when compaction failure is thrown before reply", async () => {
|
||||
state.isCompactionFailureErrorMock.mockReturnValue(true);
|
||||
state.runEmbeddedPiAgentMock.mockRejectedValueOnce(
|
||||
new Error("Auto-compaction failed: nothing to compact"),
|
||||
);
|
||||
|
||||
const activeSessionEntry = { sessionId: "session", updatedAt: 1 } as SessionEntry;
|
||||
const activeSessionStore = { "agent:main:main": activeSessionEntry };
|
||||
const { replyOperation, failMock, updateSessionIdMock } = createMockReplyOperation();
|
||||
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
|
||||
const result = await runAgentTurnWithFallback({
|
||||
...createMinimalRunAgentTurnParams({
|
||||
sessionCtx: {
|
||||
Provider: "webchat",
|
||||
MessageSid: "msg",
|
||||
} as unknown as TemplateContext,
|
||||
}),
|
||||
replyOperation,
|
||||
sessionKey: "agent:main:main",
|
||||
getActiveSessionEntry: () => activeSessionEntry,
|
||||
activeSessionStore,
|
||||
storePath: "/tmp/sessions.json",
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("final");
|
||||
if (result.kind === "final") {
|
||||
expect(result.payload.text).toContain("kept this conversation mapped to the current session");
|
||||
expect(result.payload.text).toContain("reserveTokensFloor");
|
||||
expectRecordFields(requireRecord(getReplyPayloadMetadata(result.payload), "reply metadata"), {
|
||||
deliverDespiteSourceReplySuppression: true,
|
||||
});
|
||||
}
|
||||
expect(failMock).toHaveBeenCalledWith(
|
||||
"run_failed",
|
||||
expect.objectContaining({ message: "Auto-compaction failed: nothing to compact" }),
|
||||
);
|
||||
expect(activeSessionStore["agent:main:main"]?.sessionId).toBe("session");
|
||||
expect(updateSessionIdMock).not.toHaveBeenCalled();
|
||||
expect(state.updateSessionStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("surfaces gateway reauth guidance for known OAuth refresh failures", async () => {
|
||||
state.runEmbeddedPiAgentMock.mockRejectedValueOnce(
|
||||
new Error(
|
||||
@@ -4407,7 +4482,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -4447,7 +4521,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -4485,7 +4558,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -4525,7 +4597,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -4565,7 +4636,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -4601,7 +4671,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -4636,7 +4705,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -4675,7 +4743,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -4742,7 +4809,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -4797,7 +4863,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -4882,7 +4947,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -4940,7 +5004,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -5003,7 +5066,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -5072,7 +5134,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -5143,7 +5204,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -5207,7 +5267,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
@@ -5274,7 +5333,6 @@ describe("runAgentTurnWithFallback", () => {
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
|
||||
@@ -875,15 +875,18 @@ function resolveHeartbeatBleedHint(params: {
|
||||
|
||||
export function buildContextOverflowRecoveryText(params: {
|
||||
duringCompaction?: boolean;
|
||||
preserveSessionMapping?: boolean;
|
||||
cfg: FollowupRun["run"]["config"];
|
||||
agentId?: string;
|
||||
primaryProvider?: string;
|
||||
primaryModel?: string;
|
||||
activeSessionEntry?: SessionEntry;
|
||||
}): string {
|
||||
const prefix = params.duringCompaction
|
||||
? "⚠️ Context limit exceeded during compaction. I've reset our conversation to start fresh - please try again."
|
||||
: "⚠️ Context limit exceeded. I've reset our conversation to start fresh - please try again.";
|
||||
const prefix = params.preserveSessionMapping
|
||||
? "⚠️ Auto-compaction could not recover this turn. I kept this conversation mapped to the current session. Please try again, use /compact, or use /new to start a fresh session."
|
||||
: params.duringCompaction
|
||||
? "⚠️ Context limit exceeded during compaction. I've reset our conversation to start fresh - please try again."
|
||||
: "⚠️ Context limit exceeded. I've reset our conversation to start fresh - please try again.";
|
||||
return (
|
||||
prefix +
|
||||
(resolveHeartbeatBleedHint({
|
||||
@@ -1134,7 +1137,6 @@ export async function runAgentTurnWithFallback(params: {
|
||||
shouldEmitToolResult: () => boolean;
|
||||
shouldEmitToolOutput: () => boolean;
|
||||
pendingToolTasks: Set<Promise<void>>;
|
||||
resetSessionAfterCompactionFailure: (reason: string) => Promise<boolean>;
|
||||
resetSessionAfterRoleOrderingConflict: (reason: string) => Promise<boolean>;
|
||||
isHeartbeat: boolean;
|
||||
sessionKey?: string;
|
||||
@@ -1315,7 +1317,6 @@ export async function runAgentTurnWithFallback(params: {
|
||||
let fallbackProvider = params.followupRun.run.provider;
|
||||
let fallbackModel = params.followupRun.run.model;
|
||||
let fallbackAttempts: RuntimeFallbackAttempt[] = [];
|
||||
let didResetAfterCompactionFailure = false;
|
||||
let didRetryTransientHttpError = false;
|
||||
let liveModelSwitchRetries = 0;
|
||||
let bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
|
||||
@@ -2231,20 +2232,19 @@ export async function runAgentTurnWithFallback(params: {
|
||||
});
|
||||
|
||||
// Some embedded runs surface context overflow as an error payload instead of throwing.
|
||||
// Treat those as a session-level failure and auto-recover by starting a fresh session.
|
||||
// Preserve the active session mapping and surface explicit guidance instead
|
||||
// of silently rotating the session key to a new session id.
|
||||
const embeddedError = runResult.meta?.error;
|
||||
if (
|
||||
embeddedError &&
|
||||
isContextOverflowError(embeddedError.message) &&
|
||||
!didResetAfterCompactionFailure &&
|
||||
(await params.resetSessionAfterCompactionFailure(embeddedError.message))
|
||||
) {
|
||||
didResetAfterCompactionFailure = true;
|
||||
if (embeddedError && isContextOverflowError(embeddedError.message)) {
|
||||
defaultRuntime.error(
|
||||
`Auto-compaction failed (${embeddedError.message}). Preserving existing session mapping for ${params.sessionKey ?? params.followupRun.run.sessionId}.`,
|
||||
);
|
||||
params.replyOperation?.fail("run_failed", embeddedError);
|
||||
return {
|
||||
kind: "final",
|
||||
payload: markAgentRunFailureReplyPayload({
|
||||
text: buildContextOverflowRecoveryText({
|
||||
preserveSessionMapping: true,
|
||||
cfg: runtimeConfig,
|
||||
agentId: params.followupRun.run.agentId,
|
||||
primaryProvider: params.followupRun.run.provider,
|
||||
@@ -2364,18 +2364,17 @@ export async function runAgentTurnWithFallback(params: {
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
isCompactionFailure &&
|
||||
!didResetAfterCompactionFailure &&
|
||||
(await params.resetSessionAfterCompactionFailure(message))
|
||||
) {
|
||||
didResetAfterCompactionFailure = true;
|
||||
if (isCompactionFailure) {
|
||||
defaultRuntime.error(
|
||||
`Auto-compaction failed (${message}). Preserving existing session mapping for ${params.sessionKey ?? params.followupRun.run.sessionId}.`,
|
||||
);
|
||||
params.replyOperation?.fail("run_failed", err);
|
||||
return {
|
||||
kind: "final",
|
||||
payload: markAgentRunFailureReplyPayload({
|
||||
text: buildContextOverflowRecoveryText({
|
||||
duringCompaction: true,
|
||||
preserveSessionMapping: true,
|
||||
cfg: runtimeConfig,
|
||||
agentId: params.followupRun.run.agentId,
|
||||
primaryProvider: params.followupRun.run.provider,
|
||||
|
||||
@@ -1437,12 +1437,6 @@ export async function runReplyAgent(params: {
|
||||
activeIsNewSession = true;
|
||||
},
|
||||
});
|
||||
const resetSessionAfterCompactionFailure = async (reason: string): Promise<boolean> =>
|
||||
resetSession({
|
||||
failureLabel: "compaction failure",
|
||||
buildLogMessage: (nextSessionId) =>
|
||||
`Auto-compaction failed (${reason}). Restarting session ${sessionKey} -> ${nextSessionId} and retrying.`,
|
||||
});
|
||||
const resetSessionAfterRoleOrderingConflict = async (reason: string): Promise<boolean> =>
|
||||
resetSession({
|
||||
failureLabel: "role ordering conflict",
|
||||
@@ -1471,7 +1465,6 @@ export async function runReplyAgent(params: {
|
||||
shouldEmitToolResult,
|
||||
shouldEmitToolOutput,
|
||||
pendingToolTasks,
|
||||
resetSessionAfterCompactionFailure,
|
||||
resetSessionAfterRoleOrderingConflict,
|
||||
isHeartbeat,
|
||||
sessionKey,
|
||||
|
||||
Reference in New Issue
Block a user