Only memory comes from the server's /mcp; notes, knowledge and skills stay local

Harness spec 3.0.0 (acp.md *Memory*): memory records are the server's for the
device as for a chat; everything else in the library is the device's own.

- `library: lembas` (the default when the login's token has the `memory`
  scope, or the older `library`) adds the server's /mcp with its `memory`
  tool alone, in place of this machine's memory tool; notes_*, knowledge_*
  and skill_* are always this machine's. No prompts, resources or
  instructions are taken from it.
- The memory in the prompt is the server's: the resource lembas://memory,
  read as a session starts (McpManager.readResource) and kept for it — put in
  while nothing has been sent, so the prompt never changes under a session.
- A session the client opened (session/new, or opened from the store) sends
  its id as `_meta.lembas.scope` on every memory call and on the resource
  read (App.setScope); a session of the device's own sends none.
- An instance's `library` context (its knowledge bases) is no longer read:
  the knowledge block names this machine's bases only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-10-27 21:07:37 +00:00
1 parent 6d5a25cdad
commit a73adc01f0
10 files changed
+134 -93

No files matched your search

+4 -3
View File
@@ -332,7 +332,7 @@ test("show (spec 3.0.0): offered only to a client that shows files; the file goe
expect(fake.requests[2].tools.map((t: any) => t.function.name)).not.toContain("show")
})
test("the context reaches the prompt: the instance's instructions, its personalization, the library's scope, prototypes", async () => {
test("the context reaches the prompt: the instance's instructions, its personalization, prototypes; not its knowledge bases", async () => {
fake = fakeProvider([{ chunks: [delta({ content: "one" })] }, { chunks: [delta({ content: "two" })] }, { chunks: [delta({ content: "three" })] }])
config(fake.url, "personality: concise\ninstructions: local words\n")
const root = project()
@@ -356,8 +356,9 @@ test("the context reaches the prompt: the instance's instructions, its personali
expect(system()).toContain("prototype-chat")
expect(system()).toContain("The page may load nothing from the network.")
expect(system()).toContain("It is saved as a file you can open, and the LLeMbas web UI renders it live.")
// The library's scope is named only where its tools are offered (none here: no local base, no
// instance library) — nothing unfilled reaches the model either way.
// The instance's knowledge bases are not the device's (spec 3.0.0): an older client's `library`
// names nothing to the model — nothing unfilled reaches it either.
expect(system()).not.toContain("Manuals")
expect(system()).not.toContain("{{")
// A later prompt replaces the context: personalization back to the device's, nothing from the instance.
await c.request("session/prompt", { sessionId: s.sessionId, prompt: [{ type: "text", text: "again" }], _meta: { lembas: { turnId: "T2", system_prompt: "", personalization: null, prototypes: { offered: false, sources: "" } } } })
+1 -1
View File
@@ -220,7 +220,7 @@ test("every {{variable}} in a description is declared, and none reaches a model"
else expect(system).not.toContain("The current plan")
}
}
app.setContext({ instructions: "Be brief.", prototypes: { offered: true, sources: "" }, library: { bases: [{ id: "k", name: "K" }] } }, "example")
app.setContext({ instructions: "Be brief.", prototypes: { offered: true, sources: "" } }, "example")
const withContext = app.engine.o.system("manual", app.engine.model, plan)
expect(withContext).not.toContain("{{")
expect(withContext).toContain("## Instructions from example\n\nBe brief.")
+50 -43
View File
@@ -1,5 +1,6 @@
// library: lembas: the account's library through the instance's /mcp, in place of this
// machine's — the tools under their own names, as a LLeMbas chat has them, with the device token.
// library: lembas (harness spec 3.0.0, acp.md *Memory*): the account's memory through the server's
// /mcp — its `memory` tool in place of this machine's, and its entries for the prompt from the
// resource lembas://memory — with the device token. Notes, knowledge bases and skills stay local.
import { afterEach, expect, test } from "bun:test"
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
@@ -13,8 +14,8 @@ afterEach(() => {
for (const s of stops.splice(0)) s()
})
function fakeLembasMcp() {
const calls: { auth: string; name?: string; args?: unknown; meta?: unknown }[] = []
function fakeLembasMcp(memory = "MEMORY (the account's)\nPrefers tabs.") {
const calls: { auth: string; name?: string; args?: unknown; meta?: unknown; uri?: string }[] = []
const server = Bun.serve({
port: 0,
async fetch(req) {
@@ -26,11 +27,16 @@ function fakeLembasMcp() {
if (auth !== "Bearer lmb_lib") return new Response("no", { status: 401 })
if (msg.id === undefined) return new Response(null, { status: 202 })
const reply = (result: unknown) => Response.json({ jsonrpc: "2.0", id: msg.id, result })
if (msg.method === "initialize") return reply({ protocolVersion: msg.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "lembas", version: "1.18.0" } })
if (msg.method === "initialize") return reply({ protocolVersion: msg.params.protocolVersion, capabilities: { tools: {}, resources: {} }, serverInfo: { name: "lembas", version: "2.0.0" } })
if (msg.method === "resources/read") {
calls.push({ auth, uri: msg.params.uri, ...(msg.params._meta?.lembas ? { meta: msg.params._meta.lembas } : {}) })
return reply({ contents: [{ uri: msg.params.uri, mimeType: "text/plain", text: msg.params.uri === "lembas://memory" ? memory : "" }] })
}
if (msg.method === "resources/list") return reply({ resources: [{ uri: "lembas://memory", name: "memory" }] })
if (msg.method === "tools/list")
return reply({
tools: [
{ name: "memory_add", description: "Remember one fact.", inputSchema: { type: "object", properties: { content: { type: "string" } }, required: ["content"] }, annotations: { readOnlyHint: false } },
{ name: "memory", description: "The account's memory.", inputSchema: { type: "object", properties: { action: { type: "string" }, content: { type: "string" } }, required: ["action"] }, annotations: { readOnlyHint: false } },
{ name: "notes_search", description: "Search notes.", inputSchema: { type: "object", properties: { query: { type: "string" } } }, annotations: { readOnlyHint: true } },
],
})
@@ -46,63 +52,64 @@ function fakeLembasMcp() {
return { base: `http://127.0.0.1:${server.port}`, calls }
}
test("the instance's library replaces this machine's, under the tools' own names", async () => {
const lembas = fakeLembasMcp()
function login(base: string) {
mkdirSync(join(paths.config, "lembas"), { recursive: true })
writeFileSync(join(paths.config, "lembas.json"), JSON.stringify({ instances: { ai: { base_url: lembas.base, connection: "ai", logged_in_at: "" } } }))
writeFileSync(join(paths.config, "lembas.json"), JSON.stringify({ instances: { ai: { base_url: base, connection: "ai", logged_in_at: "" } } }))
writeFileSync(join(paths.config, "lembas", "ai.key"), "lmb_lib\n", { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n f:\n type: webui\n url: http://127.0.0.1:1/v1\n models: { f/m: {} }\n", { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\nlibrary: lembas\n")
}
test("memory is the server's; notes, knowledge and skills stay this machine's", async () => {
const lembas = fakeLembasMcp()
login(lembas.base)
const app = createApp({ cwd: mkdtempSync(join(tmpdir(), "ph-lib-")), store: false, asker: { ask: async (): Promise<AskReply> => ({ kind: "once" }) } })
await app.mcpReady
await Bun.sleep(50)
const names = app.engine.o.tools.map((t) => t.name)
expect(names).toContain("memory_add")
expect(names).toContain("notes_search")
// This machine's library tools are gone; notes_search is the instance's, once.
for (const local of ["memory", "note_view", "note_manage", "skills_list", "skill_manage", "knowledge_search"]) expect(names).not.toContain(local)
// The server's memory, once; its notes_search is not taken (only memory comes from /mcp).
expect(names.filter((n) => n === "memory").length).toBe(1)
expect(names.filter((n) => n === "notes_search").length).toBe(1)
const add = app.engine.o.tools.find((t) => t.name === "memory_add")!
expect(add.permission({ content: "x" } as never, { ...app.engine.o.toolCtx, signal: new AbortController().signal }).class).toBe("interact")
const out = await add.run({ content: "Prefers tabs." } as never, { ...app.engine.o.toolCtx, signal: new AbortController().signal })
expect(out.output).toContain("ok: memory_add")
expect(lembas.calls).toEqual([{ auth: "Bearer lmb_lib", name: "memory_add", args: { content: "Prefers tabs." } }])
for (const local of ["notes_search", "note_view", "note_manage", "skills_list", "skill_view", "skill_manage"]) expect(names).toContain(local)
const local = (n: string) => app.engine.o.tools.find((t) => t.name === n)!
expect(local("notes_search").description).not.toContain("Search notes.")
const mem = app.engine.o.tools.find((t) => t.name === "memory")!
expect(mem.description).toBe("The account's memory.")
expect(mem.permission({ action: "add" } as never, { ...app.engine.o.toolCtx, signal: new AbortController().signal }).class).toBe("interact")
const out = await mem.run({ action: "add", content: "Prefers tabs." } as never, { ...app.engine.o.toolCtx, signal: new AbortController().signal })
expect(out.output).toContain("ok: memory")
expect(lembas.calls.filter((c) => c.name)).toEqual([{ auth: "Bearer lmb_lib", name: "memory", args: { action: "add", content: "Prefers tabs." } }])
// The entries for the prompt: read from the resource as the session started.
for (let i = 0; i < 50 && !lembas.calls.some((c) => c.uri); i++) await Bun.sleep(10)
expect(lembas.calls.find((c) => c.uri)).toEqual({ auth: "Bearer lmb_lib", uri: "lembas://memory" })
for (let i = 0; i < 50 && !app.engine.o.system("manual", app.engine.model).includes("Prefers tabs."); i++) await Bun.sleep(10)
expect(app.engine.o.system("manual", app.engine.model)).toContain("MEMORY (the account's)\nPrefers tabs.")
await app.close()
})
test("a session the client gave a library scope sends its id on every call to the instance's library; base passes through", async () => {
test("a session the client opened sends its id on every memory call and on the resource read", async () => {
const lembas = fakeLembasMcp()
mkdirSync(join(paths.config, "lembas"), { recursive: true })
writeFileSync(join(paths.config, "lembas.json"), JSON.stringify({ instances: { ai: { base_url: lembas.base, connection: "ai", logged_in_at: "" } } }))
writeFileSync(join(paths.config, "lembas", "ai.key"), "lmb_lib\n", { mode: 0o600 })
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n f:\n type: webui\n url: http://127.0.0.1:1/v1\n models: { f/m: {} }\n", { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\nlibrary: lembas\n")
login(lembas.base)
const app = createApp({ cwd: mkdtempSync(join(tmpdir(), "ph-lib-")), asker: { ask: async (): Promise<AskReply> => ({ kind: "once" }) } })
stops.push(() => void app.close())
await app.mcpReady
await Bun.sleep(50)
const search = app.engine.o.tools.find((t) => t.name === "notes_search")!
const mem = app.engine.o.tools.find((t) => t.name === "memory")!
const ctx = () => ({ ...app.engine.o.toolCtx, signal: new AbortController().signal })
// Nothing said about the session's library: no scope, as before.
await search.run({ query: "a" } as never, ctx())
expect(lembas.calls[0]).toEqual({ auth: "Bearer lmb_lib", name: "notes_search", args: { query: "a" } })
// The client named the bases: the session's id rides on the call as _meta.lembas.scope, and
// the instance answers within that chat's scope (beside the SDK's own progress token); `base`
// is the model's word, passed untouched.
app.setContext({ library: { bases: [{ id: "kb1", name: "Manuals" }] } }, "ai")
await search.run({ query: "b", base: "Manuals" } as never, ctx())
expect(lembas.calls[1]).toEqual({ auth: "Bearer lmb_lib", name: "notes_search", args: { query: "b", base: "Manuals" }, meta: { scope: app.engine.sessionId } })
// The prompt names what the session may search.
expect(app.engine.o.system("manual", app.engine.model)).toContain("Knowledge searches here cover only: Manuals. Finding nothing there means nothing is there, not that the library is empty.")
// Said again with no bases: the account's whole library — nothing named to the model, while the
// scope still rides (the instance answers in the chat's data group).
app.setContext({ library: { bases: [] } }, "ai")
await search.run({ query: "c" } as never, ctx())
expect(lembas.calls[2]!.meta).toEqual({ scope: app.engine.sessionId })
// A session of this device's own: no scope.
await mem.run({ action: "view" } as never, ctx())
expect(lembas.calls.filter((c) => c.name)[0]).toEqual({ auth: "Bearer lmb_lib", name: "memory", args: { action: "view" } })
// The client's chat (acp/agent.ts sets it): the id rides on the call, and the entries are read again in it.
app.setScope(app.engine.sessionId)
await mem.run({ action: "view" } as never, ctx())
expect(lembas.calls.filter((c) => c.name)[1]).toEqual({ auth: "Bearer lmb_lib", name: "memory", args: { action: "view" }, meta: { scope: app.engine.sessionId } })
for (let i = 0; i < 50 && !lembas.calls.some((c) => c.uri && c.meta); i++) await Bun.sleep(10)
expect(lembas.calls.find((c) => c.uri && c.meta)).toEqual({ auth: "Bearer lmb_lib", uri: "lembas://memory", meta: { scope: app.engine.sessionId } })
// Knowledge is this machine's: nothing the server holds is named to the model.
expect(app.engine.o.system("manual", app.engine.model)).not.toContain("Knowledge searches here cover only")
expect(app.engine.o.system("manual", app.engine.model)).toContain("search it with knowledge_search")
})
test("without a login it says so and keeps this machine's library", () => {
test("without a login it says so and keeps this machine's memory", () => {
writeFileSync(join(paths.config, "lembas.json"), JSON.stringify({ instances: {} }))
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n f:\n type: webui\n url: http://127.0.0.1:1/v1\n models: { f/m: {} }\n", { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\nlibrary: lembas\n")