fix(channels): improve health metadata and reply diagnostics

This commit is contained in:
Peter Steinberger
2026-04-29 16:27:14 +01:00
parent 1390eadd92
commit 4dd2768c4b
6 changed files with 432 additions and 106 deletions

View File

@@ -92,6 +92,7 @@ Docs: https://docs.openclaw.ai
- Gateway/hooks: keep successful `deliver:false` agent hooks silent, log a hook audit record for suppressed success announcements, and suppress fallback summaries after attempted hook delivery while still surfacing failed hook runs. Repairs #55761; builds on #36332 and #49234. Thanks @EffortlessSteven, @cioclawcode, and @BrennerSpear.
- Plugin SDK/Discord: restore a deprecated `openclaw/plugin-sdk/discord` compatibility facade and the legacy compat group-policy warning export for the published `@openclaw/discord@2026.3.13` package, covering its config, account, directory, status, and thread-binding imports while keeping new plugins on generic SDK subpaths. Fixes #73685; supersedes #73703. Thanks @rderickson9 and @SymbolStar.
- Channels/Discord: suppress duplicate gateway monitors when multiple enabled accounts resolve to the same bot token, preferring config tokens over default env fallback and reporting skipped duplicates as disabled. Supersedes #73608. Thanks @kagura-agent.
- CLI/health: build channel health summaries from inspected credential metadata plus runtime state, so `openclaw health --json` reports Discord `running`, `connected`, and `tokenSource` consistently with channel status. Fixes #44354. Thanks @ferenc-acs.
- Control UI/Talk: decode Google Live binary WebSocket JSON frames and stop queued browser audio on interruption or shutdown, so browser Talk leaves `Connecting Talk...` and barge-in no longer plays stale audio. Fixes #73601 and #73460; supersedes #73466. Thanks @Spolen23 and @WadydX.
- Channels/Discord: ignore stale route-shaped conversation bindings after a Discord channel is reconfigured to another agent, while preserving explicit focus and subagent bindings. Fixes #73626. Thanks @ramitrkar-hash.
- Agents/bootstrap: pass pending BOOTSTRAP.md contents through the first-run user prompt while keeping them out of privileged system context, and show limited bootstrap guidance when workspace file access is unavailable. Fixes #73622. Thanks @mark1010.
@@ -104,6 +105,7 @@ Docs: https://docs.openclaw.ai
- Plugins/runtime-deps: cache bundled runtime-deps JSON/package files and root chunk import scans by file signature, reducing repeated staged-runtime scanning during bundled channel startup. Refs #73647 and #73705. Thanks @mattmcintyre and @bmilne1981.
- CLI/TUI: keep `chat.history` off model-catalog discovery so initial Gateway-backed TUI history loads cannot block behind slow provider/plugin model scans on low-core hosts. Refs #73524. Thanks @harshcatsystems-collab.
- Channels/WhatsApp: flag recently reconnected linked accounts in channel status even when the socket is currently healthy, so flapping WhatsApp Web sessions no longer look clean after a brief reconnect. Refs #73602. Thanks @Vksh07.
- Channels/WhatsApp: log shared dispatcher delivery failures with reply kind, message id, chat id, and connection id, so typing-without-send reports can identify whether the WhatsApp send path rejected a generated reply. Refs #74269. Thanks @tomcosta-git.
- Feishu: suppress distinct late `final` text deliveries after a streaming card has already closed, while keeping media attachments deliverable, so late-finals no longer reopen duplicate Feishu cards. Fixes #71977. (#72294) Thanks @MonkeyLeeT.
- Gateway: expose `gateway.handshakeTimeoutMs` in config, schema, and docs while preserving `OPENCLAW_HANDSHAKE_TIMEOUT_MS` precedence, so loaded or low-powered hosts can tune local WebSocket pre-auth handshakes without patching dist files. Supersedes #51282; refs #73592 and #73652. Thanks @henry-the-frog.
- Gateway/TUI/status: align configured and env-based WebSocket handshake budgets across local clients, probes, and fallback RPCs while preserving explicit status timeouts and paired-device auth fallback, so slow local gateways are not marked unreachable by a shorter client watchdog. Refs #73524, #73535, #73592, and #73602. Thanks @harshcatsystems-collab, @DJBlackhawk, and @Vksh07.

