Skip to content

fix(gateway): merge runtime channel state into health snapshot#42543

Closed
ajayr wants to merge 184 commits into
openclaw:mainfrom
ajayr:fix/whatsapp-health-runtime-status
Closed

fix(gateway): merge runtime channel state into health snapshot#42543
ajayr wants to merge 184 commits into
openclaw:mainfrom
ajayr:fix/whatsapp-health-runtime-status

Conversation

@ajayr

@ajayr ajayr commented Mar 10, 2026

Copy link
Copy Markdown

Summary

  • Fixes a bug where openclaw gateway call health --json could report stale/default running / connected values for channels like WhatsApp.
  • Passes the live gateway runtime snapshot into health snapshot generation so health and channels.status agree on runtime-backed channel state.
  • Adds a regression test covering runtime-backed health state.

Why this matters

channels.status was correctly showing live channel runtime state, while health could still show running: false, connected: false for an active WhatsApp channel. That made health-based monitoring and debugging misleading.

What changed

  • Added optional runtimeSnapshot plumbing to getHealthSnapshot().
  • Updated gateway health refresh paths to pass getRuntimeSnapshot() into health snapshot generation.
  • Merged per-channel/per-account runtime fields into ChannelAccountSnapshot during health snapshot creation.
  • Added a test that verifies runtime fields are reflected in the resulting health snapshot.

Scope

  • Bug fix only.
  • No config changes.
  • No new permissions or network calls.

Linked issue

Closes #42538

Verification

  • Reproduced the mismatch locally before the fix:
    • health reported stale/default runtime fields
    • channels.status reported the live runtime values
  • Verified the code path now threads runtime state through health snapshot generation.
  • Added a regression test for runtime-backed channel state.

Notes

I was not able to run the full test suite in the temp environment because pnpm was unavailable there, but the change is small and the added regression test documents the intended behavior clearly.

@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime commands Command implementations size: S labels Mar 10, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a7d2d492a8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/gateway/server/health-state.ts Outdated
@greptile-apps

