Skip to content

Bug: in-loop context-overflow guard undercounts bashExecution/compactionSummary turns as flat 256 chars and skips overflow protection #97927

Description

@yetval

Summary

The in-loop context-overflow guard (installToolResultContextGuard) sizes each transcript message with estimateMessageChars, which special-cases only user, assistant, and toolResult roles. The harness roles bashExecution, compactionSummary, branchSummary, and custom fall through to a flat return 256, even though convertToLlm expands each of these into a full-text user message that is actually sent to the provider. As a result, the guard undercounts summary- and bash-dominated context and never trips its last-resort overflow protection for those transcripts, so an over-window prompt can be submitted.

This is a distinct, un-covered sibling of the just-merged #97861 (24626e5), which fixed the same defect class in the twin estimator estimateMessageTokenPressure (the pre-prompt precheck) but did not update this estimator that backs the live in-loop guard.

Environment

  • Commit: 15de9d8 (origin/main at time of filing)
  • Surface: embedded agent runner, in-loop tool-result context guard (src/agents/embedded-agent-runner/tool-result-context-guard.ts -> tool-result-char-estimator.ts)

Steps to reproduce

  1. Drive the real guard's transformContext (installed by installToolResultContextGuard) with a transcript whose real provider text far exceeds the context window, carried in harness roles (compactionSummary + bashExecution).
  2. Drive the same guard with byte-identical text carried in user + toolResult roles.
  3. Observe that only the second transcript trips PREEMPTIVE_CONTEXT_OVERFLOW_MESSAGE.

Command run (real runtime, no mocks of the guard):

node_modules/.bin/tsx /tmp/openclaw-bug-repros/audit-2026-06-29b/compaction/repro-context-guard-undercount.mjs

Expected

Both transcripts represent the same ~1,000,000-char provider payload (the harness roles expand to identical user text via convertToLlm), so both should trip the guard's overflow protection equally.

Actual

The harness-role transcript is let through (guard does not fire); the user+toolResult transcript fires. The guard is blind to summary/bash context size.

Root cause

src/agents/embedded-agent-runner/tool-result-char-estimator.ts:142 - estimateMessageChars handles user (line 89), assistant (line 100), and toolResult (line 132), then returns a flat 256 for everything else:

  if (isToolResultMessage(msg)) {
    // `details` is stripped before provider conversion; estimate only visible content.
    const content = getToolResultContent(msg);
    const chars = estimateContentBlockChars(content);
    const weightedChars = Math.ceil(
      chars * (CHARS_PER_TOKEN_ESTIMATE / TOOL_RESULT_CHARS_PER_TOKEN_ESTIMATE),
    );
    return Math.max(chars, weightedChars);
  }

  return 256;
}

But convertToLlm (packages/agent-core/src/harness/messages.ts:128) expands each harness role into a full-text user message that is sent to the provider:

  • bashExecution -> bashExecutionToText(message)
  • compactionSummary -> COMPACTION_SUMMARY_PREFIX + summary + COMPACTION_SUMMARY_SUFFIX
  • branchSummary -> BRANCH_SUMMARY_PREFIX + summary + BRANCH_SUMMARY_SUFFIX
  • custom -> content

The guard runs transformContext on the un-expanded harness-role messages (before convertToLlm), so exceedsPreemptiveOverflowThreshold (tool-result-context-guard.ts:247, via estimateContextChars -> estimateMessageCharsCached) sums these as 256 each and never crosses maxContextChars. enforceToolResultLimitInPlace (tool-result-context-guard.ts:277) only truncates toolResult messages, so large bashExecution output is neither counted nor trimmed.

The fix mirrors #97861: estimate each affected role from the exact text convertToLlm renders (bashExecutionToText, the summary prefix/suffix plus summary text, and the custom content), returning 0 for excludeFromContext bash records.

Sibling surfaces

Severity

P1, with an honest mitigation: the pre-prompt precheck now uses the corrected estimator (#97861), so the realistic failure mode is mid-tool-loop accumulation of bash output slipping past this last-resort in-loop guard within a turn, rather than every turn. compactionSummary is present in every compacted session, so the undercount applies broadly, but the precheck catches the start-of-turn case.

Real behavior proof

Behavior addressed: in-loop overflow guard undercounts bashExecution/compactionSummary/branchSummary/custom as a flat 256 chars and fails to fire overflow protection on summary/bash-dominated context.
Real environment tested: real installToolResultContextGuard transformContext driven directly at commit 15de9d8; corrected run produced by patching estimateMessageChars locally to size those roles from the convertToLlm text, then reverted.
Exact steps or command run after this patch: node_modules/.bin/tsx /tmp/openclaw-bug-repros/audit-2026-06-29b/compaction/repro-context-guard-undercount.mjs
Evidence after fix:

# OBSERVED (buggy, origin/main 15de9d881a)
contextWindowTokens     = 100000
maxContextChars         = 360000
real big-text chars     = 500000 x2 messages = 1000000

A harness roles (compactionSummary+bashExecution):
   guard threw overflow = false 
B visible roles (user+toolResult), identical text:
   guard threw overflow = true (Context overflow: estimated context size exceeds safe threshold during tool loop.)

ACTUAL: A=false B=true
RESULT: BUG CONFIRMED - guard is BLIND to summary/bash context; over-window prompt is let through.

# EXPECTED (estimator counts harness roles from convertToLlm text; identical inputs)
contextWindowTokens     = 100000
maxContextChars         = 360000
real big-text chars     = 500000 x2 messages = 1000000

A harness roles (compactionSummary+bashExecution):
   guard threw overflow = true (Context overflow: estimated context size exceeds safe threshold during tool loop.)
B visible roles (user+toolResult), identical text:
   guard threw overflow = true (Context overflow: estimated context size exceeds safe threshold during tool loop.)

ACTUAL: A=true B=true

Observed result after fix: on identical ~1,000,000-char payloads the guard fires for both role layouts only once the estimator sizes harness roles from their rendered text; on current main it fires only for the user/toolResult layout and lets the summary/bash layout through.
What was not tested: full gateway end-to-end submission to a live provider was not driven; the repro exercises the guard's transformContext directly. The corrected branch shown is a local demonstration patch, not a proposed final diff.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.no-staleExclude from stale automation

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions