Skip to content

Commit 7759677

Browse files
committed
Auto-merge upstream openclaw/openclaw
2 parents 018ecee + a165f7b commit 7759677

50 files changed

Lines changed: 900 additions & 144 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ Docs: https://docs.openclaw.ai
3434
- 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.
3535
- 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.
3636
- 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.
37+
- 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.
38+
- Agents/OpenAI: map `minimal` thinking to OpenAI's supported `low` reasoning effort for GPT-5.4 requests, so embedded runs stop failing request validation.
3739

3840
## 2026.4.12
3941

extensions/discord/src/monitor/inbound-dedupe.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,11 @@ export function releaseDiscordInboundReplay(params: {
6969
function normalizeDiscordInboundReplayKeys(
7070
replayKeys?: readonly (string | null | undefined)[],
7171
): string[] {
72-
return [...new Set((replayKeys ?? []).map((replayKey) => replayKey?.trim()).filter(Boolean))];
72+
return [
73+
...new Set(
74+
(replayKeys ?? [])
75+
.map((replayKey) => replayKey?.trim())
76+
.filter((replayKey): replayKey is string => Boolean(replayKey)),
77+
),
78+
];
7379
}

extensions/discord/src/monitor/message-handler.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ export type DiscordMessageHandlerWithLifecycle = DiscordMessageHandler & {
4747
deactivate: () => void;
4848
};
4949

50+
function isNonEmptyString(value: string | undefined): value is string {
51+
return typeof value === "string" && value.length > 0;
52+
}
53+
5054
export function createDiscordMessageHandler(
5155
params: DiscordMessageHandlerParams,
5256
): DiscordMessageHandlerWithLifecycle {
@@ -114,7 +118,7 @@ export function createDiscordMessageHandler(
114118
if (!last) {
115119
return;
116120
}
117-
const replayKeys = entries.map((entry) => entry.replayKey).filter(Boolean);
121+
const replayKeys = entries.map((entry) => entry.replayKey).filter(isNonEmptyString);
118122
const abortSignal = last.abortSignal;
119123
if (abortSignal?.aborted) {
120124
releaseDiscordInboundReplay({
@@ -177,7 +181,7 @@ export function createDiscordMessageHandler(
177181
}
178182
applyImplicitReplyBatchGate(ctx, params.replyToMode, true);
179183
if (entries.length > 1) {
180-
const ids = entries.map((entry) => entry.data.message?.id).filter(Boolean) as string[];
184+
const ids = entries.map((entry) => entry.data.message?.id).filter(isNonEmptyString);
181185
if (ids.length > 0) {
182186
const ctxBatch = ctx as typeof ctx & {
183187
MessageSids?: string[];
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
declare module "qrcode-terminal" {
2+
type GenerateOptions = {
3+
small?: boolean;
4+
};
5+
6+
type QrCodeTerminal = {
7+
generate: (input: string, options?: GenerateOptions, cb?: (output: string) => void) => void;
8+
};
9+
10+
const qrcode: QrCodeTerminal;
11+
export default qrcode;
12+
}

extensions/feishu/src/test-support/lifecycle-test-support.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -374,18 +374,28 @@ export async function expectFeishuReplyPipelineDedupedAfterPostSendFailure(param
374374
event: unknown;
375375
dispatchReplyFromConfigMock: ReturnType<typeof vi.fn>;
376376
runtimeErrorMock: ReturnType<typeof vi.fn>;
377+
waitTimeoutMs?: number;
377378
}) {
379+
const waitTimeoutMs = params.waitTimeoutMs ?? FEISHU_LIFECYCLE_WAIT_TIMEOUT_MS;
378380
await replayFeishuLifecycleEvent({
379381
handler: params.handler,
380382
event: params.event,
381-
waitForFirst: () => {
382-
expect(params.dispatchReplyFromConfigMock).toHaveBeenCalledTimes(1);
383-
expect(params.runtimeErrorMock).toHaveBeenCalledTimes(1);
384-
},
385-
waitForSecond: () => {
386-
expect(params.dispatchReplyFromConfigMock).toHaveBeenCalledTimes(1);
387-
expect(params.runtimeErrorMock).toHaveBeenCalledTimes(1);
388-
},
383+
waitForFirst: () =>
384+
vi.waitFor(
385+
() => {
386+
expect(params.dispatchReplyFromConfigMock).toHaveBeenCalledTimes(1);
387+
expect(params.runtimeErrorMock).toHaveBeenCalledTimes(1);
388+
},
389+
{ timeout: waitTimeoutMs },
390+
),
391+
waitForSecond: () =>
392+
vi.waitFor(
393+
() => {
394+
expect(params.dispatchReplyFromConfigMock).toHaveBeenCalledTimes(1);
395+
expect(params.runtimeErrorMock).toHaveBeenCalledTimes(1);
396+
},
397+
{ timeout: waitTimeoutMs },
398+
),
389399
});
390400
}
391401

extensions/matrix/.npmignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.DS_Store
2+
dist/.boundary-tsc.tsbuildinfo
3+
**/*.map
4+
**/*.test.ts
5+
**/*.test.tsx
6+
**/*.spec.ts
7+
**/*.spec.tsx
8+
**/*.test-helpers.ts
9+
**/*.test-harness.ts
10+
src/test-*.ts
11+
src/test-support/

extensions/memory-core/src/memory/qmd-manager.test.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,8 @@ describe("QmdMemoryManager", () => {
797797
expect(legacyCollections.has("memory-dir-main")).toBe(true);
798798
expect(legacyCollections.has("memory-root")).toBe(false);
799799
expect(legacyCollections.has("memory-dir")).toBe(false);
800+
expect(legacyCollections.has("memory-alt-main")).toBe(false);
801+
expect(legacyCollections.has("memory-alt")).toBe(false);
800802
});
801803

802804
it("rebinds conflicting collection name when path+pattern slot is already occupied", async () => {
@@ -876,6 +878,87 @@ describe("QmdMemoryManager", () => {
876878
expect(logWarnMock).toHaveBeenCalledWith(expect.stringContaining("rebinding"));
877879
});
878880

881+
it("rebinds legacy memory-alt when it still owns the root slot for MEMORY.md", async () => {
882+
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), "# canonical root");
883+
cfg = {
884+
...cfg,
885+
memory: {
886+
backend: "qmd",
887+
qmd: {
888+
includeDefaultMemory: true,
889+
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
890+
paths: [],
891+
},
892+
},
893+
} as OpenClawConfig;
894+
895+
const listedCollections = new Map<
896+
string,
897+
{
898+
path: string;
899+
pattern: string;
900+
}
901+
>([["memory-alt", { path: workspaceDir, pattern: "memory.md" }]]);
902+
const removeCalls: string[] = [];
903+
904+
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
905+
if (args[0] === "collection" && args[1] === "list") {
906+
const child = createMockChild({ autoClose: false });
907+
emitAndClose(
908+
child,
909+
"stdout",
910+
JSON.stringify(
911+
[...listedCollections.entries()].map(([name, info]) => ({
912+
name,
913+
path: info.path,
914+
mask: info.pattern,
915+
})),
916+
),
917+
);
918+
return child;
919+
}
920+
if (args[0] === "collection" && args[1] === "remove") {
921+
const child = createMockChild({ autoClose: false });
922+
const name = args[2] ?? "";
923+
removeCalls.push(name);
924+
listedCollections.delete(name);
925+
queueMicrotask(() => child.closeWith(0));
926+
return child;
927+
}
928+
if (args[0] === "collection" && args[1] === "add") {
929+
const child = createMockChild({ autoClose: false });
930+
const pathArg = args[2] ?? "";
931+
const name = args[args.indexOf("--name") + 1] ?? "";
932+
const pattern = args[args.indexOf("--glob") + 1] ?? args[args.indexOf("--mask") + 1] ?? "";
933+
const hasConflict = [...listedCollections.entries()].some(([existingName, info]) => {
934+
if (existingName === name || info.path !== pathArg) {
935+
return false;
936+
}
937+
const isRootPatternPair =
938+
(info.pattern === "MEMORY.md" || info.pattern === "memory.md") &&
939+
(pattern === "MEMORY.md" || pattern === "memory.md");
940+
return info.pattern === pattern || isRootPatternPair;
941+
});
942+
if (hasConflict) {
943+
emitAndClose(child, "stderr", "A collection already exists for this path and pattern", 1);
944+
return child;
945+
}
946+
listedCollections.set(name, { path: pathArg, pattern });
947+
queueMicrotask(() => child.closeWith(0));
948+
return child;
949+
}
950+
return createMockChild();
951+
});
952+
953+
const { manager } = await createManager({ mode: "full" });
954+
await manager.close();
955+
956+
expect(removeCalls).toContain("memory-alt");
957+
expect(listedCollections.has("memory-root-main")).toBe(true);
958+
expect(listedCollections.has("memory-alt")).toBe(false);
959+
expect(logWarnMock).toHaveBeenCalledWith(expect.stringContaining("rebinding"));
960+
});
961+
879962
it("warns instead of silently succeeding when add conflict metadata is unavailable", async () => {
880963
cfg = {
881964
...cfg,
@@ -912,6 +995,48 @@ describe("QmdMemoryManager", () => {
912995
);
913996
});
914997

998+
it("falls back to --mask when qmd collection add rejects --glob", async () => {
999+
cfg = {
1000+
...cfg,
1001+
memory: {
1002+
backend: "qmd",
1003+
qmd: {
1004+
includeDefaultMemory: true,
1005+
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
1006+
paths: [],
1007+
},
1008+
},
1009+
} as OpenClawConfig;
1010+
1011+
const addFlagCalls: string[] = [];
1012+
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
1013+
if (args[0] === "collection" && args[1] === "list") {
1014+
const child = createMockChild({ autoClose: false });
1015+
emitAndClose(child, "stdout", "[]");
1016+
return child;
1017+
}
1018+
if (args[0] === "collection" && args[1] === "add") {
1019+
const child = createMockChild({ autoClose: false });
1020+
const flag = args.includes("--glob") ? "--glob" : args.includes("--mask") ? "--mask" : "";
1021+
addFlagCalls.push(flag);
1022+
if (flag === "--glob") {
1023+
emitAndClose(child, "stderr", "unknown flag: --glob", 1);
1024+
return child;
1025+
}
1026+
queueMicrotask(() => child.closeWith(0));
1027+
return child;
1028+
}
1029+
return createMockChild();
1030+
});
1031+
1032+
const { manager } = await createManager({ mode: "full" });
1033+
await manager.close();
1034+
1035+
expect(addFlagCalls).toEqual(["--glob", "--mask", "--mask"]);
1036+
expect(logWarnMock).toHaveBeenCalledWith(
1037+
expect.stringContaining("retrying with legacy compatibility flag"),
1038+
);
1039+
});
9151040
it("migrates unscoped legacy collections from plain-text collection list output", async () => {
9161041
cfg = {
9171042
...cfg,
@@ -1845,6 +1970,46 @@ describe("QmdMemoryManager", () => {
18451970
await manager.close();
18461971
});
18471972

1973+
it("does not query phantom memory-alt collections when MEMORY.md exists", async () => {
1974+
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), "# canonical root");
1975+
cfg = {
1976+
...cfg,
1977+
memory: {
1978+
backend: "qmd",
1979+
qmd: {
1980+
includeDefaultMemory: true,
1981+
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
1982+
paths: [],
1983+
},
1984+
},
1985+
} as OpenClawConfig;
1986+
1987+
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
1988+
if (args[0] === "search") {
1989+
const child = createMockChild({ autoClose: false });
1990+
emitAndClose(child, "stdout", "[]");
1991+
return child;
1992+
}
1993+
return createMockChild();
1994+
});
1995+
1996+
const { manager, resolved } = await createManager();
1997+
1998+
await manager.search("test", { sessionKey: "agent:main:slack:dm:u123" });
1999+
const maxResults = resolved.qmd?.limits.maxResults;
2000+
if (!maxResults) {
2001+
throw new Error("qmd maxResults missing");
2002+
}
2003+
const searchCalls = spawnMock.mock.calls
2004+
.map((call: unknown[]) => call[1] as string[])
2005+
.filter((args: string[]) => args[0] === "search");
2006+
expect(searchCalls).toEqual([
2007+
["search", "test", "--json", "-n", String(maxResults), "-c", "memory-root-main"],
2008+
["search", "test", "--json", "-n", String(maxResults), "-c", "memory-dir-main"],
2009+
]);
2010+
await manager.close();
2011+
});
2012+
18482013
it("uses explicit external custom collection names verbatim at query time", async () => {
18492014
const sharedMirrorDir = path.join(tmpRoot, "shared-notion-mirror");
18502015
await fs.mkdir(sharedMirrorDir);

0 commit comments

Comments
 (0)