mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 19:08:22 -06:00
fix(sessions): propagate compaction transcript read errors (#125741)
This commit is contained in:
committed by
GitHub
parent
f0dd7eecaa
commit
a80ffd0431
@@ -74,7 +74,7 @@ export async function preflightSessionTranscriptForManualCompact(
|
||||
scope: SessionTranscriptRuntimeScope,
|
||||
params: { maxLines: number; sessionFile?: string },
|
||||
): Promise<SessionTranscriptManualTrimPreflightResult> {
|
||||
const events = await loadTranscriptEvents(scope).catch(() => []);
|
||||
const events = await loadTranscriptEvents(scope);
|
||||
if (events.length === 0) {
|
||||
return { compacted: false, reason: "no transcript" };
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ export const sessionCompactHandlers: GatewayRequestHandlers = {
|
||||
sessionId,
|
||||
sessionKey: compactTarget.primaryKey,
|
||||
storePath,
|
||||
}).catch(() => []);
|
||||
});
|
||||
if (transcriptEvents.length === 0) {
|
||||
respond(
|
||||
true,
|
||||
@@ -355,7 +355,7 @@ export const sessionCompactHandlers: GatewayRequestHandlers = {
|
||||
sessionId,
|
||||
sessionKey: compactTarget.primaryKey,
|
||||
storePath,
|
||||
}).catch(() => []);
|
||||
});
|
||||
if (transcriptEvents.length === 0) {
|
||||
respond(
|
||||
true,
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import { beforeEach, expect, test, vi } from "vitest";
|
||||
import {
|
||||
appendTranscriptEvent,
|
||||
appendTranscriptMessage,
|
||||
upsertSessionEntryCore,
|
||||
} from "../config/sessions/session-accessor.js";
|
||||
import { rpcReq } from "./test-helpers.js";
|
||||
import {
|
||||
sessionStoreEntry,
|
||||
setupGatewaySessionsTestHarness,
|
||||
} from "./test/server-sessions.test-helpers.js";
|
||||
|
||||
type LoadTranscriptEvents =
|
||||
(typeof import("../config/sessions/session-accessor.sqlite-read.js"))["loadTranscriptEvents"];
|
||||
|
||||
const transcriptReads = vi.hoisted(() => ({
|
||||
actual: undefined as LoadTranscriptEvents | undefined,
|
||||
load: vi.fn<LoadTranscriptEvents>(),
|
||||
}));
|
||||
|
||||
vi.mock("../config/sessions/session-accessor.sqlite-read.js", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("../config/sessions/session-accessor.sqlite-read.js")>();
|
||||
transcriptReads.actual = actual.loadTranscriptEvents;
|
||||
transcriptReads.load.mockImplementation(actual.loadTranscriptEvents);
|
||||
return { ...actual, loadTranscriptEvents: transcriptReads.load };
|
||||
});
|
||||
|
||||
const { createSessionStoreDir, openClient } = setupGatewaySessionsTestHarness();
|
||||
|
||||
function requireTranscriptReader(): LoadTranscriptEvents {
|
||||
if (!transcriptReads.actual) {
|
||||
throw new Error("transcript reader mock was not initialized");
|
||||
}
|
||||
return transcriptReads.actual;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
transcriptReads.load.mockReset();
|
||||
transcriptReads.load.mockImplementation(requireTranscriptReader());
|
||||
});
|
||||
|
||||
async function seedCompactionSession(params: {
|
||||
sessionId: string;
|
||||
storePath: string;
|
||||
nativeHarness?: boolean;
|
||||
withTranscript?: boolean;
|
||||
}) {
|
||||
const scope = {
|
||||
agentId: "main",
|
||||
sessionId: params.sessionId,
|
||||
sessionKey: "agent:main:main",
|
||||
storePath: params.storePath,
|
||||
};
|
||||
await upsertSessionEntryCore(
|
||||
scope,
|
||||
sessionStoreEntry(
|
||||
params.sessionId,
|
||||
params.nativeHarness
|
||||
? {
|
||||
agentHarnessId: "codex",
|
||||
cliSessionBindings: { "codex-cli": { sessionId: "thread-1" } },
|
||||
cliSessionIds: { "codex-cli": "thread-1" },
|
||||
modelSelectionLocked: true,
|
||||
}
|
||||
: {},
|
||||
),
|
||||
);
|
||||
if (params.withTranscript === false) {
|
||||
return scope;
|
||||
}
|
||||
await appendTranscriptEvent(scope, {
|
||||
type: "session",
|
||||
version: 3,
|
||||
id: params.sessionId,
|
||||
timestamp: "2026-08-18T12:00:00.000Z",
|
||||
cwd: "/tmp",
|
||||
});
|
||||
await appendTranscriptMessage(scope, {
|
||||
message: { role: "user", content: "compact me", timestamp: 1 },
|
||||
now: Date.parse("2026-08-18T12:00:01.000Z"),
|
||||
});
|
||||
return scope;
|
||||
}
|
||||
|
||||
const transcriptReadError = () =>
|
||||
new Error("SQLITE_IOERR: failed to read session transcript storage");
|
||||
|
||||
test("sessions.compact reports initial transcript read failures as unavailable", async () => {
|
||||
const { storePath } = await createSessionStoreDir();
|
||||
await seedCompactionSession({ sessionId: "sess-read-failure", storePath });
|
||||
transcriptReads.load.mockRejectedValueOnce(transcriptReadError());
|
||||
|
||||
const { ws } = await openClient();
|
||||
try {
|
||||
const response = await rpcReq(ws, "sessions.compact", { key: "main" });
|
||||
|
||||
expect(response.ok).toBe(false);
|
||||
expect(response.error).toMatchObject({
|
||||
code: "UNAVAILABLE",
|
||||
message: expect.stringContaining("failed to read session transcript storage"),
|
||||
});
|
||||
} finally {
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("sessions.compact reports model compaction transcript re-read failures as unavailable", async () => {
|
||||
const { storePath } = await createSessionStoreDir();
|
||||
const scope = await seedCompactionSession({
|
||||
sessionId: "sess-model-read-failure",
|
||||
storePath,
|
||||
nativeHarness: true,
|
||||
});
|
||||
const events = await requireTranscriptReader()(scope);
|
||||
transcriptReads.load.mockResolvedValueOnce(events).mockRejectedValueOnce(transcriptReadError());
|
||||
|
||||
const { ws } = await openClient();
|
||||
try {
|
||||
const response = await rpcReq(ws, "sessions.compact", { key: "main" });
|
||||
|
||||
expect(response.ok).toBe(false);
|
||||
expect(response.error).toMatchObject({
|
||||
code: "UNAVAILABLE",
|
||||
message: expect.stringContaining("failed to read session transcript storage"),
|
||||
});
|
||||
} finally {
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("sessions.compact maxLines reports transcript preflight read failures as unavailable", async () => {
|
||||
const { storePath } = await createSessionStoreDir();
|
||||
await seedCompactionSession({ sessionId: "sess-max-lines-read-failure", storePath });
|
||||
transcriptReads.load.mockRejectedValueOnce(transcriptReadError());
|
||||
|
||||
const { ws } = await openClient();
|
||||
try {
|
||||
const response = await rpcReq(ws, "sessions.compact", { key: "main", maxLines: 50 });
|
||||
|
||||
expect(response.ok).toBe(false);
|
||||
expect(response.error).toMatchObject({
|
||||
code: "UNAVAILABLE",
|
||||
message: expect.stringContaining("failed to read session transcript storage"),
|
||||
});
|
||||
} finally {
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.each([{ maxLines: undefined }, { maxLines: 50 }])(
|
||||
"sessions.compact keeps an empty transcript as a successful no-op (maxLines=$maxLines)",
|
||||
async ({ maxLines }) => {
|
||||
const { storePath } = await createSessionStoreDir();
|
||||
await seedCompactionSession({
|
||||
sessionId: `sess-empty-${maxLines ?? "model"}`,
|
||||
storePath,
|
||||
withTranscript: false,
|
||||
});
|
||||
|
||||
const { ws } = await openClient();
|
||||
try {
|
||||
const response = await rpcReq<{ compacted: boolean; ok: true; reason: string }>(
|
||||
ws,
|
||||
"sessions.compact",
|
||||
{ key: "main", ...(maxLines === undefined ? {} : { maxLines }) },
|
||||
);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.payload).toMatchObject({
|
||||
ok: true,
|
||||
compacted: false,
|
||||
reason: "no transcript",
|
||||
});
|
||||
} finally {
|
||||
ws.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user