mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
fix(memory): suppress stale warning during session catch-up (#128894)
This commit is contained in:
@@ -27,6 +27,7 @@ type MemoryManagerParams = {
|
||||
|
||||
let workspaceDir = "/workspace";
|
||||
let statusDirty = false;
|
||||
let pendingSyncSources: MemorySource[] | undefined;
|
||||
let customStatus: Record<string, unknown> | undefined;
|
||||
let sourceCounts: Array<{ source: MemorySource; files: number; chunks: number }> = [
|
||||
{ source: "memory", files: 1, chunks: 1 },
|
||||
@@ -56,6 +57,7 @@ const stubManager = {
|
||||
files: 1,
|
||||
chunks: 1,
|
||||
dirty: statusDirty,
|
||||
pendingSyncSources,
|
||||
workspaceDir,
|
||||
dbPath: "/workspace/.memory/index.sqlite",
|
||||
provider: "builtin",
|
||||
@@ -95,6 +97,10 @@ export function setMemoryStatusDirty(next: boolean): void {
|
||||
statusDirty = next;
|
||||
}
|
||||
|
||||
export function setMemoryPendingSyncSources(next: MemorySource[] | undefined): void {
|
||||
pendingSyncSources = next;
|
||||
}
|
||||
|
||||
export function setMemorySourceCounts(
|
||||
next: Array<{ source: MemorySource; files: number; chunks: number }>,
|
||||
): void {
|
||||
@@ -131,6 +137,7 @@ export function resetMemoryToolMockState(overrides?: {
|
||||
}): void {
|
||||
workspaceDir = "/workspace";
|
||||
statusDirty = false;
|
||||
pendingSyncSources = undefined;
|
||||
customStatus = undefined;
|
||||
sourceCounts = [{ source: "memory", files: 1, chunks: 1 }];
|
||||
getManagerImpl = undefined;
|
||||
|
||||
@@ -498,6 +498,23 @@ describe("memory index", () => {
|
||||
await pendingSync;
|
||||
});
|
||||
|
||||
it("reports session-only refreshes from the manager sync owner", async () => {
|
||||
const manager = await getPersistentManager(
|
||||
createCfg({ provider: "none", minScore: 0, onSearch: true, hybrid: { enabled: true } }),
|
||||
);
|
||||
await manager.sync({ reason: "test" });
|
||||
|
||||
Reflect.set(manager, "dirty", false);
|
||||
Reflect.set(manager, "sessionsDirty", true);
|
||||
Reflect.set(manager, "syncing", new Promise<void>(() => {}));
|
||||
try {
|
||||
expect(manager.status().pendingSyncSources).toEqual(["sessions"]);
|
||||
} finally {
|
||||
Reflect.set(manager, "syncing", null);
|
||||
Reflect.set(manager, "sessionsDirty", false);
|
||||
}
|
||||
});
|
||||
|
||||
it("waits for dirty sync before querying", async () => {
|
||||
providerFixture.forceNoProvider = true;
|
||||
const manager = await getPersistentManager(
|
||||
|
||||
@@ -482,12 +482,22 @@ export class MemoryIndexManager extends MemorySearchOrchestration implements Mem
|
||||
requestedProvider: this.requestedProvider,
|
||||
configuredModel: this.settings.model || undefined,
|
||||
});
|
||||
const pendingSyncSources: MemorySource[] = [];
|
||||
if (this.syncing) {
|
||||
if (this.dirty) {
|
||||
pendingSyncSources.push("memory");
|
||||
}
|
||||
if (this.sessionsDirty) {
|
||||
pendingSyncSources.push("sessions");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
backend: "builtin",
|
||||
files: aggregateState.files,
|
||||
chunks: aggregateState.chunks,
|
||||
dirty: this.dirty || this.sessionsDirty || this.indexIdentityDirty,
|
||||
pendingSyncSources: pendingSyncSources.length > 0 ? pendingSyncSources : undefined,
|
||||
workspaceDir: this.workspaceDir,
|
||||
dbPath: this.settings.store.databasePath,
|
||||
provider: providerInfo.provider,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
resetMemoryToolMockState,
|
||||
setMemoryCloseImpl,
|
||||
setMemoryCustomStatus,
|
||||
setMemoryPendingSyncSources,
|
||||
setMemorySearchImpl,
|
||||
setMemorySearchManagerImpl,
|
||||
setMemorySourceCounts,
|
||||
@@ -554,6 +555,25 @@ describe("memory_search unavailable payloads", () => {
|
||||
expect(getMemorySyncMockCalls()).toBe(0);
|
||||
});
|
||||
|
||||
it("does not qualify results while session-only catch-up is in progress", async () => {
|
||||
setMemoryStatusDirty(true);
|
||||
setMemoryPendingSyncSources(["sessions"]);
|
||||
setMemorySearchImpl(async () => []);
|
||||
const tool = createMemorySearchToolOrThrow({
|
||||
config: {
|
||||
agents: { list: [{ id: "main", default: true }] },
|
||||
memory: { citations: "off" },
|
||||
},
|
||||
});
|
||||
|
||||
const result = await tool.execute("session-catch-up", { query: "hidden codeword" });
|
||||
|
||||
expect(result.details).toMatchObject({ results: [] });
|
||||
expect(result.details).not.toHaveProperty("stale");
|
||||
expect(result.details).not.toHaveProperty("warning");
|
||||
expect(result.details).not.toHaveProperty("action");
|
||||
});
|
||||
|
||||
it("surfaces embedding bootstrap degradation when keyword search has no hits", async () => {
|
||||
let searchCalls = 0;
|
||||
setMemorySearchImpl(async (opts) => {
|
||||
|
||||
@@ -147,6 +147,8 @@ export type MemoryProviderStatus = {
|
||||
files?: number;
|
||||
chunks?: number;
|
||||
dirty?: boolean;
|
||||
/** Sources currently being refreshed by an admitted sync. */
|
||||
pendingSyncSources?: MemorySource[];
|
||||
workspaceDir?: string;
|
||||
dbPath?: string;
|
||||
extraPaths?: MemoryExtraPath[];
|
||||
@@ -186,7 +188,7 @@ export type MemoryProviderStatus = {
|
||||
};
|
||||
|
||||
export function resolveMemorySearchStaleness(
|
||||
status: Pick<MemoryProviderStatus, "dirty" | "custom">,
|
||||
status: Pick<MemoryProviderStatus, "dirty" | "pendingSyncSources" | "custom">,
|
||||
agentId?: string,
|
||||
): { stale: true; warning: string; action: string } | null {
|
||||
const identity = status.custom?.indexIdentity as Record<string, unknown> | undefined;
|
||||
@@ -195,7 +197,11 @@ export function resolveMemorySearchStaleness(
|
||||
typeof identity.reason === "string"
|
||||
? identity.reason.trim()
|
||||
: undefined;
|
||||
if (!status.dirty && !identityReason) {
|
||||
const refreshingSessionsOnly =
|
||||
status.dirty === true &&
|
||||
status.pendingSyncSources?.length === 1 &&
|
||||
status.pendingSyncSources[0] === "sessions";
|
||||
if ((!status.dirty || refreshingSessionsOnly) && !identityReason) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AgentSelectionRequiredError } from "../../agents/agent-scope-config.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { MemorySearchResult } from "../../memory-host-sdk/host/types.js";
|
||||
import type { MemoryProviderStatus, MemorySearchResult } from "../../memory-host-sdk/host/types.js";
|
||||
import {
|
||||
createOpenClawTestState,
|
||||
type OpenClawTestState,
|
||||
@@ -56,12 +56,14 @@ async function invokeMemorySearch(params: unknown, cfg: OpenClawConfig) {
|
||||
function createStubManager() {
|
||||
return {
|
||||
search: vi.fn(async (): Promise<MemorySearchResult[]> => []),
|
||||
status: vi.fn(() => ({
|
||||
backend: "builtin" as const,
|
||||
provider: "none",
|
||||
dirty: false,
|
||||
custom: { searchMode: "fts-only" },
|
||||
})),
|
||||
status: vi.fn(
|
||||
(): MemoryProviderStatus => ({
|
||||
backend: "builtin" as const,
|
||||
provider: "none",
|
||||
dirty: false,
|
||||
custom: { searchMode: "fts-only" },
|
||||
}),
|
||||
),
|
||||
close: vi.fn(async () => undefined),
|
||||
};
|
||||
}
|
||||
@@ -284,4 +286,30 @@ describe("memory.search gateway method", () => {
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not qualify results while session-only catch-up is in progress", async () => {
|
||||
const cfg = createConfig(testState.workspaceDir);
|
||||
const manager = createStubManager();
|
||||
manager.status.mockReturnValue({
|
||||
backend: "builtin",
|
||||
provider: "none",
|
||||
dirty: true,
|
||||
pendingSyncSources: ["sessions"],
|
||||
custom: { searchMode: "fts-only" },
|
||||
});
|
||||
getActiveMemorySearchManagerCore.mockResolvedValue({ manager });
|
||||
|
||||
const respond = await invokeMemorySearch({ query: "hidden codeword" }, cfg);
|
||||
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
true,
|
||||
{
|
||||
agentId: "main",
|
||||
provider: "none",
|
||||
searchMode: "fts-only",
|
||||
results: [],
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user