openclaw - 💡(How to fix) Fix Idle persistent ACP sub-agent burns full token cost on every 30-min heartbeat even when HEARTBEAT.md is empty

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…

A persistent ACP sub-agent declared in agents.list[] keeps incurring full per-turn token cost on every heartbeat — including the full workspace bootstrap re-injection (AGENTS.md + SOUL.md + USER.md), and the cascading pre-compaction memory flush + post-compaction context refresh once the accumulated heartbeat history crosses the soft compaction threshold — even when the agent has never been used and HEARTBEAT.md is the unmodified comment-only template.

The template HEARTBEAT.md shipped with new workspaces opens with:

# Keep this file empty (or with only comments) to skip heartbeat API calls.

…but the runtime doesn't honor that. It dispatches the heartbeat anyway, the agent burns a full turn just to read the empty file and reply HEARTBEAT_OK, and on persistent ACP runtimes the rolling history compounds until compaction kicks in.

Error Message

body: {"error":{"type":"usage_limit_reached","plan_type":"team", ... }}

Root Cause

For mode: persistent specifically, the per-turn cost compounds further because the rolling history accumulates across heartbeats, eventually crossing compaction.memoryFlush.softThresholdTokens and triggering an extra full-cost flush turn followed by a post-compaction context refresh. An idle agent shouldn't be in a position to trigger compaction.

Fix Action

Fix / Workaround

…but the runtime doesn't honor that. It dispatches the heartbeat anyway, the agent burns a full turn just to read the empty file and reply HEARTBEAT_OK, and on persistent ACP runtimes the rolling history compounds until compaction kicks in.

  • One rollout file (agents/codex/agent/codex-home/sessions/2026/05/19/rollout-2026-05-19T21-50-56-...jsonl) appending continuously: 26 MB, 6,660 events, 2,624 response_items.
  • 463 user-side messages in those 10 days. Distribution:
    • Heartbeat dispatches: ~378
    • Post-compaction context refreshes: most of the rest (auto-fired after compaction)
    • Midnight current_date rollovers
    • Zero real user messages.
  • Top assistant outputs: HEARTBEAT_OK × 374, plus 270+ near-duplicate variants of "HEARTBEAT.md is comment-only, so there are no configured tasks to run".
  • Top tool call: cat .../codex/HEARTBEAT.md × 101.
  • Daily Codex usage on this sub-agent alone (codex_api::endpoint::responses_websocket events from ~/.openclaw-archer/agents/codex/agent/codex-home/logs_2.sqlite):

Workarounds in place today

Code Example

{ "id": "codex",
     "runtime": { "type": "acp",
                  "acp": { "agent": "codex", "backend": "acpx", "mode": "persistent" } } }

---

POST https://chatgpt.com/backend-api/codex/responses → 429
  x-codex-rate-limit-reached-type: workspace_member_credits_depleted
  body: {"error":{"type":"usage_limit_reached","plan_type":"team", ... }}
RAW_BUFFERClick to expand / collapse

Summary

A persistent ACP sub-agent declared in agents.list[] keeps incurring full per-turn token cost on every heartbeat — including the full workspace bootstrap re-injection (AGENTS.md + SOUL.md + USER.md), and the cascading pre-compaction memory flush + post-compaction context refresh once the accumulated heartbeat history crosses the soft compaction threshold — even when the agent has never been used and HEARTBEAT.md is the unmodified comment-only template.

The template HEARTBEAT.md shipped with new workspaces opens with:

# Keep this file empty (or with only comments) to skip heartbeat API calls.

…but the runtime doesn't honor that. It dispatches the heartbeat anyway, the agent burns a full turn just to read the empty file and reply HEARTBEAT_OK, and on persistent ACP runtimes the rolling history compounds until compaction kicks in.

Repro

  1. Add a persistent ACP sub-agent to agents.list[]:

    { "id": "codex",
      "runtime": { "type": "acp",
                   "acp": { "agent": "codex", "backend": "acpx", "mode": "persistent" } } }
  2. Leave the codex provider as primary (agents.defaults.model.primary: "openai-codex/gpt-5.4").

  3. Default agents.defaults.heartbeat.every: "30m", default compaction.memoryFlush.enabled: true with softThresholdTokens: 50000.

  4. Run the workspace's setup once (so AGENTS.md, SOUL.md, USER.md are present), then never send a real message to the codex sub-agent. Leave HEARTBEAT.md as the comment-only stub.

  5. Let openclaw run for several days.

