From 70695846874e7d4a66150db03102017c7551ba55 Mon Sep 17 00:00:00 2001 From: Dominic Anaeto Date: Sat, 31 Oct 2026 18:42:08 -0400 Subject: [PATCH] fix(server): keep a recipient's pending permission when another agent messages it A message from another agent (send_agent_message with sourceAgentId) was enqueued with clearPendingPermissions: true, so it superseded the recipient's pending permission request and the request was denied before a human saw it. Agent-sourced messages now leave pending permissions alone. Typed messages and sourceless `paseo send` still clear them. Fixes #7602 --- packages/server/src/server/session.test.ts | 69 +++++++++++++++++++++- packages/server/src/server/session.ts | 2 +- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/packages/server/src/server/session.test.ts b/packages/server/src/server/session.test.ts index d1408f9880..9a13c70534 100644 --- a/packages/server/src/server/session.test.ts +++ b/packages/server/src/server/session.test.ts @@ -67,6 +67,7 @@ import { createProviderSnapshot, createProviderSnapshotManagerStub, } from "./test-utils/session-stubs.js"; +import { createTestAgentMessages } from "./test-utils/agent-messages.js"; import { isPlatform } from "../test-utils/platform.js"; import { GitHubAuthenticationError, @@ -312,6 +313,7 @@ interface SessionForTestOptions { permissions?: readonly DaemonPermission[]; agentManager?: { [K in keyof SessionOptions["agentManager"]]?: unknown }; agentStorage?: { [K in keyof SessionOptions["agentStorage"]]?: unknown }; + agentMessages?: SessionOptions["agentMessages"]; github?: Partial; checkoutDiffManager?: { scheduleRefreshForCwd: ReturnType }; workspaceGitService?: { @@ -388,7 +390,7 @@ function createSessionForTest(options: SessionForTestOptions = {}): Session { const sessionOptions: SessionOptions = { workspaceRuntimeStore: options.workspaceRuntimeStore ?? createWorkspaceRuntimeStore(), messageReceipts: createMessageReceiptsStub(), - agentMessages: createAgentMessageDeliveryStub(), + agentMessages: options.agentMessages ?? createAgentMessageDeliveryStub(), creationService: createTestCreationService(), clientId: options.clientId ?? "test-client", onMessage: (message) => messages.push(message), @@ -5676,6 +5678,71 @@ test("project.list returns every active project descriptor", async () => { ]); }); +describe("send_agent_message_request pending permissions", () => { + const recipientId = "22222222-2222-4222-8222-222222222222"; + const senderId = "33333333-3333-4333-8333-333333333333"; + + function steerableRecipient() { + return { + getAgent: vi.fn((agentId: string) => + agentId === recipientId + ? { id: recipientId, provider: "claude", lifecycle: "running" } + : null, + ), + waitForAgentClose: vi.fn().mockResolvedValue(undefined), + tryRunOutOfBand: vi.fn().mockResolvedValue(false), + reloadStalePluginSession: vi.fn().mockResolvedValue(undefined), + steerOrReplaceActiveTurn: vi.fn().mockResolvedValue({ status: "steered" }), + }; + } + + test("an agent-sourced message steers without clearing the recipient's pending permissions", async () => { + const agentManager = steerableRecipient(); + const agentMessages = createTestAgentMessages({ + agentManager: asAgentManager(agentManager), + agentStorage: asAgentStorage({ get: vi.fn().mockResolvedValue(undefined) }), + logger: pino({ level: "silent" }), + }); + const session = createSessionForTest({ agentManager, agentMessages }); + + await session.handleMessage({ + type: "send_agent_message_request", + requestId: "agent-send", + agentId: recipientId, + text: "Status check: reply when done.", + sourceAgentId: senderId, + messageId: "agent-message", + }); + await agentMessages.close(); + + expect(agentManager.steerOrReplaceActiveTurn).toHaveBeenCalledWith( + recipientId, + expect.anything(), + { clientMessageId: "agent-message" }, + ); + }); + + test("a sourceless steer still clears the recipient's pending permissions", async () => { + const agentManager = steerableRecipient(); + const session = createSessionForTest({ agentManager }); + + await session.handleMessage({ + type: "send_agent_message_request", + requestId: "sourceless-send", + agentId: recipientId, + text: "Status check: reply when done.", + messageId: "sourceless-message", + activeTurnBehavior: "steer", + }); + + expect(agentManager.steerOrReplaceActiveTurn).toHaveBeenCalledWith( + recipientId, + expect.anything(), + { clientMessageId: "sourceless-message", clearPendingPermissions: true }, + ); + }); +}); + describe("agent config setters", () => { function liveAgentManager(overrides: { [K in keyof SessionOptions["agentManager"]]?: unknown }): { [K in keyof SessionOptions["agentManager"]]?: unknown; diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 00c8529482..0af650273b 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -8318,7 +8318,7 @@ export class Session { }, activeTurnBehavior: msg.activeTurnBehavior ?? "steer", sessionMode: msg.sessionMode, - clearPendingPermissions: true, + clearPendingPermissions: false, }); deliveryStatus = "queued"; queued();