Skip to content

fix(gateway): preserve runtime-backed health state#39921

Closed
FAL1989 wants to merge 3 commits into
openclaw:mainfrom
FAL1989:fix/health-runtime-status
Closed

fix(gateway): preserve runtime-backed health state#39921
FAL1989 wants to merge 3 commits into
openclaw:mainfrom
FAL1989:fix/health-runtime-status

Conversation

@FAL1989

@FAL1989 FAL1989 commented Mar 8, 2026

Copy link
Copy Markdown

Summary

  • stop implicit health probing by default and preserve runtime-backed channel/account fields in getHealthSnapshot
  • serve gateway.health from a fresh snapshot that overlays context.getRuntimeSnapshot() before updating the cache/broadcast
  • mark Discord accounts connected as soon as login succeeds, even when the gateway plugin is not yet marked connected
  • add regression coverage for runtime-backed health snapshots and the Discord startup race

Problem

On a live 2026.3.7 npm install with a local gateway and 6 Discord bots, the runtime was healthy but the diagnostics were not:

  • openclaw doctor could report Gateway not running
  • openclaw gateway health --json could drop live runtime state and fall back to stale/incomplete channel data
  • openclaw channels status --json could leave Discord accounts as connected:false even after successful login

The root causes were:

  • getHealthSnapshot() implicitly probing by default instead of treating probe as opt-in
  • the gateway health RPC reusing cached health state without overlaying the current runtime snapshot
  • Discord only setting connected state on startup when gateway.isConnected === true, which races with provider login

What changed

  • getHealthSnapshot() now treats probe as explicit opt-in and accepts an optional runtimeSnapshot
  • channel/account summaries are rebuilt through buildChannelAccountSnapshot(...) so runtime-backed fields survive refreshes
  • gateway.health now computes a fresh snapshot using context.getRuntimeSnapshot(), updates the shared cache, and still kicks off the background refresh
  • Discord startup always publishes the connected status patch after login succeeds

Verification

  • pnpm exec vitest run src/commands/health.snapshot.test.ts src/discord/monitor/provider.test.ts
  • manually validated on a live npm install after applying the equivalent runtime fix:
    • openclaw doctor stopped reporting Gateway not running
    • openclaw gateway health --json reflected running Discord/Telegram channels
    • openclaw channels status --json showed all 6 Discord bots as running:true and connected:true

Related

@greptile-apps

greptile-apps Bot commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes three related issues affecting live gateway health diagnostics: getHealthSnapshot() no longer implicitly probes (now explicit opt-in), the gateway.health RPC overlays live runtime state before serving and caching the response, and Discord providers always publish their connected status after a successful login regardless of gateway.isConnected.

Key changes:

  • getHealthSnapshot(): probe is now opt-in (=== true), and an optional runtimeSnapshot parameter is used to overlay live channel/account fields via buildChannelAccountSnapshot.
  • gateway.health handler: rewritten to compute a fresh runtime-backed snapshot on every call rather than serving stale cache, then stores it via setHealthCache().
  • setHealthCache(): extracted as a shared helper to ensure healthVersion increment and broadcastHealthUpdate are always called together.
  • Discord provider: unconditionally publishes the connected status patch after login, removing the race with gateway.isConnected.

Issue found:

  • The handler computes a fresh runtime-backed snapshot and calls setHealthCache(snap), but immediately triggers a background refreshHealthSnapshot({ probe: false }) that calls getHealthSnapshot() without a runtimeSnapshot. This background refresh will eventually overwrite the just-set runtime-backed snapshot with a stale version (lacking all runtime fields like running, lastStartAt, connected), breaking the fix for WebSocket broadcast subscribers. The RPC response is unaffected as it uses the locally-computed snapshot.

Confidence Score: 2/5

  • The RPC response is correct and core runtime-overlay logic is sound, but the background refresh will immediately overwrite the runtime-backed cache with stale data, partially reversing the fix for WebSocket subscribers.
  • Three of the four changes (probe opt-in, Discord connected patch, setHealthCache refactor) are clean and correct. The fourth — the health handler — has a logic issue: the background refreshHealthSnapshot fires without a runtimeSnapshot and will overwrite the freshly-set runtime-backed cache, breaking subscribers relying on broadcast updates. This needs to be addressed before the PR fully solves the stated problem for non-RPC consumers.
  • src/gateway/server-methods/health.ts — the background refreshHealthSnapshot call at lines 21-23 needs to either be removed (since the cache was just refreshed) or pass the runtime snapshot through to preserve runtime fields.

Last reviewed commit: 6aca363

