Skip to content

Commit 38c35c4

Browse files
committed
fix: use transport activity for stale health
1 parent a44443a commit 38c35c4

29 files changed

Lines changed: 177 additions & 107 deletions
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
7c1b8b34618f44d56817ff54b930701710087dc7e76beaf4a554b6a5a25ba87c config-baseline.json
2-
ed0c093e8acab2364608be3e65b98836600aea07df73ebb51d11919969c6c8fe config-baseline.core.json
1+
c25e4b5e1c1469ec66bd9ced3759c2542a05b6ecb0db9aa71fa5a8054f8ef0a2 config-baseline.json
2+
ed4e305904b4b954ffa72c07ea1900a116bfd874ac0c637227883abb99f753f9 config-baseline.core.json
33
6c0069b971ae298ae68516ebcd3eae0e8c82820d2e8f42ecbd2f53a2f9077371 config-baseline.channel.json
44
e5b7756b5f45ba227aa1bfab990dcf8a2a8b409b9ca01ea8bb1d5cd7adc06c90 config-baseline.plugin.json
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
6f605be396ee42efbe26cfd0cc90d7710ca378959aecd6388dd81a5b97996b43 plugin-sdk-api-baseline.json
2-
9c34c7c068f6d3bc5cf44817fe14c470c1c091595296f829e1efb4d6e7ba3599 plugin-sdk-api-baseline.jsonl
1+
3fa4d37ea6dbe5dfd540bcaa8c3bb2b81f2cfd439fa446d548ba159388c7b520 plugin-sdk-api-baseline.json
2+
bfb88286eeb8dd11871242d17de4d4bd9196fb27ce5d30dbaa1c0d277c27be85 plugin-sdk-api-baseline.jsonl

extensions/discord/src/monitor/provider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,8 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
10661066
const trackInboundEvent = opts.setStatus
10671067
? () => {
10681068
const at = Date.now();
1069+
// Carbon handles gateway heartbeats internally but does not expose a
1070+
// stable heartbeat-ack event, so Discord app events stay app-level only.
10691071
opts.setStatus?.({ lastEventAt: at, lastInboundAt: at });
10701072
}
10711073
: undefined;

extensions/matrix/src/matrix/monitor/status.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/channel-contract";
2-
import { createConnectedChannelStatusPatch } from "openclaw/plugin-sdk/gateway-runtime";
2+
import {
3+
createConnectedChannelStatusPatch,
4+
createTransportActivityStatusPatch,
5+
} from "openclaw/plugin-sdk/gateway-runtime";
36
import { formatMatrixErrorMessage } from "../errors.js";
47
import {
58
isMatrixDisconnectedSyncState,
@@ -52,12 +55,15 @@ export function createMatrixMonitorStatusController(params: {
5255
});
5356
};
5457

