Keep a bot's workspace memory from reading as the workspace's record

A workspace bot asked to "update the workspace record" wrote to its own
memory there, called it the workspace's record, and then, when the user
said nothing had changed, denied making any write at all: it checked
nothing and confessed to work it had done.

- Say in the prompt that the workspace's context is the user's, and that
  memory in a workspace is the bot's own notes, readable in its settings
- Say the same in update_memory and update_instructions results, with
  where the user reads it and that the workspace's own fields didn't
  change
- Have ActionCheckNote look at what actually happened before agreeing
  with a user that nothing was done, and never deny work that happened
This commit is contained in:
2026-10-20 14:59:56 +01:00
parent b34a8fdaf1
commit 9778d0b9ff
5 changed files with 21 additions and 11 deletions
+1
View File
@@ -47,6 +47,7 @@ Every action counts here: saving memory or instructions, creating or changing a
- Before you create anything, look for one that already exists: find_bot for bots, find_workspace for workspaces, list_schedules for schedules, your memory and instructions in this prompt, and for anything outside (files, records, emails, events) list or search it with the tools you have. If it's there, update it instead of making a second one. Create something new only once you've checked there's none.
- After you act, verify it took effect before you tell the user it's done: read it back with the same lookups, check the tool's result, or on a computer re-read the file, page or sent folder. Only say "done" for what you verified.
- If you couldn't verify it, say so plainly instead of claiming it's done. If it didn't take, try again once, then report what failed.
- Never deny something you did either. When the user says a change isn't where they looked, check what actually happened before you answer: the tool results in this conversation, and what you saved. Then say what you did and where it went, and if it went somewhere other than where they looked, point them to it. Never take the blame for work that happened, and never write a false account of it into your memory or instructions; if you already wrote one, correct it with update_memory.
- Starting or handing over a task isn't finishing it: say it's under way, not done.`;
/**
@@ -35,7 +35,7 @@ export default function updateInstructionsTool({
return {
name: "update_instructions",
description: workspace_id
? `Replace your instructions with this full text (markdown). scope "workspace" (the default) is your instructions for this workspace, under "# Your instructions in ..."; "core" is your core instructions, the start of your prompt in every workspace and your own chat. Both are shared: the workspace's with everyone working in it, your core ones with every user you work for. Change them only for how you should work for all of them, such as a standing rule for your job; a preference of this one user goes in your memory of them (update_memory, scope "user"). Keep everything that still applies.`
? `Replace your instructions with this full text (markdown). scope "workspace" (the default) is your instructions for this workspace, under "# Your instructions in ...": yours there, not the workspace's description or context; "core" is your core instructions, the start of your prompt in every workspace and your own chat. Both are shared: the workspace's with everyone working in it, your core ones with every user you work for. Change them only for how you should work for all of them, such as a standing rule for your job; a preference of this one user goes in your memory of them (update_memory, scope "user"). Keep everything that still applies.`
: `Replace your core instructions with this full text (markdown): the start of your prompt, before any workspace or memory, shared with every user you work for. Change them only for how you should work for every user you work for, such as a standing rule for your job; a preference of this one user goes in your memory of them (update_memory, scope "user"). Keep everything that still applies.`,
schema: workspace_id
? z.object({ instructions, scope: z.enum(["workspace", "core"]).default("workspace") })
@@ -54,7 +54,7 @@ export default function updateInstructionsTool({
workspace_id,
expected: args.instructions,
});
return `Instructions for this workspace saved and checked. They apply from your next run here.`;
return `Instructions for this workspace saved and checked. They apply from your next run here. They are yours in this workspace, not its context: the workspace's own description and context did not change.`;
}
await updateBotSystemPrompt({
+4 -2
View File
@@ -28,6 +28,8 @@ const UserScopeNote = `what you remember about this user, under "# Memory about
const CoreScopeNote = `scope "core" is your core memory, under "# Memory", shared with every user you work for: only general knowledge about your job that holds for anyone, never anything about a particular user.`;
const WorkspaceScopeNote = `scope "workspace" (the default) is your own memory for this workspace, under "# Memory in ...": what holds for the work here. It is yours, not the workspace's description or context, which the user sets and which you never change from here.`;
const ReplaceNote = `Anything you leave out is forgotten, so include the parts that still matter.`;
/**
@@ -45,7 +47,7 @@ export default function updateMemoryTool({
return {
name: "update_memory",
description: workspace_id
? `Replace a memory with this full text (markdown). scope "workspace" (the default) is your memory for this workspace, under "# Memory in ...": what holds for the work here. scope "user" is ${UserScopeNote} ${CoreScopeNote} ${ReplaceNote}`
? `Replace a memory with this full text (markdown). ${WorkspaceScopeNote} scope "user" is ${UserScopeNote} ${CoreScopeNote} ${ReplaceNote}`
: `Replace a memory with this full text (markdown). scope "user" (the default) is ${UserScopeNote} ${CoreScopeNote} ${ReplaceNote}`,
schema: workspace_id
? z.object({ memory, scope: z.enum(["workspace", "user", "core"]).default("workspace") })
@@ -60,7 +62,7 @@ export default function updateMemoryTool({
if (workspace_id && args.scope == "workspace") {
await updateWorkspaceBot({ user_id, workspace_id, bot_id, memory: args.memory });
await checkSavedText({ kind: "workspace_memory", bot_id, workspace_id, expected: args.memory });
return `Memory for this workspace saved and checked.`;
return `Memory for this workspace saved and checked. It is your own memory, not the workspace's description or context: those did not change. The user reads it in the workspace's settings, under this bot's row. Tell the user that, never that you changed the workspace.`;
}
await updateBotUserMemory({ user_id, bot_id, memory: args.memory });
+2 -2
View File
@@ -21,11 +21,11 @@ describe("composeBotPrompt", () => {
).toBe(
[
"# You are Scout\n\nFind things.",
`# Workspace: Launch\n\nYou're working in the workspace "Launch". Follow its context and your instructions for it below, over your general habits where they differ.\n\nThe spring launch.\n\nShip by Friday.`,
`# Workspace: Launch\n\nYou're working in the workspace "Launch". Follow its context and your instructions for it below, over your general habits where they differ.\n\nThe workspace's context, which the user sets:\n\nThe spring launch.\n\nShip by Friday.`,
"# Your instructions in Launch\n\nOwn the changelog.",
"# Memory\n\n- Cite sources.",
"# Memory about the user\n\n- The user prefers short replies.",
"# Memory in Launch\n\n- Marketing signs off on copy.",
`# Memory in Launch\n\nYour own notes for this workspace: what you have learned here. They are not the workspace's description or context, which the user sets above; never call them the workspace's record. The user reads them in the workspace's settings, under this bot's row.\n\n- Marketing signs off on copy.`,
].join("\n\n"),
);
});
+12 -5
View File
@@ -30,8 +30,9 @@ type Params = {
* The full prompt a bot runs with (markdown): its own core instructions,
* then, in a workspace, the workspace's context and the bot's
* instructions there, then its core memory, its memory about the user and
* its memory in the workspace. Empty parts are left out; the workspace
* heading always shows, so the bot knows where it is.
* its memory in the workspace (said to be the bot's own notes, not the
* workspace's context). Empty parts are left out; the workspace heading
* always shows, so the bot knows where it is.
*/
export default function composeBotPrompt({
system_prompt,
@@ -48,12 +49,16 @@ export default function composeBotPrompt({
}
if (workspace) {
const context = [workspace.description?.trim(), workspace.system_prompt?.trim()].filter(
Boolean,
) as string[];
parts.push(
[
`# Workspace: ${name}`,
`You're working in the workspace "${name}". Follow its context and your instructions for it below, over your general habits where they differ.`,
workspace.description?.trim() || "",
workspace.system_prompt?.trim() || "",
context.length ? "The workspace's context, which the user sets:" : "",
...context,
]
.filter(Boolean)
.join("\n\n"),
@@ -73,7 +78,9 @@ export default function composeBotPrompt({
}
if (workspace && workspace_bot?.memory?.trim()) {
parts.push(`# Memory in ${name}\n\n${workspace_bot.memory.trim()}`);
parts.push(
`# Memory in ${name}\n\nYour own notes for this workspace: what you have learned here. They are not the workspace's description or context, which the user sets above; never call them the workspace's record. The user reads them in the workspace's settings, under this bot's row.\n\n${workspace_bot.memory.trim()}`,
);
}
return parts.join("\n\n");