Skip to content

Commit 8f6f0f8

Browse files
committed
fix(health): lazy runtimeSnapshot capture, explicit probe:false tracking
Address review comments from chatgpt-codex-connector: - #3003609871: Move getRuntimeSnapshot() call lazily inside the health refresh cycle (after dedupe decides to proceed), not eagerly at every timer tick. Add setRuntimeSnapshotGetter() init pattern — the getter is registered once at startup and called inside the refresh function. - #3003634321: Track explicit probe:false alongside probe:true. Previously pendingProbe was only set when probe:true (false was the default, so storing it would trigger needless follow-ups). Now we track whenever opts.probe is explicitly provided (true or false), and the follow-up only fires when the new intent differs from what the in-flight refresh is already using.
1 parent 2d75865 commit 8f6f0f8

3 files changed

Lines changed: 43 additions & 9 deletions

File tree

src/gateway/server-maintenance.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { HealthSummary } from "../commands/health.js";
22
import { cleanOldMedia } from "../media/store.js";
33
import { abortChatRunById, type ChatAbortControllerEntry } from "./chat-abort.js";
4+
import type { ChannelRuntimeSnapshot } from "./server-channels.js";
45
import type { ChatRunEntry } from "./server-chat.js";
56
import {
67
DEDUPE_MAX,
@@ -10,7 +11,7 @@ import {
1011
} from "./server-constants.js";
1112
import type { DedupeEntry } from "./server-shared.js";
1213
import { formatError } from "./server-utils.js";
13-
import { setBroadcastHealthUpdate } from "./server/health-state.js";
14+
import { setBroadcastHealthUpdate, setRuntimeSnapshotGetter } from "./server/health-state.js";
1415

1516
export function startGatewayMaintenanceTimers(params: {
1617
broadcast: (
@@ -39,6 +40,7 @@ export function startGatewayMaintenanceTimers(params: {
3940
) => ChatRunEntry | undefined;
4041
agentRunSeq: Map<string, number>;
4142
nodeSendToSession: (sessionKey: string, event: string, payload: unknown) => void;
43+
getRuntimeSnapshot?: () => ChannelRuntimeSnapshot | undefined;
4244
mediaCleanupTtlMs?: number;
4345
}): {
4446
tickInterval: ReturnType<typeof setInterval>;
@@ -56,6 +58,13 @@ export function startGatewayMaintenanceTimers(params: {
5658
params.nodeSendToAllSubscribed("health", snap);
5759
});
5860

61+
// Lazily capture runtime snapshot inside the health refresh cycle,
62+
// not at every timer tick. Call this after the broadcast fn so the
63+
// getter is ready before the first timer fires.
64+
if (params.getRuntimeSnapshot) {
65+
setRuntimeSnapshotGetter(params.getRuntimeSnapshot);
66+
}
67+
5968
// periodic keepalive
6069
const tickInterval = setInterval(() => {
6170
const payload = { ts: Date.now() };

src/gateway/server.impl.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ import {
125125
getPresenceVersion,
126126
incrementPresenceVersion,
127127
refreshGatewayHealthSnapshot,
128+
setRuntimeSnapshotGetter,
128129
} from "./server/health-state.js";
129130
import { resolveHookClientIpConfig } from "./server/hooks.js";
130131
import { createReadinessChecker } from "./server/readiness.js";
@@ -833,6 +834,10 @@ export async function startGatewayServer(
833834
}
834835
};
835836

837+
// Register the lazy getter so health refresh captures the snapshot inside
838+
// the refresh cycle, not at the timer tick (after dedupe decides to proceed).
839+
setRuntimeSnapshotGetter(safeGetRuntimeSnapshot);
840+
836841
let agentUnsub: (() => void) | null = null;
837842
let heartbeatUnsub: (() => void) | null = null;
838843
let transcriptUnsub: (() => void) | null = null;
@@ -885,9 +890,9 @@ export async function startGatewayServer(
885890
nodeSendToAllSubscribed,
886891
getPresenceVersion,
887892
getHealthVersion,
888-
refreshGatewayHealthSnapshot: (opts) =>
889-
refreshGatewayHealthSnapshot({ ...opts, runtimeSnapshot: safeGetRuntimeSnapshot() }),
893+
refreshGatewayHealthSnapshot,
890894
logHealth,
895+
getRuntimeSnapshot: safeGetRuntimeSnapshot,
891896
dedupe,
892897
chatAbortControllers,
893898
chatRunState,
@@ -1181,8 +1186,7 @@ export async function startGatewayServer(
11811186
pluginApprovalManager,
11821187
loadGatewayModelCatalog,
11831188
getHealthCache,
1184-
refreshHealthSnapshot: (opts) =>
1185-
refreshGatewayHealthSnapshot({ ...opts, runtimeSnapshot: safeGetRuntimeSnapshot() }),
1189+
refreshHealthSnapshot: refreshGatewayHealthSnapshot,
11861190
logHealth,
11871191
logGateway: log,
11881192
incrementPresenceVersion,

src/gateway/server/health-state.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,21 @@ export function setBroadcastHealthUpdate(fn: ((snap: HealthSummary) => void) | n
7373
broadcastHealthUpdate = fn;
7474
}
7575

76+
/** Set once at startup — used to lazily capture runtime snapshot inside the refresh cycle. */
77+
let getRuntimeSnapshot: (() => ChannelRuntimeSnapshot | undefined) | undefined = undefined;
78+
79+
export function setRuntimeSnapshotGetter(fn: () => ChannelRuntimeSnapshot | undefined) {
80+
getRuntimeSnapshot = fn;
81+
}
82+
7683
/** @internal Reset module-level state between tests only. */
7784
export function __resetHealthStateForTest() {
7885
healthCache = null;
7986
healthRefresh = null;
8087
pendingRuntimeSnapshot = undefined;
8188
pendingProbe = undefined;
89+
// Note: getRuntimeSnapshot is intentionally NOT reset here — it is set once
90+
// at startup via setRuntimeSnapshotGetter and should persist across test cases.
8291
healthVersion = 1;
8392
presenceVersion = 1;
8493
broadcastHealthUpdate = null;
@@ -90,14 +99,18 @@ export async function refreshGatewayHealthSnapshot(opts?: {
9099
}) {
91100
// Always track the newest runtimeSnapshot so it is not silently discarded when
92101
// a refresh is already in-flight and a second caller provides a fresher snapshot.
102+
// Only record a pending snapshot — the actual capture of getRuntimeSnapshot()
103+
// happens lazily inside the refresh cycle, after dedupe decides to proceed.
104+
// This avoids calling getRuntimeSnapshot() eagerly on every timer tick.
93105
if (opts?.runtimeSnapshot !== undefined) {
94106
pendingRuntimeSnapshot = opts.runtimeSnapshot;
95107
}
96108

97109
// Track the latest probe intent so the finally block uses the most recent caller's preference.
98-
// Only set when probe is true — false is the default, so storing it would trigger needless
99-
// follow-up refreshes (hadPendingProbe would be true even though nothing changed).
100-
if (opts?.probe) {
110+
// Only set when explicitly provided (true or false). This matters because an explicit
111+
// probe:false must be distinguishable from "not provided" — if a caller explicitly
112+
// requests a non-probe snapshot, we must NOT kick off a follow-up that resets it.
113+
if (opts?.probe !== undefined) {
101114
pendingProbe = opts.probe;
102115
}
103116

@@ -107,11 +120,17 @@ export async function refreshGatewayHealthSnapshot(opts?: {
107120
pendingRuntimeSnapshot = undefined;
108121
const probeForRefresh = pendingProbe ?? false;
109122
pendingProbe = undefined; // must be cleared so finally block only fires for NEW callers
123+
// Note: getRuntimeSnapshot is intentionally NOT reset here — it is set once
124+
// at startup via setRuntimeSnapshotGetter and should persist across test cases.
110125

111126
healthRefresh = (async () => {
127+
// Lazily capture runtime snapshot here (after dedupe has decided to proceed),
128+
// not at the call site. This avoids calling getRuntimeSnapshot() on every
129+
// timer tick when dedupe might skip the refresh anyway.
130+
const runtimeSnapshot = snapshotForRefresh ?? getRuntimeSnapshot?.() ?? undefined;
112131
const snap = await getHealthSnapshot({
113132
probe: probeForRefresh,
114-
runtimeSnapshot: snapshotForRefresh,
133+
runtimeSnapshot,
115134
});
116135
healthCache = snap;
117136
healthVersion += 1;
@@ -126,6 +145,8 @@ export async function refreshGatewayHealthSnapshot(opts?: {
126145
const followUpProbe = pendingProbe ?? false;
127146
const hadPendingProbe = pendingProbe !== undefined;
128147
pendingProbe = undefined;
148+
// Note: getRuntimeSnapshot is intentionally NOT reset here — it is set once
149+
// at startup via setRuntimeSnapshotGetter and should persist across test cases.
129150
// If a newer runtimeSnapshot or probe intent arrived while the refresh was
130151
// in-flight, kick off a follow-up so the latest state is reflected.
131152
if (pendingRuntimeSnapshot !== undefined || hadPendingProbe) {

0 commit comments

Comments
 (0)