diff --git a/src/core/bot.ts b/src/core/bot.ts index cf84bee..bca4f07 100644 --- a/src/core/bot.ts +++ b/src/core/bot.ts @@ -363,6 +363,13 @@ export class LettaBot implements AgentSession { } if (directive.type === 'send-file') { + // Reject partial targeting: both channel and chat must be set together. + // Without this guard, a missing field silently falls back to the triggering chat. + if ((directive.channel && !directive.chat) || (!directive.channel && directive.chat)) { + log.warn(`Directive send-file skipped: cross-channel targeting requires both channel and chat (got channel=${directive.channel || 'missing'}, chat=${directive.chat || 'missing'})`); + continue; + } + // Resolve target adapter: use cross-channel targeting if both channel and chat are set, // otherwise fall back to the adapter/chatId that triggered this response. const targetAdapter = (directive.channel && directive.chat) @@ -1908,11 +1915,29 @@ export class LettaBot implements AgentSession { continue; } + // Parse and execute directives from the response. + // Targeted directives (send-message, cross-channel send-file) work in any context. + // Non-targeted directives work when source adapter context is available. + let executedDirectives = false; + if (response.trim()) { + const parsed = parseDirectives(response); + if (parsed.directives.length > 0) { + const sourceAdapter = (context?.sourceChannel ? this.channels.get(context.sourceChannel) : undefined) + ?? this.channels.values().next().value; + if (sourceAdapter) { + executedDirectives = await this.executeDirectives( + parsed.directives, sourceAdapter, context?.sourceChatId ?? '', + ); + } + response = parsed.cleanText; + } + } + if (isSilent && response.trim()) { - if (usedMessageCli) { - log.info(`Silent mode: agent used lettabot-message CLI, collected ${response.length} chars (not delivered)`); + if (usedMessageCli || executedDirectives) { + log.info(`Silent mode: agent delivered via ${[usedMessageCli && 'CLI', executedDirectives && 'directives'].filter(Boolean).join(' + ')}, remaining text (${response.length} chars) not delivered`); } else { - log.warn(`Silent mode: agent produced ${response.length} chars but did NOT use lettabot-message CLI — response discarded. If this keeps happening, the agent's model may not be following silent mode instructions.`); + log.warn(`Silent mode: agent produced ${response.length} chars but did NOT use lettabot-message CLI or directives — response discarded. If this keeps happening, the agent's model may not be following silent mode instructions.`); } } return response; diff --git a/src/core/directives.test.ts b/src/core/directives.test.ts index 00e29d3..a8cfd62 100644 --- a/src/core/directives.test.ts +++ b/src/core/directives.test.ts @@ -306,6 +306,24 @@ describe('parseDirectives', () => { { type: 'send-file', path: 'report.pdf', caption: 'Report' }, ]); }); + + it('parses send-file with only channel (no chat) -- stores partial targeting', () => { + const result = parseDirectives( + '', + ); + expect(result.directives).toEqual([ + { type: 'send-file', path: 'report.pdf', channel: 'whatsapp' }, + ]); + }); + + it('parses send-file with only chat (no channel) -- stores partial targeting', () => { + const result = parseDirectives( + '', + ); + expect(result.directives).toEqual([ + { type: 'send-file', path: 'report.pdf', chat: '123' }, + ]); + }); }); describe('stripActionsBlock', () => {