Skip to content

fix(context-engine): forward isHeartbeat to afterTurn hook [AI-assisted]#89311

Open
luyao618 wants to merge 1 commit into
openclaw:mainfrom
luyao618:fix/context-engine-after-turn-heartbeat
Open

fix(context-engine): forward isHeartbeat to afterTurn hook [AI-assisted]#89311
luyao618 wants to merge 1 commit into
openclaw:mainfrom
luyao618:fix/context-engine-after-turn-heartbeat

Conversation

@luyao618

@luyao618 luyao618 commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

🤖 AI-assisted (built with Hermes orchestration; reviewer = 麻酱, code review = Codex CLI). Test level: fully tested. Prompt summary available on request.

Summary

  • Problem: ContextEngine.afterTurn() SDK type declares isHeartbeat?: boolean, but every runtime call site omitted the field, leaving plugins unable to distinguish heartbeat turns from real user turns.
  • Solution: Thread isHeartbeat through finalizeHarnessContextEngineTurn and installContextEngineLoopHook, and forward it from the embedded attempt runner and the Codex app-server harness using each attempt's existing trigger === "heartbeat" signal.
  • What changed: 4 production files + 2 test files, ~108 added lines, no deletions. Only the afterTurn call paths got a new optional field; absence-vs-false is preserved by spreading conditionally.
  • What did NOT change (scope boundary): No change to ingest / ingestBatch / assemble / compact signatures. No change to engine implementations. No change to heartbeat detection logic in auto-reply/heartbeat-filter.ts or infra/agent-events.ts. No change to channel/provider plugins.

Motivation

Bundled and third-party context-engine plugins (e.g. OpenViking) already check afterTurnParams.isHeartbeat defensively to avoid polluting persistent context with heartbeat turns. Because the runtime never sent that field, those plugins silently treated heartbeats as normal turns, growing memory state with no real user content. Fixing this honors the published SDK contract and unblocks heartbeat-aware engines on existing OpenClaw releases.

Change Type (select all)

  • Bug fix

Scope (select all touched areas)

  • Gateway / orchestration
  • API / contracts

Linked Issue/PR

Real behavior proof (required for external PRs)

  • Behavior or issue addressed: contextEngine.afterTurn was never receiving isHeartbeat despite the SDK type declaring it.
  • Real environment tested: local OpenClaw checkout @ upstream/main 0e16e720915de385d2a6b6471fecbee9cd121a93, macOS, Node v22.15.0. Repro drives the real finalizeHarnessContextEngineTurn and installContextEngineLoopHook source modules via tsx (no mocks of the modules under test).
  • Exact steps or command run after this patch:
    1. Write repro-89302.mjs that imports src/agents/harness/context-engine-lifecycle.ts and src/agents/embedded-agent-runner/tool-result-context-guard.ts directly, installs a fake ContextEngine whose afterTurn records its params, and invokes both finalize paths with isHeartbeat: true.
    2. node_modules/.bin/tsx repro-89302.mjs
  • Evidence after fix (redacted terminal capture):
    === BEFORE the fix (current main) ===
    captured: [
      { "where": "harness" },
      { "where": "loop" }
    ]
    ❌ Bug #89302 reproduced: isHeartbeat not forwarded to contextEngine.afterTurn
    
    === AFTER the fix (this branch) ===
    captured: [
      { "where": "harness", "isHeartbeat": true },
      { "where": "loop",    "isHeartbeat": true }
    ]
    ✅ isHeartbeat forwarded from both harness and loop afterTurn call sites
    
  • Observed result after fix: Both runtime afterTurn paths now propagate isHeartbeat to the engine, matching the SDK type contract.
  • What was not tested: End-to-end heartbeat wake against the full gateway with a real provider was not exercised; the regression tests and direct-source repro lock in the seam.
  • Before evidence: same as the BEFORE block above (captured by reverting the patch and rerunning the same script).

Root Cause (if applicable)

  • Root cause: When afterTurn was introduced its call sites were copied without the heartbeat field, even though the SDK type and ingest / ingestBatch already documented isHeartbeat. The runtime already knows the heartbeat status via EmbeddedRunAttemptParams.trigger === "heartbeat" (also stored in AgentRunContext.isHeartbeat); the seam just never forwarded it.
  • Missing detection / guardrail: A unit test asserting isHeartbeat round-trips through finalizeHarnessContextEngineTurn / installContextEngineLoopHook would have caught it.
  • Contributing context: afterTurn is newer than ingest/ingestBatch, so its parameter coverage drifted from those siblings.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
  • Target test or file: src/agents/harness/context-engine-lifecycle.test.ts, src/agents/embedded-agent-runner/tool-result-context-guard.test.ts
  • Scenario the test should lock in: When the caller sets isHeartbeat=true|false, the engine's afterTurn receives the same value; when the caller omits it, the field is absent on the payload (preserving the undefined contract). it.each parameterizes true/false plus a separate omitted case at each seam.
  • Why this is the smallest reliable guardrail: Both seams are pure async functions taking the engine as an arg, so direct mock-engine assertions cover the contract without spinning up a session.
  • Existing test that already covers this (if any): N/A
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

