mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-28 19:01:13 +00:00
fix(qa): retry transient FTS settle races
This commit is contained in:
@@ -183,6 +183,55 @@ describe("qa suite runtime agent session helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("retries transient FTS integrity mismatches while child transcripts settle", async () => {
|
||||
const readEntries = vi
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error(
|
||||
'SQLite integrity_check failed for qa.sqlite: fts5: checksum mismatch for table "session_transcript_fts"',
|
||||
);
|
||||
})
|
||||
.mockReturnValueOnce([
|
||||
{
|
||||
sessionKey: "session-1",
|
||||
entry: { sessionId: "session-1", updatedAt: 10 },
|
||||
},
|
||||
]);
|
||||
vi.useFakeTimers();
|
||||
|
||||
const pending = readRawQaSessionStore(
|
||||
{ gateway: { tempRoot: "/tmp/qa-fts-settle" } } as never,
|
||||
{ readEntries, retryDelaysMs: [1] },
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
|
||||
await expect(pending).resolves.toEqual({
|
||||
"session-1": { sessionId: "session-1", updatedAt: 10 },
|
||||
});
|
||||
expect(readEntries).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("fails closed when an FTS integrity mismatch does not settle", async () => {
|
||||
const mismatch = new Error(
|
||||
'SQLite integrity_check failed for qa.sqlite: fts5: checksum mismatch for table "session_transcript_fts"',
|
||||
);
|
||||
const readEntries = vi.fn(() => {
|
||||
throw mismatch;
|
||||
});
|
||||
vi.useFakeTimers();
|
||||
|
||||
const assertion = expect(
|
||||
readRawQaSessionStore({ gateway: { tempRoot: "/tmp/qa-fts-persistent" } } as never, {
|
||||
readEntries,
|
||||
retryDelaysMs: [1],
|
||||
}),
|
||||
).rejects.toThrow(mismatch.message);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
await assertion;
|
||||
expect(readEntries).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("seeds QA session metadata and transcript messages in SQLite", async () => {
|
||||
const tempRoot = await makeTempDir("qa-session-seed-");
|
||||
const sessionId = "seeded-session";
|
||||
|
||||
@@ -43,6 +43,7 @@ type QaSessionTranscriptSeedParams = {
|
||||
};
|
||||
|
||||
const SESSION_STORE_LOCK_RETRY_DELAYS_MS = [1_000, 3_000, 5_000] as const;
|
||||
const SESSION_STORE_FTS_SETTLE_RETRY_DELAYS_MS = [100, 250, 500, 1_000, 2_000] as const;
|
||||
|
||||
type QaSessionTranscriptSummary = {
|
||||
assistantToolCallCounts: Record<string, number>;
|
||||
@@ -67,6 +68,15 @@ function isSessionStoreLockTimeout(error: unknown) {
|
||||
);
|
||||
}
|
||||
|
||||
function isSessionStoreFtsSettleRace(error: unknown) {
|
||||
const text = formatErrorMessage(error);
|
||||
return (
|
||||
text.includes("SQLite integrity_check failed") &&
|
||||
text.includes("fts5: checksum mismatch") &&
|
||||
text.includes("session_transcript_fts")
|
||||
);
|
||||
}
|
||||
|
||||
function readSessionTranscriptEventMessage(event: unknown) {
|
||||
return isRecord(event) && isRecord(event.message) ? event.message : undefined;
|
||||
}
|
||||
@@ -287,12 +297,33 @@ async function seedQaSessionTranscript(
|
||||
}
|
||||
}
|
||||
|
||||
async function readRawQaSessionStore(env: Pick<QaSuiteRuntimeEnv, "gateway">) {
|
||||
return Object.fromEntries(
|
||||
listSessionEntries({ agentId: "qa", env: qaSessionRuntimeEnv(env.gateway.tempRoot) }).map(
|
||||
({ sessionKey, entry }) => [sessionKey, entry as QaRawSessionStoreEntry],
|
||||
),
|
||||
);
|
||||
async function readRawQaSessionStore(
|
||||
env: Pick<QaSuiteRuntimeEnv, "gateway">,
|
||||
options: {
|
||||
readEntries?: typeof listSessionEntries;
|
||||
retryDelaysMs?: readonly number[];
|
||||
} = {},
|
||||
) {
|
||||
const runtimeEnv = qaSessionRuntimeEnv(env.gateway.tempRoot);
|
||||
const readEntries = options.readEntries ?? listSessionEntries;
|
||||
const retryDelaysMs = options.retryDelaysMs ?? SESSION_STORE_FTS_SETTLE_RETRY_DELAYS_MS;
|
||||
for (let attempt = 0; attempt <= retryDelaysMs.length; attempt += 1) {
|
||||
try {
|
||||
return Object.fromEntries(
|
||||
readEntries({ agentId: "qa", env: runtimeEnv }).map(({ sessionKey, entry }) => [
|
||||
sessionKey,
|
||||
entry as QaRawSessionStoreEntry,
|
||||
]),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!isSessionStoreFtsSettleRace(error) || attempt === retryDelaysMs.length) {
|
||||
throw error;
|
||||
}
|
||||
// Child completion can publish before its transcript writer has settled the FTS state.
|
||||
await sleep(retryDelaysMs[attempt]);
|
||||
}
|
||||
}
|
||||
throw new Error("QA session store read failed after FTS settle retries");
|
||||
}
|
||||
|
||||
async function readSessionTranscriptSummary(
|
||||
|
||||
Reference in New Issue
Block a user