greptile-apps Bot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR attempts to fix a bug where health reports stale running/connected values while channels.status shows correct live values. The approach of threading runtime state through health snapshot generation is sound, but there are three blocking issues:

  1. Compilation error: ChannelRuntimeSnapshot is used in the function signature at line 351 but is never imported, causing a TypeScript error.

  2. Runtime fields lost in final output: The enriched snapshot correctly includes connected, lastConnectedAt, and lastEventAt, but buildChannelSummary implementations (e.g., Telegram's buildTokenChannelStatusSummary) only return a fixed subset of fields — they omit the runtime-backed connection state fields. The code does not preserve these fields if they're missing from the summary, so the new regression test assertions (lines 269–272) will fail.

  3. Race condition in dedup logic: The refreshGatewayHealthSnapshot function silently discards the runtimeSnapshot from concurrent callers due to the dedup guard. A fresher runtime snapshot arriving during an in-flight refresh will be ignored.

The PR requires fixes to all three areas before it can safely merge.

Confidence Score: 1/5

  • Not safe to merge — compilation error and test failures are blocking.
  • The PR introduces a TypeScript compilation error (missing import of ChannelRuntimeSnapshot), and the regression test it adds will fail because runtime fields like connected, lastConnectedAt, and lastEventAt are not preserved when channel plugins' buildChannelSummary implementations omit them. Additionally, a race condition exists where concurrent callers' runtimeSnapshot parameters are silently discarded.
  • src/commands/health.ts requires the most attention — it has both the missing import and the incomplete runtime field propagation through buildChannelSummary. src/gateway/server/health-state.ts also needs attention for the dedup race condition.

Comments Outside Diff (2)

  1. src/commands/health.ts, line 459-496 (link)

    Runtime fields lost when buildChannelSummary omits them

    The enriched snapshot (lines 459–470) correctly includes runtime fields like connected, lastConnectedAt, and lastEventAt from runtimeForAccount. However, when buildChannelSummary is called, the returned summary object is directly cast to ChannelAccountHealthSummary and used as record without merging back the runtime fields.

    For example, Telegram's buildTokenChannelStatusSummarybuildBaseChannelStatusSummary returns only { configured, running, lastStartAt, lastStopAt, lastError, tokenSource, probe, lastProbeAt, mode } — it does not include connected, lastConnectedAt, or lastEventAt. These runtime fields are then silently dropped.

    The regression test at lines 269–272 explicitly expects these fields:

    expect(telegram.connected).toBe(true);
    expect(telegram.lastConnectedAt).toBe(123);
    expect(telegram.lastEventAt).toBe(456);

    This test will fail because those fields are not preserved.

    Preserve runtime fields by merging them into record after buildChannelSummary returns, or ensure that the runtime fields are spread into record even if buildChannelSummary doesn't include them.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/commands/health.ts
    Line: 459-496
    
    Comment:
    **Runtime fields lost when `buildChannelSummary` omits them**
    
    The enriched `snapshot` (lines 459–470) correctly includes runtime fields like `connected`, `lastConnectedAt`, and `lastEventAt` from `runtimeForAccount`. However, when `buildChannelSummary` is called, the returned `summary` object is directly cast to `ChannelAccountHealthSummary` and used as `record` without merging back the runtime fields.
    
    For example, Telegram's `buildTokenChannelStatusSummary``buildBaseChannelStatusSummary` returns only `{ configured, running, lastStartAt, lastStopAt, lastError, tokenSource, probe, lastProbeAt, mode }` — it does not include `connected`, `lastConnectedAt`, or `lastEventAt`. These runtime fields are then silently dropped.
    
    The regression test at lines 269–272 explicitly expects these fields:
    ```typescript
    expect(telegram.connected).toBe(true);
    expect(telegram.lastConnectedAt).toBe(123);
    expect(telegram.lastEventAt).toBe(456);
    ```
    This test will fail because those fields are not preserved.
    
    Preserve runtime fields by merging them into `record` after `buildChannelSummary` returns, or ensure that the runtime fields are spread into `record` even if `buildChannelSummary` doesn't include them.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. src/gateway/server/health-state.ts, line 75-92 (link)

    runtimeSnapshot from concurrent callers is silently discarded

    The deduplication guard (if (!healthRefresh)) at line 75 causes concurrent calls to reuse the in-flight promise. If a second call arrives while the first is still pending, the second caller's runtimeSnapshot is completely ignored — only the first caller's snapshot is used.

    For example:

    1. Call A starts with runtimeSnapshot from time T₀ → initiates healthRefresh
    2. Channel reconnects at time T₁
    3. Call B arrives with fresher runtimeSnapshot from T₁ → returns the pending promise from step 1 (using stale T₀ snapshot)

    With this PR threading runtime state through health snapshots, this race condition means a stale runtime snapshot can be returned even when a caller passed fresher data.

    Consider capturing and using the latest runtimeSnapshot even during an in-flight refresh, or documenting this known limitation.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/gateway/server/health-state.ts
    Line: 75-92
    
    Comment:
    **`runtimeSnapshot` from concurrent callers is silently discarded**
    
    The deduplication guard (`if (!healthRefresh)`) at line 75 causes concurrent calls to reuse the in-flight promise. If a second call arrives while the first is still pending, the second caller's `runtimeSnapshot` is completely ignored — only the first caller's snapshot is used.
    
    For example:
    1. Call A starts with `runtimeSnapshot` from time T₀ → initiates `healthRefresh`
    2. Channel reconnects at time T₁
    3. Call B arrives with fresher `runtimeSnapshot` from T₁ → returns the pending promise from step 1 (using stale T₀ snapshot)
    
    With this PR threading runtime state through health snapshots, this race condition means a stale runtime snapshot can be returned even when a caller passed fresher data.
    
    Consider capturing and using the latest `runtimeSnapshot` even during an in-flight refresh, or documenting this known limitation.
    
    How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: a7d2d49

Comment thread src/commands/health.ts
…fix dedup logic to use latest runtimeSnapshot
ajayr and others added 2 commits March 11, 2026 09:11
When a caller provides a runtimeSnapshot while a refresh is already
in-flight, await the current refresh and return the follow-up result
instead of returning stale data from the in-flight promise. This ensures
callers that supply fresh runtime state never receive a snapshot that
omits their updates.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9b8ec809d9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/server.impl.ts Outdated
Comment on lines +1073 to +1074
refreshHealthSnapshot: (opts) =>
refreshGatewayHealthSnapshot({ ...opts, runtimeSnapshot: getRuntimeSnapshot() }),

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.

P1 Badge Guard runtime snapshot capture from channel config errors

Calling getRuntimeSnapshot() inline in the health refresh wrapper makes health RPCs fail hard when any channel's config.resolveAccount throws (for example unresolved SecretRef in Google Chat throws at extensions/googlechat/src/accounts.ts:97-106). getRuntimeSnapshot currently resolves every account without a try/catch (src/gateway/server-channels.ts:539-543), so this new call path turns what used to be per-account diagnostics in getHealthSnapshot into a top-level UNAVAILABLE response for the whole endpoint; this is a regression introduced by passing runtimeSnapshot: getRuntimeSnapshot() before invoking refreshGatewayHealthSnapshot.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 11abee760getRuntimeSnapshot() is now wrapped in try/catch in both call sites (server.impl.ts lines 811-815 and 1081-1085). On failure, it logs a warning and the health refresh proceeds without runtime snapshot data instead of crashing the entire health RPC.

BunsDev and others added 17 commits March 21, 2026 11:16
…rompt snapshot (openclaw#51721)

The context-usage banner in the web UI fell back to inputTokens when
totalTokens was missing. inputTokens is accumulated across all API
calls in a run (tool-use loops, compaction retries), so it overstates
actual context window utilization -- e.g. showing "100% context used
757.3k / 200k" when the real prompt snapshot is only 46k/200k (23%).

Drop the inputTokens fallback so the banner only fires when a genuine
prompt snapshot (totalTokens) is available.

Made-with: Cursor
Merged via squash.

Prepared head SHA: a7ab64e
Co-authored-by: Pandadadadazxf <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
Merged via squash.

Prepared head SHA: 76eb184
Co-authored-by: huntharo <[email protected]>
Co-authored-by: huntharo <[email protected]>
Reviewed-by: @huntharo
…w#51753)

* refactor(doctor): add shared doctor types

* refactor(doctor): add shared allowlist helpers

* refactor(doctor): extract empty allowlist warnings

* refactor(doctor): extract telegram allowfrom scanning

* refactor(doctor): extract telegram allowfrom repair

* refactor(doctor): extract discord id repair

* refactor(doctor): add shared object helpers

* refactor(doctor): extract mutable allowlist scanning

* refactor(doctor): extract open-policy allowfrom repair

* refactor(doctor): extract allowlist policy repair

* fix(doctor): unblock discord provider refactor checks

* refactor(doctor): fix provider layering in shared warnings
…nclaw#40126)

Merged via squash.

Prepared head SHA: 5228d19
Co-authored-by: jarimustonen <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
Merged via squash.

Prepared head SHA: 89ca035
Co-authored-by: Matthew19990919 <[email protected]>
Co-authored-by: gumadeiras <[email protected]>
Reviewed-by: @gumadeiras
…nt (openclaw#30115) (openclaw#34222)

Merged via squash.

Prepared head SHA: bce6f0b
Co-authored-by: lml2468 <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
… cancellations in heartbeat sessions (openclaw#42119)

Merged via squash.

Prepared head SHA: 3429643
Co-authored-by: samzong <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman
* ACP: add hung-turn starvation repro

* ACP: recover hung bound turns

* ACP: preserve timed-out session handles

---------

Co-authored-by: Onur <[email protected]>
@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

4 similar comments
@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

17 similar comments
@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5e5e4542a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

OPENAI_CODEX_DEFAULT_MODEL,
type ProviderPlugin,
} from "openclaw/plugin-sdk/provider-models";
import { OPENAI_CODEX_DEFAULT_MODEL } from "openclaw/plugin-sdk/provider-models";

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.

P1 Badge Remove duplicate OPENAI_CODEX_DEFAULT_MODEL import

This added import redeclares OPENAI_CODEX_DEFAULT_MODEL in the same module where it is already imported, which causes a TypeScript duplicate identifier error and blocks pnpm build/typecheck for the repo.

Useful? React with 👍 / 👎.

options: {},
});
}

