Skip to content

fix(heartbeat): route outbound mirror to isolated session key#67398

Closed
agent-merkava wants to merge 2 commits into
openclaw:mainfrom
agent-merkava:fix/heartbeat-isolated-session-mirror-routing
Closed

fix(heartbeat): route outbound mirror to isolated session key#67398
agent-merkava wants to merge 2 commits into
openclaw:mainfrom
agent-merkava:fix/heartbeat-isolated-session-mirror-routing

Conversation

@agent-merkava

Copy link
Copy Markdown
Contributor

Summary

When heartbeat.isolatedSession: true, the heartbeat runner correctly creates a fresh isolated session at <base>:heartbeat and runs the agent against it. However, outbound delivery (and the transcript mirror it triggers) was keyed off the BASE session, not the isolated one.

Result: the isolated heartbeat session is registered in sessions.json with a sessionFile path that is never written to disk, while the heartbeat assistant text gets appended to the base session's transcript file.

Fixes #56941
Fixes #57577

Root cause

src/infra/heartbeat-runner.ts:838-893 correctly sets runSessionKey = isolatedSessionKey for isolated heartbeats. But a few lines later (line 969-973), outboundSession is built from the original sessionKey:

const outboundSession = buildOutboundSessionContext({
  cfg,
  agentId,
  sessionKey,  // ← base key, not runSessionKey
});

deliverOutboundPayloads (in outbound/deliver.ts) then resolves the mirror sessionKey via params.mirror?.sessionKey ?? params.session?.key and calls appendAssistantMessageToSessionTranscript({ sessionKey: <base>, ... }). The append walks sessions.json[<base>] and writes to that entry's sessionFile — leaving the isolated heartbeat session entry's sessionFile orphaned (registered, never created on disk).

Why claude-cli backends mask the bug

Claude CLI manages its own session files at ~/.claude/projects/, so OpenClaw never writes a transcript for those backends regardless. The bug only manifests visibly for backends where OpenClaw owns transcript writing (openai-codex, anthropic, etc.). For those backends, downstream consumers (file-watchers, external dashboards reading sessions.json) miscategorize heartbeat output as DM messages because the JSONL file's owning session key in the registry is the base session, not the heartbeat session.

Fix

Pass runSessionKey to buildOutboundSessionContext. One-line change.

   const outboundSession = buildOutboundSessionContext({
     cfg,
     agentId,
-    sessionKey,
+    sessionKey: runSessionKey,
   });

For non-isolated heartbeats, runSessionKey === sessionKey, so this is a no-op. For isolated heartbeats, it routes the outbound delivery and transcript mirror to the correct isolated session.

Test

Added src/infra/heartbeat-runner.isolated-session-mirror.test.ts with two cases:

  1. Fresh base key: runner invoked with the base session key, isolated session is created, delivery's session.key must equal <base>:heartbeat.
  2. Wake re-entry: runner invoked with an already-suffixed <base>:heartbeat key (simulating wake handler re-entry), delivery's session.key must remain stable at the single suffix.

Verified the test fails on main (Received: "agent:main:main") and passes after the fix.

Test plan

  • New test passes locally with the fix
  • Existing heartbeat-runner.isolated-key-stability.test.ts (11 tests) still passes
  • CI runs

🤖 Generated with Claude Code

When heartbeat.isolatedSession: true, the runner correctly creates a fresh
isolated session at <base>:heartbeat and runs the agent against it
(runSessionKey = isolatedSessionKey). However, outboundSession was being
built from the original base sessionKey, so all outbound deliveries (and
their transcript mirror appends) were keyed off the base session.

Downstream effect: appendAssistantMessageToSessionTranscript resolves the
target file via params.session?.key (deliver.ts), so the heartbeat
assistant text is appended to the BASE session's sessionFile instead of
the isolated heartbeat session's. The isolated session entry is created
in sessions.json with a sessionFile path that is never written to disk —
breaking heartbeat transcript history and causing file-watchers /
external consumers to miscategorize the heartbeat output as a regular DM
message.

This only manifests for backends where OpenClaw owns transcript writing
(openai-codex, anthropic). claude-cli sessions hide the bug because
Claude CLI manages its own session files at ~/.claude/projects/.

Fix: pass runSessionKey to buildOutboundSessionContext so outbound
delivery + transcript mirroring target the isolated session.

Includes a regression test that asserts deliverOutboundPayloads is
called with session.key === <base>:heartbeat for both the fresh-base
case and the wake re-entry case (where the runner is invoked with an
already-suffixed key).

Fixes openclaw#56941
Fixes openclaw#57577
@greptile-apps

greptile-apps Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a single-line bug in src/infra/heartbeat-runner.ts where buildOutboundSessionContext was called with the base sessionKey instead of runSessionKey when heartbeat.isolatedSession: true. The fix routes outbound delivery (and its transcript mirror) to the correct isolated :heartbeat session, preventing the isolated session entry from being registered in sessions.json with a sessionFile that is never written to disk. A targeted regression test covers both fresh-base-key and wake-re-entry cases.

Confidence Score: 5/5

Safe to merge — the fix is a one-line correction with no side effects on non-isolated heartbeat runs and is backed by targeted regression tests.

The change is minimal: only sessionKeyrunSessionKey in the buildOutboundSessionContext call. For non-isolated runs runSessionKey === sessionKey, so behavior is unchanged. The remaining usages of sessionKey (task timestamp tracking, dedupe storage, restoreHeartbeatUpdatedAt) are intentionally keyed to the base session. Two well-scoped regression tests confirm the fix and provide a red-on-main signal. No P0/P1 findings.

No files require special attention.

Reviews (1): Last reviewed commit: "fix(heartbeat): route outbound mirror to..." | Re-trigger Greptile

@clawsweeper

clawsweeper Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed June 7, 2026, 1:03 AM ET / 05:03 UTC.

Summary
Review failed before ClawSweeper could summarize the requested change.

PR surface: Source +7, Tests +122. Total +129 across 2 files.

Reproducibility: unclear. The review failed before ClawSweeper could establish a reproduction path.

Review metrics: none identified.

Merge readiness
Overall: 🌊 off-meta tidepool
Proof: 🌊 off-meta tidepool
Patch quality: 🌊 off-meta tidepool
Result: rating does not apply to this item.

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

Risk before merge

  • [P1] No close action taken because the review did not complete.

Maintainer options:

  1. Decide the mitigation before merge
    Retry the Codex review after fixing the execution failure.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Next step before merge

  • [P1] Review did not complete, so no work-lane recommendation was made.
Review details

Best possible solution:

Retry the Codex review after fixing the execution failure.

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

Unclear. The review failed before ClawSweeper could establish a reproduction path.

Is this the best way to solve the issue?

Unclear. Retry the review first so ClawSweeper can evaluate the actual issue and fix direction.

AGENTS.md: unclear because the file could not be read completely.

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

Label changes

Label changes:

  • remove P2: Current review triage priority is none.
  • remove merge-risk: 🚨 session-state: Current PR review selected no merge-risk labels.
  • remove merge-risk: 🚨 message-delivery: Current PR review selected no merge-risk labels.
  • remove merge-risk: 🚨 security-boundary: Current PR review selected no merge-risk labels.

Label justifications:

  • rating: 🌊 off-meta tidepool: Overall readiness is 🌊 off-meta tidepool; proof is 🌊 off-meta tidepool and patch quality is 🌊 off-meta tidepool.
Evidence reviewed

PR surface:

Source +7, Tests +122. Total +129 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 8 1 +7
Tests 1 122 0 +122
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 130 1 +129

What I checked:

  • failure reason: codex execution failed.
  • codex failure detail: Codex review failed for this PR with exit 1.
  • codex stdout: Per-item Codex failure; continuing with the rest of the shard.

Likely related people:

  • unknown: Codex failed before it could trace repository history. (role: review did not complete; confidence: low)
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 commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge.

What this changes:

The PR changes heartbeat outbound delivery context to use the isolated runSessionKey and adds regression coverage for fresh-base and wake re-entry isolated heartbeat delivery keys.

Maintainer follow-up before merge:

This is an open implementation PR with a real current-main target, but the remaining action is maintainer review of session-vs-policy semantics and the transcript-mirror claim, not an automated replacement branch.

Review details

Best possible solution:

