Fix IRC proactive sends: use monitor's persistent client

The sendMessageIrc function was creating transient IRC connections for
proactive sends. These transient clients don't join channels before
sending PRIVMSG, so messages to channels silently fail.

This adds an active client registry that the monitor populates when
it connects. sendMessageIrc now checks for an active persistent client
first, falling back to transient connections only when no monitor client
is available.

This enables agents to initiate conversations in IRC channels from
their main session — a prerequisite for autonomy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tio 2026-03-13 21:46:19 -04:00
parent dc86b6d72a
commit 922cba6775
3 changed files with 42 additions and 7 deletions

View File

@ -0,0 +1,24 @@
import type { IrcClient } from "./client.js";
/**
* Registry of active IRC clients from the monitor.
* Keyed by accountId. Allows sendMessageIrc to use the persistent
* monitor client instead of creating a transient connection.
*/
const activeClients = new Map<string, IrcClient>();
export function setActiveClient(accountId: string, client: IrcClient): void {
activeClients.set(accountId, client);
}
export function getActiveClient(accountId: string): IrcClient | undefined {
const client = activeClients.get(accountId);
if (client && client.isReady()) {
return client;
}
return undefined;
}
export function removeActiveClient(accountId: string): void {
activeClients.delete(accountId);
}

View File

@ -1,5 +1,6 @@
import { resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
import { resolveIrcAccount } from "./accounts.js";
import { setActiveClient, removeActiveClient } from "./active-clients.js";
import { connectIrcClient, type IrcClient } from "./client.js";
import { buildIrcConnectOptions } from "./connect-options.js";
import { handleIrcInbound } from "./inbound.js";
@ -136,8 +137,11 @@ export async function monitorIrcProvider(opts: IrcMonitorOptions): Promise<{ sto
`[${account.accountId}] connected to ${account.host}:${account.port}${account.tls ? " (tls)" : ""} as ${client.nick}`,
);
setActiveClient(account.accountId, client);
return {
stop: () => {
removeActiveClient(account.accountId);
client?.quit("shutdown");
client = null;
},

View File

@ -1,4 +1,5 @@
import { resolveIrcAccount } from "./accounts.js";
import { getActiveClient } from "./active-clients.js";
import type { IrcClient } from "./client.js";
import { connectIrcClient } from "./client.js";
import { buildIrcConnectOptions } from "./connect-options.js";
@ -67,13 +68,19 @@ export async function sendMessageIrc(
if (client?.isReady()) {
client.sendPrivmsg(target, payload);
} else {
const transient = await connectIrcClient(
buildIrcConnectOptions(account, {
connectTimeoutMs: 12000,
}),
);
transient.sendPrivmsg(target, payload);
transient.quit("sent");
// Try the monitor's persistent client first (already connected and joined to channels)
const active = getActiveClient(account.accountId);
if (active) {
active.sendPrivmsg(target, payload);
} else {
const transient = await connectIrcClient(
buildIrcConnectOptions(account, {
connectTimeoutMs: 12000,
}),
);
transient.sendPrivmsg(target, payload);
transient.quit("sent");
}
}
runtime.channel.activity.record({