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', () => {