function expectPreparedResult(prepared: unknown): asserts prepared is { cfg: OpenClawConfig } {

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.

P2 Badge Avoid duplicate function implementation in Telegram test

This new expectPreparedResult declaration collides with the existing expectPreparedResult function later in the same file, which triggers a duplicate function implementation compile error and prevents the test file from being typechecked/executed.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling app: android App: android app: macos App: macos app: web-ui App: web-ui channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: feishu Channel integration: feishu channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: irc channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: signal Channel integration: signal channel: slack Channel integration: slack channel: telegram Channel integration: telegram channel: tlon Channel integration: tlon channel: twitch Channel integration: twitch channel: voice-call Channel integration: voice-call channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser clawsweeper Tracked by ClawSweeper automation cli CLI command changes commands Command implementations docker Docker and sandbox tooling docs Improvements or additions to documentation extensions: acpx extensions: anthropic extensions: byteplus extensions: cloudflare-ai-gateway extensions: device-pair extensions: diagnostics-otel Extension: diagnostics-otel extensions: fal extensions: huggingface extensions: kimi-coding extensions: lobster Extension: lobster extensions: memory-core Extension: memory-core extensions: memory-lancedb Extension: memory-lancedb extensions: modelstudio extensions: nvidia extensions: openai extensions: phone-control extensions: qianfan extensions: qwen-portal-auth Extension: qwen-portal-auth extensions: synthetic extensions: tavily extensions: together extensions: venice extensions: vercel-ai-gateway extensions: volcengine extensions: xiaomi gateway Gateway runtime scripts Repository scripts size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: health endpoint returns incorrect running=false for WhatsApp