None for runs without a context engine. For sessions with a context-engine plugin that branches on afterTurnParams.isHeartbeat, heartbeat turns are now correctly identified as heartbeat, matching the SDK type. Plugins that ignore the field are unaffected.

Diagram (if applicable)

Before: attempt.trigger="heartbeat" --> finalize... --> engine.afterTurn({ ... /* no isHeartbeat */ })
After:  attempt.trigger="heartbeat" --> finalize... --> engine.afterTurn({ ..., isHeartbeat: true })

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: N/A

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Node v22.15.0, pnpm 11.2.2
  • Model/provider: N/A (drove source modules directly via tsx)
  • Integration/channel (if any): N/A
  • Relevant config (redacted): N/A

Steps

  1. git fetch upstream && git checkout fix/context-engine-after-turn-heartbeat
  2. node_modules/.bin/vitest run src/agents/harness/context-engine-lifecycle.test.ts src/agents/embedded-agent-runner/tool-result-context-guard.test.ts → 112/112 pass
  3. pnpm exec oxfmt --check <changed-files> → clean
  4. node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json --pretty false --incremental → no errors
  5. Repro driver script BEFORE → ❌ / AFTER → ✅ (see Real behavior proof above)

The ContextEngine afterTurn() SDK type declares isHeartbeat?: boolean
to let plugins distinguish heartbeat turns from user turns, but every
call site (harness finalize, embedded-agent loop hook, attempt.ts post
finalize) omitted the field. Plugins such as OpenViking that branch on
afterTurnParams.isHeartbeat could not reliably detect heartbeat turns
and risked polluting context state.

Thread isHeartbeat through finalizeHarnessContextEngineTurn and
installContextEngineLoopHook. In attempt.ts pull the value from the
already-registered AgentRunContext (registered by auto-reply with
isHeartbeat) via getAgentRunContext(params.runId). Only emit the field
when defined so the absence-vs-false distinction is preserved.

Add it.each-parameterized regression tests covering true, false, and
the omitted-by-caller cases.

Closes openclaw#89302
[AI-assisted]
@clawsweeper

clawsweeper Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed June 1, 2026, 11:47 PM ET / 03:47 UTC.

Summary
The PR forwards heartbeat status into context-engine afterTurn calls from the harness, embedded runner, and Codex app-server harness, with regression tests for true, false, and omitted cases.

PR surface: Source +10, Tests +98. Total +108 across 6 files.

Reproducibility: yes. source inspection gives a high-confidence reproduction path: current main declares afterTurn(params).isHeartbeat but omits the field from both active runtime delivery helpers. I did not run the full gateway heartbeat scenario in this read-only review.

Review metrics: none identified.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🦪 silver shellfish
Patch quality: 🦪 silver shellfish
Result: blocked until stronger real behavior proof is added.

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

Rank-up moves:

  • Remove the unused getAgentRunContext import from src/agents/embedded-agent-runner/run/attempt.ts.
  • [P1] Update the PR body's proof fields to the exact accepted labels, including Behavior addressed and Evidence after fix, with redacted terminal output preserved.
  • Rerun the focused context-engine tests and production typecheck after those edits.

Proof guidance:

  • [P1] Needs stronger real behavior proof before merge: The PR body includes terminal-style before/after evidence, but the live proof check rejected the current format; the contributor should update the proof labels to the exact accepted fields and keep the redacted after-fix output visible. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

  • [P1] The PR head will not pass the repo's production typecheck until the unused getAgentRunContext import is removed.
  • [P1] The contributor's proof content appears to be terminal-style evidence, but the current PR body format is not accepted by the Real behavior proof check, so the merge gate still needs a body update.

Maintainer options:

  1. Decide the mitigation before merge
    Remove the unused import, keep the narrow heartbeat propagation from existing run-kind state, update the PR proof section to the exact accepted labels, and rerun the focused context-engine tests plus tsgo.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Next step before merge

  • [P1] Contributor action is needed for the proof-gate format, and the code change also has a simple typecheck blocker; do not queue automated repair while real behavior proof is still rejected.

Security
Cleared: The diff only threads an existing boolean through runtime call parameters and adds tests; it does not change secrets, permissions, dependencies, workflows, or command execution surfaces.

Review findings

  • [P2] Drop the unused run context import — src/agents/embedded-agent-runner/run/attempt.ts:316
Review details

Best possible solution:

