Skip to content

fix(heartbeat): default target none and internalize cron/exec relay prompts#25916

Merged
steipete merged 2 commits intomainfrom
fix/heartbeat-default-none-routing-25871
Feb 25, 2026
Merged

fix(heartbeat): default target none and internalize cron/exec relay prompts#25916
steipete merged 2 commits intomainfrom
fix/heartbeat-default-none-routing-25871

Conversation

@steipete
Copy link
Contributor

@steipete steipete commented Feb 25, 2026

Summary

  • Problem: heartbeat delivery defaulted to target: "last", so unattended heartbeat alerts could route to the last external DM (including Discord DMs).
  • Why it matters: this creates leakage/spam risk for mixed personal+external chats and conflicts with internal-only heartbeat workflows.
  • What changed: default heartbeat target now resolves to none; docs/config references updated; cron/exec heartbeat prompt wording now switches to internal-only language when outbound delivery is disabled.
  • What did NOT change (scope boundary): explicit target: "last" / explicit channel targets still work as before; outbound channel implementations unchanged.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

  • Default heartbeat target changed from last to none when unset.
  • Heartbeats now require explicit target config for external delivery.
  • Cron/exec heartbeat prompt text uses internal-only wording when delivery is disabled.

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation:

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Node 22 + pnpm
  • Model/provider: N/A (unit + integration tests)
  • Integration/channel (if any): heartbeat routing + prompt generation
  • Relevant config (redacted): heartbeat defaults with unset target / target: "none"

Steps

  1. Run heartbeat delivery resolution with config missing heartbeat.target.
  2. Trigger cron/exec heartbeat flows with target: "none".
  3. Observe outbound send behavior and prompt text.

Expected

  • Unset target resolves to internal-only (none).
  • No outbound send when delivery disabled.
  • Cron/exec event prompts do not tell agent to relay to user.

Actual

  • Matches expected after patch.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios:
    • resolveHeartbeatDeliveryTarget default behavior now target-none.
    • Internal-only cron prompt path with queued cron event + target: none.
    • Internal-only exec prompt path with queued exec event + target: none.
  • Edge cases checked:
    • Explicit target: "last" coverage kept in tests.
    • Existing target/account allow/deny coverage preserved.
  • What you did not verify:
    • Live Discord DM end-to-end on external runtime (covered by unit/integration tests only).

Compatibility / Migration

  • Backward compatible? (No)
  • Config/env changes? (No)
  • Migration needed? (Yes)
  • If yes, exact upgrade steps:
    1. If you rely on outbound heartbeat delivery, set agents.defaults.heartbeat.target explicitly ("last" or channel id).
    2. Optionally set heartbeat.to/accountId for deterministic routing.

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly:
    • Set agents.defaults.heartbeat.target: "last" to restore prior default behavior.
  • Files/config to restore:
    • src/infra/heartbeat-runner.ts
    • src/infra/outbound/targets.ts
  • Known bad symptoms reviewers should watch for:
    • No outbound heartbeat alerts when users expected old implicit delivery.

Risks and Mitigations

  • Risk: users depending on implicit last-recipient delivery may think heartbeat is broken.
    • Mitigation: docs/changelog updated + explicit migration note in this PR.
  • Risk: prompt wording change could alter model behavior in no-delivery mode.
    • Mitigation: added new unit/integration tests for cron + exec internal-only prompt paths.

Mintlify

0 threads from 0 users in Mintlify

  • No unresolved comments

Open in Mintlify Editor

Greptile Summary

Changed the default heartbeat delivery target from last to none, preventing unattended heartbeat alerts from routing to external channels (including Discord DMs) unless explicitly configured. When delivery is disabled (target: "none"), cron and exec event prompts now use internal-only language that avoids instructing the agent to relay messages to users.

Key Changes:

  • Default heartbeat target in src/infra/heartbeat-runner.ts:99 and src/infra/outbound/targets.ts:213 changed from "last" to "none"
  • New buildExecEventPrompt function with deliverToUser option to generate internal-only or user-facing prompts
  • Updated buildCronEventPrompt to accept deliverToUser option and generate appropriate prompt wording
  • Prompt selection logic in heartbeat-runner.ts:650-657 determines canRelayToUser based on delivery channel, target, and visibility settings
  • Comprehensive test coverage for default target behavior and internal-only prompt paths
  • Documentation updated across heartbeat.md, configuration-reference.md, and cron-vs-heartbeat.md to reflect new defaults

Migration Impact:
Users relying on implicit last recipient delivery will need to explicitly set agents.defaults.heartbeat.target: "last" to restore previous behavior.

Confidence Score: 4/5

  • This PR is safe to merge with low risk, addressing a legitimate privacy/leakage concern with thorough testing
  • The change is well-implemented with comprehensive test coverage for both default target behavior and prompt internalization. The logic correctly determines when to use internal-only prompts based on delivery configuration. However, this is a breaking change that will affect users expecting implicit external delivery, so the migration path must be clearly communicated (which it is in the PR description and CHANGELOG).
  • No files require special attention - the implementation is solid and well-tested

Last reviewed commit: 34a754d

@steipete
Copy link
Contributor Author

/landpr

@cursor
Copy link

cursor bot commented Feb 25, 2026

PR Summary

Medium Risk
Behavior change is user-visible and can silently stop outbound heartbeat notifications for configs relying on the old implicit last default; core routing logic is touched but is well-covered by new/updated tests.

Overview
Heartbeat delivery now defaults to target: "none" when unset (previously last), so unattended heartbeats won’t automatically route to the most recent external DM; docs and changelog are updated to reflect the new default and migration path.

Cron and exec-completion heartbeat flows now choose between user-relay vs internal-only prompt wording based on whether delivery is actually possible/allowed, and tests are expanded to cover the new default routing, internal-only prompt behavior, and a couple of platform/path normalization expectations.

Written by Cursor Bugbot for commit cef9cff. This will update automatically on new commits. Configure here.

@steipete
Copy link
Contributor Author

/landpr

Copy link

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

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: 5d9ba595cc

ℹ️ 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 on lines +213 to 215
let target: HeartbeatTarget = "none";
if (rawTarget === "none" || rawTarget === "last") {
target = rawTarget;

Choose a reason for hiding this comment

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

P2 Badge Normalize reserved heartbeat targets before fallback

resolveHeartbeatDeliveryTarget now initializes target to "none" and only matches rawTarget with a case-sensitive check ("none"/"last"), so configs that pass validation with mixed-case reserved values (for example target: "LAST", accepted by validateHeartbeatTarget after lowercasing in src/config/validation.ts) silently resolve to target-none and stop outbound heartbeat delivery. Before this change, the same input still behaved like last because the fallback default was "last", so this is a regression for existing mixed-case configs.

Useful? React with 👍 / 👎.

@steipete
Copy link
Contributor Author

/landpr

1 similar comment
@steipete
Copy link
Contributor Author

/landpr

@openclaw-barnacle openclaw-barnacle bot added docker Docker and sandbox tooling agents Agent runtime and tooling labels Feb 25, 2026
@steipete steipete force-pushed the fix/heartbeat-default-none-routing-25871 branch from 34fc446 to cef9cff Compare February 25, 2026 01:13
@steipete
Copy link
Contributor Author

/landpr

1 similar comment
@steipete
Copy link
Contributor Author

/landpr

@steipete steipete merged commit a177b10 into main Feb 25, 2026
27 checks passed
@steipete steipete deleted the fix/heartbeat-default-none-routing-25871 branch February 25, 2026 01:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling docker Docker and sandbox tooling docs Improvements or additions to documentation gateway Gateway runtime maintainer Maintainer-authored PR size: M

Projects

None yet

1 participant