View File

@@ -118,6 +118,16 @@ function getCapturedDeliver() {
)?.dispatcherOptions?.deliver;
}
function getCapturedOnError() {
return (
capturedDispatchParams as {
dispatcherOptions?: {
onError?: (err: unknown, info: { kind: "tool" | "block" | "final" }) => void;
};
}
)?.dispatcherOptions?.onError;
}
type BufferedReplyParams = Parameters<typeof dispatchWhatsAppBufferedReply>[0];
function makeReplyLogger(): BufferedReplyParams["replyLogger"] {
@@ -704,6 +714,44 @@ describe("whatsapp inbound dispatch", () => {
).toBe(sendComposing);
});
it("logs delivery failures from the shared dispatcher with WhatsApp context", async () => {
const replyLogger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
} as BufferedReplyParams["replyLogger"];
const error = new Error("send failed");
await dispatchBufferedReply({
connectionId: "conn-1",
conversationId: "+15550001000",
msg: makeMsg({
id: "msg-1",
from: "+15550001000",
to: "+15550002000",
chatId: "15550001000@s.whatsapp.net",
}),
replyLogger,
});
getCapturedOnError()?.(error, { kind: "final" });
expect(replyLogger.error).toHaveBeenCalledWith(
{
err: error,
replyKind: "final",
correlationId: "msg-1",
connectionId: "conn-1",
conversationId: "+15550001000",
chatId: "15550001000@s.whatsapp.net",
to: "+15550001000",
from: "+15550002000",
},
"auto-reply delivery failed",
);
});
it("updates main last route for DM when session key matches main session key", () => {
const updateLastRoute = vi.fn();

View File

@@ -56,6 +56,29 @@ type SenderContext = {
e164?: string;
};
function logWhatsAppReplyDeliveryError(params: {
err: unknown;
info: { kind: ReplyLifecycleKind };
connectionId: string;
conversationId: string;
msg: WebInboundMsg;
replyLogger: ReturnType<typeof getChildLogger>;
}) {
params.replyLogger.error(
{
err: params.err,
replyKind: params.info.kind,
correlationId: params.msg.id ?? null,
connectionId: params.connectionId,
conversationId: params.conversationId,
chatId: params.msg.chatId ?? null,
to: params.msg.from ?? null,
from: params.msg.to ?? null,
},
"auto-reply delivery failed",
);
}
function resolveWhatsAppDisableBlockStreaming(cfg: ReturnType<LoadConfigFn>): boolean | undefined {
if (typeof cfg.channels?.whatsapp?.blockStreaming !== "boolean") {
return undefined;
@@ -355,6 +378,16 @@ export async function dispatchWhatsAppBufferedReply(params: {
}
},
onReplyStart: params.msg.sendComposing,
onError: (err, info) => {
logWhatsAppReplyDeliveryError({
err,
info,
connectionId: params.connectionId,
conversationId: params.conversationId,
msg: params.msg,
replyLogger: params.replyLogger,
});
},
},
replyOptions: {
disableBlockStreaming,

View File

@@ -1,12 +1,12 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { inspectChannelAccount } from "../account-inspection.js";
import { projectSafeChannelAccountSnapshotFields } from "../account-snapshot-fields.js";
import { inspectReadOnlyChannelAccount } from "../read-only-account-inspect.js";
import type { ChannelPlugin } from "./types.plugin.js";
import type { ChannelAccountSnapshot } from "./types.public.js";
// Channel docking: status snapshots flow through plugin.status hooks here.
async function buildSnapshotFromAccount<ResolvedAccount>(params: {
export async function buildChannelAccountSnapshotFromAccount<ResolvedAccount>(params: {
plugin: ChannelPlugin<ResolvedAccount>;
cfg: OpenClawConfig;
accountId: string;
@@ -14,52 +14,46 @@ async function buildSnapshotFromAccount<ResolvedAccount>(params: {
runtime?: ChannelAccountSnapshot;
probe?: unknown;
audit?: unknown;
enabledFallback?: boolean;
configuredFallback?: boolean;
}): Promise<ChannelAccountSnapshot> {
let snapshot: ChannelAccountSnapshot;
if (params.plugin.status?.buildAccountSnapshot) {
const snapshot = await params.plugin.status.buildAccountSnapshot({
snapshot = await params.plugin.status.buildAccountSnapshot({
account: params.account,
cfg: params.cfg,
runtime: params.runtime,
probe: params.probe,
audit: params.audit,
});
return normalizeOptionalString(snapshot.accountId)
? snapshot
: {
...snapshot,
accountId: params.accountId,
};
}
const enabled = params.plugin.config.isEnabled
? params.plugin.config.isEnabled(params.account, params.cfg)
: params.account && typeof params.account === "object"
? (params.account as { enabled?: boolean }).enabled
: undefined;
const configured =
params.account && typeof params.account === "object" && "configured" in params.account
? (params.account as { configured?: boolean }).configured
: params.plugin.config.isConfigured
? await params.plugin.config.isConfigured(params.account, params.cfg)
} else {
const enabled = params.plugin.config.isEnabled
? params.plugin.config.isEnabled(params.account, params.cfg)
: params.account && typeof params.account === "object"
? (params.account as { enabled?: boolean }).enabled
: undefined;
return {
accountId: params.accountId,
enabled,
configured,
...projectSafeChannelAccountSnapshotFields(params.account),
};
}
async function inspectChannelAccount<ResolvedAccount>(params: {
plugin: ChannelPlugin<ResolvedAccount>;
cfg: OpenClawConfig;
accountId: string;
}): Promise<ResolvedAccount | null> {
return (params.plugin.config.inspectAccount?.(params.cfg, params.accountId) ??
(await inspectReadOnlyChannelAccount({
channelId: params.plugin.id,
cfg: params.cfg,
const configured =
params.account && typeof params.account === "object" && "configured" in params.account
? (params.account as { configured?: boolean }).configured
: params.plugin.config.isConfigured
? await params.plugin.config.isConfigured(params.account, params.cfg)
: undefined;
snapshot = {
accountId: params.accountId,
}))) as ResolvedAccount | null;
enabled,
configured,
...projectSafeChannelAccountSnapshotFields(params.account),
...projectSafeChannelAccountSnapshotFields(params.runtime),
};
}
return {
...snapshot,
accountId: normalizeOptionalString(snapshot.accountId) ? snapshot.accountId : params.accountId,
enabled: snapshot.enabled ?? params.enabledFallback,
configured: snapshot.configured ?? params.configuredFallback,
...(params.probe !== undefined && snapshot.probe === undefined ? { probe: params.probe } : {}),
};
}
export async function buildReadOnlySourceChannelAccountSnapshot<ResolvedAccount>(params: {
@@ -74,7 +68,7 @@ export async function buildReadOnlySourceChannelAccountSnapshot<ResolvedAccount>
if (!inspectedAccount) {
return null;
}
return await buildSnapshotFromAccount({
return await buildChannelAccountSnapshotFromAccount({
...params,
account: inspectedAccount as ResolvedAccount,
});
@@ -91,7 +85,7 @@ export async function buildChannelAccountSnapshot<ResolvedAccount>(params: {
const inspectedAccount = await inspectChannelAccount(params);
const account =
inspectedAccount ?? params.plugin.config.resolveAccount(params.cfg, params.accountId);
return await buildSnapshotFromAccount({
return await buildChannelAccountSnapshotFromAccount({
...params,
account,
});

View File

@@ -2,12 +2,14 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { ChannelAccountSnapshot } from "../channels/plugins/types.js";
import type { ChannelPlugin } from "../channels/plugins/types.js";
import { createPluginRecord } from "../plugins/status.test-helpers.js";
import type { HealthSummary } from "./health.js";
let testConfig: Record<string, unknown> = {};
let testStore: Record<string, { updatedAt?: number }> = {};
let healthPluginsForTest: HealthTestPlugin[] = [];
let setActivePluginRegistry: typeof import("../plugins/runtime.js").setActivePluginRegistry;
let createChannelTestPluginBase: typeof import("../test-utils/channel-plugins.js").createChannelTestPluginBase;
@@ -18,6 +20,8 @@ let probeTelegramAccountForTestOverride:
| ((account: TelegramHealthAccount, timeoutMs: number) => Promise<Record<string, unknown>>)
| undefined;
type HealthTestPlugin = Pick<ChannelPlugin, "id" | "meta" | "capabilities" | "config" | "status">;
type TelegramHealthAccount = {
accountId: string;
token: string;
@@ -29,6 +33,15 @@ type TelegramHealthAccount = {
};
};
type DiscordHealthAccount = {
accountId: string;
token: string;
tokenSource: string;
tokenStatus?: "available" | "configured_unavailable" | "missing";
enabled: boolean;
configured: boolean;
};
async function loadFreshHealthModulesForTest() {
vi.doMock("../config/config.js", () => ({
getRuntimeConfig: () => testConfig,
@@ -57,7 +70,7 @@ async function loadFreshHealthModulesForTest() {
logoutWeb: vi.fn(),
}));
vi.doMock("../channels/plugins/read-only.js", () => ({
listReadOnlyChannelPluginsForConfig: () => [createTelegramHealthPlugin()],
listReadOnlyChannelPluginsForConfig: () => healthPluginsForTest,
}));
const [pluginsRuntime, channelTestUtils, health] = await Promise.all([
@@ -280,10 +293,7 @@ async function runSuccessfulTelegramProbe(
return { calls, telegram };
}
function createTelegramHealthPlugin(): Pick<
ChannelPlugin,
"id" | "meta" | "capabilities" | "config" | "status"
> {
function createTelegramHealthPlugin(): HealthTestPlugin {
return {
...createChannelTestPluginBase({ id: "telegram", label: "Telegram" }),
config: {
@@ -303,6 +313,91 @@ function createTelegramHealthPlugin(): Pick<
};
}
function resolveDiscordHealthAccountForTest(params: {
cfg: Record<string, unknown>;
accountId?: string | null;
}): DiscordHealthAccount {
const channels = params.cfg.channels as Record<string, unknown> | undefined;
const discord = (channels?.discord as Record<string, unknown> | undefined) ?? {};
const accountId = params.accountId?.trim() || "default";
const token = typeof discord.token === "string" ? discord.token.trim() : "";
return {
accountId,
token,
tokenSource: token ? "config" : "none",
...(token ? { tokenStatus: "available" as const } : {}),
enabled: discord.enabled !== false,
configured: Boolean(token),
};
}
function inspectDiscordHealthAccountForTest(params: {
cfg: Record<string, unknown>;
accountId?: string | null;
}): DiscordHealthAccount {
const channels = params.cfg.channels as Record<string, unknown> | undefined;
const discord = (channels?.discord as Record<string, unknown> | undefined) ?? {};
const accountId = params.accountId?.trim() || "default";
const token = typeof discord.token === "string" ? discord.token.trim() : "";
const tokenStatus =
token.length > 0
? "available"
: discord.token && typeof discord.token === "object"
? "configured_unavailable"
: "missing";
return {
accountId,
token,
tokenSource: tokenStatus === "missing" ? "none" : "config",
tokenStatus,
enabled: discord.enabled !== false,
configured: tokenStatus !== "missing",
};
}
function createDiscordHealthPlugin(): HealthTestPlugin {
return {
...createChannelTestPluginBase({ id: "discord", label: "Discord" }),
config: {
listAccountIds: () => ["default"],
resolveAccount: (cfg, accountId) =>
resolveDiscordHealthAccountForTest({
cfg: cfg as Record<string, unknown>,
accountId,
}),
inspectAccount: (cfg, accountId) =>
inspectDiscordHealthAccountForTest({
cfg: cfg as Record<string, unknown>,
accountId,
}),
isEnabled: (account) => (account as DiscordHealthAccount).enabled,
isConfigured: (account) => (account as DiscordHealthAccount).configured,
},
status: {
buildAccountSnapshot: ({ account, runtime }) => {
const resolved = account as DiscordHealthAccount;
return {
accountId: resolved.accountId,
enabled: resolved.enabled,
configured: resolved.configured,
tokenSource: resolved.tokenSource,
tokenStatus: resolved.tokenStatus,
running: runtime?.running ?? false,
connected: runtime?.connected ?? false,
lastConnectedAt: runtime?.lastConnectedAt ?? null,
} satisfies ChannelAccountSnapshot;
},
buildChannelSummary: ({ snapshot }) => ({
configured: snapshot.configured ?? false,
tokenSource: snapshot.tokenSource ?? "none",
tokenStatus: snapshot.tokenStatus,
running: snapshot.running ?? false,
connected: snapshot.connected ?? false,
}),
},
};
}
describe("getHealthSnapshot", () => {
beforeAll(async () => {
({
@@ -316,6 +411,7 @@ describe("getHealthSnapshot", () => {
beforeEach(() => {
buildTelegramHealthSummaryForTest = buildTelegramHealthSummary;
probeTelegramAccountForTestOverride = undefined;
healthPluginsForTest = [createTelegramHealthPlugin()];
setActivePluginRegistry(
createTestRegistry([
{ pluginId: "telegram", plugin: createTelegramHealthPlugin(), source: "test" },
@@ -477,6 +573,111 @@ describe("getHealthSnapshot", () => {
expect(telegram.accounts?.default?.probe?.ok).toBe(true);
});
it("merges inspected account metadata with runtime state before building health summaries", async () => {
testConfig = { channels: { discord: { token: "discord-token" } } };
testStore = {};
healthPluginsForTest = [createDiscordHealthPlugin()];
const snap = await getHealthSnapshot({
probe: false,
includeSensitive: false,
runtimeSnapshot: {
channels: {
discord: {
accountId: "default",
running: true,
connected: true,
lastConnectedAt: 123,
},
},
channelAccounts: {},
},
});
const discord = snap.channels.discord as {
configured?: boolean;
running?: boolean;
connected?: boolean;
tokenSource?: string;
tokenStatus?: string;
accounts?: Record<
string,
{
configured?: boolean;
running?: boolean;
connected?: boolean;
tokenSource?: string;
tokenStatus?: string;
}
>;
};
expect(discord.configured).toBe(true);
expect(discord.running).toBe(true);
expect(discord.connected).toBe(true);
expect(discord.tokenSource).toBe("config");
expect(discord.tokenStatus).toBe("available");
expect(discord.accounts?.default).toMatchObject({
configured: true,
running: true,
connected: true,
tokenSource: "config",
tokenStatus: "available",
});
});
it("preserves plugin-derived configured state for unavailable SecretRef credentials", async () => {
testConfig = {
channels: {
discord: {
token: {
source: "env",
provider: "default",
id: "MISSING_DISCORD_BOT_TOKEN",
},
},
},
};
testStore = {};
healthPluginsForTest = [createDiscordHealthPlugin()];
const snap = await getHealthSnapshot({
probe: false,
includeSensitive: false,
runtimeSnapshot: {
channels: {
discord: {
accountId: "default",
running: true,
connected: true,
},
},
channelAccounts: {},
},
});
const discord = snap.channels.discord as {
configured?: boolean;
tokenSource?: string;
tokenStatus?: string;
accounts?: Record<
string,
{
configured?: boolean;
tokenSource?: string;
tokenStatus?: string;
}
>;
};
expect(discord.configured).toBe(true);
expect(discord.tokenSource).toBe("config");
expect(discord.tokenStatus).toBe("configured_unavailable");
expect(discord.accounts?.default).toMatchObject({
configured: true,
tokenSource: "config",
tokenStatus: "configured_unavailable",
});
});
it("omits secret runtime fields and raw probe payloads from non-sensitive health snapshots", async () => {
testConfig = { channels: { telegram: { botToken: "t-1" } } };
testStore = {};

View File

@@ -1,10 +1,14 @@
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import { projectSafeChannelAccountSnapshotFields } from "../channels/account-snapshot-fields.js";
import { inspectChannelAccount } from "../channels/account-inspection.js";
import {
resolveChannelAccountConfigured,
resolveChannelAccountEnabled,
} from "../channels/account-summary.js";
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
import { listReadOnlyChannelPluginsForConfig } from "../channels/plugins/read-only.js";
import { buildChannelAccountSnapshotFromAccount } from "../channels/plugins/status.js";
import type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
import type { ChannelAccountSnapshot } from "../channels/plugins/types.public.js";
import { inspectReadOnlyChannelAccount } from "../channels/read-only-account-inspect.js";
import { withProgress } from "../cli/progress.js";
import { getRuntimeConfig } from "../config/config.js";
import { resolveStorePath } from "../config/sessions/paths.js";
@@ -176,17 +180,6 @@ function buildPluginHealthSummary(): PluginHealthSummary | undefined {
return { loaded, errors };
}
async function inspectHealthAccount(plugin: ChannelPlugin, cfg: OpenClawConfig, accountId: string) {
return (
plugin.config.inspectAccount?.(cfg, accountId) ??
(await inspectReadOnlyChannelAccount({
channelId: plugin.id,
cfg,
accountId,
}))
);
}
function readBooleanField(value: unknown, key: string): boolean | undefined {
const record = asNullableRecord(value);
if (!record) {
@@ -195,12 +188,60 @@ function readBooleanField(value: unknown, key: string): boolean | undefined {
return typeof record[key] === "boolean" ? record[key] : undefined;
}
const hasAccountValue = (account: unknown): boolean => account !== null && account !== undefined;
function resolveProbeAccountEnabled(params: {
plugin: ChannelPlugin;
cfg: OpenClawConfig;
accountId: string;
account: unknown;
diagnostics: string[];
}): boolean {
const fallback = readBooleanField(params.account, "enabled") ?? true;
try {
return resolveChannelAccountEnabled({
plugin: params.plugin,
account: params.account,
cfg: params.cfg,
});
} catch (error) {
params.diagnostics.push(
`${params.plugin.id}:${params.accountId}: failed to evaluate enabled state (${formatErrorMessage(error)}).`,
);
return fallback;
}
}
async function resolveProbeAccountConfigured(params: {
plugin: ChannelPlugin;
cfg: OpenClawConfig;
accountId: string;
account: unknown;
diagnostics: string[];
}): Promise<boolean> {
const fallback = readBooleanField(params.account, "configured") ?? true;
try {
return await resolveChannelAccountConfigured({
plugin: params.plugin,
account: params.account,
cfg: params.cfg,
readAccountConfiguredField: true,
});
} catch (error) {
params.diagnostics.push(
`${params.plugin.id}:${params.accountId}: failed to evaluate configured state (${formatErrorMessage(error)}).`,
);
return fallback;
}
}
async function resolveHealthAccountContext(params: {
plugin: ChannelPlugin;
cfg: OpenClawConfig;
accountId: string;
}): Promise<{
account: unknown;
probeAccount: unknown;
snapshotAccount: unknown;
enabled: boolean;
configured: boolean;
diagnostics: string[];
@@ -213,45 +254,50 @@ async function resolveHealthAccountContext(params: {
diagnostics.push(
`${params.plugin.id}:${params.accountId}: failed to resolve account (${formatErrorMessage(error)}).`,
);
account = await inspectHealthAccount(params.plugin, params.cfg, params.accountId);
}
let inspectedAccount: unknown;
try {
inspectedAccount = await inspectChannelAccount(params);
} catch (error) {
diagnostics.push(
`${params.plugin.id}:${params.accountId}: failed to inspect account (${formatErrorMessage(error)}).`,
);
}
if (!account) {
const probeAccount = hasAccountValue(account) ? account : inspectedAccount;
if (!hasAccountValue(probeAccount)) {
return {
account: {},
probeAccount: {},
snapshotAccount: {},
enabled: false,
configured: false,
diagnostics,
};
}
const snapshotAccount = hasAccountValue(inspectedAccount) ? inspectedAccount : probeAccount;
const enabledFallback = readBooleanField(account, "enabled") ?? true;
let enabled = enabledFallback;
if (params.plugin.config.isEnabled) {
try {
enabled = params.plugin.config.isEnabled(account, params.cfg);
} catch (error) {
enabled = enabledFallback;
diagnostics.push(
`${params.plugin.id}:${params.accountId}: failed to evaluate enabled state (${formatErrorMessage(error)}).`,
);
}
}
const enabled = resolveProbeAccountEnabled({
plugin: params.plugin,
cfg: params.cfg,
accountId: params.accountId,
account: probeAccount,
diagnostics,
});
const configured = await resolveProbeAccountConfigured({
plugin: params.plugin,
cfg: params.cfg,
accountId: params.accountId,
account: probeAccount,
diagnostics,
});
const configuredFallback = readBooleanField(account, "configured") ?? true;
let configured = configuredFallback;
if (params.plugin.config.isConfigured) {
try {
configured = await params.plugin.config.isConfigured(account, params.cfg);
} catch (error) {
configured = configuredFallback;
diagnostics.push(
`${params.plugin.id}:${params.accountId}: failed to evaluate configured state (${formatErrorMessage(error)}).`,
);
}
}
return { account, enabled, configured, diagnostics };
return {
probeAccount,
snapshotAccount,
enabled,
configured,
diagnostics,
};
}
export async function getHealthSnapshot(params?: {
@@ -332,11 +378,12 @@ export async function getHealthSnapshot(params?: {
const accountSummaries: Record<string, ChannelAccountHealthSummary> = {};
for (const accountId of accountIdsToProbe) {
const { account, enabled, configured, diagnostics } = await resolveHealthAccountContext({
plugin,
cfg,
accountId,
});
const { probeAccount, snapshotAccount, enabled, configured, diagnostics } =
await resolveHealthAccountContext({
plugin,
cfg,
accountId,
});
if (diagnostics.length > 0) {
debugHealth("account.diagnostics", { channel: plugin.id, accountId, diagnostics });
}
@@ -346,7 +393,7 @@ export async function getHealthSnapshot(params?: {
if (enabled && configured && doProbe && plugin.status?.probeAccount) {
try {
probe = await plugin.status.probeAccount({
account,
account: probeAccount,
timeoutMs: cappedTimeout,
cfg,
});
@@ -370,22 +417,23 @@ export async function getHealthSnapshot(params?: {
const runtimeSnapshot =
params?.runtimeSnapshot?.channelAccounts[plugin.id]?.[accountId] ??
(accountId === defaultAccountId ? params?.runtimeSnapshot?.channels[plugin.id] : undefined);
const snapshot: ChannelAccountSnapshot = {
...projectSafeChannelAccountSnapshotFields(runtimeSnapshot),
const snapshot: ChannelAccountSnapshot = await buildChannelAccountSnapshotFromAccount({
plugin,
cfg,
accountId,
enabled,
configured,
};
if (includeSensitive && probe !== undefined) {
snapshot.probe = probe;
}
account: snapshotAccount,
runtime: runtimeSnapshot,
probe: includeSensitive ? probe : undefined,
enabledFallback: enabled,
configuredFallback: configured,
});
if (lastProbeAt) {
snapshot.lastProbeAt = lastProbeAt;
}
const summary = plugin.status?.buildChannelSummary
? await plugin.status.buildChannelSummary({
account,
account: probeAccount,
cfg,
defaultAccountId: accountId,
snapshot,
@@ -530,12 +578,12 @@ export async function healthCommand(
` ${plugin.id}: accounts=${accountIds.join(", ") || "(none)"} default=${defaultAccountId}`,
);
for (const accountId of accountIds) {
const { account, configured, diagnostics } = await resolveHealthAccountContext({
const { snapshotAccount, configured, diagnostics } = await resolveHealthAccountContext({
plugin,
cfg,
accountId,
});
const record = asNullableRecord(account);
const record = asNullableRecord(snapshotAccount);
const tokenSource =
record && typeof record.tokenSource === "string" ? record.tokenSource : undefined;
runtime.log(
@@ -650,7 +698,7 @@ export async function healthCommand(
}
try {
plugin.status.logSelfId({
account: accountContext.account,
account: accountContext.probeAccount,
cfg,
runtime,
includeChannelPrefix: true,