Skip to content

Commit 6416743

Browse files
ottoclawclaude
andcommitted
fix(reply): don't wedge preflight compaction on benign skip reasons
claude-cli-runtime sessions (provider stays "anthropic" via a per-model agentRuntime pin) wedged every dispatch when pre-flight compaction returned a benign skip ("no real conversation messages", "Already compacted", etc.): the unconditional throw failed the turn and the session could not recover. Defer to the existing isCompactionSkipReason classifier and return the session entry instead of throwing. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 3db1508 commit 6416743

3 files changed

Lines changed: 77 additions & 1 deletion

File tree

src/auto-reply/reply/agent-runner-memory.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,73 @@ describe("runMemoryFlushIfNeeded", () => {
912912
expect(compactCall.currentTokenCount).toBe(347_000);
913913
});
914914

915+
it.each([
916+
"no real conversation messages",
917+
"Already compacted",
918+
"nothing to compact",
919+
"below threshold",
920+
])(
921+
"Layer 2: benign compaction skip %j does not wedge a claude-cli-runtime session (provider=anthropic)",
922+
async (reason) => {
923+
registerMemoryFlushPlanResolverForTest(() => ({
924+
softThresholdTokens: 4_000,
925+
forceFlushTranscriptBytes: 1_000_000_000,
926+
reserveTokensFloor: 20_000,
927+
prompt: "Pre-compaction memory flush.\nNO_REPLY",
928+
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
929+
relativePath: "memory/2023-11-14.md",
930+
}));
931+
// claude-cli session: OpenClaw transcript is empty -> compaction returns a skip reason.
932+
compactEmbeddedPiSessionMock.mockReset().mockResolvedValue({
933+
ok: false,
934+
compacted: false,
935+
reason,
936+
});
937+
const sessionEntry: SessionEntry = {
938+
sessionId: "session",
939+
updatedAt: Date.now(),
940+
totalTokens: 1_207_416, // inflated cache-read sum, over the 1M window
941+
totalTokensFresh: true,
942+
};
943+
944+
const run = runPreflightCompactionIfNeeded({
945+
cfg: {
946+
models: {
947+
providers: {
948+
anthropic: { models: [{ id: "claude-opus-4-7", contextWindow: 1_048_576 }] },
949+
},
950+
},
951+
agents: {
952+
defaults: {
953+
// user's setup: claude-cli backend + per-MODEL runtime pin (provider stays "anthropic")
954+
cliBackends: { "claude-cli": { command: "/usr/bin/claude" } },
955+
models: { "anthropic/claude-opus-4-7": { agentRuntime: { id: "claude-cli" } } },
956+
compaction: { memoryFlush: {} },
957+
},
958+
},
959+
} as never,
960+
followupRun: createTestFollowupRun({
961+
provider: "anthropic",
962+
model: "claude-opus-4-7",
963+
sessionId: "session",
964+
sessionKey: "main",
965+
}),
966+
defaultModel: "anthropic/claude-opus-4-7",
967+
sessionEntry,
968+
sessionStore: { main: sessionEntry },
969+
sessionKey: "main",
970+
storePath: path.join(rootDir, "sessions.json"),
971+
isHeartbeat: false,
972+
replyOperation: createReplyOperation(),
973+
});
974+
975+
// The over-threshold session must NOT wedge: a benign skip returns the entry so the
976+
// turn proceeds. (Pre-fix this threw "Preflight compaction required but failed: <reason>".)
977+
await expect(run).resolves.toBeDefined();
978+
expect(compactEmbeddedPiSessionMock).toHaveBeenCalled();
979+
},
980+
);
981+
915982
it("keeps the OpenAI API context window for persisted PI runtime overrides", async () => {
916983
registerMemoryFlushPlanResolverForTest(() => ({
917984
softThresholdTokens: 4_000,

src/auto-reply/reply/agent-runner-memory.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
buildEmbeddedRunExecutionParams,
4646
resolveModelFallbackOptions,
4747
} from "./agent-runner-utils.js";
48+
import { isCompactionSkipReason } from "./commands-compact.js";
4849
import {
4950
hasAlreadyFlushedForCurrentCompaction,
5051
resolveMaxActiveTranscriptBytes,
@@ -768,6 +769,14 @@ export async function runPreflightCompactionIfNeeded(params: {
768769
if (!result?.ok || !result.compacted) {
769770
const reason = result?.reason ?? "not_compacted";
770771
logVerbose(`preflightCompaction failed: sessionKey=${params.sessionKey} reason=${reason}`);
772+
// A benign compaction *skip* (nothing to compact / below threshold / already
773+
// compacted / no real conversation messages) is not a failure — it routinely
774+
// happens for provider-owned CLI-runtime sessions, whose OpenClaw-side transcript
775+
// is empty. Throwing here wedges every subsequent dispatch on the session. Defer
776+
// to the same classifier used by the manual /compact path and proceed.
777+
if (isCompactionSkipReason(reason)) {
778+
return entry ?? params.sessionEntry;
779+
}
771780
throw new Error(`Preflight compaction required but failed: ${reason}`);
772781
}
773782

src/auto-reply/reply/commands-compact.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function extractCompactInstructions(params: {
5151
return rest.length ? rest : undefined;
5252
}
5353

54-
function isCompactionSkipReason(reason?: string): boolean {
54+
export function isCompactionSkipReason(reason?: string): boolean {
5555
const text = normalizeOptionalLowercaseString(reason) ?? "";
5656
return (
5757
text.includes("nothing to compact") ||

0 commit comments

Comments
 (0)