mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
fix(memory): queue transcript updates during active sync (#124024)
This commit is contained in:
committed by
GitHub
parent
5a5bf4d524
commit
2ee5c2f6ca
@@ -230,7 +230,13 @@ export abstract class MemoryManagerSessionSyncOps extends MemoryManagerWatchOps
|
||||
}
|
||||
if (pending.length > 0) {
|
||||
this.sessionsDirty = true;
|
||||
void this.sync({ reason: "session-delta" }).catch((err: unknown) => {
|
||||
// Keep both identity and file keys so every transcript backend enters the
|
||||
// targeted queue instead of letting an active sync clear this newer event.
|
||||
void this.sync({
|
||||
reason: "session-delta",
|
||||
sessions: pendingTargets,
|
||||
archiveFiles: pending,
|
||||
}).catch((err: unknown) => {
|
||||
log.warn(`memory sync failed (session update): ${String(err)}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import type { MemorySessionSyncTarget } from "openclaw/plugin-sdk/memory-core-host-engine-storage";
|
||||
import { resolveOpenClawAgentSqlitePath } from "openclaw/plugin-sdk/sqlite-runtime";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createManagerIndexFixture } from "./manager-index.test-support.js";
|
||||
|
||||
const { closeAllMemorySearchManagers, getMemorySearchManager } = await import("./index.js");
|
||||
|
||||
describe("memory session update sync", () => {
|
||||
const fixture = createManagerIndexFixture({
|
||||
getMemorySearchManager,
|
||||
closeAllMemorySearchManagers,
|
||||
});
|
||||
const { createConfig, getFreshManager, seedSessionTranscript } = fixture;
|
||||
|
||||
it("indexes an update that arrives before an active sync clears dirty state", async () => {
|
||||
fixture.setStateDir(path.join(fixture.paths.workspace, ".state-session-update-during-sync"));
|
||||
const sessionId = "session-update-during-sync";
|
||||
const sessionKey = `agent:main:proof:${sessionId}`;
|
||||
const updatedMarker = "UPDATE DURING ACTIVE SYNC 811";
|
||||
const manager = await getFreshManager(
|
||||
createConfig({ provider: "none", sources: ["sessions"], sessionMemory: true }),
|
||||
"cli",
|
||||
);
|
||||
const owner = manager as unknown as {
|
||||
queuedSessionSync: Promise<void> | null;
|
||||
sessionPendingTargets: Map<string, MemorySessionSyncTarget>;
|
||||
sessionsDirty: boolean;
|
||||
sessionsReconcileDirty: boolean;
|
||||
syncArchiveFiles: (params: unknown) => Promise<void>;
|
||||
processSessionUpdateBatch: () => Promise<void>;
|
||||
};
|
||||
let releaseActiveSync = () => {};
|
||||
const activeSyncGate = new Promise<void>((resolve) => {
|
||||
releaseActiveSync = resolve;
|
||||
});
|
||||
let markActiveSyncIndexed = () => {};
|
||||
const activeSyncIndexed = new Promise<void>((resolve) => {
|
||||
markActiveSyncIndexed = resolve;
|
||||
});
|
||||
let syncArchiveFilesSpy: { mockRestore: () => void } | undefined;
|
||||
try {
|
||||
await seedSessionTranscript({
|
||||
sessionId,
|
||||
sessionKey,
|
||||
messages: [{ role: "user", timestamp: Date.now(), content: "initial transcript" }],
|
||||
});
|
||||
await manager.sync({ reason: "test-baseline", force: true });
|
||||
|
||||
owner.sessionsDirty = true;
|
||||
owner.sessionsReconcileDirty = true;
|
||||
const syncArchiveFiles = owner.syncArchiveFiles.bind(manager);
|
||||
syncArchiveFilesSpy = vi
|
||||
.spyOn(owner, "syncArchiveFiles")
|
||||
.mockImplementationOnce(async (params) => {
|
||||
await syncArchiveFiles(params);
|
||||
markActiveSyncIndexed();
|
||||
await activeSyncGate;
|
||||
});
|
||||
const activeSync = manager.sync({ reason: "test-active" });
|
||||
await activeSyncIndexed;
|
||||
|
||||
await seedSessionTranscript({
|
||||
sessionId,
|
||||
sessionKey,
|
||||
messages: [{ role: "assistant", timestamp: Date.now(), content: updatedMarker }],
|
||||
});
|
||||
owner.sessionPendingTargets.set(sessionKey, { agentId: "main", sessionId, sessionKey });
|
||||
await owner.processSessionUpdateBatch();
|
||||
const queuedSessionSync = owner.queuedSessionSync;
|
||||
expect(queuedSessionSync).not.toBeNull();
|
||||
|
||||
releaseActiveSync();
|
||||
await activeSync;
|
||||
await queuedSessionSync;
|
||||
|
||||
const observer = new DatabaseSync(resolveOpenClawAgentSqlitePath({ agentId: "main" }), {
|
||||
readOnly: true,
|
||||
});
|
||||
try {
|
||||
const row = observer
|
||||
.prepare(
|
||||
"SELECT COUNT(*) AS count FROM memory_index_chunks WHERE source = 'sessions' AND text LIKE ?",
|
||||
)
|
||||
.get(`%${updatedMarker}%`) as { count: number };
|
||||
expect(row.count).toBeGreaterThan(0);
|
||||
} finally {
|
||||
observer.close();
|
||||
}
|
||||
expect(manager.status().dirty).toBe(false);
|
||||
} finally {
|
||||
syncArchiveFilesSpy?.mockRestore();
|
||||
releaseActiveSync();
|
||||
await manager.close?.();
|
||||
fixture.restoreStateDir();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -880,7 +880,8 @@ describe("session startup catch-up", () => {
|
||||
await Promise.resolve();
|
||||
|
||||
expect(harness.getDirtyArchiveFiles()).toEqual([session.sessionKey]);
|
||||
expect(harness.syncCalls).toEqual([{ reason: "session-delta" }]);
|
||||
expect(harness.syncCalls[0]?.archiveFiles).toEqual([session.sessionKey]);
|
||||
expect(harness.syncCalls[0]?.sessions).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("keeps targeted indexing on the SQLite store resolved by its corpus snapshot", async () => {
|
||||
@@ -1063,7 +1064,7 @@ describe("session startup catch-up", () => {
|
||||
await harness.waitForSessionSync();
|
||||
|
||||
expect(harness.getDirtyArchiveFiles()).toEqual([session.filePath]);
|
||||
expect(harness.syncCalls).toEqual([{ reason: "session-delta" }]);
|
||||
expect(harness.syncCalls[0]?.archiveFiles).toEqual([session.filePath]);
|
||||
expect(harness.indexedPaths).toEqual([
|
||||
`sessions/main/thread.jsonl.${reason}.2026-06-23T10-00-00.000Z`,
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user