refactor: move stale socket modes to channel status

This commit is contained in:
Peter Steinberger
2026-04-22 05:14:56 +01:00
parent 0a027ff591
commit 2e775fb03e
8 changed files with 39 additions and 14 deletions

View File

@@ -1,2 +1,2 @@
10f07ebae3910cfe7639c54fb97ec4011c5f8be8a5444b86d23f075f9a49cc4c plugin-sdk-api-baseline.json
fdc165b2d06f00d195e326c2d28176da5cdeb8f8b05df4ec28466d384d57a07b plugin-sdk-api-baseline.jsonl
fc00be212cab9fa24cf625fd9afb8f6d0871509afcc42baa6653d3ef26a991d1 plugin-sdk-api-baseline.json
efe8884ee3a296ae77b80f1485d17744397c5868c110b23eb5cf99ce2587a03f plugin-sdk-api-baseline.jsonl

View File

@@ -760,6 +760,7 @@ export const telegramPlugin = createChatChannelPlugin({
actions: telegramMessageActions,
status: createComputedAccountStatusAdapter<ResolvedTelegramAccount, TelegramProbe>({
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
staleSocketHealthCheckModes: ["polling"],
collectStatusIssues: collectTelegramStatusIssues,
buildChannelSummary: ({ snapshot }) => buildTokenChannelStatusSummary(snapshot),
probeAccount: async ({ account, timeoutMs }) =>

View File

@@ -173,6 +173,8 @@ export type ChannelGroupAdapter = {
export type ChannelStatusAdapter<ResolvedAccount, Probe = unknown, Audit = unknown> = {
defaultRuntime?: ChannelAccountSnapshot;
skipStaleSocketHealthCheck?: boolean;
/** Runtime `mode` values where `lastEventAt` can prove connected socket liveness. */
staleSocketHealthCheckModes?: readonly string[];
buildChannelSummary?: BivariantCallback<
(params: {
account: ResolvedAccount;

View File

@@ -125,12 +125,14 @@ export function startChannelHealthMonitor(deps: ChannelHealthMonitorDeps): Chann
if (channelManager.isManuallyStopped(channelId as ChannelId, accountId)) {
continue;
}
const channelPluginStatus = getChannelPlugin(channelId)?.status;
const healthPolicy: ChannelHealthPolicy = {
channelId,
now,
staleEventThresholdMs: timing.staleEventThresholdMs,
channelConnectGraceMs: timing.channelConnectGraceMs,
skipStaleSocketCheck: getChannelPlugin(channelId)?.status?.skipStaleSocketHealthCheck,
skipStaleSocketCheck: channelPluginStatus?.skipStaleSocketHealthCheck,
staleSocketHealthCheckModes: channelPluginStatus?.staleSocketHealthCheckModes,
};
const health = evaluateChannelHealth(status, healthPolicy);
if (health.healthy) {

View File

@@ -136,7 +136,7 @@ describe("evaluateChannelHealth", () => {
expect(evaluation).toEqual({ healthy: false, reason: "stale-socket" });
});
it("flags stale sockets for telegram polling channels", () => {
it("flags stale sockets for channels with an allowed health-check mode", () => {
const evaluation = evaluateChannelHealth(
{
running: true,
@@ -148,16 +148,17 @@ describe("evaluateChannelHealth", () => {
mode: "polling",
},
{
channelId: "telegram",
channelId: "example",
now: 100_000,
channelConnectGraceMs: 10_000,
staleEventThresholdMs: 30_000,
staleSocketHealthCheckModes: ["polling"],
},
);
expect(evaluation).toEqual({ healthy: false, reason: "stale-socket" });
});
it("skips stale-socket detection for telegram accounts without explicit polling mode", () => {
it("skips stale-socket detection when an allowlisted health-check mode is missing", () => {
const evaluation = evaluateChannelHealth(
{
running: true,
@@ -168,16 +169,17 @@ describe("evaluateChannelHealth", () => {
lastEventAt: 0,
},
{
channelId: "telegram",
channelId: "example",
now: 100_000,
channelConnectGraceMs: 10_000,
staleEventThresholdMs: 30_000,
staleSocketHealthCheckModes: ["polling"],
},
);
expect(evaluation).toEqual({ healthy: true, reason: "healthy" });
});
it("skips stale-socket detection for telegram accounts with malformed mode", () => {
it("skips stale-socket detection when the health-check mode is malformed", () => {
const evaluation = evaluateChannelHealth(
{
running: true,
@@ -189,10 +191,11 @@ describe("evaluateChannelHealth", () => {
mode: { polling: true } as unknown as string,
},
{
channelId: "telegram",
channelId: "example",
now: 100_000,
channelConnectGraceMs: 10_000,
staleEventThresholdMs: 30_000,
staleSocketHealthCheckModes: ["polling"],
},
);
expect(evaluation).toEqual({ healthy: true, reason: "healthy" });

View File

@@ -36,6 +36,7 @@ export type ChannelHealthPolicy = {
staleEventThresholdMs: number;
channelConnectGraceMs: number;
skipStaleSocketCheck?: boolean;
staleSocketHealthCheckModes?: readonly string[];
};
export type ChannelRestartReason =
@@ -55,6 +56,19 @@ const BUSY_ACTIVITY_STALE_THRESHOLD_MS = 25 * 60_000;
export const DEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS = 30 * 60_000;
export const DEFAULT_CHANNEL_CONNECT_GRACE_MS = 120_000;
function shouldCheckStaleSocketForMode(
mode: string | undefined,
healthCheckModes: readonly string[] | undefined,
): boolean {
if (healthCheckModes) {
const normalizedModes = new Set(
healthCheckModes.map((entry) => entry.trim().toLowerCase()).filter(Boolean),
);
return Boolean(mode && normalizedModes.has(mode));
}
return mode !== "webhook";
}
export function evaluateChannelHealth(
snapshot: ChannelHealthSnapshot,
policy: ChannelHealthPolicy,
@@ -112,14 +126,11 @@ export function evaluateChannelHealth(
if (snapshot.connected === false) {
return { healthy: false, reason: "disconnected" };
}
// Telegram only has reliable stale-socket liveness in explicit polling mode.
// Webhook accounts and malformed legacy mode values do not have a persistent
// outgoing socket to age-check.
const shouldCheckStaleSocket =
policy.skipStaleSocketCheck !== true &&
snapshot.connected === true &&
lastEventAt != null &&
(policy.channelId === "telegram" ? mode === "polling" : mode !== "webhook");
shouldCheckStaleSocketForMode(mode, policy.staleSocketHealthCheckModes);
if (shouldCheckStaleSocket) {
if (lastStartAt != null && lastEventAt < lastStartAt) {
const lifecycleEventGap = Math.max(0, policy.now - lastStartAt);

View File

@@ -64,12 +64,14 @@ export function createReadinessChecker(deps: {
if (!accountSnapshot) {
continue;
}
const channelPluginStatus = getChannelPlugin(channelId)?.status;
const policy: ChannelHealthPolicy = {
now,
staleEventThresholdMs: DEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS,
channelConnectGraceMs: DEFAULT_CHANNEL_CONNECT_GRACE_MS,
channelId,
skipStaleSocketCheck: getChannelPlugin(channelId)?.status?.skipStaleSocketHealthCheck,
skipStaleSocketCheck: channelPluginStatus?.skipStaleSocketHealthCheck,
staleSocketHealthCheckModes: channelPluginStatus?.staleSocketHealthCheckModes,
};
const health = evaluateChannelHealth(accountSnapshot, policy);
if (!health.healthy && !shouldIgnoreReadinessFailure(accountSnapshot, health)) {

View File

@@ -50,6 +50,10 @@ const CORE_SECRET_SURFACE_GUARDS = [
path: "src/plugin-sdk/command-auth.ts",
forbiddenPatterns: [/\bpluginId:\s*"telegram"/],
},
{
path: "src/gateway/channel-health-policy.ts",
forbiddenPatterns: [/\btelegram\b/],
},
] as const;
describe("channel secret contract surface guardrails", () => {