Remove the unused import, keep the narrow heartbeat propagation from existing run-kind state, update the PR proof section to the exact accepted labels, and rerun the focused context-engine tests plus tsgo.

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

Yes, source inspection gives a high-confidence reproduction path: current main declares afterTurn(params).isHeartbeat but omits the field from both active runtime delivery helpers. I did not run the full gateway heartbeat scenario in this read-only review.

Is this the best way to solve the issue?

No, not as submitted: forwarding the existing heartbeat trigger is the right narrow fix, but the extra unused import and rejected proof format need to be fixed before this is the best mergeable solution.

Full review comments:

  • [P2] Drop the unused run context import — src/agents/embedded-agent-runner/run/attempt.ts:316
    This added import is never referenced in runEmbeddedAttempt. tsconfig.core.json enables noUnusedLocals and includes src/**/*, so pnpm tsgo:core/production typecheck will fail with this PR until the import is removed.
    Confidence: 0.98

Overall correctness: patch is incorrect
Overall confidence: 0.95

AGENTS.md: found and applied where relevant.

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

Label changes

Label changes:

  • add P2: This is a bounded context-engine bug fix with a compile-blocking PR defect, but it does not indicate emergency runtime, security, or broad channel failure.
  • add rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🦪 silver shellfish and patch quality is 🦪 silver shellfish.
  • add status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The PR body includes terminal-style before/after evidence, but the live proof check rejected the current format; the contributor should update the proof labels to the exact accepted fields and keep the redacted after-fix output visible. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Label justifications:

  • P2: This is a bounded context-engine bug fix with a compile-blocking PR defect, but it does not indicate emergency runtime, security, or broad channel failure.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🦪 silver shellfish and patch quality is 🦪 silver shellfish.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The PR body includes terminal-style before/after evidence, but the live proof check rejected the current format; the contributor should update the proof labels to the exact accepted fields and keep the redacted after-fix output visible. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Source +10, Tests +98. Total +108 across 6 files.

View PR surface stats
Area Files Added Removed Net
Source 4 10 0 +10
Tests 2 98 0 +98
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 6 108 0 +108

What I checked:

  • Submitted diff contains an unused production import: The PR head only references getAgentRunContext on the added import line, so the imported symbol is not used anywhere in runEmbeddedAttempt. (src/agents/embedded-agent-runner/run/attempt.ts:316, 2103a25082c9)
  • Core production typecheck rejects unused locals: tsconfig.core.json enables noUnusedLocals and includes src/**/*, so the unused import is a production typecheck blocker. (tsconfig.core.json:4, 2103a25082c9)
  • Current main has the reported contract gap: Current main declares ContextEngine.afterTurn(params).isHeartbeat?: boolean, while the main-branch harness and loop-hook afterTurn payloads omit that field. (src/context-engine/types.ts:299, 7d5d62511f62)
  • The PR uses the existing run-kind signal: The PR head threads params.trigger === "heartbeat" into the loop hook and final afterTurn path without adding a new config or SDK field. (src/agents/embedded-agent-runner/run/attempt.ts:2352, 2103a25082c9)
  • Real behavior proof gate is currently failing: The live check run reports that the PR body proof was not accepted as after-fix evidence, matching the repo requirement for exact proof labels even though a terminal-style block is present. (2103a25082c9)
  • Codex dependency check: Sibling Codex source shows TurnOptions only carries output schema and abort signal, and the app-server turn/start protocol has no heartbeat field; heartbeat classification is OpenClaw-owned state, so threading OpenClaw params.trigger is the right layer. (../codex/sdk/typescript/src/turnOptions.ts:1, c955f730781d)

Likely related people:

  • vincentkoc: Current blame for the active ContextEngine contract and both current afterTurn delivery helpers points to commit 086274f. (role: recent area contributor; confidence: high; commits: 086274fd7e7a; files: src/context-engine/types.ts, src/agents/harness/context-engine-lifecycle.ts, src/agents/embedded-agent-runner/tool-result-context-guard.ts)
  • jalehman: Commit fee91fe introduced the pluggable ContextEngine interface and lifecycle wiring that this bug fixes. (role: feature introducer; confidence: high; commits: fee91fefceb4; files: src/context-engine/types.ts, src/context-engine/legacy.ts)
  • steipete: Recent history for extensions/codex/src/app-server/run-attempt.ts shows repeated Codex app-server lifecycle and harness work before this PR touched that path. (role: Codex app-server adjacent owner; confidence: medium; commits: 31a0b7bd42a5, 3b65e2302a55, 659bcc5e5b59; files: extensions/codex/src/app-server/run-attempt.ts)
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 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. labels Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling extensions: codex P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: S status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: [Bug]: ContextEngine afterTurn declares isHeartbeat but does not forward it

1 participant