mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-24 07:01:49 +00:00
fix(memory-core): register memory tools independently to prevent coupled failure (#52668)
Merged via admin squash because current required CI failures are inherited from base and match latest `main` failures outside this PR's `memory-core` surface.
Prepared head SHA: df7f968581
Co-authored-by: artwalker <44759507+artwalker@users.noreply.github.com>
Reviewed-by: @frankekn
This commit is contained in:
@@ -107,6 +107,13 @@ Docs: https://docs.openclaw.ai
|
||||
- Web tools/search provider lists: keep onboarding, configure, and docs provider lists alphabetical while preserving the separate runtime auto-detect precedence used for credential-based provider selection.
|
||||
- Media/Windows security: block remote-host `file://` media URLs and UNC/network paths before local filesystem resolution in core media loading and adjacent prompt/sandbox attachment seams, so the next release no longer allows structured local-media inputs to trigger outbound SMB credential handshakes on Windows. Thanks @RacerZ-fighting for reporting.
|
||||
- Gateway/discovery: fail closed on unresolved Bonjour and DNS-SD service endpoints in CLI discovery, onboarding, and `gateway status` so TXT-only hints can no longer steer routing or SSH auto-target selection. Thanks @nexrin for reporting.
|
||||
- Security/pairing: bind iOS setup codes to the intended node profile and reject first-use bootstrap redemption that asks for broader roles or scopes. Thanks @tdjackey.
|
||||
- Memory/core tools: register `memory_search` and `memory_get` independently so one unavailable memory tool no longer suppresses the other in new sessions. (#50198) Thanks @artwalker.
|
||||
- Web tools/Exa: align the bundled Exa plugin with the current Exa API by supporting newer search types and richer `contents` options, while fixing the result-count cap to honor Exa's higher limit. Thanks @vincentkoc.
|
||||
- Plugins/Matrix: move bundled plugin `KeyedAsyncQueue` imports onto the stable `plugin-sdk/core` surface so Matrix Docker/runtime builds do not depend on the brittle keyed-async-queue subpath. Thanks @ecohash-co and @vincentkoc.
|
||||
- Nostr/security: enforce inbound DM policy before decrypt, route Nostr DMs through the standard reply pipeline, and add pre-crypto rate and size guards so unknown senders cannot bypass pairing or force unbounded crypto work. Thanks @kuranikaran.
|
||||
- Synology Chat/security: keep reply delivery bound to stable numeric `user_id` by default, and gate mutable username/nickname recipient lookup behind `dangerouslyAllowNameMatching` with new regression coverage. Thanks @nexrin.
|
||||
- Agents/default timeout: raise the shared default agent timeout from `600s` to `48h` so long-running ACP and agent sessions do not fail unless you configure a shorter limit.
|
||||
- Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse. (#47560) Thanks @ngutman.
|
||||
- Gateway/startup: prewarm the configured primary model before channel startup and retry one transient provider-runtime miss so the first Telegram or Discord message after boot no longer fails with `Unknown model: openai-codex/gpt-5.4`. Thanks @vincentkoc.
|
||||
- CLI/startup: lazy-load channel add and root help startup paths to trim avoidable RSS and help latency on constrained hosts. (#46784) Thanks @vincentkoc.
|
||||
@@ -310,6 +317,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/context engines: retry strict legacy `assemble()` calls without the new `prompt` field when older engines reject it, preserving prompt-aware retrieval compatibility for pre-prompt plugins. (#50848) thanks @danhdoan.
|
||||
- Plugins/runtime state: share plugin-facing infra singleton state across duplicate module graphs and keep session-binding adapter ownership stable until the active owner unregisters. (#50725) thanks @huntharo.
|
||||
- Discord/pickers: keep `/codex_resume --browse-projects` picker callbacks alive in Discord by sharing component callback state across duplicate module graphs, preserving callback fallbacks, and acknowledging matched plugin interactions before dispatch. (#51260) Thanks @huntharo.
|
||||
- Memory/core tools: register `memory_search` and `memory_get` independently so one unavailable memory tool no longer suppresses the other in new sessions. (#50198) Thanks @artwalker.
|
||||
|
||||
## 2026.3.13
|
||||
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildPromptSection } from "./index.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import plugin, { buildPromptSection } from "./index.js";
|
||||
|
||||
describe("buildPromptSection", () => {
|
||||
it("returns empty when no memory tools are available", () => {
|
||||
expect(buildPromptSection({ availableTools: new Set() })).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns Memory Recall section when memory_search is available", () => {
|
||||
const result = buildPromptSection({ availableTools: new Set(["memory_search"]) });
|
||||
it("describes the two-step flow when both memory tools are available", () => {
|
||||
const result = buildPromptSection({
|
||||
availableTools: new Set(["memory_search", "memory_get"]),
|
||||
});
|
||||
expect(result[0]).toBe("## Memory Recall");
|
||||
expect(result[1]).toContain("run memory_search");
|
||||
expect(result[1]).toContain("then use memory_get");
|
||||
expect(result).toContain(
|
||||
"Citations: include Source: <path#line> when it helps the user verify memory snippets.",
|
||||
);
|
||||
expect(result.at(-1)).toBe("");
|
||||
});
|
||||
|
||||
it("returns Memory Recall section when memory_get is available", () => {
|
||||
it("limits the guidance to memory_search when only search is available", () => {
|
||||
const result = buildPromptSection({ availableTools: new Set(["memory_search"]) });
|
||||
expect(result[0]).toBe("## Memory Recall");
|
||||
expect(result[1]).toContain("run memory_search");
|
||||
expect(result[1]).not.toContain("then use memory_get");
|
||||
});
|
||||
|
||||
it("limits the guidance to memory_get when only get is available", () => {
|
||||
const result = buildPromptSection({ availableTools: new Set(["memory_get"]) });
|
||||
expect(result[0]).toBe("## Memory Recall");
|
||||
expect(result[1]).toContain("run memory_get");
|
||||
expect(result[1]).not.toContain("run memory_search");
|
||||
});
|
||||
|
||||
it("includes citations-off instruction when citationsMode is off", () => {
|
||||
@@ -30,3 +43,41 @@ describe("buildPromptSection", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin registration", () => {
|
||||
it("registers memory tools independently so one unavailable tool does not suppress the other", () => {
|
||||
const registerTool = vi.fn();
|
||||
const registerMemoryPromptSection = vi.fn();
|
||||
const registerCli = vi.fn();
|
||||
const searchTool = { name: "memory_search" };
|
||||
const getTool = null;
|
||||
const api = {
|
||||
registerTool,
|
||||
registerMemoryPromptSection,
|
||||
registerCli,
|
||||
runtime: {
|
||||
tools: {
|
||||
createMemorySearchTool: vi.fn(() => searchTool),
|
||||
createMemoryGetTool: vi.fn(() => getTool),
|
||||
registerMemoryCli: vi.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
plugin.register(api as never);
|
||||
|
||||
expect(registerMemoryPromptSection).toHaveBeenCalledWith(buildPromptSection);
|
||||
expect(registerTool).toHaveBeenCalledTimes(2);
|
||||
expect(registerTool.mock.calls[0]?.[1]).toEqual({ names: ["memory_search"] });
|
||||
expect(registerTool.mock.calls[1]?.[1]).toEqual({ names: ["memory_get"] });
|
||||
|
||||
const searchFactory = registerTool.mock.calls[0]?.[0] as
|
||||
| ((ctx: unknown) => unknown)
|
||||
| undefined;
|
||||
const getFactory = registerTool.mock.calls[1]?.[0] as ((ctx: unknown) => unknown) | undefined;
|
||||
const ctx = { config: { plugins: {} }, sessionKey: "agent:main:slack:dm:u123" };
|
||||
|
||||
expect(searchFactory?.(ctx)).toBe(searchTool);
|
||||
expect(getFactory?.(ctx)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,13 +5,26 @@ export const buildPromptSection: MemoryPromptSectionBuilder = ({
|
||||
availableTools,
|
||||
citationsMode,
|
||||
}) => {
|
||||
if (!availableTools.has("memory_search") && !availableTools.has("memory_get")) {
|
||||
const hasMemorySearch = availableTools.has("memory_search");
|
||||
const hasMemoryGet = availableTools.has("memory_get");
|
||||
|
||||
if (!hasMemorySearch && !hasMemoryGet) {
|
||||
return [];
|
||||
}
|
||||
const lines = [
|
||||
"## Memory Recall",
|
||||
"Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only the needed lines. If low confidence after search, say you checked.",
|
||||
];
|
||||
|
||||
let toolGuidance: string;
|
||||
if (hasMemorySearch && hasMemoryGet) {
|
||||
toolGuidance =
|
||||
"Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only the needed lines. If low confidence after search, say you checked.";
|
||||
} else if (hasMemorySearch) {
|
||||
toolGuidance =
|
||||
"Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md and answer from the matching results. If low confidence after search, say you checked.";
|
||||
} else {
|
||||
toolGuidance =
|
||||
"Before answering anything about prior work, decisions, dates, people, preferences, or todos that already point to a specific memory file or note: run memory_get to pull only the needed lines. If low confidence after reading them, say you checked.";
|
||||
}
|
||||
|
||||
const lines = ["## Memory Recall", toolGuidance];
|
||||
if (citationsMode === "off") {
|
||||
lines.push(
|
||||
"Citations are disabled: do not mention file paths or line numbers in replies unless the user explicitly asks.",
|
||||
@@ -34,21 +47,21 @@ export default definePluginEntry({
|
||||
api.registerMemoryPromptSection(buildPromptSection);
|
||||
|
||||
api.registerTool(
|
||||
(ctx) => {
|
||||
const memorySearchTool = api.runtime.tools.createMemorySearchTool({
|
||||
(ctx) =>
|
||||
api.runtime.tools.createMemorySearchTool({
|
||||
config: ctx.config,
|
||||
agentSessionKey: ctx.sessionKey,
|
||||
});
|
||||
const memoryGetTool = api.runtime.tools.createMemoryGetTool({
|
||||
}),
|
||||
{ names: ["memory_search"] },
|
||||
);
|
||||
|
||||
api.registerTool(
|
||||
(ctx) =>
|
||||
api.runtime.tools.createMemoryGetTool({
|
||||
config: ctx.config,
|
||||
agentSessionKey: ctx.sessionKey,
|
||||
});
|
||||
if (!memorySearchTool || !memoryGetTool) {
|
||||
return null;
|
||||
}
|
||||
return [memorySearchTool, memoryGetTool];
|
||||
},
|
||||
{ names: ["memory_search", "memory_get"] },
|
||||
}),
|
||||
{ names: ["memory_get"] },
|
||||
);
|
||||
|
||||
api.registerCli(
|
||||
|
||||
@@ -109,6 +109,11 @@ export function resolvePluginTools(params: {
|
||||
continue;
|
||||
}
|
||||
if (!resolved) {
|
||||
if (entry.names.length > 0) {
|
||||
log.debug(
|
||||
`plugin tool factory returned null (${entry.pluginId}): [${entry.names.join(", ")}]`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const listRaw = Array.isArray(resolved) ? resolved : [resolved];
|
||||
|
||||
Reference in New Issue
Block a user