mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
fix(active-memory): allow active-memory to support custom recall tools (#77906)
* fix(active-memory): allow custom recall tools * docs(active-memory): document custom recall tools * docs(active-memory): note tools allowlist change * fix(active-memory): constrain recall tool allowlist * fix(active-memory): preserve lancedb recall defaults * fix(active-memory): block non-memory recall tools * fix(active-memory): satisfy bundled lint * fix(active-memory): satisfy type-aware lint * fix(tests): satisfy type-aware lint * fix(tests): clear next type-aware lint batch * fix(tests): clear lint and test type annotations * docs(changelog): consolidate active memory entry * docs(changelog): reclassify active memory tools entry
This commit is contained in:
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- Active Memory: support concrete `plugins.entries.active-memory.config.toolsAllow` recall tool names for custom memory plugins while keeping the built-in memory-core default on `memory_search`/`memory_get` and preserving `memory_recall` automatically for `plugins.slots.memory: "memory-lancedb"`.
|
||||
- Telegram/Feishu: honor configured per-agent and global `reasoningDefault` values when deciding whether channel reasoning previews should stream or stay hidden, addressing the preview-default part of #73182. Thanks @anagnorisis2peripeteia.
|
||||
- Docker: run the runtime image under `tini` so long-lived containers reap orphaned child processes and forward signals correctly. (#77885) Thanks @VintageAyu.
|
||||
- Google/Gemini: normalize retired `google/gemini-3-pro-preview` and `google-gemini-cli/gemini-3-pro-preview` selections to `google/gemini-3.1-pro-preview` before they are written to model config.
|
||||
|
||||
@@ -332,12 +332,16 @@ flowchart LR
|
||||
I --> M["Main Reply"]
|
||||
```
|
||||
|
||||
The blocking memory sub-agent can use only the available memory recall tools:
|
||||
The blocking memory sub-agent can use only the configured memory recall tools.
|
||||
By default that is:
|
||||
|
||||
- `memory_recall`
|
||||
- `memory_search`
|
||||
- `memory_get`
|
||||
|
||||
When `plugins.slots.memory` is `memory-lancedb`, the default is `memory_recall`
|
||||
instead. Set `config.toolsAllow` when another memory provider exposes a
|
||||
different recall tool contract.
|
||||
|
||||
If the connection is weak, it should return `NONE`.
|
||||
|
||||
## Query modes
|
||||
@@ -462,6 +466,110 @@ skips recall for that turn.
|
||||
`config.modelFallbackPolicy` is retained only as a deprecated compatibility
|
||||
field for older configs. It no longer changes runtime behavior.
|
||||
|
||||
## Memory tools
|
||||
|
||||
By default Active Memory lets the blocking recall sub-agent call
|
||||
`memory_search` and `memory_get`. That matches the built-in `memory-core`
|
||||
contract. When `plugins.slots.memory` selects `memory-lancedb` and
|
||||
`config.toolsAllow` is unset, Active Memory keeps the existing LanceDB behavior
|
||||
and uses `memory_recall` instead.
|
||||
|
||||
If you use another memory plugin, set `config.toolsAllow` to the exact tool
|
||||
names that plugin registers. Active Memory lists those tools in the recall
|
||||
prompt and passes the same list to the embedded sub-agent. If none of the
|
||||
configured tools are available, or the memory sub-agent fails, Active Memory
|
||||
skips recall for that turn and the main reply continues without memory context.
|
||||
`toolsAllow` only accepts concrete memory tool names. Wildcards, `group:*`
|
||||
entries, and core agent tools such as `read`, `exec`, `message`, and
|
||||
`web_search` are ignored before the hidden memory sub-agent starts.
|
||||
|
||||
Default-behavior note: Active Memory no longer includes `memory_recall` in the
|
||||
memory-core default allowlist. Existing `memory-lancedb` setups keep working
|
||||
when `plugins.slots.memory` is set to `memory-lancedb`. Explicit `toolsAllow`
|
||||
always overrides the automatic default.
|
||||
|
||||
### Built-in memory-core
|
||||
|
||||
The default setup does not need an explicit `toolsAllow`:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"active-memory": {
|
||||
enabled: true,
|
||||
config: {
|
||||
agents: ["main"],
|
||||
// Default: ["memory_search", "memory_get"]
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### LanceDB memory
|
||||
|
||||
The bundled `memory-lancedb` plugin exposes `memory_recall`. Selecting the
|
||||
memory slot is enough for Active Memory to use that recall tool:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
slots: {
|
||||
memory: "memory-lancedb",
|
||||
},
|
||||
entries: {
|
||||
"memory-lancedb": {
|
||||
enabled: true,
|
||||
config: {
|
||||
embedding: {
|
||||
provider: "openai",
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
},
|
||||
},
|
||||
"active-memory": {
|
||||
enabled: true,
|
||||
config: {
|
||||
agents: ["main"],
|
||||
promptAppend: "Use memory_recall for long-term user preferences, past decisions, and previously discussed topics. If recall finds nothing useful, return NONE.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Lossless Claw
|
||||
|
||||
Lossless Claw is a context-engine plugin with its own recall tools. Install and
|
||||
configure it as a context engine first; see [Context engine](/concepts/context-engine).
|
||||
Then let Active Memory use the Lossless Claw recall tools:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"lossless-claw": {
|
||||
enabled: true,
|
||||
},
|
||||
"active-memory": {
|
||||
enabled: true,
|
||||
config: {
|
||||
agents: ["main"],
|
||||
toolsAllow: ["lcm_grep", "lcm_describe", "lcm_expand_query"],
|
||||
promptAppend: "Use lcm_grep first for compacted conversation recall. Use lcm_describe to inspect a specific summary. Use lcm_expand_query only when the latest user message needs exact details that may have been compacted away. Return NONE if the retrieved context is not clearly useful.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Do not include `lcm_expand` in `toolsAllow` for the main Active Memory sub-agent.
|
||||
Lossless Claw uses that as a lower-level delegated expansion tool.
|
||||
|
||||
## Advanced escape hatches
|
||||
|
||||
These options are intentionally not part of the recommended setup.
|
||||
@@ -488,6 +596,9 @@ Memory prompt and before the conversation context:
|
||||
promptAppend: "Prefer stable long-term preferences over one-off events."
|
||||
```
|
||||
|
||||
Use `promptAppend` with custom `toolsAllow` when a non-core memory plugin needs
|
||||
provider-specific tool order or query-shaping instructions.
|
||||
|
||||
`config.promptOverride` replaces the default Active Memory prompt. OpenClaw
|
||||
still appends the conversation context afterward:
|
||||
|
||||
@@ -558,25 +669,26 @@ plugins.entries.active-memory
|
||||
|
||||
The most important fields are:
|
||||
|
||||
| Key | Type | Meaning |
|
||||
| ---------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `enabled` | `boolean` | Enables the plugin itself |
|
||||
| `config.agents` | `string[]` | Agent ids that may use active memory |
|
||||
| `config.model` | `string` | Optional blocking memory sub-agent model ref; when unset, active memory uses the current session model |
|
||||
| `config.allowedChatTypes` | `("direct" \| "group" \| "channel")[]` | Session types that may run Active Memory; defaults to direct-message style sessions |
|
||||
| `config.allowedChatIds` | `string[]` | Optional per-conversation allowlist applied after `allowedChatTypes`; non-empty lists fail closed |
|
||||
| `config.deniedChatIds` | `string[]` | Optional per-conversation denylist that overrides allowed session types and allowed ids |
|
||||
| `config.queryMode` | `"message" \| "recent" \| "full"` | Controls how much conversation the blocking memory sub-agent sees |
|
||||
| `config.promptStyle` | `"balanced" \| "strict" \| "contextual" \| "recall-heavy" \| "precision-heavy" \| "preference-only"` | Controls how eager or strict the blocking memory sub-agent is when deciding whether to return memory |
|
||||
| `config.thinking` | `"off" \| "minimal" \| "low" \| "medium" \| "high" \| "xhigh" \| "adaptive" \| "max"` | Advanced thinking override for the blocking memory sub-agent; default `off` for speed |
|
||||
| `config.promptOverride` | `string` | Advanced full prompt replacement; not recommended for normal use |
|
||||
| `config.promptAppend` | `string` | Advanced extra instructions appended to the default or overridden prompt |
|
||||
| `config.timeoutMs` | `number` | Hard timeout for the blocking memory sub-agent, capped at 120000 ms |
|
||||
| `config.setupGraceTimeoutMs` | `number` | Advanced extra setup budget before the recall timeout expires; defaults to 0 and is capped at 30000 ms. See [Cold-start grace](#cold-start-grace) for v2026.4.x upgrade guidance |
|
||||
| `config.maxSummaryChars` | `number` | Maximum total characters allowed in the active-memory summary |
|
||||
| `config.logging` | `boolean` | Emits active memory logs while tuning |
|
||||
| `config.persistTranscripts` | `boolean` | Keeps blocking memory sub-agent transcripts on disk instead of deleting temp files |
|
||||
| `config.transcriptDir` | `string` | Relative blocking memory sub-agent transcript directory under the agent sessions folder |
|
||||
| Key | Type | Meaning |
|
||||
| ---------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `enabled` | `boolean` | Enables the plugin itself |
|
||||
| `config.agents` | `string[]` | Agent ids that may use active memory |
|
||||
| `config.model` | `string` | Optional blocking memory sub-agent model ref; when unset, active memory uses the current session model |
|
||||
| `config.allowedChatTypes` | `("direct" \| "group" \| "channel")[]` | Session types that may run Active Memory; defaults to direct-message style sessions |
|
||||
| `config.allowedChatIds` | `string[]` | Optional per-conversation allowlist applied after `allowedChatTypes`; non-empty lists fail closed |
|
||||
| `config.deniedChatIds` | `string[]` | Optional per-conversation denylist that overrides allowed session types and allowed ids |
|
||||
| `config.queryMode` | `"message" \| "recent" \| "full"` | Controls how much conversation the blocking memory sub-agent sees |
|
||||
| `config.promptStyle` | `"balanced" \| "strict" \| "contextual" \| "recall-heavy" \| "precision-heavy" \| "preference-only"` | Controls how eager or strict the blocking memory sub-agent is when deciding whether to return memory |
|
||||
| `config.toolsAllow` | `string[]` | Concrete memory tool names the blocking memory sub-agent may call; defaults to `["memory_search", "memory_get"]`, or `["memory_recall"]` when `plugins.slots.memory` is `memory-lancedb`; wildcards, `group:*` entries, and core agent tools are ignored |
|
||||
| `config.thinking` | `"off" \| "minimal" \| "low" \| "medium" \| "high" \| "xhigh" \| "adaptive" \| "max"` | Advanced thinking override for the blocking memory sub-agent; default `off` for speed |
|
||||
| `config.promptOverride` | `string` | Advanced full prompt replacement; not recommended for normal use |
|
||||
| `config.promptAppend` | `string` | Advanced extra instructions appended to the default or overridden prompt |
|
||||
| `config.timeoutMs` | `number` | Hard timeout for the blocking memory sub-agent, capped at 120000 ms |
|
||||
| `config.setupGraceTimeoutMs` | `number` | Advanced extra setup budget before the recall timeout expires; defaults to 0 and is capped at 30000 ms. See [Cold-start grace](#cold-start-grace) for v2026.4.x upgrade guidance |
|
||||
| `config.maxSummaryChars` | `number` | Maximum total characters allowed in the active-memory summary |
|
||||
| `config.logging` | `boolean` | Emits active memory logs while tuning |
|
||||
| `config.persistTranscripts` | `boolean` | Keeps blocking memory sub-agent transcripts on disk instead of deleting temp files |
|
||||
| `config.transcriptDir` | `string` | Relative blocking memory sub-agent transcript directory under the agent sessions folder |
|
||||
|
||||
Useful tuning fields:
|
||||
|
||||
@@ -692,8 +804,9 @@ If active memory is too slow:
|
||||
|
||||
Active Memory rides on the configured memory plugin's recall pipeline, so most
|
||||
recall surprises are embedding-provider problems, not Active Memory bugs. The
|
||||
default `memory-core` path uses `memory_search`; `memory-lancedb` uses
|
||||
`memory_recall`.
|
||||
default `memory-core` path uses `memory_search` and `memory_get`; the
|
||||
`memory-lancedb` slot uses `memory_recall`. If you use another memory plugin,
|
||||
confirm `config.toolsAllow` names the tools that plugin actually registers.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Embedding provider switched or stopped working">
|
||||
|
||||
@@ -22,6 +22,34 @@ describe("active-memory manifest config schema", () => {
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts custom toolsAllow entries", () => {
|
||||
const result = validateJsonSchemaValue({
|
||||
schema: manifest.configSchema,
|
||||
cacheKey: "active-memory.manifest.tools-allow",
|
||||
value: {
|
||||
enabled: true,
|
||||
agents: ["main"],
|
||||
toolsAllow: ["lcm_grep", "lcm_describe", "lcm_expand_query"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects wildcard and group toolsAllow entries", () => {
|
||||
const result = validateJsonSchemaValue({
|
||||
schema: manifest.configSchema,
|
||||
cacheKey: "active-memory.manifest.tools-allow.reserved",
|
||||
value: {
|
||||
enabled: true,
|
||||
agents: ["main"],
|
||||
toolsAllow: ["*", "group:plugins"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts timeoutMs values at the runtime ceiling", () => {
|
||||
const result = validateJsonSchemaValue({
|
||||
schema: manifest.configSchema,
|
||||
|
||||
@@ -67,6 +67,19 @@ describe("active-memory plugin", () => {
|
||||
},
|
||||
};
|
||||
};
|
||||
const setMemorySlot = (memory: string) => {
|
||||
const plugins = configFile.plugins as Record<string, unknown> | undefined;
|
||||
configFile = {
|
||||
...configFile,
|
||||
plugins: {
|
||||
...plugins,
|
||||
slots: {
|
||||
...(plugins?.slots as Record<string, unknown> | undefined),
|
||||
memory,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
const api: any = {
|
||||
get pluginConfig() {
|
||||
return pluginConfig;
|
||||
@@ -147,7 +160,7 @@ describe("active-memory plugin", () => {
|
||||
};
|
||||
const makeMemoryToolAllowlistError = (
|
||||
reason: string,
|
||||
sources = "runtime toolsAllow: memory_recall, memory_search, memory_get",
|
||||
sources = "runtime toolsAllow: memory_search, memory_get",
|
||||
) =>
|
||||
new Error(
|
||||
`No callable tools remain after resolving explicit tool allowlist ` +
|
||||
@@ -1285,16 +1298,17 @@ describe("active-memory plugin", () => {
|
||||
);
|
||||
expect(runParams?.prompt).toContain("Use only the available memory tools.");
|
||||
expect(runParams?.prompt).toContain(
|
||||
"Use the bounded search query as the memory_search or memory_recall query.",
|
||||
"Use the bounded search query with the configured memory tools.",
|
||||
);
|
||||
expect(runParams?.prompt).toContain("Prefer memory_recall when available.");
|
||||
expect(runParams?.prompt).toContain("Configured memory tools: memory_search, memory_get.");
|
||||
expect(runParams?.prompt).toContain(
|
||||
"If memory_recall is unavailable, use memory_search and memory_get.",
|
||||
"If the available memory tools find nothing useful, reply with NONE.",
|
||||
);
|
||||
expect(runParams?.toolsAllow).toEqual(["memory_recall", "memory_search", "memory_get"]);
|
||||
expect(runParams?.prompt).not.toContain("memory_recall");
|
||||
expect(runParams?.toolsAllow).toEqual(["memory_search", "memory_get"]);
|
||||
expect(runParams?.allowGatewaySubagentBinding).toBe(true);
|
||||
expect(runParams?.prompt).toContain(
|
||||
"When searching for preference or habit recall, use a permissive recall limit or memory_search threshold before deciding that no useful memory exists.",
|
||||
"When searching for preference or habit recall, use permissive search limits or thresholds before deciding that no useful memory exists.",
|
||||
);
|
||||
expect(runParams?.prompt).toContain(
|
||||
"If the user is directly asking about favorites, preferences, habits, routines, or personal facts, treat that as a strong recall signal.",
|
||||
@@ -1318,6 +1332,187 @@ describe("active-memory plugin", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("passes custom configured memory tools and reflects them in the default prompt", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
toolsAllow: [" lcm_grep ", "lcm_describe", "", "lcm_expand_query", "lcm_grep"],
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
prompt: "What did we decide about active memory?",
|
||||
messages: [],
|
||||
},
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
const runParams = runEmbeddedPiAgent.mock.calls.at(-1)?.[0];
|
||||
expect(runParams?.toolsAllow).toEqual(["lcm_grep", "lcm_describe", "lcm_expand_query"]);
|
||||
expect(runParams?.prompt).toContain(
|
||||
"Configured memory tools: lcm_grep, lcm_describe, lcm_expand_query.",
|
||||
);
|
||||
expect(runParams?.prompt).not.toContain("Prefer memory_recall");
|
||||
expect(runParams?.prompt).not.toContain("If memory_recall is unavailable");
|
||||
});
|
||||
|
||||
it("uses memory_recall by default when the memory slot selects LanceDB", async () => {
|
||||
setMemorySlot("memory-lancedb");
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
prompt: "What did we decide about active memory?",
|
||||
messages: [],
|
||||
},
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
const runParams = runEmbeddedPiAgent.mock.calls.at(-1)?.[0];
|
||||
expect(runParams?.toolsAllow).toEqual(["memory_recall"]);
|
||||
expect(runParams?.prompt).toContain("Configured memory tools: memory_recall.");
|
||||
});
|
||||
|
||||
it("keeps explicit custom memory tools authoritative when the memory slot selects LanceDB", async () => {
|
||||
setMemorySlot("memory-lancedb");
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
toolsAllow: ["lcm_grep"],
|
||||
};
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
prompt: "What did we decide about active memory?",
|
||||
messages: [],
|
||||
},
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
const runParams = runEmbeddedPiAgent.mock.calls.at(-1)?.[0];
|
||||
expect(runParams?.toolsAllow).toEqual(["lcm_grep"]);
|
||||
expect(runParams?.prompt).toContain("Configured memory tools: lcm_grep.");
|
||||
});
|
||||
|
||||
it("drops wildcard group and core tools from custom memory tools", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
toolsAllow: [
|
||||
"*",
|
||||
"agents_list",
|
||||
"apply_patch",
|
||||
"canvas",
|
||||
"cron",
|
||||
"edit",
|
||||
"gateway",
|
||||
"heartbeat_respond",
|
||||
"heartbeat_response",
|
||||
"image",
|
||||
"image_generate",
|
||||
"music_generate",
|
||||
"nodes",
|
||||
"pdf",
|
||||
"process",
|
||||
"session_status",
|
||||
"sessions_history",
|
||||
"sessions_list",
|
||||
"sessions_send",
|
||||
"sessions_spawn",
|
||||
"sessions_yield",
|
||||
"tts",
|
||||
"video_generate",
|
||||
"group:plugins",
|
||||
"read",
|
||||
"exec",
|
||||
"message",
|
||||
"lcm_grep",
|
||||
"web_search",
|
||||
"lcm_describe",
|
||||
],
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
prompt: "What did we decide about active memory?",
|
||||
messages: [],
|
||||
},
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
const runParams = runEmbeddedPiAgent.mock.calls.at(-1)?.[0];
|
||||
expect(runParams?.toolsAllow).toEqual(["lcm_grep", "lcm_describe"]);
|
||||
expect(runParams?.prompt).toContain("Configured memory tools: lcm_grep, lcm_describe.");
|
||||
});
|
||||
|
||||
it("falls back to default memory tools when custom memory tools only contain reserved entries", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
toolsAllow: ["*", "group:plugins", "read", "exec", "message", "web_search"],
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
prompt: "What did we decide about active memory?",
|
||||
messages: [],
|
||||
},
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
const runParams = runEmbeddedPiAgent.mock.calls.at(-1)?.[0];
|
||||
expect(runParams?.toolsAllow).toEqual(["memory_search", "memory_get"]);
|
||||
expect(runParams?.prompt).toContain("Configured memory tools: memory_search, memory_get.");
|
||||
});
|
||||
|
||||
it("falls back to LanceDB compat tools when custom memory tools only contain reserved entries", async () => {
|
||||
setMemorySlot("memory-lancedb");
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
toolsAllow: ["*", "group:plugins", "read", "exec", "message", "web_search"],
|
||||
};
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
prompt: "What did we decide about active memory?",
|
||||
messages: [],
|
||||
},
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
const runParams = runEmbeddedPiAgent.mock.calls.at(-1)?.[0];
|
||||
expect(runParams?.toolsAllow).toEqual(["memory_recall"]);
|
||||
expect(runParams?.prompt).toContain("Configured memory tools: memory_recall.");
|
||||
});
|
||||
|
||||
it("defaults prompt style by query mode when no promptStyle is configured", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
@@ -1871,7 +2066,7 @@ describe("active-memory plugin", () => {
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no memory tools registered")).toBe(true);
|
||||
expect(hasDebugLine("no configured memory tools available")).toBe(true);
|
||||
expect(hasWarnLine("No callable tools remain")).toBe(false);
|
||||
const lines = getActiveMemoryLines(sessionKey);
|
||||
expect(lines).toEqual([expect.stringContaining("🧩 Active Memory: status=empty")]);
|
||||
@@ -1886,7 +2081,7 @@ describe("active-memory plugin", () => {
|
||||
};
|
||||
const error = makeMemoryToolAllowlistError(
|
||||
"no registered tools matched",
|
||||
"tools.allow: *, lobster; runtime toolsAllow: memory_recall, memory_search, memory_get",
|
||||
"tools.allow: *, lobster; runtime toolsAllow: memory_search, memory_get",
|
||||
);
|
||||
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(true);
|
||||
runEmbeddedPiAgent.mockRejectedValueOnce(error);
|
||||
@@ -1897,14 +2092,46 @@ describe("active-memory plugin", () => {
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no memory tools registered")).toBe(true);
|
||||
expect(hasDebugLine("no configured memory tools available")).toBe(true);
|
||||
expect(hasWarnLine("No callable tools remain")).toBe(false);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=empty"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps memory-tool allowlist errors visible when upstream policy can filter memory tools", async () => {
|
||||
it("skips missing custom memory tools using the resolved custom allowlist", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
toolsAllow: ["lcm_grep", "lcm_describe", "lcm_expand_query"],
|
||||
logging: true,
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
const sessionKey = "agent:main:missing-custom-memory-tools";
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: "s-missing-custom-memory-tools",
|
||||
updatedAt: 0,
|
||||
};
|
||||
const toolsAllow = ["lcm_grep", "lcm_describe", "lcm_expand_query"];
|
||||
const error = makeMemoryToolAllowlistError(
|
||||
"no registered tools matched",
|
||||
`runtime toolsAllow: ${toolsAllow.join(", ")}`,
|
||||
);
|
||||
expect(__testing.isMissingRegisteredMemoryToolsError(error, toolsAllow)).toBe(true);
|
||||
runEmbeddedPiAgent.mockRejectedValueOnce(error);
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what did we decide? missing custom memory tools", messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no configured memory tools available")).toBe(true);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=empty"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("skips memory-tool allowlist errors when upstream policy filters memory tools", async () => {
|
||||
const sessionKey = "agent:main:memory-tools-filtered-by-policy";
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: "s-memory-tools-filtered-by-policy",
|
||||
@@ -1912,9 +2139,9 @@ describe("active-memory plugin", () => {
|
||||
};
|
||||
const error = makeMemoryToolAllowlistError(
|
||||
"no registered tools matched",
|
||||
"tools.allow: read, exec; runtime toolsAllow: memory_recall, memory_search, memory_get",
|
||||
"tools.allow: read, exec; runtime toolsAllow: memory_search, memory_get",
|
||||
);
|
||||
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(false);
|
||||
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(true);
|
||||
runEmbeddedPiAgent.mockRejectedValueOnce(error);
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
@@ -1923,38 +2150,41 @@ describe("active-memory plugin", () => {
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no memory tools registered")).toBe(false);
|
||||
expect(hasWarnLine("No callable tools remain")).toBe(true);
|
||||
expect(hasDebugLine("no configured memory tools available")).toBe(true);
|
||||
expect(hasWarnLine("No callable tools remain")).toBe(false);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=unavailable"),
|
||||
expect.stringContaining("🧩 Active Memory: status=empty"),
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["disabled tools", "tools are disabled for this run"],
|
||||
["models without tool support", "the selected model does not support tools"],
|
||||
])("keeps allowlist errors for %s visible", async (_label, reason) => {
|
||||
const sessionKey = `agent:main:${reason.replace(/\W+/g, "-")}`;
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: `s-${reason.replace(/\W+/g, "-")}`,
|
||||
updatedAt: 0,
|
||||
};
|
||||
const error = makeMemoryToolAllowlistError(reason);
|
||||
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(false);
|
||||
runEmbeddedPiAgent.mockRejectedValueOnce(error);
|
||||
])(
|
||||
"skips allowlist errors for %s without surfacing to the main thread",
|
||||
async (_label, reason) => {
|
||||
const sessionKey = `agent:main:${reason.replace(/\W+/g, "-")}`;
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: `s-${reason.replace(/\W+/g, "-")}`,
|
||||
updatedAt: 0,
|
||||
};
|
||||
const error = makeMemoryToolAllowlistError(reason);
|
||||
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(false);
|
||||
runEmbeddedPiAgent.mockRejectedValueOnce(error);
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: `what wings should i order? ${reason}`, messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: `what wings should i order? ${reason}`, messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no memory tools registered")).toBe(false);
|
||||
expect(hasWarnLine(reason)).toBe(true);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=unavailable"),
|
||||
]);
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no configured memory tools available")).toBe(false);
|
||||
expect(hasWarnLine(reason)).toBe(true);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=empty"),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not skip missing memory-tool allowlist errors after abort", async () => {
|
||||
const sessionKey = "agent:main:missing-memory-tools-after-abort";
|
||||
@@ -1976,7 +2206,7 @@ describe("active-memory plugin", () => {
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no memory tools registered")).toBe(false);
|
||||
expect(hasDebugLine("no configured memory tools available")).toBe(false);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=timeout"),
|
||||
]);
|
||||
@@ -2258,7 +2488,7 @@ describe("active-memory plugin", () => {
|
||||
expect(getActiveMemoryLines(sessionKey).join("\n")).not.toContain("partial abort summary");
|
||||
});
|
||||
|
||||
it("keeps generic subagent errors unavailable without using partial transcript output", async () => {
|
||||
it("skips generic subagent errors without using partial transcript output", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
persistTranscripts: true,
|
||||
@@ -2287,7 +2517,7 @@ describe("active-memory plugin", () => {
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=unavailable"),
|
||||
expect.stringContaining("🧩 Active Memory: status=empty"),
|
||||
]);
|
||||
expect(getActiveMemoryLines(sessionKey).join("\n")).not.toContain(
|
||||
"must not be surfaced from generic errors",
|
||||
|
||||
@@ -42,7 +42,43 @@ const DEFAULT_QMD_SEARCH_MODE = "search" as const;
|
||||
const DEFAULT_TRANSCRIPT_DIR = "active-memory";
|
||||
const DEFAULT_CIRCUIT_BREAKER_MAX_TIMEOUTS = 3;
|
||||
const DEFAULT_CIRCUIT_BREAKER_COOLDOWN_MS = 60_000;
|
||||
const ACTIVE_MEMORY_TOOL_ALLOWLIST = ["memory_recall", "memory_search", "memory_get"] as const;
|
||||
const DEFAULT_ACTIVE_MEMORY_TOOLS_ALLOW = ["memory_search", "memory_get"] as const;
|
||||
const LANCEDB_ACTIVE_MEMORY_TOOLS_ALLOW = ["memory_recall"] as const;
|
||||
const MAX_ACTIVE_MEMORY_TOOLS_ALLOW = 32;
|
||||
const ACTIVE_MEMORY_RESERVED_TOOLS_ALLOW = new Set([
|
||||
"*",
|
||||
"agents_list",
|
||||
"apply_patch",
|
||||
"browser",
|
||||
"canvas",
|
||||
"cron",
|
||||
"edit",
|
||||
"exec",
|
||||
"gateway",
|
||||
"heartbeat_respond",
|
||||
"heartbeat_response",
|
||||
"image",
|
||||
"image_generate",
|
||||
"message",
|
||||
"music_generate",
|
||||
"nodes",
|
||||
"pdf",
|
||||
"process",
|
||||
"read",
|
||||
"session_status",
|
||||
"sessions_history",
|
||||
"sessions_list",
|
||||
"sessions_send",
|
||||
"sessions_spawn",
|
||||
"sessions_yield",
|
||||
"subagents",
|
||||
"tts",
|
||||
"update_plan",
|
||||
"video_generate",
|
||||
"web_fetch",
|
||||
"web_search",
|
||||
"write",
|
||||
]);
|
||||
const TOGGLE_STATE_FILE = "session-toggles.json";
|
||||
const DEFAULT_PARTIAL_TRANSCRIPT_MAX_CHARS = 32_000;
|
||||
const DEFAULT_TRANSCRIPT_READ_MAX_LINES = 2_000;
|
||||
@@ -101,6 +137,7 @@ type ActiveRecallPluginConfig = {
|
||||
| "recall-heavy"
|
||||
| "precision-heavy"
|
||||
| "preference-only";
|
||||
toolsAllow?: string[];
|
||||
promptOverride?: string;
|
||||
promptAppend?: string;
|
||||
timeoutMs?: number;
|
||||
@@ -141,6 +178,7 @@ type ResolvedActiveRecallPluginConfig = {
|
||||
| "recall-heavy"
|
||||
| "precision-heavy"
|
||||
| "preference-only";
|
||||
toolsAllow: string[];
|
||||
promptOverride?: string;
|
||||
promptAppend?: string;
|
||||
timeoutMs: number;
|
||||
@@ -399,6 +437,46 @@ function normalizeChatIdList(value: unknown): string[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
function normalizeConfiguredToolsAllow(value: unknown): string[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
const out: string[] = [];
|
||||
for (const entry of value) {
|
||||
if (typeof entry !== "string") {
|
||||
continue;
|
||||
}
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed || isReservedActiveMemoryToolsAllowEntry(trimmed) || seen.has(trimmed)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(trimmed);
|
||||
out.push(trimmed);
|
||||
if (out.length >= MAX_ACTIVE_MEMORY_TOOLS_ALLOW) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out.length > 0 ? out : undefined;
|
||||
}
|
||||
|
||||
function isReservedActiveMemoryToolsAllowEntry(value: string): boolean {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized.startsWith("group:") || ACTIVE_MEMORY_RESERVED_TOOLS_ALLOW.has(normalized);
|
||||
}
|
||||
|
||||
function resolveDefaultToolsAllow(cfg: OpenClawConfig | undefined): string[] {
|
||||
return cfg?.plugins?.slots?.memory === "memory-lancedb"
|
||||
? [...LANCEDB_ACTIVE_MEMORY_TOOLS_ALLOW]
|
||||
: [...DEFAULT_ACTIVE_MEMORY_TOOLS_ALLOW];
|
||||
}
|
||||
|
||||
function resolveToolsAllow(params: { pluginToolsAllow: unknown; cfg?: OpenClawConfig }): string[] {
|
||||
return (
|
||||
normalizeConfiguredToolsAllow(params.pluginToolsAllow) ?? resolveDefaultToolsAllow(params.cfg)
|
||||
);
|
||||
}
|
||||
|
||||
function normalizePromptConfigText(value: unknown): string | undefined {
|
||||
const text = typeof value === "string" ? value.trim() : "";
|
||||
return text ? text : undefined;
|
||||
@@ -445,6 +523,13 @@ function resolvePersistentTranscriptBaseDir(api: OpenClawPluginApi, agentId: str
|
||||
);
|
||||
}
|
||||
|
||||
function requireTransientWorkspaceDir(tempDir: string | undefined): string {
|
||||
if (!tempDir) {
|
||||
throw new Error("Active memory transient workspace was not initialized.");
|
||||
}
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
function resolveCanonicalSessionKeyFromSessionId(params: {
|
||||
api: OpenClawPluginApi;
|
||||
agentId: string;
|
||||
@@ -497,7 +582,14 @@ function normalizeOptionalString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function isMissingRegisteredMemoryToolsError(error: unknown): boolean {
|
||||
function formatRuntimeToolsAllowSource(toolsAllow: readonly string[]): string {
|
||||
return `runtime toolsAllow: ${toolsAllow.join(", ")}`;
|
||||
}
|
||||
|
||||
function isMissingRegisteredMemoryToolsError(
|
||||
error: unknown,
|
||||
toolsAllow: readonly string[] = DEFAULT_ACTIVE_MEMORY_TOOLS_ALLOW,
|
||||
): boolean {
|
||||
if (!(error instanceof Error)) {
|
||||
return false;
|
||||
}
|
||||
@@ -509,24 +601,12 @@ function isMissingRegisteredMemoryToolsError(error: unknown): boolean {
|
||||
return false;
|
||||
}
|
||||
const sources = message.slice(prefix.length, -suffix.length);
|
||||
const runtimeSource = `runtime toolsAllow: ${ACTIVE_MEMORY_TOOL_ALLOWLIST.join(", ")}`;
|
||||
const runtimeSource = formatRuntimeToolsAllowSource(toolsAllow);
|
||||
const sourceParts = sources
|
||||
.split(";")
|
||||
.map((source) => source.trim())
|
||||
.filter(Boolean);
|
||||
if (!sourceParts.includes(runtimeSource)) {
|
||||
return false;
|
||||
}
|
||||
return sourceParts.every((source) => {
|
||||
if (source === runtimeSource) {
|
||||
return true;
|
||||
}
|
||||
const entries = source
|
||||
.slice(source.indexOf(":") + 1)
|
||||
.split(",")
|
||||
.map((entry) => entry.trim());
|
||||
return entries.includes("*");
|
||||
});
|
||||
return sourceParts.includes(runtimeSource);
|
||||
}
|
||||
|
||||
function resolveRecallRunChannelContext(params: {
|
||||
@@ -791,7 +871,10 @@ function requiresAdminToMutateActiveMemoryGlobal(gatewayClientScopes?: readonly
|
||||
const ACTIVE_MEMORY_GLOBAL_MUTATION_ADMIN_REQUIRED_TEXT =
|
||||
"⚠️ /active-memory global enable/disable changes require operator.admin for gateway clients.";
|
||||
|
||||
function normalizePluginConfig(pluginConfig: unknown): ResolvedActiveRecallPluginConfig {
|
||||
function normalizePluginConfig(
|
||||
pluginConfig: unknown,
|
||||
cfg?: OpenClawConfig,
|
||||
): ResolvedActiveRecallPluginConfig {
|
||||
const raw = (
|
||||
pluginConfig && typeof pluginConfig === "object" ? pluginConfig : {}
|
||||
) as ActiveRecallPluginConfig;
|
||||
@@ -819,6 +902,7 @@ function normalizePluginConfig(pluginConfig: unknown): ResolvedActiveRecallPlugi
|
||||
deniedChatIds: normalizeChatIdList(raw.deniedChatIds),
|
||||
thinking: resolveThinkingLevel(raw.thinking),
|
||||
promptStyle: resolvePromptStyle(raw.promptStyle, raw.queryMode),
|
||||
toolsAllow: resolveToolsAllow({ pluginToolsAllow: raw.toolsAllow, cfg }),
|
||||
promptOverride: normalizePromptConfigText(raw.promptOverride),
|
||||
promptAppend: normalizePromptConfigText(raw.promptAppend),
|
||||
timeoutMs: clampInt(
|
||||
@@ -990,11 +1074,11 @@ function buildRecallPrompt(params: {
|
||||
"Your job is to search memory and return only the most relevant memory context for that model.",
|
||||
"You receive a bounded search query plus conversation context, including the user's latest message.",
|
||||
"Use only the available memory tools.",
|
||||
"Use the bounded search query as the memory_search or memory_recall query.",
|
||||
"Use the bounded search query with the configured memory tools.",
|
||||
`Configured memory tools: ${params.config.toolsAllow.join(", ")}.`,
|
||||
"Do not use channel metadata, provider metadata, debug output, or the full conversation context as the memory tool query.",
|
||||
"Prefer memory_recall when available.",
|
||||
"If memory_recall is unavailable, use memory_search and memory_get.",
|
||||
"When searching for preference or habit recall, use a permissive recall limit or memory_search threshold before deciding that no useful memory exists.",
|
||||
"If the available memory tools find nothing useful, reply with NONE.",
|
||||
"When searching for preference or habit recall, use permissive search limits or thresholds before deciding that no useful memory exists.",
|
||||
"Do not answer the user directly.",
|
||||
`Prompt style: ${params.config.promptStyle}.`,
|
||||
...buildPromptStyleLines(params.config.promptStyle),
|
||||
@@ -2398,9 +2482,10 @@ async function runRecallSubagent(params: {
|
||||
params.config.transcriptDir,
|
||||
)
|
||||
: undefined;
|
||||
const sessionFile = params.config.persistTranscripts
|
||||
? path.join(persistedDir!, `${subagentSessionId}.jsonl`)
|
||||
: path.join(tempDir!, "session.jsonl");
|
||||
const sessionFile =
|
||||
persistedDir !== undefined
|
||||
? path.join(persistedDir, `${subagentSessionId}.jsonl`)
|
||||
: path.join(requireTransientWorkspaceDir(tempDir), "session.jsonl");
|
||||
params.onSessionFile?.(sessionFile);
|
||||
if (persistedDir) {
|
||||
await fs.mkdir(persistedDir, { recursive: true, mode: 0o700 });
|
||||
@@ -2439,7 +2524,7 @@ async function runRecallSubagent(params: {
|
||||
timeoutMs: embeddedTimeoutMs,
|
||||
runId: subagentSessionId,
|
||||
trigger: "manual",
|
||||
toolsAllow: [...ACTIVE_MEMORY_TOOL_ALLOWLIST],
|
||||
toolsAllow: [...params.config.toolsAllow],
|
||||
disableMessageTool: true,
|
||||
allowGatewaySubagentBinding: true,
|
||||
bootstrapContextMode: "lightweight",
|
||||
@@ -2482,9 +2567,19 @@ async function runRecallSubagent(params: {
|
||||
const searchDebug = partialReply ? await readActiveMemorySearchDebug(sessionFile) : undefined;
|
||||
attachPartialTimeoutData(error, partialReply, searchDebug);
|
||||
}
|
||||
if (!params.abortSignal?.aborted && isMissingRegisteredMemoryToolsError(error)) {
|
||||
if (
|
||||
!params.abortSignal?.aborted &&
|
||||
isMissingRegisteredMemoryToolsError(error, params.config.toolsAllow)
|
||||
) {
|
||||
params.api.logger.debug?.(
|
||||
`active-memory: no memory tools registered (memory-core or memory-lancedb required); skipping sub-agent`,
|
||||
`active-memory: no configured memory tools available; skipping sub-agent`,
|
||||
);
|
||||
return { rawReply: "NONE" };
|
||||
}
|
||||
if (!params.abortSignal?.aborted) {
|
||||
const message = toSingleLineLogValue(error instanceof Error ? error.message : String(error));
|
||||
params.api.logger.warn?.(
|
||||
`active-memory: memory sub-agent failed, skipping recall: ${message}`,
|
||||
);
|
||||
return { rawReply: "NONE" };
|
||||
}
|
||||
@@ -2751,10 +2846,10 @@ async function maybeResolveActiveRecall(params: {
|
||||
}
|
||||
const message = toSingleLineLogValue(error instanceof Error ? error.message : String(error));
|
||||
if (params.config.logging) {
|
||||
params.api.logger.warn?.(`${logPrefix} failed error=${message}`);
|
||||
params.api.logger.warn?.(`${logPrefix} failed error=${message}; skipping recall`);
|
||||
}
|
||||
const result: ActiveRecallResult = {
|
||||
status: "unavailable",
|
||||
status: "empty",
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
summary: null,
|
||||
};
|
||||
@@ -2777,7 +2872,17 @@ export default definePluginEntry({
|
||||
name: "Active Memory",
|
||||
description: "Proactively surfaces relevant memory before eligible conversational replies.",
|
||||
register(api: OpenClawPluginApi) {
|
||||
let config = normalizePluginConfig(api.pluginConfig);
|
||||
const readCurrentConfig = (): OpenClawConfig | undefined => {
|
||||
try {
|
||||
return (
|
||||
(api.runtime.config?.current?.() as OpenClawConfig | undefined) ??
|
||||
(api.config as OpenClawConfig | undefined)
|
||||
);
|
||||
} catch {
|
||||
return api.config as OpenClawConfig | undefined;
|
||||
}
|
||||
};
|
||||
let config = normalizePluginConfig(api.pluginConfig, readCurrentConfig());
|
||||
const warnDeprecatedModelFallbackPolicy = (pluginConfig: unknown) => {
|
||||
if (hasDeprecatedModelFallbackPolicy(pluginConfig)) {
|
||||
// Wording matters here: the previous text ("set config.modelFallback
|
||||
@@ -2805,7 +2910,7 @@ export default definePluginEntry({
|
||||
"active-memory",
|
||||
api.pluginConfig as Record<string, unknown>,
|
||||
);
|
||||
config = normalizePluginConfig(livePluginConfig ?? { enabled: false });
|
||||
config = normalizePluginConfig(livePluginConfig ?? { enabled: false }, readCurrentConfig());
|
||||
if (livePluginConfig) {
|
||||
warnDeprecatedModelFallbackPolicy(livePluginConfig);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,14 @@
|
||||
"preference-only"
|
||||
]
|
||||
},
|
||||
"toolsAllow": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^(?!\\*$)(?![Gg][Rr][Oo][Uu][Pp]:).+"
|
||||
},
|
||||
"maxItems": 32
|
||||
},
|
||||
"promptOverride": { "type": "string" },
|
||||
"promptAppend": { "type": "string" },
|
||||
"maxSummaryChars": { "type": "integer", "minimum": 40, "maximum": 1000 },
|
||||
@@ -129,6 +137,10 @@
|
||||
"label": "Prompt Style",
|
||||
"help": "Choose how eager or strict the blocking memory sub-agent should be when deciding whether to return memory."
|
||||
},
|
||||
"toolsAllow": {
|
||||
"label": "Allowed Memory Tools",
|
||||
"help": "Advanced: tool names the blocking memory sub-agent may use. Defaults to memory_search and memory_get, or memory_recall when plugins.slots.memory selects memory-lancedb; configure this for other non-core memory providers. Wildcards, group entries, and core agent tools are ignored."
|
||||
},
|
||||
"thinking": {
|
||||
"label": "Thinking Override",
|
||||
"help": "Advanced: optional thinking level for the blocking memory sub-agent. Defaults to off for speed."
|
||||
|
||||
@@ -151,7 +151,7 @@ describe("discord channel message adapter", () => {
|
||||
|
||||
await verifyChannelMessageAdapterCapabilityProofs({
|
||||
adapterName: "discordMessageAdapter",
|
||||
adapter: adapter,
|
||||
adapter,
|
||||
proofs: {
|
||||
text: proveText,
|
||||
media: proveMedia,
|
||||
|
||||
@@ -44,8 +44,9 @@ describe("matrix channel message adapter", () => {
|
||||
|
||||
it("backs declared durable-final capabilities with runtime outbound proofs", async () => {
|
||||
const adapter = matrixPlugin.message;
|
||||
if (adapter?.send?.text === undefined || adapter.send.media === undefined) {
|
||||
throw new Error("expected matrix text and media message adapter");
|
||||
expect(adapter).toBeDefined();
|
||||
if (!adapter?.send?.text || !adapter.send.media) {
|
||||
throw new Error("Expected Matrix message adapter send capabilities.");
|
||||
}
|
||||
const sendText = adapter.send.text;
|
||||
const sendMedia = adapter.send.media;
|
||||
@@ -93,7 +94,7 @@ describe("matrix channel message adapter", () => {
|
||||
|
||||
const proveReplyThread = async () => {
|
||||
mocks.sendMessageMatrix.mockClear();
|
||||
const result = await adapter.send!.text!({
|
||||
const result = await adapter.send.text({
|
||||
cfg,
|
||||
to: "room:!room:example",
|
||||
text: "threaded",
|
||||
@@ -116,14 +117,14 @@ describe("matrix channel message adapter", () => {
|
||||
|
||||
await verifyChannelMessageAdapterCapabilityProofs({
|
||||
adapterName: "matrixMessageAdapter",
|
||||
adapter: adapter,
|
||||
adapter,
|
||||
proofs: {
|
||||
text: proveText,
|
||||
media: proveMedia,
|
||||
replyTo: proveReplyThread,
|
||||
thread: proveReplyThread,
|
||||
messageSendingHooks: () => {
|
||||
expect(adapter.send!.text).toBeTypeOf("function");
|
||||
expect(adapter.send?.text).toBeTypeOf("function");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -132,7 +132,7 @@ describe("mattermost channel message adapter", () => {
|
||||
|
||||
await verifyChannelMessageAdapterCapabilityProofs({
|
||||
adapterName: "mattermostMessageAdapter",
|
||||
adapter: adapter,
|
||||
adapter,
|
||||
proofs: {
|
||||
text: proveText,
|
||||
media: proveMedia,
|
||||
|
||||
@@ -114,7 +114,7 @@ describe("tlon channel message adapter", () => {
|
||||
|
||||
await verifyChannelMessageAdapterCapabilityProofs({
|
||||
adapterName: "tlonMessageAdapter",
|
||||
adapter: adapter,
|
||||
adapter,
|
||||
proofs: {
|
||||
text: proveText,
|
||||
media: proveMedia,
|
||||
|
||||
@@ -754,6 +754,9 @@ describe("web auto-reply connection", () => {
|
||||
timestamp: 1735689600000,
|
||||
spies,
|
||||
});
|
||||
if (!capturedOnMessage) {
|
||||
throw new Error("Expected WhatsApp web runtime to register onMessage.");
|
||||
}
|
||||
await sendWebDirectInboundMessage({
|
||||
onMessage: capturedOnMessage,
|
||||
body: "second",
|
||||
|
||||
@@ -509,8 +509,9 @@ describe("CLI attempt execution", () => {
|
||||
embeddedAssistantGapFill: true,
|
||||
});
|
||||
const sessionFile = updatedFirst?.sessionFile;
|
||||
expect(sessionFile).toBeTruthy();
|
||||
if (!sessionFile) {
|
||||
throw new Error("expected embedded gap-fill persistence to create a session file");
|
||||
throw new Error("Expected CLI transcript session file.");
|
||||
}
|
||||
|
||||
await appendSessionTranscriptMessage({
|
||||
|
||||
@@ -113,8 +113,9 @@ const createCompactionHandler = () => {
|
||||
}),
|
||||
} as unknown as ExtensionAPI;
|
||||
compactionSafeguardExtension(mockApi);
|
||||
expect(compactionHandler).toBeDefined();
|
||||
if (!compactionHandler) {
|
||||
throw new Error("expected compaction safeguard handler");
|
||||
throw new Error("Expected compaction safeguard to register a handler.");
|
||||
}
|
||||
return compactionHandler;
|
||||
};
|
||||
|
||||
@@ -66,8 +66,13 @@ describe("createHostWorkspaceEditTool host access mapping", () => {
|
||||
// library replaces any access error with a misleading "File not found".
|
||||
// By resolving silently the subsequent readFile call surfaces the real
|
||||
// "Path escapes workspace root" / "outside-workspace" error instead.
|
||||
const operations = mocks.operations;
|
||||
expect(operations).toBeDefined();
|
||||
if (!operations) {
|
||||
throw new Error("Expected workspace edit operations to be registered.");
|
||||
}
|
||||
await expect(
|
||||
mocks.operations.access(path.join(workspaceDir, "escape", "secret.txt")),
|
||||
operations.access(path.join(workspaceDir, "escape", "secret.txt")),
|
||||
).resolves.toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -210,10 +210,10 @@ describe("sessions_spawn tool", () => {
|
||||
},
|
||||
});
|
||||
const schema = tool.parameters as {
|
||||
properties?: {
|
||||
thread?: { description?: string; enum?: string[]; type?: string };
|
||||
mode?: { description?: string; enum?: string[]; type?: string };
|
||||
};
|
||||
properties?: Record<
|
||||
string,
|
||||
{ description?: string; enum?: string[]; type?: string } | undefined
|
||||
>;
|
||||
};
|
||||
|
||||
expect(schema.properties?.thread).toBeUndefined();
|
||||
@@ -236,10 +236,10 @@ describe("sessions_spawn tool", () => {
|
||||
},
|
||||
});
|
||||
const schema = tool.parameters as {
|
||||
properties?: {
|
||||
thread?: { description?: string; enum?: string[]; type?: string };
|
||||
mode?: { description?: string; enum?: string[]; type?: string };
|
||||
};
|
||||
properties?: Record<
|
||||
string,
|
||||
{ description?: string; enum?: string[]; type?: string } | undefined
|
||||
>;
|
||||
};
|
||||
|
||||
const thread = requireSchemaProperty(schema.properties, "thread");
|
||||
|
||||
@@ -236,8 +236,10 @@ describe("backup commands", () => {
|
||||
|
||||
const stateAsset = result.assets.find((asset) => asset.kind === "state");
|
||||
const workspaceAsset = result.assets.find((asset) => asset.kind === "workspace");
|
||||
expect(stateAsset).toBeDefined();
|
||||
expect(workspaceAsset).toBeDefined();
|
||||
if (!stateAsset || !workspaceAsset) {
|
||||
throw new Error("expected state and workspace backup assets");
|
||||
throw new Error("Expected backup assets to include state and workspace entries.");
|
||||
}
|
||||
expect(capturedEntryPaths).toHaveLength(result.assets.length + 1);
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ function findSseEvent(events: SseEvent[], eventName: string): SseEvent {
|
||||
}
|
||||
|
||||
function parseSseData(event: SseEvent): unknown {
|
||||
return JSON.parse(event.data);
|
||||
return JSON.parse(event.data) as unknown;
|
||||
}
|
||||
|
||||
function requireSessionKey(value: string | undefined, label: string): string {
|
||||
|
||||
Reference in New Issue
Block a user