openclaw - ✅(Solved) Fix Heartbeat isolated session loses threadId from base session deliveryContext [2 pull requests, 2 comments, 2 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#65693Fetched 2026-04-14 05:40:48
View on GitHub
Comments
2
Participants
2
Timeline
6
Reactions
0
Participants
Timeline (top)
commented ×2cross-referenced ×2closed ×1referenced ×1

When a heartbeat runs with isolatedSession: true on a Telegram topic-scoped session, the newly created isolated session loses the threadId from the base session's deliveryContext. This causes heartbeat responses to be delivered to the group's General topic instead of the correct topic.

Root Cause

Root Cause (source code analysis)

Fix Action

Fixed

PR fix notes

PR #65702: fix(heartbeat): inherit delivery context from base session in isolated runs

Description (problem / solution / changelog)

Summary

When heartbeat creates an isolated session via `resolveCronSession(forceNew: true)`, the new session entry has `deliveryContext`, `lastThreadId`, `chatType`, and other routing fields cleared. For Telegram topic-scoped sessions, this causes heartbeat responses to route to the group's General topic instead of the correct topic.

Fixes #65693

Root cause

`resolveCronSession` with `forceNew: true` clears delivery routing fields on the new session entry (`session.ts:80-86`):

```ts ...(isNewSession && { lastChannel: undefined, lastTo: undefined, lastAccountId: undefined, lastThreadId: undefined, deliveryContext: undefined, }), ```

This is correct for most cron jobs (they should start fresh), but heartbeat isolated sessions need to inherit routing from the base session so responses go to the same channel/thread/topic.

Fix

After `resolveCronSession`, copy routing fields from the base session entry:

```ts const baseEntry = cronSession.store[sessionKey]; if (baseEntry) { const entry = cronSession.sessionEntry; entry.deliveryContext ??= baseEntry.deliveryContext; entry.lastThreadId ??= baseEntry.lastThreadId; entry.lastChannel ??= baseEntry.lastChannel; entry.lastTo ??= baseEntry.lastTo; entry.lastAccountId ??= baseEntry.lastAccountId; entry.chatType ??= baseEntry.chatType; } ```

Using `??=` so any explicit values already on the isolated entry are preserved.

Why heartbeat-runner.ts, not session.ts

`session.ts` is a high-contention zone with 5+ competing PRs. Patching the session entry AFTER creation in `heartbeat-runner.ts` avoids contention while achieving the same result.

Scope

  • Files: `src/infra/heartbeat-runner.ts` (+12 lines)
  • oxlint clean
  • Zero competing PRs on this specific fix path

Credit to @richardmqq for the exact JSON diff showing the missing `threadId` in #65693.

Changed files

  • src/infra/heartbeat-runner.ts (modified, +15/-0)

PR #66035: fix(heartbeat): preserve Telegram topic routing for isolated heartbeats

Description (problem / solution / changelog)

Summary

  • Problem: isolated Telegram heartbeats using target=last could drop the bound forum-topic threadId and send into the group root chat instead.
  • Why it matters: topic-bound sessions would leak scheduled heartbeat replies into General, breaking session isolation and reply routing.
  • What changed: preserved Telegram topic routing in heartbeat target resolution when the heartbeat is still addressing the same group session, and added focused regressions at the target-resolution seam plus the isolated heartbeat runner path.
  • What did NOT change (scope boundary): this does not touch broader delivery-context contamination, cron payload formatting, or dreaming reconciliation/reporting bugs.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • 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

  • Closes #65693
  • Related #65702
  • This PR fixes a bug or regression

Root Cause / Regression History (if applicable)

  • Root cause: heartbeat routing intentionally avoided inheriting session reply-thread IDs, but Telegram forum topics encode the topic as part of the destination identity, so target=last lost the topic route.
  • Missing detection / guardrail: there was no regression test proving isolated scheduled heartbeats preserved Telegram topic routing from stored session delivery context.
  • Prior context (git blame, prior PR, issue, or refactor if known): adjacent Telegram topic routing fixes already existed for announce and session-spawn paths; the open #65702 attempt targets the isolated-session store, but the actual drop happens earlier in src/infra/outbound/targets.ts.
  • Why this regressed now: the generic heartbeat rule to avoid stale reply-thread reuse was correct for most channels, but it was too broad for Telegram forum topics.
  • If unknown, what was ruled out: ruled out isolated-session creation as the primary seam for #65693 because the topic was already missing before the heartbeat session record mattered.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/infra/outbound/targets.test.ts, src/infra/heartbeat-runner.ghost-reminder.test.ts
  • Scenario the test should lock in: a topic-bound Telegram group session with only persisted deliveryContext.threadId should keep messageThreadId on isolated scheduled heartbeat delivery when target=last is used.
  • Why this is the smallest reliable guardrail: the bug lives at heartbeat target resolution, but the runner regression proves the real scheduled send path keeps the topic through to Telegram delivery.
  • Existing test that already covers this (if any): none.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

  • Isolated Telegram heartbeat replies stay in the bound forum topic instead of falling back to the group root chat.

Diagram (if applicable)

Before:
[isolated heartbeat on topic-bound Telegram session] -> [target=last drops threadId] -> [message sent to root group chat]

After:
[isolated heartbeat on topic-bound Telegram session] -> [target=last preserves topic threadId for same group route] -> [message sent to bound forum topic]

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: Linux (mb-server)
  • Runtime/container: local repo worktree
  • Model/provider: N/A
  • Integration/channel (if any): Telegram forum topics / heartbeat runner
  • Relevant config (redacted): agents.defaults.heartbeat = { every: "5m", target: "last", isolatedSession: true }

Steps

  1. Persist a Telegram group session with deliveryContext = { channel: "telegram", to: "-100...", threadId: 42 } and chatType: "group".
  2. Run an isolated scheduled heartbeat with target=last.
  3. Inspect the resolved delivery target / Telegram send options.

Expected

  • The heartbeat keeps messageThreadId: 42 and replies in the bound topic.

Actual

  • Before the fix, the heartbeat dropped the topic thread and replied into the group root chat.

Evidence

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

Human Verification (required)

  • Verified scenarios: targeted tests for heartbeat target resolution, delivery-context-only persisted shape, direct-chat non-regression, and scheduled isolated heartbeat send path.
  • Edge cases checked: direct Telegram chat should not inherit a stale thread ID; explicit turn-scoped thread routing still wins.
  • What you did not verify: live Telegram delivery against a real forum topic.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Migration needed? (No)
  • If yes, exact upgrade steps:

Risks and Mitigations

  • Risk: stale or malformed session metadata without chatType: "group" will still fail closed and not reuse a topic thread.
    • Mitigation: scope the fix to the confirmed Telegram group-topic route; keep broader metadata-repair behavior out of this PR.

Changed files

  • CHANGELOG.md (modified, +2/-0)
  • src/infra/heartbeat-runner.ghost-reminder.test.ts (modified, +69/-1)
  • src/infra/outbound/targets.test.ts (modified, +68/-0)
  • src/infra/outbound/targets.ts (modified, +37/-2)

Code Example

{
  "deliveryContext": {
    "channel": "telegram",
    "to": "telegram:-1003577364307",
    "accountId": "default",
    "threadId": 1189
  }
}

---

{
  "deliveryContext": {
    "channel": "telegram",
    "to": "telegram:-1003577364307",
    "accountId": "default"
    // threadId is missing!
  },
  "chatType": "direct"  // should be "group"
}

---

...isNewSession && {
    lastChannel: void 0,
    lastTo: void 0,
    lastAccountId: void 0,
    lastThreadId: void 0,
    deliveryContext: void 0
}
RAW_BUFFERClick to expand / collapse

Description

When a heartbeat runs with isolatedSession: true on a Telegram topic-scoped session, the newly created isolated session loses the threadId from the base session's deliveryContext. This causes heartbeat responses to be delivered to the group's General topic instead of the correct topic.

Steps to Reproduce

  1. Configure a Telegram group with topics enabled
  2. Have a topic-scoped session (e.g., agent:main:telegram:group:<groupId>:topic:1189)
  3. Enable heartbeat with isolatedSession: true
  4. Wait for heartbeat to trigger on that session

Expected Behavior

The heartbeat isolated session should inherit deliveryContext.threadId from the base session, so responses route to the correct topic.

Actual Behavior

The isolated session's deliveryContext is missing threadId. Responses route to the group's General topic instead.

Base session metadata:

{
  "deliveryContext": {
    "channel": "telegram",
    "to": "telegram:-1003577364307",
    "accountId": "default",
    "threadId": 1189
  }
}

Heartbeat isolated session metadata:

{
  "deliveryContext": {
    "channel": "telegram",
    "to": "telegram:-1003577364307",
    "accountId": "default"
    // threadId is missing!
  },
  "chatType": "direct"  // should be "group"
}

Root Cause (source code analysis)

In heartbeat-runner-*.js, resolveCronSession() is called with forceNew: true to create the isolated session. The new session entry explicitly sets deliveryContext: void 0 (line ~89):

...isNewSession && {
    lastChannel: void 0,
    lastTo: void 0,
    lastAccountId: void 0,
    lastThreadId: void 0,
    deliveryContext: void 0
}

This clears the delivery context for the new session. Later, when the heartbeat response needs to be delivered, it falls back through lastToorigin.to, neither of which carries the threadId.

Additionally, chatType is set to "direct" instead of inheriting "group" from the base session.

Additional Context

  • The MessageThreadId is correctly passed to the inbound message context (line ~708), but this doesn't fix the session metadata used for delivery routing.
  • This is separate from #64869 (Telegram topic session path fix), which addressed inbound message routing, not heartbeat delivery context inheritance.
  • Version: 2026.4.11 (769908e)

Suggested Fix

When creating a heartbeat isolated session, inherit deliveryContext (including threadId) and chatType from the base session entry.

extent analysis

TL;DR

Inherit deliveryContext and chatType from the base session when creating a heartbeat isolated session to fix the missing threadId issue.

Guidance

  • Review the resolveCronSession() function in heartbeat-runner-*.js to ensure it correctly inherits deliveryContext and chatType from the base session.
  • Verify that the deliveryContext object is not being explicitly set to void 0 when creating the isolated session.
  • Check the chatType value in the isolated session metadata to ensure it is set to "group" instead of "direct".
  • Test the heartbeat functionality with isolatedSession: true to confirm that the threadId is correctly inherited and responses are delivered to the correct topic.

Example

// In heartbeat-runner-*.js, update the new session entry to inherit deliveryContext and chatType
...isNewSession && {
    lastChannel: baseSession.lastChannel,
    lastTo: baseSession.lastTo,
    lastAccountId: baseSession.lastAccountId,
    lastThreadId: baseSession.lastThreadId,
    deliveryContext: baseSession.deliveryContext,
    chatType: baseSession.chatType
}

Notes

This fix assumes that the baseSession object has the correct deliveryContext and chatType values. If these values are not available, additional changes may be required to retrieve or calculate them.

Recommendation

Apply the suggested fix to inherit deliveryContext and chatType from the base session when creating a heartbeat isolated session, as this should resolve the issue with the missing threadId.

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