Comment thread src/gateway/server-methods/health.ts Outdated
Comment on lines 20 to 23
setHealthCache(snap);
void refreshHealthSnapshot({ probe: false }).catch((err) =>
logHealth.error(`background health refresh failed: ${formatError(err)}`),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Background refresh overwrites runtime-backed cache

After computing a fresh runtime-backed snapshot and storing it via setHealthCache(snap), the immediately-triggered background refreshHealthSnapshot will overwrite that cache using getHealthSnapshot({ probe: false }) without a runtimeSnapshot. This means:

  1. setHealthCache(snap) broadcasts the correct runtime-backed snapshot to WebSocket subscribers.
  2. Moments later, refreshGatewayHealthSnapshot completes, calls setHealthCache(stale_snap), and broadcasts a second update that lacks all the runtime fields (running, lastStartAt, connected, etc.) — undoing the fix for cache/broadcast consumers.

The RPC response itself is fine (it uses the locally-computed snap), but any subscribers relying on broadcastHealthUpdate (e.g. WebChat) will receive two rapid updates and end up with the stale, runtime-field-free snapshot.

Consider either dropping the background refresh here (the cache was just refreshed with better data, and the periodic health monitor will keep it up to date) or passing the runtime snapshot through:

setHealthCache(snap);
// Background refresh is intentionally skipped here: cache was just populated
// with runtime-backed data. The periodic health monitor handles background updates.
respond(true, snap, undefined);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server-methods/health.ts
Line: 20-23

Comment:
**Background refresh overwrites runtime-backed cache**

After computing a fresh runtime-backed snapshot and storing it via `setHealthCache(snap)`, the immediately-triggered background `refreshHealthSnapshot` will overwrite that cache using `getHealthSnapshot({ probe: false })` **without** a `runtimeSnapshot`. This means:

1. `setHealthCache(snap)` broadcasts the correct runtime-backed snapshot to WebSocket subscribers.
2. Moments later, `refreshGatewayHealthSnapshot` completes, calls `setHealthCache(stale_snap)`, and broadcasts a second update that lacks all the runtime fields (`running`, `lastStartAt`, `connected`, etc.) — undoing the fix for cache/broadcast consumers.

The RPC response itself is fine (it uses the locally-computed `snap`), but any subscribers relying on `broadcastHealthUpdate` (e.g. WebChat) will receive two rapid updates and end up with the stale, runtime-field-free snapshot.

Consider either dropping the background refresh here (the cache was just refreshed with better data, and the periodic health monitor will keep it up to date) or passing the runtime snapshot through:

```ts
setHealthCache(snap);
// Background refresh is intentionally skipped here: cache was just populated
// with runtime-backed data. The periodic health monitor handles background updates.
respond(true, snap, undefined);
```

How can I resolve this? If you propose a fix, please make it concise.

@openclaw-barnacle openclaw-barnacle Bot added the app: macos App: macos label Mar 8, 2026
@FAL1989

FAL1989 commented Mar 8, 2026

Copy link
Copy Markdown
Author

Follow-up pushed in 8746e7d.

Addressed the two failing checks and the review note:

  • removed the redundant background refreshHealthSnapshot({ probe: false }) call that could overwrite the fresh runtime-backed cache after setHealthCache(snap)
  • formatted CHANGELOG.md
  • regenerated apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift
  • updated .secrets.baseline so detect-secrets line numbers are current

Local verification:

  • pnpm check
  • pnpm exec vitest run src/commands/health.snapshot.test.ts src/discord/monitor/provider.test.ts

@FAL1989

FAL1989 commented Mar 8, 2026

Copy link
Copy Markdown
Author

Follow-up pushed in 8746e7d.

Addressed the two failing checks and the review note:

  • removed the redundant background call that could overwrite the fresh runtime-backed cache after
  • formatted
  • regenerated
  • updated so detect-secrets line numbers are current

Local verification:

[email protected] check /home/wolfstain/tmp/openclaw-upstream-fix
pnpm format:check && pnpm tsgo && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope && pnpm check:host-env-policy:swift

[email protected] format:check /home/wolfstain/tmp/openclaw-upstream-fix
oxfmt --check

Checking formatting...

All matched files use the correct format.
Finished in 3977ms on 6681 files using 12 threads.

[email protected] lint /home/wolfstain/tmp/openclaw-upstream-fix
oxlint --type-aware

Found 0 warnings and 0 errors.
Finished in 4.5s on 4955 files with 136 rules using 12 threads.

[email protected] lint:tmp:no-random-messaging /home/wolfstain/tmp/openclaw-upstream-fix
node scripts/check-no-random-messaging-tmp.mjs

[email protected] lint:tmp:channel-agnostic-boundaries /home/wolfstain/tmp/openclaw-upstream-fix
node scripts/check-channel-agnostic-boundaries.mjs

[email protected] lint:tmp:no-raw-channel-fetch /home/wolfstain/tmp/openclaw-upstream-fix
node scripts/check-no-raw-channel-fetch.mjs

[email protected] lint:agent:ingress-owner /home/wolfstain/tmp/openclaw-upstream-fix
node scripts/check-ingress-agent-owner-context.mjs

[email protected] lint:plugins:no-register-http-handler /home/wolfstain/tmp/openclaw-upstream-fix
node scripts/check-no-register-http-handler.mjs

[email protected] lint:plugins:no-monolithic-plugin-sdk-entry-imports /home/wolfstain/tmp/openclaw-upstream-fix
node --import tsx scripts/check-no-monolithic-plugin-sdk-entry-imports.ts

OK: bundled plugin source files use scoped plugin-sdk subpaths (763 checked).

[email protected] lint:webhook:no-low-level-body-read /home/wolfstain/tmp/openclaw-upstream-fix
node scripts/check-webhook-auth-body-order.mjs

[email protected] lint:auth:no-pairing-store-group /home/wolfstain/tmp/openclaw-upstream-fix
node scripts/check-no-pairing-store-group-auth.mjs

[email protected] lint:auth:pairing-account-scope /home/wolfstain/tmp/openclaw-upstream-fix
node scripts/check-pairing-account-scope.mjs

[email protected] check:host-env-policy:swift /home/wolfstain/tmp/openclaw-upstream-fix
node scripts/generate-host-env-security-policy-swift.mjs --check

OK apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift

RUN v4.0.18 /home/wolfstain/tmp/openclaw-upstream-fix

✓ src/discord/monitor/provider.test.ts (15 tests) 210ms
✓ src/commands/health.snapshot.test.ts (7 tests) 113ms

Test Files 2 passed (2)
Tests 22 passed (22)
Start at 12:32:53
Duration 7.60s (transform 5.72s, setup 341ms, import 7.27s, tests 323ms, environment 0ms)

@FAL1989
FAL1989 force-pushed the fix/health-runtime-status branch from 8746e7d to bb4ddf5 Compare March 8, 2026 15:02
@openclaw-barnacle openclaw-barnacle Bot removed the app: macos App: macos label Mar 8, 2026
@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Apr 25, 2026
@clawsweeper

clawsweeper Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Codex automated review: keeping this open.

Keep open. Current main at e672b61 still does not implement PR #39921's central health-runtime behavior, and the Discord startup connected-state race remains at least untested and not resolved in the same way. Related open PRs #42586 and #52770 overlap the health work but do not clearly supersede the whole PR.

Best possible solution:

Keep #39921 open or consolidate it into a maintainer-chosen canonical gateway health/runtime fix. The product path should thread ChannelRuntimeSnapshot into health snapshot construction and cache/broadcast refreshes, make probe intent explicit, preserve runtime-backed channel/account fields, settle the Discord successful-login connected-state race with regression coverage, and coordinate with #42586/#52770 if those become the canonical health branch.

What I checked:

  • Health snapshot has no runtime input and still probes by default: getHealthSnapshot only accepts timeoutMs/probe, computes doProbe as params?.probe !== false, and builds each ChannelAccountSnapshot from account/config/probe fields without a runtimeSnapshot overlay. (src/commands/health.ts:254, e672b61417af)
  • Gateway health still serves through cached refresh flow: The health RPC reads getHealthCache(), returns a fresh cached snapshot, starts background refreshHealthSnapshot({ probe: false }), or awaits refreshHealthSnapshot({ probe: wantsProbe }); it never calls context.getRuntimeSnapshot(). (src/gateway/server-methods/health.ts:11, e672b61417af)
  • Health cache refresh cannot preserve runtime fields: refreshGatewayHealthSnapshot calls getHealthSnapshot({ probe: opts?.probe }) directly and then updates healthCache/healthVersion/broadcasts; there is no runtime snapshot argument or setHealthCache helper matching the PR's intended cache/broadcast path. (src/gateway/server/health-state.ts:72, e672b61417af)
  • Runtime-backed status path exists elsewhere: channels.status already obtains context.getRuntimeSnapshot(), resolves account runtime, and passes it into buildChannelAccountSnapshot, so the missing health overlay is a current behavior gap rather than a missing core runtime API. (src/gateway/server-methods/channels.ts:161, e672b61417af)
  • Discord startup gate remains: monitorDiscordProvider still publishes createConnectedChannelStatusPatch() only inside if (lifecycleGateway?.isConnected), while shutdown always publishes connected:false. (extensions/discord/src/monitor/provider.ts:1118, e672b61417af)
  • Regression coverage is still missing: Current Discord provider coverage asserts startup connected:true only with a mocked gateway where isConnected:true; there is no matching isConnected:false startup regression test from PR fix(gateway): preserve runtime-backed health state #39921. (extensions/discord/src/monitor/provider.test.ts:774, e672b61417af)

Remaining risk / open question:

Codex Review notes: model gpt-5.5, reasoning high; reviewed against e672b61417af.

@openclaw-barnacle openclaw-barnacle Bot removed the stale Marked as stale due to inactivity label Apr 26, 2026
@vincentkoc

Copy link
Copy Markdown
Member

ProjectClownfish could not safely update this branch, so it opened a narrow replacement PR instead.

Replacement PR: #72417
Source PR: #39921
Contributor credit is preserved in the replacement PR body and changelog plan.

@vincentkoc vincentkoc closed this Apr 26, 2026
@vincentkoc vincentkoc added the clawsweeper Tracked by ClawSweeper automation label Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: discord Channel integration: discord clawsweeper Tracked by ClawSweeper automation commands Command implementations gateway Gateway runtime size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants