fix(heartbeat): route outbound mirror to isolated session key#67398
fix(heartbeat): route outbound mirror to isolated session key#67398agent-merkava wants to merge 2 commits into
Conversation
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 SummaryThis PR fixes a single-line bug in Confidence Score: 5/5Safe 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 No files require special attention. Reviews (1): Last reviewed commit: "fix(heartbeat): route outbound mirror to..." | Re-trigger Greptile |
|
Codex review: needs real behavior proof before merge. Reviewed June 7, 2026, 1:03 AM ET / 05:03 UTC. Summary 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 follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge
Review detailsBest 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 changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +7, Tests +122. Total +129 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
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
|
|
Codex review: needs maintainer review before merge. What this changes: The PR changes heartbeat outbound delivery context to use the isolated 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 detailsBest possible solution: Land a reviewed heartbeat fix that routes outbound delivery metadata and internal hooks to the stable isolated Acceptance criteria:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against acae48b790fa. |
|
ClawSweeper PR egg 🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat. Where did the egg go?
|
|
This pull request has been automatically marked as stale due to inactivity. |
|
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 fish notes: model gpt-5.5, reasoning xhigh; reviewed against eb04aad. |
Summary
When
heartbeat.isolatedSession: true, the heartbeat runner correctly creates a fresh isolated session at<base>:heartbeatand 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.jsonwith asessionFilepath 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-893correctly setsrunSessionKey = isolatedSessionKeyfor isolated heartbeats. But a few lines later (line 969-973),outboundSessionis built from the originalsessionKey:deliverOutboundPayloads(inoutbound/deliver.ts) then resolves the mirror sessionKey viaparams.mirror?.sessionKey ?? params.session?.keyand callsappendAssistantMessageToSessionTranscript({ sessionKey: <base>, ... }). The append walkssessions.json[<base>]and writes to that entry'ssessionFile— leaving the isolated heartbeat session entry'ssessionFileorphaned (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 readingsessions.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
runSessionKeytobuildOutboundSessionContext. 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.tswith two cases:session.keymust equal<base>:heartbeat.<base>:heartbeatkey (simulating wake handler re-entry), delivery'ssession.keymust remain stable at the single suffix.Verified the test fails on
main(Received: "agent:main:main") and passes after the fix.Test plan
heartbeat-runner.isolated-key-stability.test.ts(11 tests) still passes🤖 Generated with Claude Code