Skip to content

Steering forwarder task leaks on cancelled turns; late steer messages silently lost #4456

Description

@senamakel

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

Problem

run_turn_via_tinyagents_shared spawns an unconditional 50 ms steering-forwarder poll loop (src/openhuman/tinyagents/mod.rs:597-605). Cleanup — forwarder.abort() (mod.rs:623) and shared_steering_registry().deregister(...) (mod.rs:626) — only runs if the drive future returns normally. But cancellation in this codebase is drop-based:

  • the web channel cancels an in-flight turn via tokio::select! on a cancel token (src/openhuman/channels/providers/web/ops.rs:557-559), dropping the future;
  • detached subagents are hard-aborted via AbortHandle (src/openhuman/agent_orchestration/running_subagents.rs, task_dispatcher/registry.rs:46).

On those paths the spawned task detaches and loops sleep(50ms) → drain queue forever, pinning the Arc<RunQueue> + SteeringHandle. Worse: the RunQueue is session-owned and reused, so the zombie forwarder races the next turn's forwarder and can steal subsequent steer/collect messages into a dead handle. Steering-registry entries for aborted subagent tasks are never deregistered.

Related steer-loss gaps (same seam):

  • A steer landing after the loop's last steering checkpoint is silently dropped at turn end; the web channel replies {"queued": true} but the message vanishes (run_queue/mod.rs:64-79, web/ops.rs:672-686).
  • The legacy RunQueueMessageDelivered observability event was removed with no replacement, so these losses are invisible.

Fix plan

  1. Wrap the forwarder in an abort-on-drop guard (RAII struct whose Drop calls handle.abort() and deregisters from the steering registry), held across the drive future — cleanup then happens on normal return, error, and drop-cancellation alike.
  2. At turn end (and cancellation), drain any residual steers from the SteeringHandle back into the session RunQueue (or the followup lane) so late steers become the next turn's input instead of vanishing.
  3. Re-emit a delivery/drop observability event (equivalent of RunQueueMessageDelivered, plus a SteerDropped/requeued signal) so losses are diagnosable.
  4. Audit shared_steering_registry() for other leak paths (entries keyed by aborted runs).

Acceptance criteria

  • Cancelling an in-flight turn (web interrupt, subagent abort) leaves zero live forwarder tasks (assert via task counter or registry size in tests).
  • A steer sent in the last 50 ms window / after the final checkpoint is not lost: it lands in the next turn's input.
  • Steer delivery/requeue is visible in the event stream.

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

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions