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
- 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.
- 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.
- Re-emit a delivery/drop observability event (equivalent of
RunQueueMessageDelivered, plus a SteerDropped/requeued signal) so losses are diagnosable.
- 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.
Part of the v0.58.7 → HEAD TinyAgents-migration audit (see parent issue).
Problem
run_turn_via_tinyagents_sharedspawns an unconditional 50 ms steering-forwarder poll loop (src/openhuman/tinyagents/mod.rs:597-605). Cleanup —forwarder.abort()(mod.rs:623) andshared_steering_registry().deregister(...)(mod.rs:626) — only runs if the drive future returns normally. But cancellation in this codebase is drop-based:tokio::select!on a cancel token (src/openhuman/channels/providers/web/ops.rs:557-559), dropping the future;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 queueforever, pinning theArc<RunQueue>+SteeringHandle. Worse: theRunQueueis 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):
{"queued": true}but the message vanishes (run_queue/mod.rs:64-79,web/ops.rs:672-686).RunQueueMessageDeliveredobservability event was removed with no replacement, so these losses are invisible.Fix plan
Dropcallshandle.abort()and deregisters from the steering registry), held across the drive future — cleanup then happens on normal return, error, and drop-cancellation alike.SteeringHandleback into the sessionRunQueue(or the followup lane) so late steers become the next turn's input instead of vanishing.RunQueueMessageDelivered, plus aSteerDropped/requeued signal) so losses are diagnosable.shared_steering_registry()for other leak paths (entries keyed by aborted runs).Acceptance criteria