mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(memory): unify default root memory handling (#66141)
* fix(memory): unify default root memory handling * test(memory): align legacy migration expectation * docs(changelog): tag qmd root-memory fix * docs(changelog): append qmd root-memory entry * docs(changelog): dedupe qmd root-memory entry * docs(changelog): attribute qmd root-memory fix --------- Co-authored-by: mbelinky <mbelinky@users.noreply.github.com>
This commit is contained in:
@@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Control UI/Dreaming: stop Imported Insights and Memory Palace from calling optional `memory-wiki` gateway methods when the plugin is off, and refresh config before wiki reloads so the Dreaming tab stops showing misleading unknown-method failures. (#66140) Thanks @mbelinky.
|
||||
- Agents/tools: only mark streamed unknown-tool retries as counted when a streamed message actually classifies an unavailable tool, and keep incomplete streamed tool names from resetting the retry streak before the final assistant message arrives. (#66145) Thanks @dutifulbob.
|
||||
- Memory/active-memory: move recalled memory onto the hidden untrusted prompt-prefix path instead of system prompt injection, label the visible Active Memory status line fields, and include the resolved recall provider/model in gateway debug logs so trace/debug output matches what the model actually saw.
|
||||
- Memory/QMD: stop treating legacy lowercase `memory.md` as a second default root collection, so QMD recall no longer searches phantom `memory-alt-*` collections and builtin/QMD root-memory fallback stays aligned. (#66141) Thanks @mbelinky.
|
||||
|
||||
## 2026.4.12
|
||||
|
||||
|
||||
@@ -797,6 +797,8 @@ describe("QmdMemoryManager", () => {
|
||||
expect(legacyCollections.has("memory-dir-main")).toBe(true);
|
||||
expect(legacyCollections.has("memory-root")).toBe(false);
|
||||
expect(legacyCollections.has("memory-dir")).toBe(false);
|
||||
expect(legacyCollections.has("memory-alt-main")).toBe(false);
|
||||
expect(legacyCollections.has("memory-alt")).toBe(false);
|
||||
});
|
||||
|
||||
it("rebinds conflicting collection name when path+pattern slot is already occupied", async () => {
|
||||
@@ -876,6 +878,87 @@ describe("QmdMemoryManager", () => {
|
||||
expect(logWarnMock).toHaveBeenCalledWith(expect.stringContaining("rebinding"));
|
||||
});
|
||||
|
||||
it("rebinds legacy memory-alt when it still owns the root slot for MEMORY.md", 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 listedCollections = new Map<
|
||||
string,
|
||||
{
|
||||
path: string;
|
||||
pattern: string;
|
||||
}
|
||||
>([["memory-alt", { path: workspaceDir, pattern: "memory.md" }]]);
|
||||
const removeCalls: string[] = [];
|
||||
|
||||
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||
if (args[0] === "collection" && args[1] === "list") {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
emitAndClose(
|
||||
child,
|
||||
"stdout",
|
||||
JSON.stringify(
|
||||
[...listedCollections.entries()].map(([name, info]) => ({
|
||||
name,
|
||||
path: info.path,
|
||||
mask: info.pattern,
|
||||
})),
|
||||
),
|
||||
);
|
||||
return child;
|
||||
}
|
||||
if (args[0] === "collection" && args[1] === "remove") {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
const name = args[2] ?? "";
|
||||
removeCalls.push(name);
|
||||
listedCollections.delete(name);
|
||||
queueMicrotask(() => child.closeWith(0));
|
||||
return child;
|
||||
}
|
||||
if (args[0] === "collection" && args[1] === "add") {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
const pathArg = args[2] ?? "";
|
||||
const name = args[args.indexOf("--name") + 1] ?? "";
|
||||
const pattern = args[args.indexOf("--glob") + 1] ?? args[args.indexOf("--mask") + 1] ?? "";
|
||||
const hasConflict = [...listedCollections.entries()].some(([existingName, info]) => {
|
||||
if (existingName === name || info.path !== pathArg) {
|
||||
return false;
|
||||
}
|
||||
const isRootPatternPair =
|
||||
(info.pattern === "MEMORY.md" || info.pattern === "memory.md") &&
|
||||
(pattern === "MEMORY.md" || pattern === "memory.md");
|
||||
return info.pattern === pattern || isRootPatternPair;
|
||||
});
|
||||
if (hasConflict) {
|
||||
emitAndClose(child, "stderr", "A collection already exists for this path and pattern", 1);
|
||||
return child;
|
||||
}
|
||||
listedCollections.set(name, { path: pathArg, pattern });
|
||||
queueMicrotask(() => child.closeWith(0));
|
||||
return child;
|
||||
}
|
||||
return createMockChild();
|
||||
});
|
||||
|
||||
const { manager } = await createManager({ mode: "full" });
|
||||
await manager.close();
|
||||
|
||||
expect(removeCalls).toContain("memory-alt");
|
||||
expect(listedCollections.has("memory-root-main")).toBe(true);
|
||||
expect(listedCollections.has("memory-alt")).toBe(false);
|
||||
expect(logWarnMock).toHaveBeenCalledWith(expect.stringContaining("rebinding"));
|
||||
});
|
||||
|
||||
it("warns instead of silently succeeding when add conflict metadata is unavailable", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
@@ -912,6 +995,48 @@ describe("QmdMemoryManager", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to --mask when qmd collection add rejects --glob", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: true,
|
||||
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
|
||||
paths: [],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const addFlagCalls: string[] = [];
|
||||
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||
if (args[0] === "collection" && args[1] === "list") {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
emitAndClose(child, "stdout", "[]");
|
||||
return child;
|
||||
}
|
||||
if (args[0] === "collection" && args[1] === "add") {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
const flag = args.includes("--glob") ? "--glob" : args.includes("--mask") ? "--mask" : "";
|
||||
addFlagCalls.push(flag);
|
||||
if (flag === "--glob") {
|
||||
emitAndClose(child, "stderr", "unknown flag: --glob", 1);
|
||||
return child;
|
||||
}
|
||||
queueMicrotask(() => child.closeWith(0));
|
||||
return child;
|
||||
}
|
||||
return createMockChild();
|
||||
});
|
||||
|
||||
const { manager } = await createManager({ mode: "full" });
|
||||
await manager.close();
|
||||
|
||||
expect(addFlagCalls).toEqual(["--glob", "--mask", "--mask"]);
|
||||
expect(logWarnMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining("retrying with legacy compatibility flag"),
|
||||
);
|
||||
});
|
||||
it("migrates unscoped legacy collections from plain-text collection list output", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
@@ -1845,6 +1970,46 @@ describe("QmdMemoryManager", () => {
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("does not query phantom memory-alt collections when MEMORY.md 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;
|
||||
|
||||
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||
if (args[0] === "search") {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
emitAndClose(child, "stdout", "[]");
|
||||
return child;
|
||||
}
|
||||
return createMockChild();
|
||||
});
|
||||
|
||||
const { manager, resolved } = await createManager();
|
||||
|
||||
await manager.search("test", { sessionKey: "agent:main:slack:dm:u123" });
|
||||
const maxResults = resolved.qmd?.limits.maxResults;
|
||||
if (!maxResults) {
|
||||
throw new Error("qmd maxResults missing");
|
||||
}
|
||||
const searchCalls = spawnMock.mock.calls
|
||||
.map((call: unknown[]) => call[1] as string[])
|
||||
.filter((args: string[]) => args[0] === "search");
|
||||
expect(searchCalls).toEqual([
|
||||
["search", "test", "--json", "-n", String(maxResults), "-c", "memory-root-main"],
|
||||
["search", "test", "--json", "-n", String(maxResults), "-c", "memory-dir-main"],
|
||||
]);
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("uses explicit external custom collection names verbatim at query time", async () => {
|
||||
const sharedMirrorDir = path.join(tmpRoot, "shared-notion-mirror");
|
||||
await fs.mkdir(sharedMirrorDir);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import crypto from "node:crypto";
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -507,7 +508,13 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
if (!this.pathsMatch(details.path, collection.path)) {
|
||||
continue;
|
||||
}
|
||||
if (details.pattern !== collection.pattern) {
|
||||
if (
|
||||
!this.patternsMatchForManagedCollection(
|
||||
collection.path,
|
||||
details.pattern,
|
||||
collection.pattern,
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
return name;
|
||||
@@ -621,7 +628,14 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
if (listedLegacy.path && !this.pathsMatch(listedLegacy.path, collection.path)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof listedLegacy.pattern === "string" && listedLegacy.pattern !== collection.pattern) {
|
||||
if (
|
||||
typeof listedLegacy.pattern === "string" &&
|
||||
!this.patternsMatchForManagedCollection(
|
||||
collection.path,
|
||||
listedLegacy.pattern,
|
||||
collection.pattern,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -787,10 +801,10 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
}
|
||||
|
||||
private shouldRebindCollection(collection: ManagedCollection, listed: ListedCollection): boolean {
|
||||
if (typeof listed.pattern === "string" && listed.pattern !== collection.pattern) {
|
||||
return true;
|
||||
}
|
||||
if (!listed.path) {
|
||||
if (typeof listed.pattern === "string" && listed.pattern !== collection.pattern) {
|
||||
return true;
|
||||
}
|
||||
// Older qmd versions may only return names from `collection list --json`.
|
||||
// If the pattern is also missing, do not perform destructive rebinds when
|
||||
// metadata is incomplete: remove+add can permanently drop collections if
|
||||
@@ -800,9 +814,63 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
if (!this.pathsMatch(listed.path, collection.path)) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
typeof listed.pattern === "string" &&
|
||||
!this.patternsMatchForManagedCollection(collection.path, listed.pattern, collection.pattern)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private patternsMatchForManagedCollection(
|
||||
collectionPath: string,
|
||||
leftPattern: string,
|
||||
rightPattern: string,
|
||||
): boolean {
|
||||
if (leftPattern === rightPattern) {
|
||||
return true;
|
||||
}
|
||||
return this.isEquivalentDefaultMemoryRootPattern(collectionPath, leftPattern, rightPattern);
|
||||
}
|
||||
|
||||
private isEquivalentDefaultMemoryRootPattern(
|
||||
collectionPath: string,
|
||||
leftPattern: string,
|
||||
rightPattern: string,
|
||||
): boolean {
|
||||
if (
|
||||
!this.isDefaultMemoryRootPattern(leftPattern) ||
|
||||
!this.isDefaultMemoryRootPattern(rightPattern)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
let sawCanonical = false;
|
||||
let sawLegacyFallback = false;
|
||||
for (const entry of fsSync.readdirSync(collectionPath, { withFileTypes: true })) {
|
||||
if (entry.isSymbolicLink() || !entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
if (entry.name === "MEMORY.md") {
|
||||
sawCanonical = true;
|
||||
} else if (entry.name === "memory.md") {
|
||||
sawLegacyFallback = true;
|
||||
}
|
||||
}
|
||||
if (sawCanonical && sawLegacyFallback) {
|
||||
return false;
|
||||
}
|
||||
return sawCanonical || sawLegacyFallback;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private isDefaultMemoryRootPattern(pattern: string): boolean {
|
||||
return pattern === "MEMORY.md" || pattern === "memory.md";
|
||||
}
|
||||
|
||||
private pathsMatch(left: string, right: string): boolean {
|
||||
const normalize = (value: string): string => {
|
||||
const resolved = path.isAbsolute(value)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import syncFs from "node:fs";
|
||||
import type { Dirent } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { resolveAgentWorkspaceDir } from "../../../../src/agents/agent-scope.js";
|
||||
import type { OpenClawConfig } from "../../../../src/config/config.js";
|
||||
import { resolveMemoryBackendConfig } from "./backend-config.js";
|
||||
@@ -28,7 +30,7 @@ describe("resolveMemoryBackendConfig", () => {
|
||||
} as OpenClawConfig;
|
||||
const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" });
|
||||
expect(resolved.backend).toBe("qmd");
|
||||
expect(resolved.qmd?.collections.length).toBeGreaterThanOrEqual(3);
|
||||
expect(resolved.qmd?.collections.length).toBe(2);
|
||||
expect(resolved.qmd?.command).toBe("qmd");
|
||||
expect(resolved.qmd?.searchMode).toBe("search");
|
||||
expect(resolved.qmd?.update.intervalMs).toBeGreaterThan(0);
|
||||
@@ -38,8 +40,91 @@ describe("resolveMemoryBackendConfig", () => {
|
||||
expect(resolved.qmd?.update.embedTimeoutMs).toBe(120_000);
|
||||
const names = new Set((resolved.qmd?.collections ?? []).map((collection) => collection.name));
|
||||
expect(names.has("memory-root-main")).toBe(true);
|
||||
expect(names.has("memory-alt-main")).toBe(true);
|
||||
expect(names.has("memory-dir-main")).toBe(true);
|
||||
expect(names.has("memory-alt-main")).toBe(false);
|
||||
const rootCollection = resolved.qmd?.collections.find(
|
||||
(collection) => collection.name === "memory-root-main",
|
||||
);
|
||||
expect(rootCollection?.pattern).toBe("MEMORY.md");
|
||||
});
|
||||
|
||||
it("uses lowercase memory.md as the root fallback when MEMORY.md is absent", () => {
|
||||
const workspaceDir = "/workspace/root";
|
||||
const legacyEntry = {
|
||||
name: "memory.md",
|
||||
isFile: () => true,
|
||||
isSymbolicLink: () => false,
|
||||
} as Dirent;
|
||||
const readdirSpy = vi
|
||||
.spyOn(syncFs, "readdirSync")
|
||||
.mockReturnValue([legacyEntry] as unknown as ReturnType<typeof syncFs.readdirSync>);
|
||||
try {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: { workspace: workspaceDir },
|
||||
list: [{ id: "main", default: true, workspace: workspaceDir }],
|
||||
},
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" });
|
||||
const rootCollection = resolved.qmd?.collections.find(
|
||||
(collection) => collection.name === "memory-root-main",
|
||||
);
|
||||
expect(rootCollection?.pattern).toBe("memory.md");
|
||||
expect(
|
||||
(resolved.qmd?.collections ?? []).some(
|
||||
(collection) => collection.name === "memory-alt-main",
|
||||
),
|
||||
).toBe(false);
|
||||
} finally {
|
||||
readdirSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("prefers MEMORY.md over legacy memory.md when both root files exist", () => {
|
||||
const workspaceDir = "/workspace/root";
|
||||
const entries = [
|
||||
{
|
||||
name: "MEMORY.md",
|
||||
isFile: () => true,
|
||||
isSymbolicLink: () => false,
|
||||
},
|
||||
{
|
||||
name: "memory.md",
|
||||
isFile: () => true,
|
||||
isSymbolicLink: () => false,
|
||||
},
|
||||
] as Dirent[];
|
||||
const readdirSpy = vi
|
||||
.spyOn(syncFs, "readdirSync")
|
||||
.mockReturnValue(entries as unknown as ReturnType<typeof syncFs.readdirSync>);
|
||||
try {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: { workspace: workspaceDir },
|
||||
list: [{ id: "main", default: true, workspace: workspaceDir }],
|
||||
},
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" });
|
||||
const rootCollection = resolved.qmd?.collections.find(
|
||||
(collection) => collection.name === "memory-root-main",
|
||||
);
|
||||
expect(rootCollection?.pattern).toBe("MEMORY.md");
|
||||
expect(
|
||||
(resolved.qmd?.collections ?? []).some(
|
||||
(collection) => collection.name === "memory-alt-main",
|
||||
),
|
||||
).toBe(false);
|
||||
} finally {
|
||||
readdirSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("parses quoted qmd command paths", () => {
|
||||
|
||||
@@ -318,6 +318,34 @@ function resolveMcporterConfig(raw?: MemoryQmdMcporterConfig): ResolvedQmdMcport
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function isRegularDefaultMemoryEntry(
|
||||
entry: Pick<fs.Dirent, "name" | "isFile" | "isSymbolicLink">,
|
||||
expectedName: string,
|
||||
): boolean {
|
||||
return entry.name === expectedName && entry.isFile() && !entry.isSymbolicLink();
|
||||
}
|
||||
|
||||
function findDefaultMemoryRootPattern(workspaceDir: string): string | null {
|
||||
try {
|
||||
let sawLegacyFallback = false;
|
||||
for (const entry of fs.readdirSync(workspaceDir, { withFileTypes: true })) {
|
||||
if (isRegularDefaultMemoryEntry(entry, "MEMORY.md")) {
|
||||
return "MEMORY.md";
|
||||
}
|
||||
if (isRegularDefaultMemoryEntry(entry, "memory.md")) {
|
||||
sawLegacyFallback = true;
|
||||
}
|
||||
}
|
||||
return sawLegacyFallback ? "memory.md" : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDefaultMemoryRootPattern(workspaceDir: string): string {
|
||||
return findDefaultMemoryRootPattern(workspaceDir) ?? "MEMORY.md";
|
||||
}
|
||||
|
||||
function resolveDefaultCollections(
|
||||
include: boolean,
|
||||
workspaceDir: string,
|
||||
@@ -328,8 +356,13 @@ function resolveDefaultCollections(
|
||||
return [];
|
||||
}
|
||||
const entries: Array<{ path: string; pattern: string; base: string }> = [
|
||||
{ path: workspaceDir, pattern: "MEMORY.md", base: "memory-root" },
|
||||
{ path: workspaceDir, pattern: "memory.md", base: "memory-alt" },
|
||||
// The root memory slot is singular: prefer MEMORY.md, but keep lowercase
|
||||
// memory.md as a legacy fallback when the canonical file is absent.
|
||||
{
|
||||
path: workspaceDir,
|
||||
pattern: resolveDefaultMemoryRootPattern(workspaceDir),
|
||||
base: "memory-root",
|
||||
},
|
||||
{ path: path.join(workspaceDir, "memory"), pattern: "**/*.md", base: "memory-dir" },
|
||||
];
|
||||
return entries.map((entry) => ({
|
||||
|
||||
@@ -78,6 +78,25 @@ describe("listMemoryFiles", () => {
|
||||
expect(files.some((file) => file.endsWith("standalone.md"))).toBe(true);
|
||||
});
|
||||
|
||||
it("uses lowercase memory.md as the root fallback when MEMORY.md is absent", async () => {
|
||||
const tmpDir = getTmpDir();
|
||||
await fs.writeFile(path.join(tmpDir, "memory.md"), "# Legacy memory");
|
||||
|
||||
const files = await listMemoryFiles(tmpDir);
|
||||
|
||||
expect(files).toEqual([path.join(tmpDir, "memory.md")]);
|
||||
});
|
||||
|
||||
it("prefers MEMORY.md when both root files exist", async () => {
|
||||
const tmpDir = getTmpDir();
|
||||
await fs.writeFile(path.join(tmpDir, "MEMORY.md"), "# Default memory");
|
||||
await fs.writeFile(path.join(tmpDir, "memory.md"), "# Legacy memory");
|
||||
|
||||
const files = await listMemoryFiles(tmpDir);
|
||||
|
||||
expect(files).toEqual([path.join(tmpDir, "MEMORY.md")]);
|
||||
});
|
||||
|
||||
it("handles relative paths in additional paths", async () => {
|
||||
const tmpDir = getTmpDir();
|
||||
await fs.writeFile(path.join(tmpDir, "MEMORY.md"), "# Default memory");
|
||||
|
||||
@@ -113,14 +113,33 @@ async function walkDir(dir: string, files: string[], multimodal?: MemoryMultimod
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveDefaultMemoryRootFile(workspaceDir: string): Promise<string | null> {
|
||||
try {
|
||||
let legacyFallback: string | null = null;
|
||||
const entries = await fs.readdir(workspaceDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isSymbolicLink() || !entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
if (entry.name === "MEMORY.md") {
|
||||
return path.join(workspaceDir, entry.name);
|
||||
}
|
||||
if (entry.name === "memory.md") {
|
||||
legacyFallback = path.join(workspaceDir, entry.name);
|
||||
}
|
||||
}
|
||||
return legacyFallback;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function listMemoryFiles(
|
||||
workspaceDir: string,
|
||||
extraPaths?: string[],
|
||||
multimodal?: MemoryMultimodalSettings,
|
||||
): Promise<string[]> {
|
||||
const result: string[] = [];
|
||||
const memoryFile = path.join(workspaceDir, "MEMORY.md");
|
||||
const altMemoryFile = path.join(workspaceDir, "memory.md");
|
||||
const memoryDir = path.join(workspaceDir, "memory");
|
||||
|
||||
const addMarkdownFile = async (absPath: string) => {
|
||||
@@ -136,8 +155,10 @@ export async function listMemoryFiles(
|
||||
} catch {}
|
||||
};
|
||||
|
||||
await addMarkdownFile(memoryFile);
|
||||
await addMarkdownFile(altMemoryFile);
|
||||
const rootMemoryFile = await resolveDefaultMemoryRootFile(workspaceDir);
|
||||
if (rootMemoryFile) {
|
||||
await addMarkdownFile(rootMemoryFile);
|
||||
}
|
||||
try {
|
||||
const dirStat = await fs.lstat(memoryDir);
|
||||
if (!dirStat.isSymbolicLink() && dirStat.isDirectory()) {
|
||||
|
||||
Reference in New Issue
Block a user