openclaw - 💡(How to fix) Fix [Feature]: Honor session.dmScope when resolving target session for exec-event/cron-event heartbeat wakes [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#71581Fetched 2026-04-26 05:11:10
View on GitHub
Comments
0
Participants
1
Timeline
1
Reactions
0
Author
Participants
Timeline (top)
labeled ×1

Extend session.dmScope=main so that internal heartbeat wakes triggered by exec-event and cron-event also resolve to the agent's main session, not just inbound DMs.

Root Cause

  • In resolveHeartbeatSession (or a new helper called from runOnce), after deriving forcedCandidate, check whether session.dmScope=main is effective for the resolved agent and whether the candidate belongs to that agent (resolveAgentIdFromSessionKey === resolvedAgentId).
  • If both conditions hold, return mainSessionKey instead of canonical, with suppressOriginatingContext=false so the system event is still queued/visible.
  • The system-events queue itself should remain keyed by the original sessionKey, OR be re-keyed to mainSessionKey at enqueue time when dmScope=main is in effect — whichever the maintainers prefer for queue semantics. The latter is cleaner because it keeps drainSystemEventEntries(mainSessionKey) consistent.

Fix Action

Fix / Workaround

  • Leave behavior as-is and document it. Rejected: it contradicts the stated purpose of dmScope=main ("unify a single-owner agent into one session"), and silently splits assistant context across two sessions when background execs complete.
  • Periodic merge/cleanup of orphan sessions into main. Rejected: complex, lossy, and doesn't fix the underlying inconsistency.
  • Operator-side workaround (avoid background execs). Rejected: backgrounding is the recommended path for long-running commands; avoiding it conflicts with the documented exec ergonomics.
RAW_BUFFERClick to expand / collapse

Summary

Extend session.dmScope=main so that internal heartbeat wakes triggered by exec-event and cron-event also resolve to the agent's main session, not just inbound DMs.

Problem to solve

session.dmScope=main is documented as a way to unify a single-owner agent's interactions into one session (agent:<id>:main) for context continuity. Today it only affects inbound DM routing from external channels — internal heartbeat wakes triggered by exec-event (background exec completion) and cron-event continue to be processed in whichever per-channel session originally launched them (e.g. agent:main:telegram:default:direct:<userId>).

For an operator who configured dmScope=main precisely to keep all assistant context in agent:main:main, this means:

  • exec-event follow-ups (e.g. "Exec completed (..)" system events from background commands) land and run in the per-channel "orphan" session, not in the main session. They are processed with the heartbeat model and most often produce NO_REPLY, so user-facing delivery is unaffected, but the assistant's main session never sees the event in its history.
  • The same applies to cron-event auto-replies whose configured session resolves to a non-canonical alias.

Code path verified on 2026.4.23:

  1. bash-tools.exec-runtime → enqueueSystemEvent({ sessionKey }) + requestHeartbeatNow({ sessionKey }) using the launching session's literal sessionKey.
  2. heartbeat-runner → runOnce({ sessionKey: forcedSessionKey }) → resolveHeartbeatSession(..., forcedSessionKey).
  3. resolveHeartbeatSession calls canonicalizeMainSessionAlias, which only rewrites the sessionKey if it literally matches "main", the configured mainKey, agent:<id>:main, or one of two legacy aliases. A per-channel sessionKey like agent:main:telegram:default:direct:<userId> does not match any alias and is returned unchanged.
  4. dmScope is not consulted anywhere in this path. It is only read by resolvePinnedMainDmOwnerFromAllowlist in dm-policy-shared.js, which is invoked from the inbound monitor path (monitor-*.js around line 578) when a DM message arrives.

Net effect: dmScope partially achieves its stated goal of "single session for single-owner agents" — inbound is unified, but internal triggers are not.

Proposed solution

When session.dmScope=main and the agent has a single-owner allowlist (the same precondition resolvePinnedMainDmOwnerFromAllowlist uses), the heartbeat-runner should redirect forcedSessionKey to the agent's main session before invoking runOnce. Concretely:

  • In resolveHeartbeatSession (or a new helper called from runOnce), after deriving forcedCandidate, check whether session.dmScope=main is effective for the resolved agent and whether the candidate belongs to that agent (resolveAgentIdFromSessionKey === resolvedAgentId).
  • If both conditions hold, return mainSessionKey instead of canonical, with suppressOriginatingContext=false so the system event is still queued/visible.
  • The system-events queue itself should remain keyed by the original sessionKey, OR be re-keyed to mainSessionKey at enqueue time when dmScope=main is in effect — whichever the maintainers prefer for queue semantics. The latter is cleaner because it keeps drainSystemEventEntries(mainSessionKey) consistent.

Either way, the net behavior should be: with dmScope=main, all exec-event and cron-event wakes for that agent are processed in agent:<id>:main, with their event text visible in the main session history.

Alternatives considered

  • Leave behavior as-is and document it. Rejected: it contradicts the stated purpose of dmScope=main ("unify a single-owner agent into one session"), and silently splits assistant context across two sessions when background execs complete.
  • Periodic merge/cleanup of orphan sessions into main. Rejected: complex, lossy, and doesn't fix the underlying inconsistency.
  • Operator-side workaround (avoid background execs). Rejected: backgrounding is the recommended path for long-running commands; avoiding it conflicts with the documented exec ergonomics.

Impact

Affected: Single-owner agents with session.dmScope=main and frequent use of background exec commands. Severity: Low — no user-facing message is lost (delivery context is independent of processing sessionKey, and runs typically respond NO_REPLY). Frequency: Every time a background exec or relevant cron-event completes outside the originating turn — daily for agents that delegate work to background. Consequence: The assistant's main session loses visibility into completed background work, which can cause it to miss follow-up state ("did command X finish?"). Cross-session context is also harder to reason about for the operator: there is one session pinned for inbound DMs and a per-channel orphan session that keeps receiving exec-event traffic and growing.

Evidence/examples

OpenClaw 2026.4.23 (a979721) on Ubuntu 24.04 / aarch64.

Config relevant: session.dmScope=main, agents.defaults.heartbeat.isolatedSession=false, agents.defaults.heartbeat.model=anthropic/claude-opus-4-7, single-owner Telegram allowlist.

Observed: orphan session agent:main:telegram:default:direct:<userId> received 5 turns in a single day, each one of the form System (untrusted): [HH:MM] Exec completed (xxx-xx, code 0) :: ... followed by NO_REPLY. Inbound user messages from the same Telegram chat were correctly routed to agent:main:main.

Code references (paths under node_modules/openclaw/dist on 2026.4.23):

  • bash-tools.exec-runtime — maybeNotifyOnExit → enqueueSystemEvent({ sessionKey }) + requestHeartbeatNow({ sessionKey, reason: "exec-event" }).
  • heartbeat-runner — wakeHandler → runOnce → resolveHeartbeatSession(cfg, agentId, heartbeat, forcedSessionKey).
  • main-session — canonicalizeMainSessionAlias only matches literal main aliases (no dmScope check).
  • dm-policy-shared — resolvePinnedMainDmOwnerFromAllowlist is the only consumer of session.dmScope, called from monitor-*.js for inbound DM routing.

Additional information

Backward compatibility: change should be gated behind dmScope=main + single-owner allowlist (the same precondition resolvePinnedMainDmOwnerFromAllowlist already enforces), so multi-owner / dmScope=channel deployments are unaffected.

Related but distinct: agents.defaults.heartbeat.model already controls which model processes exec-event/cron-event wakes, so the model coherence side of "background work follow-up should match main session" is solvable today (we set it to claude-opus-4-7). What remains uncovered is sessionKey routing — this request.

extent analysis

TL;DR

Modify the resolveHeartbeatSession function to redirect the forcedSessionKey to the agent's main session when session.dmScope=main and the agent has a single-owner allowlist.

Guidance

  • Check if session.dmScope=main is effective for the resolved agent and if the candidate belongs to that agent.
  • If both conditions hold, return mainSessionKey instead of canonical with suppressOriginatingContext=false.
  • Consider re-keying the system-events queue to mainSessionKey at enqueue time when dmScope=main is in effect for consistency.
  • Ensure the change is gated behind dmScope=main + single-owner allowlist to maintain backward compatibility.

Example

// In resolveHeartbeatSession or a new helper called from runOnce
if (session.dmScope === 'main' && isSingleOwnerAllowlist(agentId)) {
  const mainSessionKey = getMainSessionKey(agentId);
  return { sessionKey: mainSessionKey, suppressOriginatingContext: false };
}

Notes

The proposed solution requires careful consideration of the system-events queue semantics and potential impact on existing deployments. Thorough testing is necessary to ensure the change does not introduce unintended behavior.

Recommendation

Apply the proposed workaround by modifying the resolveHeartbeatSession function to redirect the forcedSessionKey to the agent's main session when session.dmScope=main and the agent has a single-owner allowlist. This change will unify the session for single-owner agents and ensure that internal triggers are processed in the main session.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix [Feature]: Honor session.dmScope when resolving target session for exec-event/cron-event heartbeat wakes [1 participants]