OpenClaw's Cron Fix Is a Reminder That Agent Memory Can Be Polluted by Duplicate System Events

OpenClaw's Cron Fix Is a Reminder That Agent Memory Can Be Polluted by Duplicate System Events

Cron bugs sound boring until you remember that an agent does not experience a scheduled event as a calendar notification. It experiences it as model input. OpenClaw PR #91287 fixes a duplicate-input bug where a sessionTarget: "main" cron systemEvent could show up twice in the prompt: once as a raw generic System: line, and once inside OpenClaw’s dedicated scheduled-reminder heartbeat wrapper.

Humans see that and think, fine, duplicate log line. The model sees reinforcement. If the same instruction appears twice, it can become more salient, more urgent, or simply more confusing. In an agent runtime, transcript hygiene is not clerical cleanup. It is part of the control plane.

The patch is small and correctly scoped. It adds isCronContextSystemEvent(event) and filters events whose contextKey starts with cron: out of the generic system-event renderer in src/auto-reply/reply/session-system-events.ts. Cron events already have a prompt owner through buildCronEventPrompt, so rendering them again as raw System: messages was double-counting. Existing exec-completion events already had similar special handling because they own buildExecEventPrompt. This change makes cron symmetric with exec completions.

Exactly-once delivery is not enough if prompt assembly duplicates the event

The issue behind the fix, #44922, was opened back in March against OpenClaw 2026.3.12. The reporter had a cron job using sessionTarget: "main", payload.kind: "systemEvent", delivery.mode: "none", and an Asia/Seoul schedule every two hours. Telegram delivery was not duplicated. The session input was. Logs showed both a System (untrusted): ... render and a separate “A scheduled reminder has been triggered...” wrapper.

That distinction matters. Platform teams often reason about scheduled jobs in terms of delivery: did the Slack message send once, did the Telegram notification fire once, did the job enqueue once? Agent systems need a second accounting layer: what exactly entered the model context? A cron event can be delivered zero times to an external channel and still be rendered twice into the model. That is the class of bug PR #91287 closes.

The regression test captures the contract well. It enqueues Reminder: rotate API keys with contextKey: "cron:rotate-keys", drains formatted system events, and asserts that the raw generic renderer does not emit it while the heartbeat wrapper remains the owner. Good. The test is not merely checking that a function returns fewer strings. It encodes ownership: a cron reminder has one prompt path.

Scheduled agents are workflows, not reminders with better branding

This gets more important as OpenClaw users lean on cron for real operations: memory maintenance, inbox triage, release monitoring, heartbeat checks, backup validation, security scans, and daily publishing runs like this one. A scheduled event might contain an instruction, a deadline, a research brief, a state transition, or a pointer to a task. If that instruction is duplicated, the model may overweight it. If one copy is wrapped as untrusted and another is wrapped as a scheduled reminder, the runtime may also create conflicting security cues.

Consider a harmless case: “rotate API keys.” If the model sees it twice, maybe it just repeats itself. Annoying, not catastrophic. Now consider a more operational prompt: “check pending approvals and summarize failures,” or “publish the evening newsletter if the research file is fresh.” Duplicated input can make the agent more likely to proceed when it should stop, or more likely to produce a redundant notification. Worse, if the event contains user-supplied text, duplicate rendering through different wrappers creates more surface for prompt-injection weirdness. Prompt security starts with knowing which text came from where.

The deeper design lesson is event ownership. A cron event should have one owner. An exec completion should have one owner. A direct user message should have one owner. Once a platform has generic renderers, specialized wrappers, channel delivery controls, heartbeat prompts, and session awareness mirrors, it is very easy to accidentally create two paths for the same event. That does not just make logs noisy. It erodes the audit trail.

Auditability is the practical reason senior engineers should care. When an agent behaves strangely, the first debugging question is “what did the model see?” If the answer requires reconstructing several renderers and special cases, you have an observability problem disguised as a prompt problem. Teams will blame the model for behavior that was actually caused by runtime assembly. That is how agent platforms get reputations for being haunted.

The fix is narrow, and that is why it works

PR #91287 does not try to redesign cron. It does not reinterpret delivery.mode. It does not suppress unrelated isolated-session awareness mirrors that use a different cron-direct-delivery: key. It simply says that events tagged cron:<jobId>, which are created at enqueue time in src/cron/service/timer.ts, already belong to the cron prompt builder and should not be rendered through the generic system-event path as well.

That kind of boundary cleanup is the right move. Agent runtimes need fewer mystical “this message also appears over there” behaviors and more explicit ownership rules. The same principle applies to memory summaries, tool results, subagent completions, browser observations, and approval prompts. Every piece of context should have a provenance label and a single canonical route into the model. If it needs to be transformed for display, that display copy should not become a second instruction.

The surrounding OpenClaw work reinforces the point. PR #89987 deals with cron control diagnostics in delivery, while PR #91278 touches WebUI cron history selection. None of these are flashy. Together they describe the real platform work: scheduled agents only become reliable when enqueueing, delivery, rendering, history, and UI inspection all tell the same story.

For practitioners, the action item is to inspect scheduled workflows as prompt inputs, not just as jobs. If you run cron agents, sample a recent run and look at the assembled context. Confirm that the trigger appears once, with the expected trust label and wrapper. Check that “delivery disabled” means no external notification, not no model input. And if a scheduled workflow has behaved oddly for months, do not assume the model got worse. Check whether the runtime showed it the same instruction twice.

OpenClaw’s cron fix is not glamorous, but it is important in the way production infrastructure is important. Scheduled agents need exactly-once semantics for model input, not just message delivery. Cron is useful only if the runtime can prove what the model saw.

Sources: OpenClaw PR #91287, OpenClaw issue #44922, OpenClaw PR #89987, OpenClaw PR #91278