55-
const noteConnected = (at = Date.now()) => {
58+
const noteConnected = (at = Date.now(), options?: { transportActivity?: boolean }) => {
5659
if (status.connected === true) {
5760
status.lastEventAt = at;
5861
} else {
5962
Object.assign(status, createConnectedChannelStatusPatch(at));
6063
}
64+
if (options?.transportActivity) {
65+
Object.assign(status, createTransportActivityStatusPatch(at));
66+
}
6167
status.lastError = null;
6268
status.lastDisconnect = null;
6369
status.healthState = "healthy";
@@ -83,7 +89,10 @@ export function createMatrixMonitorStatusController(params: {
8389
return {
8490
noteSyncState(state: MatrixSyncState, error?: unknown, at = Date.now()) {
8591
if (isMatrixReadySyncState(state)) {
86-
noteConnected(at);
92+
// matrix-js-sdk emits SYNCING after each successful /sync response.
93+
// PREPARED can be cache-backed and CATCHUP is a lifecycle bridge, so
94+
// neither should refresh transport liveness.
95+
noteConnected(at, { transportActivity: state === "SYNCING" });
8796
return;
8897
}
8998
if (isMatrixDisconnectedSyncState(state)) {

extensions/matrix/src/matrix/monitor/sync-lifecycle.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,42 @@ describe("createMatrixMonitorSyncLifecycle", () => {
129129
);
130130
});
131131

132+
it("only refreshes transport liveness for successful sync responses", async () => {
133+
vi.useFakeTimers();
134+
vi.setSystemTime(new Date("2026-04-10T16:21:00.000Z"));
135+
const { client, lifecycle, setStatus } = createSyncLifecycleHarness();
136+
try {
137+
setStatus.mockClear();
138+
139+
client.emit("sync.state", "PREPARED", null, undefined);
140+
expect(setStatus).toHaveBeenLastCalledWith(
141+
expect.not.objectContaining({
142+
lastTransportActivityAt: expect.any(Number),
143+
}),
144+
);
145+
146+
await vi.advanceTimersByTimeAsync(2_000);
147+
client.emit("sync.state", "SYNCING", "PREPARED", undefined);
148+
const syncAt = Date.now();
149+
expect(setStatus).toHaveBeenLastCalledWith(
150+
expect.objectContaining({
151+
lastTransportActivityAt: syncAt,
152+
}),
153+
);
154+
155+
await vi.advanceTimersByTimeAsync(3_000);
156+
client.emit("sync.state", "CATCHUP", "SYNCING", undefined);
157+
expect(setStatus).toHaveBeenLastCalledWith(
158+
expect.objectContaining({
159+
lastTransportActivityAt: syncAt,
160+
}),
161+
);
162+
} finally {
163+
lifecycle.dispose();
164+
vi.useRealTimers();
165+
}
166+
});
167+
132168
it("does not downgrade a fatal error to stopped during shutdown", async () => {
133169
const { client, lifecycle, setStatus, setStopping, statusController } =
134170
createSyncLifecycleHarness({

extensions/slack/src/channel.test.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,10 +284,6 @@ describe("slackPlugin actions", () => {
284284
});
285285

286286
describe("slackPlugin status", () => {
287-
it("opts out of the generic stale socket health check", () => {
288-
expect(slackPlugin.status?.skipStaleSocketHealthCheck).toBe(true);
289-
});
290-
291287
it("uses the direct Slack probe helper when runtime is not initialized", async () => {
292288
const probeSpy = vi.spyOn(probeModule, "probeSlack").mockResolvedValueOnce({
293289
ok: true,

extensions/slack/src/monitor/provider.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -508,9 +508,8 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
508508
removeAckAfterReply,
509509
});
510510

511-
// Wire up event liveness tracking: update lastEventAt on every inbound event
512-
// so the health monitor can detect "half-dead" sockets that pass health checks
513-
// but silently stop delivering events.
511+
// Slack's socket-mode client keeps ping/pong health private and closes on
512+
// missed pongs. App events are useful status activity, but not transport proof.
514513
const trackEvent = opts.setStatus
515514
? () => {
516515
opts.setStatus!({ lastEventAt: Date.now(), lastInboundAt: Date.now() });

extensions/slack/src/monitor/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export type MonitorSlackOpts = {
1414
abortSignal?: AbortSignal;
1515
mediaMaxMb?: number;
1616
slashCommand?: SlackSlashCommandConfig;
17-
/** Callback to update the channel account status snapshot (e.g. lastEventAt). */
17+
/** Callback to update app-level channel account activity (e.g. lastEventAt). */
1818
setStatus?: (next: Record<string, unknown>) => void;
1919
/** Callback to read the current channel account status snapshot. */
2020
getStatus?: () => Record<string, unknown>;

extensions/telegram/src/polling-session.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,7 @@ describe("TelegramPollingSession", () => {
590590
connected: false,
591591
lastConnectedAt: null,
592592
lastEventAt: null,
593+
lastTransportActivityAt: null,
593594
});
594595
const connectedPatch = setStatus.mock.calls.find(
595596
([patch]) => (patch as Record<string, unknown>).connected === true,
@@ -599,9 +600,11 @@ describe("TelegramPollingSession", () => {
599600
mode: "polling",
600601
lastConnectedAt: expect.any(Number),
601602
lastEventAt: expect.any(Number),
603+
lastTransportActivityAt: expect.any(Number),
602604
lastError: null,
603605
});
604606
expect(connectedPatch?.lastConnectedAt).toBe(connectedPatch?.lastEventAt);
607+
expect(connectedPatch?.lastTransportActivityAt).toBe(connectedPatch?.lastEventAt);
605608

606609
abort.abort();
607610
resolveFirstTask();
@@ -681,6 +684,7 @@ describe("TelegramPollingSession", () => {
681684
mode: "polling",
682685
lastConnectedAt: null,
683686
lastEventAt: null,
687+
lastTransportActivityAt: null,
684688
});
685689
expect(disconnectedPatches[1]?.[0]).toEqual({
686690
mode: "polling",

extensions/telegram/src/polling-status.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ describe("createTelegramPollingStatusPublisher", () => {
1515
connected: false,
1616
lastConnectedAt: null,
1717
lastEventAt: null,
18+
lastTransportActivityAt: null,
1819
});
1920
expect(setStatus).toHaveBeenNthCalledWith(2, {
2021
mode: "polling",
2122
connected: true,
2223
lastConnectedAt: 1234,
2324
lastEventAt: 1234,
25+
lastTransportActivityAt: 1234,
2426
lastError: null,
2527
});
2628
expect(setStatus).toHaveBeenNthCalledWith(3, {

0 commit comments

Comments
 (0)