Observed

From an actual 10-day run on my setup (openclaw 2026.5.19, gpt-5.4, Codex Team plan, single archer macOS host, only this one sub-agent declared):

  • One rollout file (agents/codex/agent/codex-home/sessions/2026/05/19/rollout-2026-05-19T21-50-56-...jsonl) appending continuously: 26 MB, 6,660 events, 2,624 response_items.

  • 463 user-side messages in those 10 days. Distribution:

    • Heartbeat dispatches: ~378
    • Post-compaction context refreshes: most of the rest (auto-fired after compaction)
    • Midnight current_date rollovers
    • Zero real user messages.
  • Top assistant outputs: HEARTBEAT_OK × 374, plus 270+ near-duplicate variants of "HEARTBEAT.md is comment-only, so there are no configured tasks to run".

  • Top tool call: cat .../codex/HEARTBEAT.md × 101.

  • Daily Codex usage on this sub-agent alone (codex_api::endpoint::responses_websocket events from ~/.openclaw-archer/agents/codex/agent/codex-home/logs_2.sqlite):

    dayeventsfunction_calls in sessionnotes
    2026-05-1918423half day
    2026-05-205948(rotation, baseline ≈ 614 across all sources)
    2026-05-2131949flush spike day
    2026-05-22650
    2026-05-23449first 429 `usage_limit_reached` appears in log
    2026-05-24447
    2026-05-254683flush spike
    2026-05-26496
    2026-05-271130

    Averaged ~49 user-turns/day from this one idle persistent sub-agent. ChatGPT analytics (chatgpt.com/codex/...) reported 51.9 turns/day average / 1,505 turns/30d for the workspace member — the idle sub-agent alone accounts for the vast majority of that.

  • Direct probe against the upstream endpoint confirms the quota was a real workspace_member_credits_depleted 429 (not a model retirement or transient issue):

    POST https://chatgpt.com/backend-api/codex/responses → 429
    x-codex-rate-limit-reached-type: workspace_member_credits_depleted
    body: {"error":{"type":"usage_limit_reached","plan_type":"team", ... }}

Expected

Either:

  1. The runtime actually honors the template HEARTBEAT.md claim — when the resolved HEARTBEAT.md content is empty or comment-only, skip the heartbeat API call entirely for that agent on that tick. Today this guidance is documentation inside the file, not a runtime gate.
  2. Or, narrower: when a sub-agent has never received a real user message and its HEARTBEAT.md is empty/comment-only, skip heartbeats until either condition flips.
  3. Or, defensive default: don't apply agents.defaults.heartbeat.every to newly declared agents (especially runtime.acp.mode: persistent) until they've been activated by the user at least once.

For mode: persistent specifically, the per-turn cost compounds further because the rolling history accumulates across heartbeats, eventually crossing compaction.memoryFlush.softThresholdTokens and triggering an extra full-cost flush turn followed by a post-compaction context refresh. An idle agent shouldn't be in a position to trigger compaction.

Workarounds in place today

  • Per-agent override: "heartbeat": { "every": "disabled" } in the agent's agents.list[] entry.
  • Or use runtime.acp.mode: "ephemeral" so rolling history doesn't accumulate and compaction never fires.
  • Or, the route I ended up taking: remove the unused sub-agent from agents.list[] and move its agents/<id>/ + workspace/<id>/ aside.

Environment

  • openclaw 2026.5.19 (4945d63)
  • macOS Darwin 25.5.0 / arm64 (Mac mini M1)
  • Codex provider via ChatGPT Team plan OAuth (auth_mode: chatgpt, originator: pi from @earendil-works/pi-ai)
  • Single agent declared besides main: the persistent codex ACP sub-agent described above

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 Idle persistent ACP sub-agent burns full token cost on every 30-min heartbeat even when HEARTBEAT.md is empty