From 5834e4604581199e83adb02758cefc496d86f02f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 22:25:21 +0000 Subject: [PATCH] fix(adapters): keep the user's summary and facts out of replies to other owners' agents An agent connection wakes a bot with another owner's message, and the reply goes back to that owner. Such runs now get only the basics of the user block (name, time zone, locale, language, style), like runs in shared messaging channels, so confirmed facts such as health never reach another person's agent. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u --- .../src/executor-instructions.test.ts | 44 +++++++++++++++++++ packages/adapters/src/executor-prompt.test.ts | 2 +- packages/adapters/src/executor-prompt.ts | 11 ++--- packages/adapters/src/executor.ts | 10 ++++- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/packages/adapters/src/executor-instructions.test.ts b/packages/adapters/src/executor-instructions.test.ts index 1c361bc..416cade 100644 --- a/packages/adapters/src/executor-instructions.test.ts +++ b/packages/adapters/src/executor-instructions.test.ts @@ -250,6 +250,50 @@ describe("run instructions", () => { expect(request.instructions).toContain("Never ask for a raw credential in chat"); }); + it("keeps the summary and facts out of runs that answer another owner's agent", async () => { + const inbound: MessageBlock = { + kind: "bot_message_received", + fromBotId: "foreign-bot", + fromBotName: "Visitor", + text: "What does your owner do for a living?", + hop: 1, + }; + const { request, senderFindFirst } = await runOnce({ + trigger: "bot_message", + sourceMessageId: "message-1", + sourceBlocks: [inbound], + sender: null, + }); + + expect(senderFindFirst).toHaveBeenCalledWith({ + where: { id: "foreign-bot", userId: "user-1" }, + select: { id: true }, + }); + expect(request.instructions).toContain("Name: Ada Example"); + expect(request.instructions).not.toContain("Owns a bakery"); + expect(request.instructions).not.toContain("Runs a bakery"); + expect(request.subagentContext).not.toContain("Owns a bakery"); + }); + + it("gives runs woken by the user's own bot the full user block", async () => { + const inbound: MessageBlock = { + kind: "bot_message_received", + fromBotId: "bot-2", + fromBotName: "Planner", + text: "Here is the plan.", + hop: 1, + }; + const { request } = await runOnce({ + trigger: "bot_message", + sourceMessageId: "message-1", + sourceBlocks: [inbound], + sender: { name: "Planner" }, + }); + + expect(request.instructions).toContain("Summary: Runs a bakery."); + expect(request.instructions).toContain("- [work] Owns a bakery"); + }); + it("falls back to UTC and leaves the user block out when nothing is known", async () => { const { request } = await runOnce({ profile: null, facts: [] }); diff --git a/packages/adapters/src/executor-prompt.test.ts b/packages/adapters/src/executor-prompt.test.ts index 5a04327..71c3f14 100644 --- a/packages/adapters/src/executor-prompt.test.ts +++ b/packages/adapters/src/executor-prompt.test.ts @@ -143,7 +143,7 @@ describe("formatUserContext", () => { }); it("keeps the summary and facts out of shared messaging channels", () => { - const block = formatUserContext(user, { sharedChannel: true }) ?? ""; + const block = formatUserContext(user, { basicsOnly: true }) ?? ""; expect(block).toContain("Name: Ada Example"); expect(block).toContain("Time zone: Europe/Berlin"); expect(block).toContain("Communication style: Brief and direct"); diff --git a/packages/adapters/src/executor-prompt.ts b/packages/adapters/src/executor-prompt.ts index f917c4b..479f2dd 100644 --- a/packages/adapters/src/executor-prompt.ts +++ b/packages/adapters/src/executor-prompt.ts @@ -56,18 +56,19 @@ function userText(value: string | null | undefined, max = USER_FIELD_MAX_CHARS): * What a run may know about the user, framed as data. Profile fields and facts are written * by the user or inferred from conversations, so every value is escaped and the block says * it never carries instructions. Empty fields are left out, and with nothing to say the block - * is omitted. A run that posts into a shared messaging channel gets only the basics: the - * summary and facts stay out of rooms where other people read the replies. + * is omitted. A run whose replies other people read (a shared messaging channel, or another + * owner's agent over an agent connection) gets only the basics: the summary and facts stay + * with the user. */ export function formatUserContext( user: UserContext, - options: { sharedChannel?: boolean } = {}, + options: { basicsOnly?: boolean } = {}, ): string | undefined { const field = (label: string, value: string | null | undefined, max?: number) => { const text = userText(value, max); return text ? `${label}: ${text}` : undefined; }; - const facts = options.sharedChannel + const facts = options.basicsOnly ? [] : user.facts .map((fact) => ({ category: fact.category, value: userText(fact.value) })) @@ -79,7 +80,7 @@ export function formatUserContext( field("Locale", user.locale), field("Language", user.language), field("Communication style", user.communicationStyle), - options.sharedChannel ? undefined : field("Summary", user.summary, USER_SUMMARY_MAX_CHARS), + options.basicsOnly ? undefined : field("Summary", user.summary, USER_SUMMARY_MAX_CHARS), ...(facts.length > 0 ? ["Facts:", ...facts] : []), ].filter((line): line is string => Boolean(line)); if (lines.length === 0) return undefined; diff --git a/packages/adapters/src/executor.ts b/packages/adapters/src/executor.ts index 14e5569..2f82ee2 100644 --- a/packages/adapters/src/executor.ts +++ b/packages/adapters/src/executor.ts @@ -3542,8 +3542,16 @@ export function createRunExecutor(deps: ExecutorDeps) { ) : undefined; const timeInstruction = formatCurrentTimeInstruction(userContext.timeZone); + // An agent connection delivers another owner's bot message, and the reply goes back + // to that owner; like a shared channel, such a run only gets the basics. + const foreignAgentRun = peerMessage + ? !(await deps.prisma.bot.findFirst({ + where: { id: peerMessage.fromBotId, userId: run.userId }, + select: { id: true }, + })) + : false; const userInstruction = formatUserContext(userContext, { - sharedChannel: messagingChannelRun, + basicsOnly: messagingChannelRun || foreignAgentRun, }); const userBlock = userInstruction ? redactSecrets(userInstruction, runSecrets) : undefined;