openclaw - 💡(How to fix) Fix createFollowupRunner bypasses cliBackends config → embedded runner hits Anthropic third-party billing classification

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…

createFollowupRunner in dist/agent-runner.runtime-*.js calls runEmbeddedPiAgent unconditionally for queued/follow-up turns, ignoring agents.defaults.cliBackends. This forces every queued follow-up message through OpenClaw's embedded @anthropic-ai/sdk client (User-Agent: Anthropic/JS 0.70.1), which Anthropic classifies as a "third-party app" and bills from the paid extra-usage pool rather than plan limits.

Result on Claude.ai subscription auth: HTTP 402 "Third-party apps now draw from your extra usage, not your plan limits. Add more at claude.ai/settings/usage and keep going." The auth profile then enters a multi-hour disabledUntil cooldown that also blocks legitimate CLI-backend calls, effectively taking the agent offline.

The sibling runAgentTurnWithFallback (same file, lines ~938–1052) handles this correctly: it calls resolveCliRuntimeExecutionProvider + isCliProvider and routes through runCliAgent when CLI backends are configured. createFollowupRunner does not mirror this logic.

Error Message

runId: 15526d47-34de-4ab2-9807-3a83a6465af4 lane: session:agent:slack:slack:channel:c07rvk1my6s:thread:1778516534.557379 subsystem: agent/embedded provider: anthropic model: claude-sonnet-4-6 error: "Third-party apps now draw from your extra usage..." trigger: drained by createFollowupRunner after lane congestion

Root Cause

createFollowupRunner in dist/agent-runner.runtime-*.js calls runEmbeddedPiAgent unconditionally for queued/follow-up turns, ignoring agents.defaults.cliBackends. This forces every queued follow-up message through OpenClaw's embedded @anthropic-ai/sdk client (User-Agent: Anthropic/JS 0.70.1), which Anthropic classifies as a "third-party app" and bills from the paid extra-usage pool rather than plan limits.

Result on Claude.ai subscription auth: HTTP 402 "Third-party apps now draw from your extra usage, not your plan limits. Add more at claude.ai/settings/usage and keep going." The auth profile then enters a multi-hour disabledUntil cooldown that also blocks legitimate CLI-backend calls, effectively taking the agent offline.

The sibling runAgentTurnWithFallback (same file, lines ~938–1052) handles this correctly: it calls resolveCliRuntimeExecutionProvider + isCliProvider and routes through runCliAgent when CLI backends are configured. createFollowupRunner does not mirror this logic.

Fix Action

Fix / Workaround

I have this patch deployed in production via a node_modules-level dist edit guarded by a systemd ExecStartPre that refuses gateway startup if the patch is missing post-upgrade. Confirmed working: queued follow-up turns now route through claude-cli, no more 402s, auth profile stays clean.

Workaround (until merged)

Local dist patch + verify-only systemd ExecStartPre. Available on request.

Code Example

{
     "agents": {
       "defaults": {
         "agentRuntime": { "id": "claude-cli" },
         "cliBackends": {
           "claude-cli": { "command": "claude", "args": [] }
         }
       }
     }
   }

---

runId:     15526d47-34de-4ab2-9807-3a83a6465af4
lane:      session:agent:slack:slack:channel:c07rvk1my6s:thread:1778516534.557379
subsystem: agent/embedded
provider:  anthropic
model:     claude-sonnet-4-6
error:     "Third-party apps now draw from your extra usage..."
trigger:   drained by createFollowupRunner after lane congestion

---

const cliExecutionProvider = resolveCliRuntimeExecutionProvider({ provider, cfg: runtimeConfig, agentId, runtimeOverride });
if (isCliProvider(cliExecutionProvider, runtimeConfig)) {
  const cliSessionBinding = getCliSessionBinding(activeSessionEntry, cliExecutionProvider);
  const authProfile = resolveRunAuthProfile(run, cliExecutionProvider, { config: runtimeConfig });
  return (async () => { const result = await runCliAgent({...}); return result; })();
}
// embedded fallback

---