Land a reviewed heartbeat fix that routes outbound delivery metadata and internal hooks to the stable isolated :heartbeat run key, while preserving the base session for delivery target and policy semantics where intended. The regression coverage should assert the outbound session.key for fresh-base and wake re-entry paths, and add explicit mirror coverage if transcript appending is part of the intended behavior.

Acceptance criteria:

  • pnpm test src/infra/heartbeat-runner.isolated-session-mirror.test.ts src/infra/heartbeat-runner.isolated-key-stability.test.ts

What I checked:

  • Current main still builds outbound context from the base key: runSessionKey is set to the isolated :heartbeat key for isolated heartbeats and used for the agent turn, but buildOutboundSessionContext still receives the original sessionKey. (src/infra/heartbeat-runner.ts:1055, acae48b790fa)
  • Heartbeat delivery consumes that outbound session: Both heartbeat OK delivery and main heartbeat reply delivery pass session: outboundSession into deliverOutboundPayloads, so the key chosen for outboundSession affects downstream delivery metadata. (src/infra/heartbeat-runner.ts:1106, acae48b790fa)
  • Outbound delivery uses session key, but transcript mirror is explicit: deliverOutboundPayloads uses params.session?.key for media access, internal message:sent hooks, diagnostics, and as fallback for payload planning; transcript append happens only when params.mirror is present and then uses params.mirror.sessionKey. (src/infra/outbound/deliver.ts:919, acae48b790fa)
  • Existing tests do not cover outbound delivery key: Current isolated heartbeat tests mock deliverOutboundPayloads and assert the reply context SessionKey, but they do not assert the session.key passed into outbound delivery. (src/infra/heartbeat-runner.isolated-key-stability.test.ts:14, acae48b790fa)
  • Docs support the existing feature boundary: Heartbeat docs describe isolatedSession as fresh-session execution while delivery routing still uses the main session context, which supports a narrow fix but raises review attention around preserving base policy/target context. Public docs: docs/gateway/heartbeat.md. (docs/gateway/heartbeat.md:229, acae48b790fa)
  • Security review of supplied PR surface: Provided PR context lists only src/infra/heartbeat-runner.ts and a new focused heartbeat runner test; it does not touch workflows, action refs, dependency manifests, lockfiles, install/build/release scripts, package publishing metadata, generated/vendor code, permissions, or secrets handling. (53bfd6d292da)

Likely related people:

  • Peter Steinberger: Blame and file history for the heartbeat runner, outbound delivery/session-context helper, and heartbeat docs in this checkout all point to Peter for the current implementation surface. (role: current-main history owner; confidence: medium; commits: a972c9ec4547; files: src/infra/heartbeat-runner.ts, src/infra/outbound/deliver.ts, src/infra/outbound/session-context.ts)

Remaining risk / open question:

  • The PR body frames the fix as transcript mirroring, but current deliverOutboundPayloads appends transcript text only when an explicit mirror object is supplied; heartbeat calls currently pass session without mirror.
  • Changing session.key from the base key to the isolated key may also change payload-planning or media-access policy resolution unless the base key is preserved through policySessionKey or an equivalent context field.

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

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. labels May 19, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 19, 2026
@clawsweeper clawsweeper Bot added the merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. label May 19, 2026
@clawsweeper

clawsweeper Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat.

Where did the egg go?
  • The egg game starts only after the PR passes the real-behavior proof check.
  • Before that, no creature or rarity is rolled. The treat waits for real proof.
  • This is still just collectible flavor: proof affects review readiness, not creature quality.

@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Jun 6, 2026
@clawsweeper clawsweeper Bot added rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 6, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the stale Marked as stale due to inactivity label Jun 7, 2026
@openclaw-clownfish

Copy link
Copy Markdown
Contributor

Clownfish 🐠 reef update

Thanks for the work on this. Clownfish did not have permission to update this branch directly, so it opened a narrow replacement PR instead. that's a branch access thing, not a knock on the contribution.

Replacement PR: #92807
Source PR: #67398
This PR stays open for now, with the replacement linked as the current fix path.
The replacement PR carries the original credit trail forward.

fish notes: model gpt-5.5, reasoning xhigh; reviewed against eb04aad.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. size: S triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

1 participant