openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
737 lines (736 loc) • 28.3 kB
JavaScript
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { r as runtimeForLogger, t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js";
import { n as normalizeAccountId, r as normalizeOptionalAccountId } from "./account-id-Df9e41E6.js";
import { t as runTasksWithConcurrency } from "./run-with-concurrency-DivrDqxu.js";
import { i as listChannelPlugins, t as getChannelPlugin } from "./registry-MkxNn1Ue.js";
import { n as resolveChannelApprovalCapability } from "./plugins-t2ejWcVy.js";
import { n as sleepWithAbort, t as computeBackoff } from "./backoff-S1yazmy1.js";
import { n as resolveNormalizedAccountEntry, t as resolveAccountEntry } from "./account-lookup-DL1YTqjF.js";
import { i as resolveChannelDefaultAccountId } from "./helpers-BLGguEiN.js";
import { r as resetDirectoryCache } from "./target-resolver-CUW6ke8W.js";
import { t as CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "./approval-handler-adapter-runtime-DLWtIqHq.js";
import { a as isExecApprovalChannelRuntimeTerminalStartError } from "./approval-native-runtime-BKbmE99v.js";
import { n as createChannelApprovalHandlerFromCapability } from "./approval-handler-runtime-CFLINLFC.js";
import { i as watchChannelRuntimeContexts, n as getChannelRuntimeContext, t as createTaskScopedChannelRuntime } from "./channel-runtime-context-Bx08uB94.js";
import { n as withPluginHttpRouteRegistry } from "./http-registry-C6KnnU9A.js";
//#region src/infra/approval-handler-bootstrap.ts
const APPROVAL_HANDLER_BOOTSTRAP_RETRY_MS = 1e3;
function isRetryableApprovalBootstrapStartError(error) {
const message = String(error);
return message.includes("gateway readiness unavailable before approval client start") || message.includes("gateway approval client start aborted before readiness") || message.includes("gateway readiness unavailable before exec approval runtime start") || message.includes("gateway approval runtime start aborted before readiness") || message.includes("gateway event loop readiness timeout") || message.includes("gateway starting") || message.includes("code=1013") || message.includes("close code 1013");
}
function formatRetryableApprovalBootstrapStartError(error) {
const message = String(error);
if (message.includes("gateway event loop readiness timeout")) return "gateway readiness unavailable before approval handler start";
return message;
}
/** Starts the native approval handler for a channel runtime context and returns its cleanup hook. */
async function startChannelApprovalHandlerBootstrap(params) {
const capability = resolveChannelApprovalCapability(params.plugin);
if (!capability?.nativeRuntime || !params.channelRuntime) return async () => {};
const channelLabel = params.plugin.meta.label || params.plugin.id;
const logger = params.logger ?? createSubsystemLogger(`${params.plugin.id}/approval-bootstrap`);
let activeGeneration = 0;
let activeHandler = null;
let retryTimer = null;
const invalidateActiveHandler = () => {
activeGeneration += 1;
};
const clearRetryTimer = () => {
if (!retryTimer) return;
clearTimeout(retryTimer);
retryTimer = null;
};
const stopHandler = async () => {
const handler = activeHandler;
activeHandler = null;
if (!handler) return;
await handler.stop();
};
const startHandlerForContext = async (context, generation) => {
if (generation !== activeGeneration) return;
await stopHandler();
if (generation !== activeGeneration) return;
const handler = await createChannelApprovalHandlerFromCapability({
capability,
label: `${params.plugin.id}/native-approvals`,
clientDisplayName: `${channelLabel} Native Approvals (${params.accountId})`,
channel: params.plugin.id,
channelLabel,
cfg: params.cfg,
accountId: params.accountId,
context
});
if (!handler) return;
if (generation !== activeGeneration) {
await handler.stop().catch(() => {});
return;
}
activeHandler = handler;
try {
await handler.start();
} catch (error) {
if (activeHandler === handler) activeHandler = null;
await handler.stop().catch(() => {});
throw error;
}
};
const spawn = (label, promise) => {
promise.catch((error) => {
logger.error(`${label}: ${String(error)}`);
});
};
const scheduleRetryForContext = (context, generation) => {
if (generation !== activeGeneration) return;
clearRetryTimer();
retryTimer = setTimeout(() => {
retryTimer = null;
if (generation !== activeGeneration) return;
spawn("failed to retry native approval handler", startHandlerForRegisteredContext(context, generation));
}, APPROVAL_HANDLER_BOOTSTRAP_RETRY_MS);
retryTimer.unref?.();
};
const startHandlerForRegisteredContext = async (context, generation) => {
try {
await startHandlerForContext(context, generation);
} catch (error) {
if (generation === activeGeneration) {
if (isExecApprovalChannelRuntimeTerminalStartError(error)) {
logger.error(`native approval handler disabled: ${String(error)}`);
return;
}
if (isRetryableApprovalBootstrapStartError(error)) {
logger.warn(`native approval handler deferred until gateway readiness recovers: ${formatRetryableApprovalBootstrapStartError(error)}`);
scheduleRetryForContext(context, generation);
return;
}
logger.error(`failed to start native approval handler: ${String(error)}`);
scheduleRetryForContext(context, generation);
}
}
};
const unsubscribe = watchChannelRuntimeContexts({
channelRuntime: params.channelRuntime,
channelId: params.plugin.id,
accountId: params.accountId,
capability: "approval.native",
onEvent: (event) => {
if (event.type === "registered") {
clearRetryTimer();
invalidateActiveHandler();
const generation = activeGeneration;
spawn("failed to start native approval handler", startHandlerForRegisteredContext(event.context, generation));
return;
}
clearRetryTimer();
invalidateActiveHandler();
spawn("failed to stop native approval handler", stopHandler());
}
}) ?? (() => {});
const existingContext = getChannelRuntimeContext({
channelRuntime: params.channelRuntime,
channelId: params.plugin.id,
accountId: params.accountId,
capability: CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY
});
if (existingContext !== void 0) {
clearRetryTimer();
invalidateActiveHandler();
spawn("failed to start native approval handler", startHandlerForRegisteredContext(existingContext, activeGeneration));
}
return async () => {
unsubscribe();
clearRetryTimer();
invalidateActiveHandler();
await stopHandler();
};
}
//#endregion
//#region src/gateway/server-channels.ts
const CHANNEL_RESTART_POLICY = {
initialMs: 5e3,
maxMs: 5 * 6e4,
factor: 2,
jitter: .1
};
const MAX_RESTART_ATTEMPTS = 10;
const CHANNEL_STOP_ABORT_TIMEOUT_MS = 5e3;
const CHANNEL_STARTUP_CONCURRENCY = 4;
function waitForChannelStartupHandoff() {
return new Promise((resolve) => {
setImmediate(resolve).unref?.();
});
}
function sanitizeAbortedTaskStatusPatch(patch, current) {
const next = { ...patch };
delete next.running;
delete next.restartPending;
delete next.reconnectAttempts;
delete next.lastStartAt;
delete next.lastStopAt;
if (next.connected === true) {
delete next.connected;
delete next.lastConnectedAt;
delete next.lastEventAt;
delete next.lastTransportActivityAt;
}
if (next.lastError === null && current.lastError) delete next.lastError;
return next;
}
function createRuntimeStore() {
return {
aborts: /* @__PURE__ */ new Map(),
starting: /* @__PURE__ */ new Map(),
tasks: /* @__PURE__ */ new Map(),
runtimes: /* @__PURE__ */ new Map()
};
}
function isAccountEnabled(account) {
if (!account || typeof account !== "object") return true;
return account.enabled !== false;
}
function resolveDefaultRuntime(channelId) {
return getChannelPlugin(channelId)?.status?.defaultRuntime ?? { accountId: "default" };
}
function cloneDefaultRuntime(channelId, accountId) {
return {
...resolveDefaultRuntime(channelId),
accountId
};
}
async function waitForChannelStopGracefully(task, timeoutMs) {
if (!task) return true;
return await new Promise((resolve) => {
let settled = false;
const timer = setTimeout(() => {
if (!settled) {
settled = true;
resolve(false);
}
}, timeoutMs);
timer.unref?.();
const resolveSettled = () => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve(true);
};
task.then(resolveSettled, resolveSettled);
});
}
function applyDescribedAccountFields(next, described) {
if (!described) {
next.configured ??= true;
return next;
}
if (typeof described.configured === "boolean") next.configured = described.configured;
else next.configured ??= true;
if (described.mode !== void 0) next.mode = described.mode;
return next;
}
async function waitForDeferredAccountStart(deferred, abortSignal) {
if (abortSignal.aborted) return;
await Promise.race([deferred, new Promise((resolve) => {
abortSignal.addEventListener("abort", () => resolve(), { once: true });
})]);
}
function createChannelManager(opts) {
const { getRuntimeConfig, channelLogs, channelRuntimeEnvs, channelRuntime, resolveChannelRuntime, getPluginHttpRouteRegistry, startupTrace } = opts;
const channelStores = /* @__PURE__ */ new Map();
const restartAttempts = /* @__PURE__ */ new Map();
const manuallyStopped = /* @__PURE__ */ new Set();
const recoveryStopTimedOut = /* @__PURE__ */ new Set();
const recoveryStartRequested = /* @__PURE__ */ new Set();
const restartKey = (channelId, accountId) => `${channelId}:${accountId}`;
const ensureChannelLog = (channelId) => {
channelLogs[channelId] ??= createSubsystemLogger("channels").child(channelId);
return channelLogs[channelId];
};
const ensureChannelRuntime = (channelId) => {
channelRuntimeEnvs[channelId] ??= runtimeForLogger(ensureChannelLog(channelId));
return channelRuntimeEnvs[channelId];
};
const resolveAccountHealthMonitorOverride = (channelConfig, accountId) => {
if (!channelConfig?.accounts) return;
const direct = resolveAccountEntry(channelConfig.accounts, accountId);
if (typeof direct?.healthMonitor?.enabled === "boolean") return direct.healthMonitor.enabled;
const normalizedAccountId = normalizeOptionalAccountId(accountId);
if (!normalizedAccountId) return;
const match = resolveNormalizedAccountEntry(channelConfig.accounts, normalizedAccountId, normalizeAccountId);
if (typeof match?.healthMonitor?.enabled !== "boolean") return;
return match.healthMonitor.enabled;
};
const isHealthMonitorEnabled = (channelId, accountId) => {
const cfg = getRuntimeConfig();
const channelConfig = cfg.channels?.[channelId];
const accountOverride = resolveAccountHealthMonitorOverride(channelConfig, accountId);
const channelOverride = channelConfig?.healthMonitor?.enabled;
if (typeof accountOverride === "boolean") return accountOverride;
if (typeof channelOverride === "boolean") return channelOverride;
const plugin = getChannelPlugin(channelId);
if (!plugin) return true;
try {
plugin.config.resolveAccount(cfg, accountId);
} catch (err) {
ensureChannelLog(channelId).warn?.(`[${channelId}:${accountId}] health-monitor: failed to resolve account; skipping monitor (${formatErrorMessage(err)})`);
return false;
}
return true;
};
const getStore = (channelId) => {
const existing = channelStores.get(channelId);
if (existing) return existing;
const next = createRuntimeStore();
channelStores.set(channelId, next);
return next;
};
const getRuntime = (channelId, accountId) => {
return getStore(channelId).runtimes.get(accountId) ?? cloneDefaultRuntime(channelId, accountId);
};
const setRuntime = (channelId, accountId, patch) => {
const store = getStore(channelId);
const next = {
...getRuntime(channelId, accountId),
...patch,
accountId
};
store.runtimes.set(accountId, next);
return next;
};
const setRuntimeFromTaskStatus = (channelId, accountId, patch, abortSignal) => {
return setRuntime(channelId, accountId, abortSignal?.aborted ? sanitizeAbortedTaskStatusPatch(patch, getRuntime(channelId, accountId)) : patch);
};
const setStoppedRuntime = (channelId, accountId, patch = {}) => {
return setRuntime(channelId, accountId, {
accountId,
running: false,
...typeof getRuntime(channelId, accountId).connected === "boolean" ? { connected: false } : {},
...patch
});
};
const getChannelRuntime = async () => {
if (channelRuntime) return channelRuntime;
return await resolveChannelRuntime?.();
};
const measureStartup = async (name, run) => {
return startupTrace ? startupTrace.measure(name, run) : await run();
};
const evictStaleChannelAccountState = (channelId, store, accountIds) => {
const activeAccountIds = new Set(accountIds);
for (const id of store.runtimes.keys()) {
if (activeAccountIds.has(id) || store.aborts.has(id) || store.starting.has(id) || store.tasks.has(id)) continue;
store.runtimes.delete(id);
restartAttempts.delete(restartKey(channelId, id));
manuallyStopped.delete(restartKey(channelId, id));
recoveryStartRequested.delete(restartKey(channelId, id));
}
};
const startChannelInternal = async (channelId, accountId, optsValue = {}) => {
const plugin = getChannelPlugin(channelId);
const startAccount = plugin?.gateway?.startAccount;
if (!startAccount) return;
const { preserveRestartAttempts = false, preserveManualStop = false } = optsValue;
const cfg = getRuntimeConfig();
resetDirectoryCache({
channel: channelId,
accountId
});
const store = getStore(channelId);
const accountIds = accountId ? [accountId] : await measureStartup(`channels.${channelId}.list-accounts`, () => plugin.config.listAccountIds(cfg));
if (!accountId) evictStaleChannelAccountState(channelId, store, accountIds);
if (accountIds.length === 0) return;
const startup = await runTasksWithConcurrency({
limit: CHANNEL_STARTUP_CONCURRENCY,
tasks: accountIds.map((id) => async () => {
const rKey = restartKey(channelId, id);
if (store.tasks.has(id)) {
if (recoveryStopTimedOut.has(rKey)) {
if (!preserveManualStop) manuallyStopped.delete(rKey);
if (manuallyStopped.has(rKey)) return;
recoveryStartRequested.add(rKey);
setRuntime(channelId, id, {
accountId: id,
restartPending: true
});
}
return;
}
const existingStart = store.starting.get(id);
if (existingStart) {
await existingStart;
return;
}
let resolveStart;
const startGate = new Promise((resolve) => {
resolveStart = resolve;
});
store.starting.set(id, startGate);
const abort = new AbortController();
store.aborts.set(id, abort);
let handedOffTask = false;
const log = ensureChannelLog(channelId);
const runtime = ensureChannelRuntime(channelId);
let scopedChannelRuntime = null;
let channelRuntimeForTask;
let stopApprovalBootstrap = async () => {};
const stopTaskScopedApprovalRuntime = async () => {
const scopedRuntime = scopedChannelRuntime;
scopedChannelRuntime = null;
const stopBootstrap = stopApprovalBootstrap;
stopApprovalBootstrap = async () => {};
scopedRuntime?.dispose();
await stopBootstrap();
};
const cleanupTaskScopedApprovalRuntime = async (label) => {
try {
await stopTaskScopedApprovalRuntime();
} catch (error) {
log.error?.(`[${id}] ${label}: ${formatErrorMessage(error)}`);
}
};
try {
const account = plugin.config.resolveAccount(cfg, id);
if (!(plugin.config.isEnabled ? plugin.config.isEnabled(account, cfg) : isAccountEnabled(account))) {
setRuntime(channelId, id, {
accountId: id,
enabled: false,
configured: true,
running: false,
restartPending: false,
lastError: plugin.config.disabledReason?.(account, cfg) ?? "disabled"
});
return;
}
let configured = true;
if (plugin.config.isConfigured) configured = await measureStartup(`channels.${channelId}.is-configured`, () => plugin.config.isConfigured(account, cfg));
if (!configured) {
setRuntime(channelId, id, {
accountId: id,
enabled: true,
configured: false,
running: false,
restartPending: false,
lastError: plugin.config.unconfiguredReason?.(account, cfg) ?? "not configured"
});
return;
}
if (!preserveManualStop) manuallyStopped.delete(rKey);
if (abort.signal.aborted || manuallyStopped.has(rKey)) {
setStoppedRuntime(channelId, id, {
restartPending: false,
lastStopAt: Date.now()
});
return;
}
scopedChannelRuntime = await measureStartup(`channels.${channelId}.runtime`, async () => createTaskScopedChannelRuntime({ channelRuntime: await getChannelRuntime() }));
channelRuntimeForTask = scopedChannelRuntime.channelRuntime;
if (!preserveRestartAttempts) restartAttempts.delete(rKey);
try {
stopApprovalBootstrap = await measureStartup(`channels.${channelId}.approval-bootstrap`, () => startChannelApprovalHandlerBootstrap({
plugin,
cfg,
accountId: id,
channelRuntime: channelRuntimeForTask,
logger: log
}));
} catch (error) {
log.error?.(`[${id}] native approval bootstrap failed: ${formatErrorMessage(error)}`);
}
setRuntime(channelId, id, {
accountId: id,
enabled: true,
configured: true,
running: true,
restartPending: false,
lastStartAt: Date.now(),
lastError: null,
reconnectAttempts: preserveRestartAttempts ? restartAttempts.get(rKey) ?? 0 : 0
});
const trackedPromise = Promise.resolve().then(async () => {
if (optsValue.deferAccountStartUntil) await waitForDeferredAccountStart(optsValue.deferAccountStartUntil, abort.signal);
else if (startupTrace) await waitForChannelStartupHandoff();
if (abort.signal.aborted || manuallyStopped.has(rKey)) return;
let startAccountTask;
await measureStartup(`channels.${channelId}.start-account-handoff`, () => {
if (abort.signal.aborted || manuallyStopped.has(rKey)) return;
const runStartAccount = () => startAccount({
cfg,
accountId: id,
account,
runtime,
abortSignal: abort.signal,
log,
getStatus: () => getRuntime(channelId, id),
setStatus: (next) => setRuntimeFromTaskStatus(channelId, id, next, abort.signal),
...channelRuntimeForTask ? { channelRuntime: channelRuntimeForTask } : {}
});
const routeRegistry = getPluginHttpRouteRegistry?.();
startAccountTask = routeRegistry ? withPluginHttpRouteRegistry(routeRegistry, runStartAccount) : runStartAccount();
});
if (!startAccountTask) return;
await startAccountTask;
}).then(() => {
if (abort.signal.aborted || manuallyStopped.has(rKey)) return;
const message = "channel exited without an error";
setRuntime(channelId, id, {
accountId: id,
lastError: message
});
log.error?.(`[${id}] ${message}`);
}).catch((err) => {
const message = formatErrorMessage(err);
setRuntime(channelId, id, {
accountId: id,
lastError: message
});
log.error?.(`[${id}] channel exited: ${message}`);
}).then(async () => {
await cleanupTaskScopedApprovalRuntime("channel cleanup failed");
setStoppedRuntime(channelId, id, { lastStopAt: Date.now() });
}).then(async () => {
if (manuallyStopped.has(rKey)) {
recoveryStopTimedOut.delete(rKey);
recoveryStartRequested.delete(rKey);
return;
}
if (recoveryStopTimedOut.has(rKey)) {
recoveryStopTimedOut.delete(rKey);
if (!recoveryStartRequested.delete(rKey)) {
setRuntime(channelId, id, {
accountId: id,
restartPending: false,
reconnectAttempts: 0
});
if (store.tasks.get(id) === trackedPromise) store.tasks.delete(id);
if (store.aborts.get(id) === abort) store.aborts.delete(id);
return;
}
restartAttempts.delete(rKey);
log.info?.(`[${id}] restarting after timed-out channel stop completed`);
setRuntime(channelId, id, {
accountId: id,
restartPending: true,
reconnectAttempts: 0
});
if (store.tasks.get(id) === trackedPromise) store.tasks.delete(id);
if (store.aborts.get(id) === abort) store.aborts.delete(id);
try {
await startChannelInternal(channelId, id, { preserveManualStop: true });
} catch {}
return;
}
const attempt = (restartAttempts.get(rKey) ?? 0) + 1;
restartAttempts.set(rKey, attempt);
if (attempt > MAX_RESTART_ATTEMPTS) {
setRuntime(channelId, id, {
accountId: id,
restartPending: false,
reconnectAttempts: attempt
});
log.error?.(`[${id}] giving up after ${MAX_RESTART_ATTEMPTS} restart attempts`);
return;
}
const delayMs = computeBackoff(CHANNEL_RESTART_POLICY, attempt);
log.info?.(`[${id}] auto-restart attempt ${attempt}/${MAX_RESTART_ATTEMPTS} in ${Math.round(delayMs / 1e3)}s`);
setRuntime(channelId, id, {
accountId: id,
restartPending: true,
reconnectAttempts: attempt
});
try {
await sleepWithAbort(delayMs, abort.signal);
if (manuallyStopped.has(rKey)) return;
if (store.tasks.get(id) === trackedPromise) store.tasks.delete(id);
if (store.aborts.get(id) === abort) store.aborts.delete(id);
await startChannelInternal(channelId, id, {
preserveRestartAttempts: true,
preserveManualStop: true
});
} catch {}
}).finally(() => {
if (store.tasks.get(id) === trackedPromise) store.tasks.delete(id);
if (store.aborts.get(id) === abort) store.aborts.delete(id);
});
handedOffTask = true;
store.tasks.set(id, trackedPromise);
} catch (error) {
if (!handedOffTask) setStoppedRuntime(channelId, id, {
restartPending: false,
lastError: formatErrorMessage(error)
});
throw error;
} finally {
resolveStart?.();
if (store.starting.get(id) === startGate) store.starting.delete(id);
if (!handedOffTask) await cleanupTaskScopedApprovalRuntime("channel startup cleanup failed");
if (!handedOffTask && store.aborts.get(id) === abort) store.aborts.delete(id);
}
})
});
if (startup.hasError) throw startup.firstError;
};
const startChannel = async (channelId, accountId) => {
await startChannelInternal(channelId, accountId);
};
const stopChannel = async (channelId, accountId, optsLocal = {}) => {
const manual = optsLocal.manual ?? true;
const plugin = getChannelPlugin(channelId);
const store = getStore(channelId);
const lifecycleIds = new Set([
...store.aborts.keys(),
...store.starting.keys(),
...store.tasks.keys()
]);
if (!accountId && lifecycleIds.size === 0) return;
if (!plugin?.gateway?.stopAccount && lifecycleIds.size === 0) return;
const cfg = getRuntimeConfig();
const knownIds = new Set([...lifecycleIds, ...plugin ? plugin.config.listAccountIds(cfg) : []]);
if (accountId) {
knownIds.clear();
knownIds.add(accountId);
}
await Promise.all(Array.from(knownIds.values()).map(async (id) => {
const abort = store.aborts.get(id);
const task = store.tasks.get(id);
if (!abort && !task && !plugin?.gateway?.stopAccount) return;
const rKey = restartKey(channelId, id);
if (manual) manuallyStopped.add(rKey);
abort?.abort();
const log = ensureChannelLog(channelId);
const runtime = ensureChannelRuntime(channelId);
if (plugin?.gateway?.stopAccount) {
const account = plugin.config.resolveAccount(cfg, id);
await plugin.gateway.stopAccount({
cfg,
accountId: id,
account,
runtime,
abortSignal: abort?.signal ?? new AbortController().signal,
log,
getStatus: () => getRuntime(channelId, id),
setStatus: (next) => setRuntime(channelId, id, next)
});
}
if (!await waitForChannelStopGracefully(task, CHANNEL_STOP_ABORT_TIMEOUT_MS)) {
log.warn?.(`[${id}] channel stop exceeded ${CHANNEL_STOP_ABORT_TIMEOUT_MS}ms after abort; continuing shutdown`);
const stoppedPatch = {
restartPending: !manual,
lastError: `channel stop timed out after ${CHANNEL_STOP_ABORT_TIMEOUT_MS}ms`
};
if (manual) setRuntime(channelId, id, {
accountId: id,
running: true,
...stoppedPatch
});
else setStoppedRuntime(channelId, id, stoppedPatch);
if (!manual) recoveryStopTimedOut.add(rKey);
return;
}
recoveryStopTimedOut.delete(rKey);
recoveryStartRequested.delete(rKey);
store.aborts.delete(id);
store.tasks.delete(id);
setStoppedRuntime(channelId, id, {
restartPending: false,
lastStopAt: Date.now()
});
}));
};
const startChannels = async () => {
let releaseAccountStarts;
const deferAccountStartUntil = opts.deferStartupAccountStartsUntil ?? (startupTrace ? new Promise((resolve) => {
releaseAccountStarts = () => {
setImmediate(resolve).unref?.();
};
}) : void 0);
try {
await runTasksWithConcurrency({
limit: CHANNEL_STARTUP_CONCURRENCY,
tasks: [...listChannelPlugins()].map((plugin) => async () => {
try {
await measureStartup(`channels.${plugin.id}.start`, () => startChannelInternal(plugin.id, void 0, deferAccountStartUntil ? { deferAccountStartUntil } : {}));
} catch (err) {
ensureChannelLog(plugin.id).error?.(`[${plugin.id}] channel startup failed: ${formatErrorMessage(err)}`);
}
})
});
} finally {
releaseAccountStarts?.();
}
};
const markChannelLoggedOut = (channelId, cleared, accountId) => {
const plugin = getChannelPlugin(channelId);
if (!plugin) return;
const cfg = getRuntimeConfig();
const resolvedId = accountId ?? resolveChannelDefaultAccountId({
plugin,
cfg
});
const current = getRuntime(channelId, resolvedId);
const next = {
accountId: resolvedId,
running: false,
restartPending: false,
lastError: cleared ? "logged out" : current.lastError
};
if (typeof current.connected === "boolean") next.connected = false;
setRuntime(channelId, resolvedId, next);
};
const getRuntimeSnapshot = () => {
const cfg = getRuntimeConfig();
const channels = {};
const channelAccounts = {};
for (const plugin of listChannelPlugins()) {
const store = getStore(plugin.id);
const accountIds = plugin.config.listAccountIds(cfg);
const defaultAccountId = resolveChannelDefaultAccountId({
plugin,
cfg,
accountIds
});
const accounts = {};
for (const id of accountIds) {
const account = plugin.config.resolveAccount(cfg, id);
const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, cfg) : isAccountEnabled(account);
const described = plugin.config.describeAccount?.(account, cfg);
const next = {
...store.runtimes.get(id) ?? cloneDefaultRuntime(plugin.id, id),
accountId: id
};
next.enabled = enabled;
applyDescribedAccountFields(next, described);
const configured = described?.configured;
if (!next.running) {
if (!enabled) next.lastError ??= plugin.config.disabledReason?.(account, cfg) ?? "disabled";
else if (configured === false) next.lastError ??= plugin.config.unconfiguredReason?.(account, cfg) ?? "not configured";
}
accounts[id] = next;
}
const defaultAccount = accounts[defaultAccountId] ?? cloneDefaultRuntime(plugin.id, defaultAccountId);
channels[plugin.id] = defaultAccount;
channelAccounts[plugin.id] = accounts;
}
return {
channels,
channelAccounts
};
};
const isManuallyStoppedFlag = (channelId, accountId) => {
return manuallyStopped.has(restartKey(channelId, accountId));
};
const resetRestartAttemptsForTest = (channelId, accountId) => {
restartAttempts.delete(restartKey(channelId, accountId));
};
return {
getRuntimeSnapshot,
startChannels,
startChannel,
stopChannel,
markChannelLoggedOut,
isManuallyStopped: isManuallyStoppedFlag,
resetRestartAttempts: resetRestartAttemptsForTest,
isHealthMonitorEnabled
};
}
//#endregion
export { createChannelManager };