run: async (provider, model, runOptions) => {
  const cliExecutionProvider = resolveCliRuntimeExecutionProvider({
    provider,
    cfg: runtimeConfig,
    agentId: run.agentId
  }) ?? provider;
  if (isCliProvider(cliExecutionProvider, runtimeConfig)) {
    const cliSessionBinding = getCliSessionBinding(activeSessionEntry, cliExecutionProvider);
    const cliAuth = resolveRunAuthProfile(run, cliExecutionProvider, { config: runtimeConfig });
    const result = await runCliAgent({
      sessionId: run.sessionId,
      sessionKey: run.sessionKey ?? replySessionKey,
      agentId: run.agentId,
      trigger: "user",
      sessionFile: run.sessionFile,
      workspaceDir: run.workspaceDir,
      config: runtimeConfig,
      prompt: queued.prompt,
      transcriptPrompt: queued.transcriptPrompt,
      provider: cliExecutionProvider,
      model,
      thinkLevel: run.thinkLevel,
      timeoutMs: run.timeoutMs,
      runId,
      lane: "main",
      extraSystemPrompt: run.extraSystemPrompt,
      sourceReplyDeliveryMode: run.sourceReplyDeliveryMode,
      silentReplyPromptMode: run.silentReplyPromptMode,
      ownerNumbers: run.ownerNumbers,
      cliSessionId: cliSessionBinding?.sessionId,
      cliSessionBinding,
      authProfileId: cliAuth.authProfileId,
      bootstrapPromptWarningSignaturesSeen,
      bootstrapPromptWarningSignature: bootstrapPromptWarningSignaturesSeen[bootstrapPromptWarningSignaturesSeen.length - 1],
      images: queuedImages,
      imageOrder: queuedImageOrder,
      skillsSnapshot: run.skillsSnapshot,
      messageChannel: queued.originatingChannel ?? undefined,
      messageProvider: run.messageProvider,
      agentAccountId: run.agentAccountId,
      senderIsOwner: run.senderIsOwner,
      abortSignal: replyOperation?.abortSignal,
      replyOperation
    });
    bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(result.meta?.systemPromptReport);
    return result;
  }
  const authProfile = resolveRunAuthProfile(run, provider, { config: runtimeConfig });
  // ... unchanged
}
RAW_BUFFERClick to expand / collapse

Summary

createFollowupRunner in dist/agent-runner.runtime-*.js calls runEmbeddedPiAgent unconditionally for queued/follow-up turns, ignoring agents.defaults.cliBackends. This forces every queued follow-up message through OpenClaw's embedded @anthropic-ai/sdk client (User-Agent: Anthropic/JS 0.70.1), which Anthropic classifies as a "third-party app" and bills from the paid extra-usage pool rather than plan limits.

Result on Claude.ai subscription auth: HTTP 402 "Third-party apps now draw from your extra usage, not your plan limits. Add more at claude.ai/settings/usage and keep going." The auth profile then enters a multi-hour disabledUntil cooldown that also blocks legitimate CLI-backend calls, effectively taking the agent offline.

The sibling runAgentTurnWithFallback (same file, lines ~938–1052) handles this correctly: it calls resolveCliRuntimeExecutionProvider + isCliProvider and routes through runCliAgent when CLI backends are configured. createFollowupRunner does not mirror this logic.

Version

  • OpenClaw 2026.5.7
  • @anthropic-ai/sdk 0.70.1 (bundled)
  • File: dist/agent-runner.runtime-DQsCsHUA.js (hash varies per build)

Reproduction

  1. Configure ~/.openclaw/openclaw.json so the claude-cli backend is selected:
    {
      "agents": {
        "defaults": {
          "agentRuntime": { "id": "claude-cli" },
          "cliBackends": {
            "claude-cli": { "command": "claude", "args": [] }
          }
        }
      }
    }
  2. Send a Slack/messaging-channel message to an agent → routes through runCliAgent
  3. While that turn is still processing, send a second message to the same agent.
  4. The second message is queued, then drained via createFollowupRunnerrunEmbeddedPiAgent → 400/402 billing rejection.

Evidence from a real failed run

runId:     15526d47-34de-4ab2-9807-3a83a6465af4
lane:      session:agent:slack:slack:channel:c07rvk1my6s:thread:1778516534.557379
subsystem: agent/embedded
provider:  anthropic
model:     claude-sonnet-4-6
error:     "Third-party apps now draw from your extra usage..."
trigger:   drained by createFollowupRunner after lane congestion

53 such failures accumulated against a single auth profile in ~14 hours, pushing it into a multi-hour cooldown that then blocked even the otherwise-correct CLI path (cooldown is provider-keyed at anthropic:claude-cli, applies to both backends).

Source references (agent-runner.runtime-DQsCsHUA.js in [email protected])

Correct path: runAgentTurnWithFallback, lines 938–1053:

const cliExecutionProvider = resolveCliRuntimeExecutionProvider({ provider, cfg: runtimeConfig, agentId, runtimeOverride });
if (isCliProvider(cliExecutionProvider, runtimeConfig)) {
  const cliSessionBinding = getCliSessionBinding(activeSessionEntry, cliExecutionProvider);
  const authProfile = resolveRunAuthProfile(run, cliExecutionProvider, { config: runtimeConfig });
  return (async () => { const result = await runCliAgent({...}); return result; })();
}
// embedded fallback

Buggy path: createFollowupRunner, lines 2702–2923. The run: async (provider, model, runOptions) => {} callback at line 2851 calls runEmbeddedPiAgent directly with no isCliProvider gate.

Imports already present in the file (no new imports needed):

  • isCliProvider (line 24)
  • resolveCliRuntimeExecutionProvider (line 36)
  • runCliAgent (line 74)
  • getCliSessionBinding (line 73)

Proposed fix

Inside createFollowupRunner, prepend a CLI-routing branch to the run: async callback before the existing const authProfile = resolveRunAuthProfile(run, provider, ...):

run: async (provider, model, runOptions) => {
  const cliExecutionProvider = resolveCliRuntimeExecutionProvider({
    provider,
    cfg: runtimeConfig,
    agentId: run.agentId
  }) ?? provider;
  if (isCliProvider(cliExecutionProvider, runtimeConfig)) {
    const cliSessionBinding = getCliSessionBinding(activeSessionEntry, cliExecutionProvider);
    const cliAuth = resolveRunAuthProfile(run, cliExecutionProvider, { config: runtimeConfig });
    const result = await runCliAgent({
      sessionId: run.sessionId,
      sessionKey: run.sessionKey ?? replySessionKey,
      agentId: run.agentId,
      trigger: "user",
      sessionFile: run.sessionFile,
      workspaceDir: run.workspaceDir,
      config: runtimeConfig,
      prompt: queued.prompt,
      transcriptPrompt: queued.transcriptPrompt,
      provider: cliExecutionProvider,
      model,
      thinkLevel: run.thinkLevel,
      timeoutMs: run.timeoutMs,
      runId,
      lane: "main",
      extraSystemPrompt: run.extraSystemPrompt,
      sourceReplyDeliveryMode: run.sourceReplyDeliveryMode,
      silentReplyPromptMode: run.silentReplyPromptMode,
      ownerNumbers: run.ownerNumbers,
      cliSessionId: cliSessionBinding?.sessionId,
      cliSessionBinding,
      authProfileId: cliAuth.authProfileId,
      bootstrapPromptWarningSignaturesSeen,
      bootstrapPromptWarningSignature: bootstrapPromptWarningSignaturesSeen[bootstrapPromptWarningSignaturesSeen.length - 1],
      images: queuedImages,
      imageOrder: queuedImageOrder,
      skillsSnapshot: run.skillsSnapshot,
      messageChannel: queued.originatingChannel ?? undefined,
      messageProvider: run.messageProvider,
      agentAccountId: run.agentAccountId,
      senderIsOwner: run.senderIsOwner,
      abortSignal: replyOperation?.abortSignal,
      replyOperation
    });
    bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(result.meta?.systemPromptReport);
    return result;
  }
  const authProfile = resolveRunAuthProfile(run, provider, { config: runtimeConfig });
  // ... unchanged
}

This mirrors the working CLI block in runAgentTurnWithFallback. Pure additive — embedded path remains for installs without cliBackends configured.

I have this patch deployed in production via a node_modules-level dist edit guarded by a systemd ExecStartPre that refuses gateway startup if the patch is missing post-upgrade. Confirmed working: queued follow-up turns now route through claude-cli, no more 402s, auth profile stays clean.

Tests that would catch this

  • Unit test asserting createFollowupRunner honors cliBackends, parallel to existing tests for runAgentTurnWithFallback.
  • Integration test sending two messages to the same agent in quick succession, verifying the second turn runs through runCliAgent (not runEmbeddedPiAgent).

Impact

Critical for any OpenClaw deployment that:

  • Uses Anthropic models, and
  • Relies on a Claude.ai subscription/OAuth credential rather than direct API key, and
  • Receives any concurrent messages to the same agent session (i.e. any production Slack or multi-channel deployment).

Cumulative billing failures push the auth profile into multi-hour cooldowns that block CLI calls too (profile state is provider-keyed, not backend-keyed). A single batch of queued messages can take an agent fully offline for hours.

Workaround (until merged)

Local dist patch + verify-only systemd ExecStartPre. Available on request.

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 createFollowupRunner bypasses cliBackends config → embedded runner hits Anthropic third-party billing classification