mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 20:20:42 +00:00
fix(memory): recreate stale qmd collections
This commit is contained in:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Memory/QMD: recreate stale managed QMD collections when startup repair finds the collection name already exists, so root memory narrows back to `MEMORY.md` instead of staying on broad workspace markdown indexing.
|
||||||
- Agents/OpenAI: surface selected-model capacity failures from PI, Codex, and auto-reply harness paths with a model-switch hint instead of the generic empty-response error. Thanks @vincentkoc.
|
- Agents/OpenAI: surface selected-model capacity failures from PI, Codex, and auto-reply harness paths with a model-switch hint instead of the generic empty-response error. Thanks @vincentkoc.
|
||||||
- Providers/OpenAI: route `openai/gpt-image-2` through configured Codex OAuth directly when an `openai-codex` profile is active, instead of probing `OPENAI_API_KEY` first.
|
- Providers/OpenAI: route `openai/gpt-image-2` through configured Codex OAuth directly when an `openai-codex` profile is active, instead of probing `OPENAI_API_KEY` first.
|
||||||
- Providers/OpenAI: stop advertising the removed `gpt-5.3-codex-spark` Codex model through fallback catalogs, and suppress stale rows with a GPT-5.5 recovery hint.
|
- Providers/OpenAI: stop advertising the removed `gpt-5.3-codex-spark` Codex model through fallback catalogs, and suppress stale rows with a GPT-5.5 recovery hint.
|
||||||
|
|||||||
@@ -990,6 +990,129 @@ describe("QmdMemoryManager", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("recreates a managed collection when list fails but add reports the same name exists", async () => {
|
||||||
|
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), "# canonical root");
|
||||||
|
cfg = {
|
||||||
|
...cfg,
|
||||||
|
memory: {
|
||||||
|
backend: "qmd",
|
||||||
|
qmd: {
|
||||||
|
includeDefaultMemory: true,
|
||||||
|
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
|
||||||
|
paths: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as OpenClawConfig;
|
||||||
|
|
||||||
|
const removed: string[] = [];
|
||||||
|
const added = new Map<string, string>();
|
||||||
|
const addAttempts = new Map<string, number>();
|
||||||
|
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||||
|
if (args[0] === "collection" && args[1] === "list") {
|
||||||
|
const child = createMockChild({ autoClose: false });
|
||||||
|
emitAndClose(child, "stderr", "temporary qmd list failure", 1);
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
if (args[0] === "collection" && args[1] === "remove") {
|
||||||
|
const child = createMockChild({ autoClose: false });
|
||||||
|
const name = args[2] ?? "";
|
||||||
|
removed.push(name);
|
||||||
|
queueMicrotask(() => child.closeWith(0));
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
if (args[0] === "collection" && args[1] === "add") {
|
||||||
|
const child = createMockChild({ autoClose: false });
|
||||||
|
const name = args[args.indexOf("--name") + 1] ?? "";
|
||||||
|
const pattern = args[args.indexOf("--glob") + 1] ?? args[args.indexOf("--mask") + 1] ?? "";
|
||||||
|
const attempts = addAttempts.get(name) ?? 0;
|
||||||
|
addAttempts.set(name, attempts + 1);
|
||||||
|
if (name === "memory-root-main" && attempts === 0) {
|
||||||
|
emitAndClose(child, "stderr", "Collection 'memory-root-main' already exists.", 1);
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
added.set(name, pattern);
|
||||||
|
queueMicrotask(() => child.closeWith(0));
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
return createMockChild();
|
||||||
|
});
|
||||||
|
|
||||||
|
const { manager } = await createManager({ mode: "full" });
|
||||||
|
await manager.close();
|
||||||
|
|
||||||
|
expect(removed).toContain("memory-root-main");
|
||||||
|
expect(added.get("memory-root-main")).toBe("MEMORY.md");
|
||||||
|
expect(logWarnMock).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining(
|
||||||
|
"qmd collection add conflict for memory-root-main: collection name already exists",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(logWarnMock).not.toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("qmd collection add skipped for memory-root-main"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rebinds memory-root when qmd table output has a stale broad pattern", async () => {
|
||||||
|
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), "# canonical root");
|
||||||
|
cfg = {
|
||||||
|
...cfg,
|
||||||
|
memory: {
|
||||||
|
backend: "qmd",
|
||||||
|
qmd: {
|
||||||
|
includeDefaultMemory: true,
|
||||||
|
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
|
||||||
|
paths: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as OpenClawConfig;
|
||||||
|
|
||||||
|
const removed: string[] = [];
|
||||||
|
const added = new Map<string, string>();
|
||||||
|
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||||
|
if (args[0] === "collection" && args[1] === "list") {
|
||||||
|
const child = createMockChild({ autoClose: false });
|
||||||
|
emitAndClose(
|
||||||
|
child,
|
||||||
|
"stdout",
|
||||||
|
[
|
||||||
|
"Collections (2):",
|
||||||
|
"",
|
||||||
|
"memory-dir-main (qmd://memory-dir-main/)",
|
||||||
|
" Pattern: **/*.md",
|
||||||
|
"",
|
||||||
|
"memory-root-main (qmd://memory-root-main/)",
|
||||||
|
" Pattern: **/*.md",
|
||||||
|
"",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
if (args[0] === "collection" && args[1] === "remove") {
|
||||||
|
const child = createMockChild({ autoClose: false });
|
||||||
|
const name = args[2] ?? "";
|
||||||
|
removed.push(name);
|
||||||
|
queueMicrotask(() => child.closeWith(0));
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
if (args[0] === "collection" && args[1] === "add") {
|
||||||
|
const child = createMockChild({ autoClose: false });
|
||||||
|
const name = args[args.indexOf("--name") + 1] ?? "";
|
||||||
|
const pattern = args[args.indexOf("--glob") + 1] ?? args[args.indexOf("--mask") + 1] ?? "";
|
||||||
|
added.set(name, pattern);
|
||||||
|
queueMicrotask(() => child.closeWith(0));
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
return createMockChild();
|
||||||
|
});
|
||||||
|
|
||||||
|
const { manager } = await createManager({ mode: "full" });
|
||||||
|
await manager.close();
|
||||||
|
|
||||||
|
expect(removed).toContain("memory-root-main");
|
||||||
|
expect(added.get("memory-root-main")).toBe("MEMORY.md");
|
||||||
|
expect(removed).not.toContain("memory-dir-main");
|
||||||
|
});
|
||||||
|
|
||||||
it("falls back to --mask when qmd collection add rejects --glob", async () => {
|
it("falls back to --mask when qmd collection add rejects --glob", async () => {
|
||||||
cfg = {
|
cfg = {
|
||||||
...cfg,
|
...cfg,
|
||||||
|
|||||||
@@ -509,12 +509,22 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = formatErrorMessage(err);
|
const message = formatErrorMessage(err);
|
||||||
if (this.isCollectionAlreadyExistsError(message)) {
|
if (this.isCollectionAlreadyExistsError(message)) {
|
||||||
const rebound = await this.tryRebindConflictingCollection({
|
const rebound =
|
||||||
collection,
|
(await this.tryRebindSameNameCollection({
|
||||||
existing,
|
collection,
|
||||||
addErrorMessage: message,
|
addErrorMessage: message,
|
||||||
});
|
})) ||
|
||||||
if (!rebound) {
|
(await this.tryRebindConflictingCollection({
|
||||||
|
collection,
|
||||||
|
existing,
|
||||||
|
addErrorMessage: message,
|
||||||
|
}));
|
||||||
|
if (rebound) {
|
||||||
|
existing.set(collection.name, {
|
||||||
|
path: collection.path,
|
||||||
|
pattern: collection.pattern,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
log.warn(`qmd collection add skipped for ${collection.name}: ${message}`);
|
log.warn(`qmd collection add skipped for ${collection.name}: ${message}`);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -524,6 +534,49 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async tryRebindSameNameCollection(params: {
|
||||||
|
collection: ManagedCollection;
|
||||||
|
addErrorMessage: string;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
const { collection, addErrorMessage } = params;
|
||||||
|
if (!this.isSameNameCollectionAlreadyExistsError(collection.name, addErrorMessage)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log.warn(
|
||||||
|
`qmd collection add conflict for ${collection.name}: collection name already exists; recreating managed collection`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await this.removeCollection(collection.name);
|
||||||
|
} catch (removeErr) {
|
||||||
|
const removeMessage = formatErrorMessage(removeErr);
|
||||||
|
if (!this.isCollectionMissingError(removeMessage)) {
|
||||||
|
log.warn(`qmd collection remove failed for ${collection.name}: ${removeMessage}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.ensureCollectionPath(collection);
|
||||||
|
await this.addCollection(collection.path, collection.name, collection.pattern);
|
||||||
|
return true;
|
||||||
|
} catch (retryErr) {
|
||||||
|
const retryMessage = formatErrorMessage(retryErr);
|
||||||
|
log.warn(
|
||||||
|
`qmd collection add failed for ${collection.name} after recreating same-name collection: ${retryMessage} (initial: ${addErrorMessage})`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isSameNameCollectionAlreadyExistsError(name: string, message: string): boolean {
|
||||||
|
const lowerName = normalizeLowercaseStringOrEmpty(name);
|
||||||
|
const lowerMessage = normalizeLowercaseStringOrEmpty(message);
|
||||||
|
return (
|
||||||
|
lowerMessage.includes(`collection '${lowerName}' already exists`) ||
|
||||||
|
lowerMessage.includes(`collection "${lowerName}" already exists`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private async listCollectionsBestEffort(): Promise<Map<string, ListedCollection>> {
|
private async listCollectionsBestEffort(): Promise<Map<string, ListedCollection>> {
|
||||||
const existing = new Map<string, ListedCollection>();
|
const existing = new Map<string, ListedCollection>();
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user