Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions src/daemon/memory/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { describe, test, expect } from "bun:test";
import { SqliteEpisodeStore } from "./store";
import { MemoryEngine } from "./engine";
import { buildMemoryMcpServer } from "./mcp";
import { MEMORY_MCP_SERVER_NAME } from "./mcp-http";
import { MEMORY_TOOL_NAMES } from "./tools";
import { isSafeTool } from "../providers/tool-safety";
import type { Embedder } from "./embedder";

class FakeEmbedder implements Embedder {
Expand All @@ -15,15 +18,33 @@ class FakeEmbedder implements Embedder {
}

describe("buildMemoryMcpServer (thin adapter over the registry)", () => {
test("builds an in-process SDK MCP server named codeoid-memory from the shared defs", async () => {
test("builds an in-process SDK MCP server from the shared defs", async () => {
const engine = new MemoryEngine({ store: new SqliteEpisodeStore(":memory:"), embedder: new FakeEmbedder() });
await engine.init();
const server = buildMemoryMcpServer(engine, { workspaceId: "ws", sessionId: "s1" });
// The adapter enumerates memoryToolDefs() into SDK tool()s under one server;
// asserting the config shape exercises the whole adapter path without
// depending on SDK internals.
expect(server.type).toBe("sdk");
expect(server.name).toBe("codeoid-memory");
expect(server.instance).toBeDefined();
});

test("names the server with the canonical constant, not a literal", async () => {
// Regression: this asserted the literal "codeoid-memory" while every mount
// key and `isSafeTool` prefix derives from MEMORY_MCP_SERVER_NAME
// ("codeoid_memory"). Backends disagree about which of the two they
// namespace tools by — Claude uses the map key, qwen-code the instance
// name — so the divergence silently produced `mcp__codeoid-memory__*` on
// qwen, matching neither the allowedTools grant nor the safe-tool prefixes,
// and the read-only recall tools prompted on every call. Asserting against
// the constant (not a literal) is what keeps them from drifting apart again.
const engine = new MemoryEngine({ store: new SqliteEpisodeStore(":memory:"), embedder: new FakeEmbedder() });
await engine.init();
const server = buildMemoryMcpServer(engine, { workspaceId: "ws", sessionId: "s1" });
expect(server.name).toBe(MEMORY_MCP_SERVER_NAME);
// And the tools that name produces must be auto-approvable.
for (const t of MEMORY_TOOL_NAMES) {
expect(isSafeTool(`mcp__${server.name}__${t}`)).toBe(true);
}
});
});
11 changes: 10 additions & 1 deletion src/daemon/memory/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
type McpSdkServerConfigWithInstance,
} from "@anthropic-ai/claude-agent-sdk";
import type { MemoryEngine } from "./engine.js";
import { MEMORY_MCP_SERVER_NAME } from "./mcp-http.js";
import { memoryToolDefs, type MemoryToolContext } from "./tools.js";

export interface MemoryMcpBinding {
Expand All @@ -38,7 +39,15 @@ export function buildMemoryMcpServer(
})),
);
return createSdkMcpServer({
name: "codeoid-memory",
// MUST equal MEMORY_MCP_SERVER_NAME. Backends disagree about which name
// they namespace mounted tools by: the Claude SDK uses the mcpServers map
// KEY (always this constant), while qwen-code uses the server INSTANCE's
// own name. While these differed (`codeoid_memory` vs `codeoid-memory`)
// the qwen backend exposed `mcp__codeoid-memory__*`, which matched neither
// the provider's `allowedTools` grant nor `isSafeTool`'s prefixes — so the
// read-only recall tools prompted for approval on every single call.
// One name removes the whole class of mismatch.
name: MEMORY_MCP_SERVER_NAME,
version: "0.1.0",
tools,
});
Expand Down
6 changes: 5 additions & 1 deletion src/daemon/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1787,7 +1787,11 @@ export class Session {
if (wasWorking && this.#activeRun?.pushMidTurn) {
const hint =
effectivePriority === "now"
? "⎆ Queued mid-turn — Claude is re-integrating with new context"
? // Provider-agnostic: this path is reached by any keep-warm backend
// that supports mid-turn injection, so naming Claude here surfaced
// "Claude is re-integrating" in the middle of a Qwen conversation —
// which reads like the wrong backend answered.
"⎆ Queued mid-turn — the agent is re-integrating with new context"
: effectivePriority === "next"
? "⎆ Queued — will be picked up after current turn completes"
: "⎆ Queued";
Expand Down
Loading