#1645

[BUG] Sub-agents receive unbounded parent history before compaction triggers

Opentechnoconsulxopened 5d ago

title: "[BUG] Sub-agents receive unbounded parent history before compaction triggers" labels: ["bug", "sub-agents", "context-window"]

Summary

When a parent agent spawns a child (sub-agent), the child receives the full parent parent_history via child_initial_input() — serialized as JSON and passed as the first user message. This happens before the child's first _run_cycle, which is the only place maybe_compact() is called (line 792 of strix/core/execution.py).

As a result, the child's context window can be exceeded on its very first LLM call — before any compaction can trigger.

Root Cause

1. child_initial_input() passes verbatim history

In strix/core/inputs.py (lines 342–377):

def child_initial_input(
    *,
    name: str,
    child_id: str,
    parent_id: str,
    task: str,
    parent_history: list[Any],
) -> list[dict[str, Any]]:
    ...
    if parent_history:
        rendered = json.dumps(
            scrub_images_from_items(parent_history),
            ensure_ascii=False,
            default=str,
        )
        parts.append(
            "== Inherited context from parent (background only) ==\n"
            f"{rendered}\n"
            "== End of inherited context ==\n"
            ...
        )

This serializes the entire parent history with json.dumps() — no token counting, no truncation, no compaction. The scrub_images_from_items() call only removes image blocks; it does not trim text content or history length.

2. Compaction only triggers inside _run_cycle

In strix/core/execution.py, _compact_session() is called at line 792 inside _run_cycle():

# Line 792 of execution.py
try:
    await _compact_session(agent, session, run_config, force=False)
except Exception:
    logger.exception("proactive compaction failed for %s", agent_id)

This is the first thing the while loop does each cycle. But for child agents:

  1. spawn_child_agent() (line 389) calls child_initial_input(parent_history=...) — passes full history.
  2. _start_child_runner() (line 1155) receives the initial_input and passes it to run_agent_loop().
  3. run_agent_loop() → _run_agent_loop() → _run_until_lifecycle() → _run_cycle() — only then does compaction trigger.

The full unbounded history is already sitting in the session when _run_cycle starts.

3. No pre-spawn compaction

The parent agent also does not compact its session before calling spawn_child_agent(). So even if the parent's session was compacted during its run, the parent_history captured at spawn time can be very large.

Reproduction Scenario

  1. Parent agent runs many turns with tools (e.g., file scanning, code analysis).
  2. Parent session accumulates thousands of tokens of history.
  3. Parent calls agent_spawn() / spawn_child_agent().
  4. Child receives the full parent_history as its initial input.
  5. Child's first LLM request includes the full history + child's own prompt + tool definitions.
  6. If total tokens exceed the model's context window → context overflow error on the child's very first call.

Proposed Fixes

Option A: Compact parent session before spawning children

In spawn_child_agent() (line 389 of execution.py), call maybe_compact() on the parent session before passing parent_history to child_initial_input():

# Before creating child, compact the parent if needed
if session is not None and run_config is not None:
    try:
        await _compact_session(parent_agent, parent_session, run_config, force=False)
    except Exception:
        pass  # Non-critical; continue with existing history

Pros: Simple, uses existing compaction mechanism. Cons: Requires access to parent session and agent from the spawn site; may add latency.

Option B: Trim inherited context in child_initial_input()

Add a configurable limit (e.g., STRIX_INHERIT_CONTEXT_MAX_TOKENS, default ~4000 tokens) to child_initial_input(). Truncate the beginning of parent_history to fit within the limit:

MAX_INHERIT_TOKENS = int(os.environ.get("STRIX_INHERIT_CONTEXT_MAX_TOKENS", "4000"))

def child_initial_input(..., parent_history: list[Any], ...):
    # Trim parent_history if it exceeds MAX_INHERIT_TOKENS
    total_tokens = 0
    trimmed = []
    for item in reversed(parent_history):
        tokens = count_tokens(json.dumps(item), model=None)  # or auto-detect
        if total_tokens + tokens > MAX_INHERIT_TOKENS:
            break
        trimmed.append(item)
        total_tokens += tokens
    parent_history = list(reversed(trimmed))
    ...

Pros: More targeted; doesn't affect parent's session state. Cons: Requires token counting overhead at spawn time; loses older context.

Option C: Hybrid

Compact the parent session proactively before spawning children (Option A) AND trim any remaining excess in child_initial_input() (Option B). This provides defense-in-depth.

Environment Variables

Add support for a new configuration variable:

STRIX_INHERIT_CONTEXT_MAX_TOKENS=4000  # Max tokens to inherit from parent
STRIX_PRECOMPACT_BEFORE_SPAWN=true     # Enable pre-spawn compaction (default: true)

Related Issues / PRs

  • Issue #680: Request for /compact command in root agent (no PR yet). Note: the fix here is not about adding a new /compact command — it's about ensuring children receive compacted/bounded context.
  • PR #1217: Proposed STRIX_INHERIT_CONTEXT_MAX_TOKENS for context trimming in children (not merged). The proposed approach is similar to Option B above.
  • PR #881: Compaction mechanism (merged). Works correctly within a run cycle but timing is wrong for child initialization.

Impact

This bug affects any workflow where:

  • A parent agent has a long conversation history
  • The parent spawns a child/sub-agent
  • The child's model has a limited context window (especially small models or models with narrow windows)

Affected providers include any LiteLLM backend with strict context limits (e.g., Perplexity, llama.cpp, smaller OpenRouter models).

Activity

0

No activity yet.