Skip to content

fix(cron): de-duplicate main-session systemEvent in heartbeat model input#91287

Open
ZengWen-DT wants to merge 1 commit into
openclaw:mainfrom
ZengWen-DT:fix/cron-main-systemevent-duplicate-reminder
Open

fix(cron): de-duplicate main-session systemEvent in heartbeat model input#91287
ZengWen-DT wants to merge 1 commit into
openclaw:mainfrom
ZengWen-DT:fix/cron-main-systemevent-duplicate-reminder

Conversation

@ZengWen-DT

@ZengWen-DT ZengWen-DT commented Jun 8, 2026

Copy link
Copy Markdown

AI-assisted (Claude). All behavior proof below was produced and verified by a human-run command in a real local checkout.

Summary

  • Problem: A cron job with sessionTarget: "main" and payload.kind: "systemEvent" surfaced its text to the model twice in the same turn — once as a raw System: line rendered by the reply pipeline, and again wrapped by the heartbeat (A scheduled reminder has been triggered…). delivery.mode: "none" only governs channel relay, so it did not suppress the duplicate.
  • Why now: Reported in [Bug]: Cron job with sessionTarget: "main" triggers both systemEvent and reminder despite delivery.mode: "none" #44922; the duplicate clutters the main session transcript and feeds the same instruction/reminder to the model twice.
  • Root cause: selectGenericSystemEvents (src/auto-reply/reply/session-system-events.ts) already excludes exec-completion events from the generic System: render because they own a dedicated heartbeat prompt (buildExecEventPrompt). Cron events own the same kind of dedicated prompt (buildCronEventPrompt) but were never added to that exclusion, so they were rendered raw and wrapped.
  • Outcome: Exclude events tagged cron:<jobId> (set at enqueue in src/cron/service/timer.ts:1409) from the generic render, leaving the heartbeat path as the single owner that consumes and surfaces them — symmetric with exec-completions. The same latent double-surfacing on the auto-disabled notice (cron:<jobId>:auto-disabled) is fixed too.
  • Out of scope / unaffected: Isolated sessions (they run under a separate :heartbeat session key, so only the wrapper ever surfaced). The isolated→main awareness mirror uses a cron-direct-delivery: key, which does not match cron:, so it still renders raw as before.
  • Reviewer focus: that startsWith("cron:") matches exactly the heartbeat-owned cron events and nothing that relies on the raw render.

Linked context

Closes #44922

Related: none. Maintainer/owner requested: no (open queueable-style bug; the prior commenter who claimed a fix never opened a PR).

Real behavior proof (required for external PRs)

  • Behavior or issue addressed: cron job sessionTarget:"main" + payload.kind:"systemEvent" surfacing its text to the model twice (raw System: line + heartbeat reminder wrapper) — [Bug]: Cron job with sessionTarget: "main" triggers both systemEvent and reminder despite delivery.mode: "none" #44922.
  • Real environment tested: local checkout at upstream/main 2a21de6322 with the patch applied; the repo's tsx runner driving the real, unmocked assembly functions drainFormattedSystemEvents (reply-pipeline raw render) and buildCronEventPrompt (heartbeat wrapper) — the exact two surfaces that build the model input. Node 22, Linux.
  • Exact steps or command run after this patch:
    enqueueSystemEvent("Reminder: rotate API keys", {
      sessionKey: "agent:main:main", contextKey: "cron:rotate-keys" });           // as cron service does
    const raw = await drainFormattedSystemEvents({ cfg, sessionKey: "agent:main:main",
      isMainSession: true, isNewSession: false });                                // reply-pipeline raw render
    const wrapper = buildCronEventPrompt(["Reminder: rotate API keys"], { deliverToUser: false }); // heartbeat (delivery.mode none)
    run via node --import tsx repro-44922.mts.
  • Evidence after fix: terminal capture / console output below:
    === (1) RAW System: block (reply pipeline) ===
    (none)
    === (2) Heartbeat cron wrapper (turn Body) ===
    A scheduled reminder has been triggered. The reminder content is:
    Reminder: rotate API keys
    Handle this reminder internally. Do not relay it to the user unless explicitly requested.
    --- result ---
    reminder text in RAW block?      false
    reminder text in wrapper?        true
    SURFACED TWICE TO MODEL?         NO   <-- single surface
    still queued for heartbeat owner? ["Reminder: rotate API keys"]
    
  • Observed result after fix: the cron text reaches the model exactly once (via the heartbeat wrapper); the duplicate raw System: line is gone; the event stays queued so the heartbeat path remains its single owner.
  • What was not tested: a full live gateway run with a Telegram-delivered cron job firing on a real schedule; multi-turn transcript inspection in a running agent.
  • Proof limitations or environment constraints: the duplication is purely in prompt assembly (no network/channel I/O), so it is reproduced directly at the two real assembly functions rather than through a live gateway. Before/after was captured by toggling only the production change (git stash of the one prod file).
  • Before evidence (fix reverted, same command):
    === (1) RAW System: block (reply pipeline) ===
    System: [2026-06-08 08:54:51 GMT+8] Reminder: rotate API keys
    === (2) Heartbeat cron wrapper (turn Body) ===
    A scheduled reminder has been triggered. The reminder content is:
    Reminder: rotate API keys
    Handle this reminder internally. Do not relay it to the user unless explicitly requested.
    --- result ---
    reminder text in RAW block?      true
    reminder text in wrapper?        true
    SURFACED TWICE TO MODEL?         YES  <-- bug
    still queued for heartbeat owner? []
    

Tests and validation

  • Commands run: pnpm tsgo, node scripts/run-oxlint.mjs <files>, pnpm format:check <files>, pnpm build, and node scripts/run-vitest.mjs on session.test.ts (104), heartbeat-runner.ghost-reminder.test.ts (16), system-events.test.ts (40), get-reply-run.media-only.test.ts (80) — all green.
  • Regression coverage added: drainFormattedSystemEvents test in session.test.ts asserting a cron:-tagged event is excluded from the generic System: render and left queued, while a non-cron event still renders.
  • What failed before this fix: with the prod change reverted, the same drainFormattedSystemEvents path rendered the cron event as a raw System: line in addition to the heartbeat wrapper (see Before evidence).
  • No test added? n/a — regression test added.

…nput

A sessionTarget:"main" cron systemEvent was surfaced to the model twice:
the reply pipeline rendered it as a raw System: line and the heartbeat also
wrapped it via buildCronEventPrompt. selectGenericSystemEvents excluded
exec-completion events (which own a dedicated heartbeat prompt) but not cron
events, which own buildCronEventPrompt. Exclude cron:<jobId>-tagged events so
the heartbeat path is their single owner, symmetric with exec-completions.

Closes openclaw#44922
@openclaw-barnacle openclaw-barnacle Bot added size: XS triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels Jun 8, 2026
@clawsweeper

clawsweeper Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 7, 2026, 9:44 PM ET / 01:44 UTC.

Summary
The PR filters cron: context system events out of generic System: rendering and adds regression coverage that leaves them queued for heartbeat prompt handling.

PR surface: Source +13, Tests +28. Total +41 across 2 files.

Reproducibility: yes. source-level. Current main enqueues cron: system events, heartbeat wraps cron: events into a dedicated prompt, and the generic drain only excludes exec completions before this PR.

Review metrics: none identified.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Risk before merge

  • [P1] A full live scheduled Gateway or Telegram cron flow was not run in this read-only review; the supplied proof covers the unmocked prompt-assembly functions where the duplicate is built.

Maintainer options:

  1. Decide the mitigation before merge
    Land this narrow owner-boundary fix after normal maintainer review and checks, keeping heartbeat as the single owner for cron: events while preserving generic rendering for non-cron and direct-delivery mirror events.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Next step before merge

  • No ClawSweeper repair lane is needed because the submitted PR is already narrow and has no blocking review findings.

Security
Cleared: The diff only changes local TypeScript prompt/event filtering and a colocated test; it does not touch dependencies, CI, secrets, publishing, or code-download paths.

Review details

Best possible solution:

Land this narrow owner-boundary fix after normal maintainer review and checks, keeping heartbeat as the single owner for cron: events while preserving generic rendering for non-cron and direct-delivery mirror events.

Do we have a high-confidence way to reproduce the issue?

Yes, source-level. Current main enqueues cron: system events, heartbeat wraps cron: events into a dedicated prompt, and the generic drain only excludes exec completions before this PR.

Is this the best way to solve the issue?

Yes. Filtering heartbeat-owned cron: entries out of generic rendering mirrors the existing exec-completion ownership model and is narrower than changing cron delivery semantics.

AGENTS.md: found and applied where relevant.

Codex review notes: model gpt-5.5, reasoning high; reviewed against b16a43597d49.

Label changes

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes before/after terminal output from a local checkout exercising the unmocked prompt-assembly surfaces and showing duplicate raw-plus-wrapper input reduced to wrapper-only input.

Label justifications:

  • P2: The PR fixes a real but bounded duplicate model-input/session-transcript bug for cron-triggered main-session system events.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes before/after terminal output from a local checkout exercising the unmocked prompt-assembly surfaces and showing duplicate raw-plus-wrapper input reduced to wrapper-only input.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes before/after terminal output from a local checkout exercising the unmocked prompt-assembly surfaces and showing duplicate raw-plus-wrapper input reduced to wrapper-only input.
Evidence reviewed

PR surface:

Source +13, Tests +28. Total +41 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 14 1 +13
Tests 1 28 0 +28
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 42 1 +41

What I checked:

Likely related people:

  • steipete: Current-line blame points to recent squashed main history across the generic system-event drain, cron prompt, heartbeat event selection, and cron enqueue sites; history also shows heartbeat event filter refactoring. (role: recent area contributor; confidence: medium; commits: c2d825ae534c, 5a431f57fc18; files: src/auto-reply/reply/session-system-events.ts, src/infra/heartbeat-events-filter.ts, src/infra/heartbeat-runner.ts)
  • pvtclawn: Commit c12f693c59ea introduced embedding actual cron event text in the heartbeat cron prompt, which is one half of the duplicate surface. (role: introduced related prompt behavior; confidence: high; commits: c12f693c59ea; files: src/infra/heartbeat-runner.ts, src/infra/heartbeat-events-filter.ts)
  • vignesh07: Commit 4c4d2558e34c preserved cron prompts for tagged interval events in the same context-key-based heartbeat path, and GitHub metadata shows this account as committer on the dynamic prompt change. (role: adjacent cron heartbeat contributor; confidence: medium; commits: 4c4d2558e34c, c12f693c59ea; files: src/cron/service/timer.ts, src/infra/heartbeat-runner.ts)
  • vincentkoc: Commit 1c9053802a98 split main and detached cron dispatch, making executeMainSessionCronJob the current entry point involved in this behavior. (role: recent cron dispatch refactor contributor; confidence: medium; commits: 1c9053802a98; files: src/cron/service/timer.ts)
  • mbelinky: Commit d4e59a3666d8 is adjacent history for the cron-owned delivery contract that separates delivery mode from heartbeat prompt construction. (role: cron delivery contract contributor; confidence: medium; commits: d4e59a3666d8; files: src/cron/service/timer.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal backlog priority with limited blast radius. labels Jun 8, 2026
@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. proof: sufficient ClawSweeper judged the real behavior proof convincing. labels Jun 8, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. size: XS status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Cron job with sessionTarget: "main" triggers both systemEvent and reminder despite delivery.mode: "none"

2 participants