Skip to content

fix(heartbeat): guard bridge availability before send and fix sentToTelegram TOCTOU#357

Merged
xlabtg merged 4 commits intoxlabtg:mainfrom
konard:issue-356-daed9dd9ab93
Apr 23, 2026
Merged

fix(heartbeat): guard bridge availability before send and fix sentToTelegram TOCTOU#357
xlabtg merged 4 commits intoxlabtg:mainfrom
konard:issue-356-daed9dd9ab93

Conversation

@konard
Copy link
Copy Markdown

@konard konard commented Apr 23, 2026

Summary

Fixes #356

Two concrete bugs were found in the Heartbeat function and corrected:

  1. runHeartbeat() (index.ts) — called bridge.sendMessage() unconditionally even when the bridge was disconnected, causing an error to be thrown and caught inside the heartbeat tick. Now checks bridge.isAvailable() first and logs a warn if the bridge is down so alert-drop failures are visible in logs instead of appearing as uncaught errors.

  2. /heartbeat/trigger (agent-actions.ts) — the sentToTelegram field in the response was computed by calling bridge.isAvailable() a second time after the message was already sent (TOCTOU). If the bridge disconnected between the send and the response, the reported value would be wrong. Now a sentToTelegram boolean flag is set at the exact moment sendMessage() is awaited successfully, making the value accurate. When the bridge is unavailable, a warn log is now emitted instead of silently dropping the alert.

  3. Missing loggercreateLogger was not imported in agent-actions.ts; added.

How to reproduce the original issues

  • Set up a heartbeat-enabled config, disconnect the Telegram bridge (e.g. revoke the session), and trigger a heartbeat. The agent log would show an uncaught send error (Heartbeat error) instead of a clean warning about bridge unavailability.
  • Trigger /heartbeat/trigger when the bridge drops between the send attempt and the response — sentToTelegram in the API response would be false even though the message had been sent.

Tests added

src/webui/__tests__/agent-actions.test.ts — 8 unit tests for POST /heartbeat/trigger:

  • Returns 422 when heartbeat is disabled
  • Returns 422 when no admin_ids configured
  • Suppresses NO_ACTION response — does not send to Telegram
  • Suppresses __SILENT__ response — does not send to Telegram
  • Sends actionable response to Telegram when bridge is available
  • Does not send and reports sentToTelegram=false when bridge unavailable
  • Returns 500 when processMessage throws
  • sentToTelegram reflects actual send, not a re-check of availability (TOCTOU regression test)

All 8 tests pass locally.

Adding .gitkeep for PR creation (default mode).
This file will be removed when the task is complete.

Issue: xlabtg#356
…elegram TOCTOU

- In runHeartbeat() (index.ts): check bridge.isAvailable() before
  attempting sendMessage() so a disconnected bridge logs a warning
  instead of throwing an uncaught error inside the heartbeat tick.

- In /heartbeat/trigger (agent-actions.ts): replace the TOCTOU pattern
  where sentToTelegram was computed by calling isAvailable() a second
  time after the send. Now a boolean flag is set at the moment the
  message is actually sent, ensuring the reported value is accurate
  even if bridge availability changes between the send and the response.

- Log a warn-level message when the bridge is unavailable so silent
  alert-drop failures are visible in logs.

- Add createLogger to agent-actions.ts (was missing).

- Add src/webui/__tests__/agent-actions.test.ts with 8 unit tests
  covering: disabled heartbeat, missing admin_ids, NO_ACTION suppression,
  __SILENT__ suppression, actionable alert with bridge available,
  actionable alert with bridge unavailable, processMessage error path,
  and TOCTOU sentToTelegram correctness.
@konard konard changed the title [WIP] Full verification of the logic of the Heartbeat function fix(heartbeat): guard bridge availability before send and fix sentToTelegram TOCTOU Apr 23, 2026
@konard konard marked this pull request as ready for review April 23, 2026 16:35
@konard
Copy link
Copy Markdown
Author

konard commented Apr 23, 2026

Solution summary

Done. Here's a summary of what was done:


What was fixed

Root cause analysis of issue #356 ("errors occur in the agent's logs when the heartbeat function is called"):

Bug 1 — runHeartbeat() missing bridge availability check (index.ts:1498)

The timer-based heartbeat called bridge.sendMessage() unconditionally, even when the Telegram bridge was disconnected. This caused the sendMessage() to throw, the error was caught and logged as "Heartbeat error" — which is the log error the issue describes.

Fix: Added this.bridge.isAvailable() check before sendMessage(). When the bridge is down, a warn-level log is emitted instead of an error from a thrown exception.

Bug 2 — TOCTOU sentToTelegram in /heartbeat/trigger (agent-actions.ts:124)

The sentToTelegram response field was calculated by calling bridge.isAvailable() a second time after the message was already sent. If the bridge disconnected between the send and the response construction, the field would be incorrectly false. The code also had no log when silently dropping an alert due to bridge unavailability.

Fix: Track actual send with a boolean flag set at the moment sendMessage() succeeds. Added a warn log when the bridge is unavailable.

Tests added — src/webui/__tests__/agent-actions.test.ts

8 unit tests covering all paths including a TOCTOU regression test. All pass.

PR: #357


This summary was automatically extracted from the AI working session output.

@konard
Copy link
Copy Markdown
Author

konard commented Apr 23, 2026

🤖 Solution Draft Log

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost: $1.775644

📊 Context and tokens usage:

Claude Sonnet 4.6:

  • 66.1K / 1M (7%) input tokens, 15.6K / 64K (24%) output tokens

Total: (59.8K + 3.6M cached) input tokens, 15.6K output tokens, $1.551538 cost

Claude Haiku 4.5:

  • 6.5K / 64K (10%) output tokens

Total: (82.8K + 880.1K cached) input tokens, 6.5K output tokens, $0.224106 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: sonnet
  • Main model: Claude Sonnet 4.6 (claude-sonnet-4-6)
  • Additional models:
    • Claude Haiku 4.5 (claude-haiku-4-5-20251001)

📎 Log file uploaded as Gist (2412KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard
Copy link
Copy Markdown
Author

konard commented Apr 23, 2026

🔄 Auto-restart triggered (iteration 1)

Reason: Merge conflicts detected

Starting new session to address the issues.


Auto-restart-until-mergeable mode is active. Will continue until PR becomes mergeable.

@konard
Copy link
Copy Markdown
Author

konard commented Apr 23, 2026

🔄 Auto-restart-until-mergeable Log (iteration 1)

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost: $0.180248

📊 Context and tokens usage:

  • 24.5K / 1M (2%) input tokens, 2.2K / 64K (3%) output tokens

Total: (14.3K + 315.1K cached) input tokens, 2.2K output tokens, $0.180248 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: sonnet
  • Model: Claude Sonnet 4.6 (claude-sonnet-4-6)

📎 Log file uploaded as Gist (2802KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard
Copy link
Copy Markdown
Author

konard commented Apr 23, 2026

✅ Ready to merge

This pull request is now ready to be merged:

  • All CI checks have passed
  • No merge conflicts
  • No pending changes

Monitored by hive-mind with --auto-restart-until-mergeable flag

@xlabtg xlabtg merged commit 1d25d73 into xlabtg:main Apr 23, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Full verification of the logic of the Heartbeat function

2 participants