diff --git a/packages/api/src/mcp/tools/inbox-handlers.test.ts b/packages/api/src/mcp/tools/inbox-handlers.test.ts index 18394dba..0ab33d12 100644 --- a/packages/api/src/mcp/tools/inbox-handlers.test.ts +++ b/packages/api/src/mcp/tools/inbox-handlers.test.ts @@ -420,6 +420,36 @@ describe('handleSendToInbox - threadKey', () => { ); }); + it('should dispatch trigger for new self-thread session_resume (strategy first kickoff)', async () => { + const { getAgentGateway } = await import('../../channels/agent-gateway.js'); + const mockGateway = (getAgentGateway as ReturnType)(); + // findThread returns null → new thread path + const { findThread } = await import('./thread-handlers.js'); + vi.mocked(findThread).mockResolvedValue(null); + + const mockSb = createThreadMockSupabase(); + const mockDc = createThreadMockDataComposer(mockSb); + + await handleSendToInbox( + { + email: 'test@test.com', + recipientAgentId: 'wren', + senderAgentId: 'wren', + messageType: 'session_resume', + threadKey: 'strategy:new-group-123', + content: 'Strategy kickoff — first trigger', + }, + mockDc as never + ); + + expect(mockGateway.dispatchTrigger).toHaveBeenCalledWith( + expect.objectContaining({ + toAgentId: 'wren', + threadKey: 'strategy:new-group-123', + }) + ); + }); + // =================================================================== // 2FA SECURITY — permission_grant from agent senders must be rejected // =================================================================== diff --git a/packages/api/src/mcp/tools/inbox-handlers.ts b/packages/api/src/mcp/tools/inbox-handlers.ts index 80a1fb7d..ae28e9dc 100644 --- a/packages/api/src/mcp/tools/inbox-handlers.ts +++ b/packages/api/src/mcp/tools/inbox-handlers.ts @@ -547,8 +547,11 @@ export async function handleSendToInbox(args: unknown, dataComposer: DataCompose selfStudioTarget, }); } else { - // New thread: trigger all recipients (exclude sender unless cross-studio self-message) - agentsToTrigger = allRecipients.filter((a) => selfStudioTarget || a !== senderAgentId); + // New thread: trigger all recipients (exclude sender unless cross-studio + // self-message or actionable self-target like strategy kickoff) + const actionableSelf = new Set(['task_request', 'session_resume']); + const allowSelf = selfStudioTarget || (!!messageType && actionableSelf.has(messageType)); + agentsToTrigger = allRecipients.filter((a) => allowSelf || a !== senderAgentId); } } diff --git a/packages/api/src/mcp/tools/thread-handlers.test.ts b/packages/api/src/mcp/tools/thread-handlers.test.ts index 28b2d501..5a96ee80 100644 --- a/packages/api/src/mcp/tools/thread-handlers.test.ts +++ b/packages/api/src/mcp/tools/thread-handlers.test.ts @@ -170,7 +170,7 @@ describe('resolveTriggeredAgents', () => { }); describe('self-thread (1 participant)', () => { - it('should trigger no one', () => { + it('should trigger no one for plain messages', () => { const result = resolveTriggeredAgents({ senderAgentId: 'wren', participants: ['wren'], @@ -178,6 +178,36 @@ describe('resolveTriggeredAgents', () => { }); expect(result).toEqual([]); }); + + it('should trigger self for session_resume (strategy self-trigger)', () => { + const result = resolveTriggeredAgents({ + senderAgentId: 'wren', + participants: ['wren'], + creatorAgentId: 'wren', + messageType: 'session_resume', + }); + expect(result).toEqual(['wren']); + }); + + it('should trigger self for task_request', () => { + const result = resolveTriggeredAgents({ + senderAgentId: 'wren', + participants: ['wren'], + creatorAgentId: 'wren', + messageType: 'task_request', + }); + expect(result).toEqual(['wren']); + }); + + it('should not trigger self for notification', () => { + const result = resolveTriggeredAgents({ + senderAgentId: 'wren', + participants: ['wren'], + creatorAgentId: 'wren', + messageType: 'notification', + }); + expect(result).toEqual([]); + }); }); describe('triggerAll override', () => { diff --git a/packages/api/src/mcp/tools/thread-handlers.ts b/packages/api/src/mcp/tools/thread-handlers.ts index 49d805b1..6a91516b 100644 --- a/packages/api/src/mcp/tools/thread-handlers.ts +++ b/packages/api/src/mcp/tools/thread-handlers.ts @@ -198,9 +198,14 @@ export function resolveTriggeredAgents(opts: { // Precedence 3: default rules by thread size const otherParticipants = participants.filter((a) => a !== senderAgentId); - // Self-thread (1 participant): trigger only if cross-studio self-message + // Self-thread (1 participant): trigger if cross-studio OR actionable message type. + // session_resume / task_request to self are inherently "wake me up" signals + // (e.g., strategy triggers) and must not be silently dropped. if (otherParticipants.length === 0) { - return selfStudioTarget ? [senderAgentId] : []; + if (selfStudioTarget) return [senderAgentId]; + const selfActionable = new Set(['task_request', 'session_resume']); + if (messageType && selfActionable.has(messageType)) return [senderAgentId]; + return []; } // 1:1 thread (2 participants): trigger the other one