Skip to content

Mid-turn steering corrupts persisted history — pre-steer tool rounds and the steer text are dropped #4455

Description

@senamakel

Part of the v0.58.7 → HEAD TinyAgents-migration audit (see parent issue).

Problem

Mid-turn steer/collect messages corrupt persisted history on both the parent and subagent paths. forward_steers/forward_collects (src/openhuman/tinyagents/mod.rs:108-130) inject steers as Message::user(...); the crate appends them to the working transcript. Post-run persistence slices the returned conversation with messages_since_last_user (src/openhuman/tinyagents/convert.rs:311-318, rposition(Message::User)) — so an injected steer moves the boundary:

  • Parent: self.history.extend(...) at src/openhuman/agent/harness/session/turn/core.rs:940 persists only the post-steer suffix — every assistant/tool round executed before the steer, plus the steer text itself, vanish from history, the persisted transcript, and the next turn's KV-cache prefix (which now diverges from what the provider saw, busting the cache).
  • Subagent: identical pattern at subagent_runner/ops/graph.rs:286-294; the ask_user_clarification checkpoint (ops/runner.rs:1061-1074), the cap digest (graph.rs:554-582), and the worker-thread mirror (graph.rs:375-390) all consume the same truncated conversation, so resume replays corrupted state.

The legacy engine appended drained steers and all rounds to history incrementally in place, so nothing was lost.

Failure scenario

User steers a running turn ("also check the logs") while tools execute → turn completes → the transcript and next-turn context are missing both the pre-steer work and the user's steer. Checkpoint/resume of a steered subagent replays a hole in the middle of the run.

Fix plan

  1. Replace the "suffix after last user message" convention with an explicit boundary: record the index (or a sentinel id) of the request's final user message before the run, and persist everything after that index from outcome.conversation — injected steers are then included, in order, tagged as user messages.
    • Concretely: capture base_len = request_messages.len() before run_loop; persist outcome.conversation[base_len..] (after verifying the crate returns the full transcript, which it does — the compression middleware only rewrites the per-call clone).
  2. Apply the same fix to the subagent path (ops/graph.rs) so checkpoints, cap digests, and worker mirrors see the full post-request transcript.
  3. Persist steer messages with their existing steer framing so they're distinguishable from the original prompt in transcripts.

Acceptance criteria

  • A steered turn's persisted history contains: all pre-steer assistant/tool rounds, the steer message, and all post-steer rounds, in execution order.
  • The next turn's provider request prefix matches what the provider actually saw during the steered turn (KV-cache preserved).
  • e2e: scripted run with a steer injected between two tool rounds; assert full history on both chat and subagent paths, plus checkpoint/resume round-trip.

Metadata

Metadata

Assignees

No one assigned

    Labels

    agentBuilt-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/.bugpriority: highBreaks a specific featuresubtaskSubtask of a larger tracked effort.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions