Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 41 additions & 17 deletions src/agent-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,29 @@ export class AgentImpl
return [...this._channels.values()];
}

private async sendOutboundMessage(
jid: string,
rawText: string,
channel?: Channel,
): Promise<boolean> {
const targetChannel = channel ?? findChannel(this.channelArray, jid);
if (!targetChannel) {
logger.warn({ jid }, 'No channel owns JID, cannot send');
return false;
}

const text = formatOutbound(rawText);
if (!text) return false;

await targetChannel.sendMessage(jid, text);
this.emit('message.out', {
jid,
text,
timestamp: new Date().toISOString(),
});
return true;
}

// ─── Group management ────────────────────────────────────────────

/** Register a group for message processing. Only after start(). */
Expand Down Expand Up @@ -524,19 +547,24 @@ export class AgentImpl
this.config.dataDir,
);
if (result.ok) {
await channel.sendMessage(chatJid, result.url);
await this.sendOutboundMessage(chatJid, result.url, channel);
} else {
await channel.sendMessage(
await this.sendOutboundMessage(
chatJid,
`Remote Control failed: ${result.error}`,
channel,
);
}
} else {
const result = stopRemoteControl(this.config.dataDir);
if (result.ok) {
await channel.sendMessage(chatJid, 'Remote Control session ended.');
await this.sendOutboundMessage(
chatJid,
'Remote Control session ended.',
channel,
);
} else {
await channel.sendMessage(chatJid, result.error);
await this.sendOutboundMessage(chatJid, result.error, channel);
}
}
}
Expand Down Expand Up @@ -623,8 +651,11 @@ export class AgentImpl
`Agent output: ${raw.length} chars`,
);
if (text) {
await channel.sendMessage(chatJid, text);
outputSentToUser = true;
outputSentToUser = await this.sendOutboundMessage(
chatJid,
raw,
channel,
);
}
resetIdleTimer();
}
Expand Down Expand Up @@ -904,13 +935,7 @@ export class AgentImpl
onProcess: (groupJid, boxName, _containerName, groupFolder) =>
this.queue.registerBox(groupJid, boxName, groupFolder),
sendMessage: async (jid, rawText) => {
const channel = findChannel(this.channelArray, jid);
if (!channel) {
logger.warn({ jid }, 'No channel owns JID, cannot send');
return;
}
const text = formatOutbound(rawText);
if (text) await channel.sendMessage(jid, text);
await this.sendOutboundMessage(jid, rawText);
},
});

Expand All @@ -919,10 +944,9 @@ export class AgentImpl
ipcPollInterval: this.runtimeConfig.ipcPollInterval,
timezone: this.runtimeConfig.timezone,
db: this.db,
sendMessage: (jid, text) => {
const channel = findChannel(this.channelArray, jid);
if (!channel) throw new Error(`No channel for JID: ${jid}`);
return channel.sendMessage(jid, text);
sendMessage: async (jid, text) => {
const sent = await this.sendOutboundMessage(jid, text);
if (!sent) throw new Error(`No channel for JID: ${jid}`);
},
registeredGroups: () => this._registeredGroups,
registerGroup: (jid, group) => this.registerGroup(jid, group),
Expand Down
90 changes: 90 additions & 0 deletions src/message-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from './agent-config.js';
import { buildRuntimeConfig } from './runtime-config.js';
import { _initTestDatabase, AgentDb } from './db.js';
import type { Channel } from './types.js';

let tmpDir: string;
const rtConfig = buildRuntimeConfig({}, '/tmp/agentlite-test-pkg');
Expand All @@ -34,6 +35,26 @@ function createAgent(name: string): AgentImpl {

let db: AgentDb;

function createMockChannel(): Channel & {
sendCalls: Array<{ jid: string; text: string }>;
} {
return {
name: 'mock',
sendCalls: [],
async connect(): Promise<void> {},
async disconnect(): Promise<void> {},
async sendMessage(jid: string, text: string): Promise<void> {
this.sendCalls.push({ jid, text });
},
isConnected(): boolean {
return true;
},
ownsJid(jid: string): boolean {
return jid.startsWith('mock:');
},
};
}

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentlite-msg-'));
db = _initTestDatabase();
Expand Down Expand Up @@ -205,3 +226,72 @@ describe('message.in event', () => {
});
});
});

describe('message.out event', () => {
it('emits message.out when sending outbound text', async () => {
const agent = createAgent('test');
const channel = createMockChannel();
const events: unknown[] = [];
agent.on('message.out', (evt) => events.push(evt));

(
agent as unknown as {
_channels: Map<string, Channel>;
sendOutboundMessage: (jid: string, text: string) => Promise<boolean>;
}
)._channels.set('mock', channel);

const sent = await (
agent as unknown as {
sendOutboundMessage: (jid: string, text: string) => Promise<boolean>;
}
).sendOutboundMessage('mock:123', 'partial update');

expect(sent).toBe(true);
expect(channel.sendCalls).toEqual([
{ jid: 'mock:123', text: 'partial update' },
]);
expect(events).toHaveLength(1);
expect(events[0]).toMatchObject({
jid: 'mock:123',
text: 'partial update',
});
expect(
Number.isNaN(Date.parse((events[0] as { timestamp: string }).timestamp)),
).toBe(false);
});

it('strips internal tags and suppresses empty outbound chunks', async () => {
const agent = createAgent('test');
const channel = createMockChannel();
const events: unknown[] = [];
agent.on('message.out', (evt) => events.push(evt));

(
agent as unknown as {
_channels: Map<string, Channel>;
sendOutboundMessage: (jid: string, text: string) => Promise<boolean>;
}
)._channels.set('mock', channel);

const sentVisible = await (
agent as unknown as {
sendOutboundMessage: (jid: string, text: string) => Promise<boolean>;
}
).sendOutboundMessage('mock:123', '<internal>hidden</internal>\nhello');
const sentHidden = await (
agent as unknown as {
sendOutboundMessage: (jid: string, text: string) => Promise<boolean>;
}
).sendOutboundMessage('mock:123', '<internal>hidden only</internal>');

expect(sentVisible).toBe(true);
expect(sentHidden).toBe(false);
expect(channel.sendCalls).toEqual([{ jid: 'mock:123', text: 'hello' }]);
expect(events).toHaveLength(1);
expect(events[0]).toMatchObject({
jid: 'mock:123',
text: 'hello',
});
});
});
Loading