feat: multi-agent routing + multi-account providers

This commit is contained in:
Peter Steinberger
2026-01-06 18:25:37 +00:00
parent 50d4b17417
commit dbfa316d19
129 changed files with 3760 additions and 1126 deletions

View File

@@ -2,7 +2,12 @@ import { randomUUID } from "node:crypto";
import { agentCommand } from "../../commands/agent.js";
import { loadConfig } from "../../config/config.js";
import { type SessionEntry, saveSessionStore } from "../../config/sessions.js";
import {
resolveAgentIdFromSessionKey,
resolveAgentMainSessionKey,
type SessionEntry,
saveSessionStore,
} from "../../config/sessions.js";
import { registerAgentRunContext } from "../../infra/agent-events.js";
import { defaultRuntime } from "../../runtime.js";
import { resolveSendPolicy } from "../../sessions/send-policy.js";
@@ -41,7 +46,7 @@ export const agentHandlers: GatewayRequestHandlers = {
sessionKey?: string;
thinking?: string;
deliver?: boolean;
channel?: string;
provider?: string;
lane?: string;
extraSystemPrompt?: string;
idempotencyKey: string;
@@ -72,7 +77,7 @@ export const agentHandlers: GatewayRequestHandlers = {
cfgForAgent = cfg;
const now = Date.now();
const sessionId = entry?.sessionId ?? randomUUID();
sessionEntry = {
const nextEntry: SessionEntry = {
sessionId,
updatedAt: now,
thinkingLevel: entry?.thinkingLevel,
@@ -80,14 +85,15 @@ export const agentHandlers: GatewayRequestHandlers = {
systemSent: entry?.systemSent,
sendPolicy: entry?.sendPolicy,
skillsSnapshot: entry?.skillsSnapshot,
lastChannel: entry?.lastChannel,
lastProvider: entry?.lastProvider,
lastTo: entry?.lastTo,
};
sessionEntry = nextEntry;
const sendPolicy = resolveSendPolicy({
cfg,
entry,
sessionKey: requestedSessionKey,
surface: entry?.surface,
provider: entry?.provider,
chatType: entry?.chatType,
});
if (sendPolicy === "deny") {
@@ -102,14 +108,22 @@ export const agentHandlers: GatewayRequestHandlers = {
return;
}
if (store) {
store[requestedSessionKey] = sessionEntry;
store[requestedSessionKey] = nextEntry;
if (storePath) {
await saveSessionStore(storePath, store);
}
}
resolvedSessionId = sessionId;
const mainKey = (cfg.session?.mainKey ?? "main").trim() || "main";
if (requestedSessionKey === mainKey) {
const agentId = resolveAgentIdFromSessionKey(requestedSessionKey);
const mainSessionKey = resolveAgentMainSessionKey({
cfg,
agentId,
});
const rawMainKey = (cfg.session?.mainKey ?? "main").trim() || "main";
if (
requestedSessionKey === mainSessionKey ||
requestedSessionKey === rawMainKey
) {
context.addChatRun(idem, {
sessionKey: requestedSessionKey,
clientRunId: idem,
@@ -121,42 +135,42 @@ export const agentHandlers: GatewayRequestHandlers = {
const runId = idem;
const requestedChannelRaw =
typeof request.channel === "string" ? request.channel.trim() : "";
const requestedChannelNormalized = requestedChannelRaw
? requestedChannelRaw.toLowerCase()
const requestedProviderRaw =
typeof request.provider === "string" ? request.provider.trim() : "";
const requestedProviderNormalized = requestedProviderRaw
? requestedProviderRaw.toLowerCase()
: "last";
const requestedChannel =
requestedChannelNormalized === "imsg"
const requestedProvider =
requestedProviderNormalized === "imsg"
? "imessage"
: requestedChannelNormalized;
: requestedProviderNormalized;
const lastChannel = sessionEntry?.lastChannel;
const lastProvider = sessionEntry?.lastProvider;
const lastTo =
typeof sessionEntry?.lastTo === "string"
? sessionEntry.lastTo.trim()
: "";
const resolvedChannel = (() => {
if (requestedChannel === "last") {
const resolvedProvider = (() => {
if (requestedProvider === "last") {
// WebChat is not a deliverable surface. Treat it as "unset" for routing,
// so VoiceWake and CLI callers don't get stuck with deliver=false.
return lastChannel && lastChannel !== "webchat"
? lastChannel
return lastProvider && lastProvider !== "webchat"
? lastProvider
: "whatsapp";
}
if (
requestedChannel === "whatsapp" ||
requestedChannel === "telegram" ||
requestedChannel === "discord" ||
requestedChannel === "signal" ||
requestedChannel === "imessage" ||
requestedChannel === "webchat"
requestedProvider === "whatsapp" ||
requestedProvider === "telegram" ||
requestedProvider === "discord" ||
requestedProvider === "signal" ||
requestedProvider === "imessage" ||
requestedProvider === "webchat"
) {
return requestedChannel;
return requestedProvider;
}
return lastChannel && lastChannel !== "webchat"
? lastChannel
return lastProvider && lastProvider !== "webchat"
? lastProvider
: "whatsapp";
})();
@@ -167,11 +181,11 @@ export const agentHandlers: GatewayRequestHandlers = {
: undefined;
if (explicit) return explicit;
if (
resolvedChannel === "whatsapp" ||
resolvedChannel === "telegram" ||
resolvedChannel === "discord" ||
resolvedChannel === "signal" ||
resolvedChannel === "imessage"
resolvedProvider === "whatsapp" ||
resolvedProvider === "telegram" ||
resolvedProvider === "discord" ||
resolvedProvider === "signal" ||
resolvedProvider === "imessage"
) {
return lastTo || undefined;
}
@@ -182,7 +196,7 @@ export const agentHandlers: GatewayRequestHandlers = {
// If we derived a WhatsApp recipient from session "lastTo", ensure it is still valid
// for the configured allowlist. Otherwise, fall back to the first allowed number so
// voice wake doesn't silently route to stale/test recipients.
if (resolvedChannel !== "whatsapp") return resolvedTo;
if (resolvedProvider !== "whatsapp") return resolvedTo;
const explicit =
typeof request.to === "string" && request.to.trim()
? request.to.trim()
@@ -207,7 +221,7 @@ export const agentHandlers: GatewayRequestHandlers = {
return allowFrom[0];
})();
const deliver = request.deliver === true && resolvedChannel !== "webchat";
const deliver = request.deliver === true && resolvedProvider !== "webchat";
const accepted = {
runId,
@@ -229,10 +243,10 @@ export const agentHandlers: GatewayRequestHandlers = {
sessionId: resolvedSessionId,
thinking: request.thinking,
deliver,
provider: resolvedChannel,
provider: resolvedProvider,
timeout: request.timeout?.toString(),
bestEffortDeliver,
surface: "VoiceWake",
messageProvider: "voicewake",
runId,
lane: request.lane,
extraSystemPrompt: request.extraSystemPrompt,

View File

@@ -202,7 +202,7 @@ export const chatHandlers: GatewayRequestHandlers = {
verboseLevel: entry?.verboseLevel,
systemSent: entry?.systemSent,
sendPolicy: entry?.sendPolicy,
lastChannel: entry?.lastChannel,
lastProvider: entry?.lastProvider,
lastTo: entry?.lastTo,
};
const clientRunId = p.idempotencyKey;
@@ -212,7 +212,7 @@ export const chatHandlers: GatewayRequestHandlers = {
cfg,
entry,
sessionKey: p.sessionKey,
surface: entry?.surface,
provider: entry?.provider,
chatType: entry?.chatType,
});
if (sendPolicy === "deny") {
@@ -262,7 +262,7 @@ export const chatHandlers: GatewayRequestHandlers = {
thinking: p.thinking,
deliver: p.deliver,
timeout: Math.ceil(timeoutMs / 1000).toString(),
surface: "WebChat",
messageProvider: "webchat",
abortSignal: abortController.signal,
},
defaultRuntime,

View File

@@ -6,7 +6,6 @@ import {
} from "../../config/config.js";
import { type DiscordProbe, probeDiscord } from "../../discord/probe.js";
import { type IMessageProbe, probeIMessage } from "../../imessage/probe.js";
import { webAuthExists } from "../../providers/web/index.js";
import { probeSignal, type SignalProbe } from "../../signal/probe.js";
import { probeSlack, type SlackProbe } from "../../slack/probe.js";
import {
@@ -15,7 +14,15 @@ import {
} from "../../slack/token.js";
import { probeTelegram, type TelegramProbe } from "../../telegram/probe.js";
import { resolveTelegramToken } from "../../telegram/token.js";
import { getWebAuthAgeMs, readWebSelfId } from "../../web/session.js";
import {
listEnabledWhatsAppAccounts,
resolveDefaultWhatsAppAccountId,
} from "../../web/accounts.js";
import {
getWebAuthAgeMs,
readWebSelfId,
webAuthExists,
} from "../../web/session.js";
import {
ErrorCodes,
errorShape,
@@ -148,10 +155,55 @@ export const providersHandlers: GatewayRequestHandlers = {
imessageLastProbeAt = Date.now();
}
const linked = await webAuthExists();
const authAgeMs = getWebAuthAgeMs();
const self = readWebSelfId();
const runtime = context.getRuntimeSnapshot();
const defaultWhatsAppAccountId = resolveDefaultWhatsAppAccountId(cfg);
const enabledWhatsAppAccounts = listEnabledWhatsAppAccounts(cfg);
const defaultWhatsAppAccount =
enabledWhatsAppAccounts.find(
(account) => account.accountId === defaultWhatsAppAccountId,
) ?? enabledWhatsAppAccounts[0];
const linked = defaultWhatsAppAccount
? await webAuthExists(defaultWhatsAppAccount.authDir)
: false;
const authAgeMs = defaultWhatsAppAccount
? getWebAuthAgeMs(defaultWhatsAppAccount.authDir)
: null;
const self = defaultWhatsAppAccount
? readWebSelfId(defaultWhatsAppAccount.authDir)
: { e164: null, jid: null };
const defaultWhatsAppStatus = {
running: false,
connected: false,
reconnectAttempts: 0,
lastConnectedAt: null,
lastDisconnect: null,
lastMessageAt: null,
lastEventAt: null,
lastError: null,
} as const;
const whatsappAccounts = await Promise.all(
enabledWhatsAppAccounts.map(async (account) => {
const rt =
runtime.whatsappAccounts?.[account.accountId] ??
defaultWhatsAppStatus;
return {
accountId: account.accountId,
enabled: account.enabled,
linked: await webAuthExists(account.authDir),
authAgeMs: getWebAuthAgeMs(account.authDir),
self: readWebSelfId(account.authDir),
running: rt.running,
connected: rt.connected,
lastConnectedAt: rt.lastConnectedAt ?? null,
lastDisconnect: rt.lastDisconnect ?? null,
reconnectAttempts: rt.reconnectAttempts,
lastMessageAt: rt.lastMessageAt ?? null,
lastEventAt: rt.lastEventAt ?? null,
lastError: rt.lastError ?? null,
};
}),
);
respond(
true,
@@ -171,6 +223,8 @@ export const providersHandlers: GatewayRequestHandlers = {
lastEventAt: runtime.whatsapp.lastEventAt ?? null,
lastError: runtime.whatsapp.lastError ?? null,
},
whatsappAccounts,
whatsappDefaultAccountId: defaultWhatsAppAccountId,
telegram: {
configured: telegramEnabled && Boolean(telegramToken),
tokenSource,

View File

@@ -6,6 +6,7 @@ import { sendMessageSignal } from "../../signal/index.js";
import { sendMessageSlack } from "../../slack/send.js";
import { sendMessageTelegram } from "../../telegram/send.js";
import { resolveTelegramToken } from "../../telegram/token.js";
import { resolveDefaultWhatsAppAccountId } from "../../web/accounts.js";
import { sendMessageWhatsApp, sendPollWhatsApp } from "../../web/outbound.js";
import {
ErrorCodes,
@@ -37,6 +38,7 @@ export const sendHandlers: GatewayRequestHandlers = {
mediaUrl?: string;
gifPlayback?: boolean;
provider?: string;
accountId?: string;
idempotencyKey: string;
};
const idem = request.idempotencyKey;
@@ -148,10 +150,17 @@ export const sendHandlers: GatewayRequestHandlers = {
});
respond(true, payload, undefined, { provider });
} else {
const cfg = loadConfig();
const accountId =
typeof request.accountId === "string" &&
request.accountId.trim().length > 0
? request.accountId.trim()
: resolveDefaultWhatsAppAccountId(cfg);
const result = await sendMessageWhatsApp(to, message, {
mediaUrl: request.mediaUrl,
verbose: shouldLogVerbose(),
gifPlayback: request.gifPlayback,
accountId,
});
const payload = {
runId: idem,
@@ -199,6 +208,7 @@ export const sendHandlers: GatewayRequestHandlers = {
maxSelections?: number;
durationHours?: number;
provider?: string;
accountId?: string;
idempotencyKey: string;
};
const idem = request.idempotencyKey;
@@ -245,8 +255,15 @@ export const sendHandlers: GatewayRequestHandlers = {
});
respond(true, payload, undefined, { provider });
} else {
const cfg = loadConfig();
const accountId =
typeof request.accountId === "string" &&
request.accountId.trim().length > 0
? request.accountId.trim()
: resolveDefaultWhatsAppAccountId(cfg);
const result = await sendPollWhatsApp(to, poll, {
verbose: shouldLogVerbose(),
accountId,
});
const payload = {
runId: idem,

View File

@@ -24,11 +24,11 @@ import { loadConfig } from "../../config/config.js";
import {
loadSessionStore,
resolveMainSessionKey,
resolveStorePath,
type SessionEntry,
saveSessionStore,
} from "../../config/sessions.js";
import { clearCommandLane } from "../../process/command-queue.js";
import { isSubagentSessionKey } from "../../routing/session-key.js";
import { normalizeSendPolicy } from "../../sessions/send-policy.js";
import {
ErrorCodes,
@@ -43,7 +43,8 @@ import {
import {
archiveFileOnDisk,
listSessionsFromStore,
loadSessionEntry,
loadCombinedSessionStoreForGateway,
resolveGatewaySessionStoreTarget,
resolveSessionTranscriptCandidates,
type SessionsPatchResult,
} from "../session-utils.js";
@@ -64,8 +65,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
}
const p = params as import("../protocol/index.js").SessionsListParams;
const cfg = loadConfig();
const storePath = resolveStorePath(cfg.session?.store);
const store = loadSessionStore(storePath);
const { storePath, store } = loadCombinedSessionStoreForGateway(cfg);
const result = listSessionsFromStore({
cfg,
storePath,
@@ -98,11 +98,18 @@ export const sessionsHandlers: GatewayRequestHandlers = {
}
const cfg = loadConfig();
const storePath = resolveStorePath(cfg.session?.store);
const target = resolveGatewaySessionStoreTarget({ cfg, key });
const storePath = target.storePath;
const store = loadSessionStore(storePath);
const now = Date.now();
const existing = store[key];
const primaryKey = target.storeKeys[0] ?? key;
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
store[primaryKey] = store[existingKey];
delete store[existingKey];
}
const existing = store[primaryKey];
const next: SessionEntry = existing
? {
...existing,
@@ -134,7 +141,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
);
return;
}
if (!key.startsWith("subagent:")) {
if (!isSubagentSessionKey(primaryKey)) {
respond(
false,
undefined,
@@ -311,12 +318,12 @@ export const sessionsHandlers: GatewayRequestHandlers = {
}
}
store[key] = next;
store[primaryKey] = next;
await saveSessionStore(storePath, store);
const result: SessionsPatchResult = {
ok: true,
path: storePath,
key,
key: target.canonicalKey,
entry: next,
};
respond(true, result, undefined);
@@ -344,7 +351,17 @@ export const sessionsHandlers: GatewayRequestHandlers = {
return;
}
const { storePath, store, entry } = loadSessionEntry(key);
const cfg = loadConfig();
const target = resolveGatewaySessionStoreTarget({ cfg, key });
const storePath = target.storePath;
const store = loadSessionStore(storePath);
const primaryKey = target.storeKeys[0] ?? key;
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
store[primaryKey] = store[existingKey];
delete store[existingKey];
}
const entry = store[primaryKey];
const now = Date.now();
const next: SessionEntry = {
sessionId: randomUUID(),
@@ -356,13 +373,17 @@ export const sessionsHandlers: GatewayRequestHandlers = {
model: entry?.model,
contextTokens: entry?.contextTokens,
sendPolicy: entry?.sendPolicy,
lastChannel: entry?.lastChannel,
lastProvider: entry?.lastProvider,
lastTo: entry?.lastTo,
skillsSnapshot: entry?.skillsSnapshot,
};
store[key] = next;
store[primaryKey] = next;
await saveSessionStore(storePath, store);
respond(true, { ok: true, key, entry: next }, undefined);
respond(
true,
{ ok: true, key: target.canonicalKey, entry: next },
undefined,
);
},
"sessions.delete": async ({ params, respond }) => {
if (!validateSessionsDeleteParams(params)) {
@@ -387,8 +408,10 @@ export const sessionsHandlers: GatewayRequestHandlers = {
return;
}
const mainKey = resolveMainSessionKey(loadConfig());
if (key === mainKey) {
const cfg = loadConfig();
const mainKey = resolveMainSessionKey(cfg);
const target = resolveGatewaySessionStoreTarget({ cfg, key });
if (target.canonicalKey === mainKey) {
respond(
false,
undefined,
@@ -403,10 +426,18 @@ export const sessionsHandlers: GatewayRequestHandlers = {
const deleteTranscript =
typeof p.deleteTranscript === "boolean" ? p.deleteTranscript : true;
const { storePath, store, entry } = loadSessionEntry(key);
const storePath = target.storePath;
const store = loadSessionStore(storePath);
const primaryKey = target.storeKeys[0] ?? key;
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
store[primaryKey] = store[existingKey];
delete store[existingKey];
}
const entry = store[primaryKey];
const sessionId = entry?.sessionId;
const existed = Boolean(store[key]);
clearCommandLane(resolveEmbeddedSessionLane(key));
const existed = Boolean(entry);
clearCommandLane(resolveEmbeddedSessionLane(target.canonicalKey));
if (sessionId && isEmbeddedPiRunActive(sessionId)) {
abortEmbeddedPiRun(sessionId);
const ended = await waitForEmbeddedPiRunEnd(sessionId, 15_000);
@@ -422,7 +453,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
return;
}
}
if (existed) delete store[key];
if (existed) delete store[primaryKey];
await saveSessionStore(storePath, store);
const archived: string[] = [];
@@ -430,6 +461,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
for (const candidate of resolveSessionTranscriptCandidates(
sessionId,
storePath,
target.agentId,
)) {
if (!fs.existsSync(candidate)) continue;
try {
@@ -440,7 +472,11 @@ export const sessionsHandlers: GatewayRequestHandlers = {
}
}
respond(true, { ok: true, key, deleted: existed, archived }, undefined);
respond(
true,
{ ok: true, key: target.canonicalKey, deleted: existed, archived },
undefined,
);
},
"sessions.compact": async ({ params, respond }) => {
if (!validateSessionsCompactParams(params)) {
@@ -470,12 +506,27 @@ export const sessionsHandlers: GatewayRequestHandlers = {
? Math.max(1, Math.floor(p.maxLines))
: 400;
const { storePath, store, entry } = loadSessionEntry(key);
const cfg = loadConfig();
const target = resolveGatewaySessionStoreTarget({ cfg, key });
const storePath = target.storePath;
const store = loadSessionStore(storePath);
const primaryKey = target.storeKeys[0] ?? key;
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
store[primaryKey] = store[existingKey];
delete store[existingKey];
}
const entry = store[primaryKey];
const sessionId = entry?.sessionId;
if (!sessionId) {
respond(
true,
{ ok: true, key, compacted: false, reason: "no sessionId" },
{
ok: true,
key: target.canonicalKey,
compacted: false,
reason: "no sessionId",
},
undefined,
);
return;
@@ -484,11 +535,17 @@ export const sessionsHandlers: GatewayRequestHandlers = {
const filePath = resolveSessionTranscriptCandidates(
sessionId,
storePath,
target.agentId,
).find((candidate) => fs.existsSync(candidate));
if (!filePath) {
respond(
true,
{ ok: true, key, compacted: false, reason: "no transcript" },
{
ok: true,
key: target.canonicalKey,
compacted: false,
reason: "no transcript",
},
undefined,
);
return;
@@ -499,7 +556,12 @@ export const sessionsHandlers: GatewayRequestHandlers = {
if (lines.length <= maxLines) {
respond(
true,
{ ok: true, key, compacted: false, kept: lines.length },
{
ok: true,
key: target.canonicalKey,
compacted: false,
kept: lines.length,
},
undefined,
);
return;
@@ -509,11 +571,11 @@ export const sessionsHandlers: GatewayRequestHandlers = {
const keptLines = lines.slice(-maxLines);
fs.writeFileSync(filePath, `${keptLines.join("\n")}\n`, "utf-8");
if (store[key]) {
delete store[key].inputTokens;
delete store[key].outputTokens;
delete store[key].totalTokens;
store[key].updatedAt = Date.now();
if (store[primaryKey]) {
delete store[primaryKey].inputTokens;
delete store[primaryKey].outputTokens;
delete store[primaryKey].totalTokens;
store[primaryKey].updatedAt = Date.now();
await saveSessionStore(storePath, store);
}
@@ -521,7 +583,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
true,
{
ok: true,
key,
key: target.canonicalKey,
compacted: true,
archived,
kept: keptLines.length,

View File

@@ -69,10 +69,10 @@ export type GatewayRequestContext = {
findRunningWizard: () => string | null;
purgeWizardSession: (id: string) => void;
getRuntimeSnapshot: () => ProviderRuntimeSnapshot;
startWhatsAppProvider: () => Promise<void>;
stopWhatsAppProvider: () => Promise<void>;
startWhatsAppProvider: (accountId?: string) => Promise<void>;
stopWhatsAppProvider: (accountId?: string) => Promise<void>;
stopTelegramProvider: () => Promise<void>;
markWhatsAppLoggedOut: (cleared: boolean) => void;
markWhatsAppLoggedOut: (cleared: boolean, accountId?: string) => void;
wizardRunner: (
opts: import("../../commands/onboard-types.js").OnboardOptions,
runtime: import("../../runtime.js").RuntimeEnv,

View File

@@ -1,4 +1,6 @@
import { loadConfig } from "../../config/config.js";
import { defaultRuntime } from "../../runtime.js";
import { resolveWhatsAppAccount } from "../../web/accounts.js";
import { startWebLoginWithQr, waitForWebLogin } from "../../web/login-qr.js";
import { logoutWeb } from "../../web/session.js";
import {
@@ -25,7 +27,11 @@ export const webHandlers: GatewayRequestHandlers = {
return;
}
try {
await context.stopWhatsAppProvider();
const accountId =
typeof (params as { accountId?: unknown }).accountId === "string"
? (params as { accountId?: string }).accountId
: undefined;
await context.stopWhatsAppProvider(accountId);
const result = await startWebLoginWithQr({
force: Boolean((params as { force?: boolean }).force),
timeoutMs:
@@ -33,6 +39,7 @@ export const webHandlers: GatewayRequestHandlers = {
? (params as { timeoutMs?: number }).timeoutMs
: undefined,
verbose: Boolean((params as { verbose?: boolean }).verbose),
accountId,
});
respond(true, result, undefined);
} catch (err) {
@@ -56,14 +63,19 @@ export const webHandlers: GatewayRequestHandlers = {
return;
}
try {
const accountId =
typeof (params as { accountId?: unknown }).accountId === "string"
? (params as { accountId?: string }).accountId
: undefined;
const result = await waitForWebLogin({
timeoutMs:
typeof (params as { timeoutMs?: unknown }).timeoutMs === "number"
? (params as { timeoutMs?: number }).timeoutMs
: undefined,
accountId,
});
if (result.connected) {
await context.startWhatsAppProvider();
await context.startWhatsAppProvider(accountId);
}
respond(true, result, undefined);
} catch (err) {
@@ -74,11 +86,26 @@ export const webHandlers: GatewayRequestHandlers = {
);
}
},
"web.logout": async ({ respond, context }) => {
"web.logout": async ({ params, respond, context }) => {
try {
await context.stopWhatsAppProvider();
const cleared = await logoutWeb(defaultRuntime);
context.markWhatsAppLoggedOut(cleared);
const rawAccountId =
params && typeof params === "object" && "accountId" in params
? (params as { accountId?: unknown }).accountId
: undefined;
const accountId =
typeof rawAccountId === "string" ? rawAccountId.trim() : "";
const cfg = loadConfig();
const account = resolveWhatsAppAccount({
cfg,
accountId: accountId || undefined,
});
await context.stopWhatsAppProvider(account.accountId);
const cleared = await logoutWeb({
authDir: account.authDir,
isLegacyAuthDir: account.isLegacyAuthDir,
runtime: defaultRuntime,
});
context.markWhatsAppLoggedOut(cleared, account.accountId);
respond(true, { cleared }, undefined);
} catch (err) {
respond(