# Loop Lessons
- `ToolExecutionUpdate` must include the originating tool call `id` and `name`, and partial tool updates must flow through an awaited relay instead of `try_send`; otherwise concurrent tool progress becomes unattributed and can be silently dropped under channel backpressure.
- Post-turn assistant replacements must preserve the original `ToolCall` blocks. A text-only replacement on a tool turn breaks the core invariant that assistant tool calls stay paired with the committed `ToolResult` messages that follow.
- `TurnPolicyContext.context_messages` must always be a committed turn snapshot. Text-only turns need the assistant message committed before post-turn policies run, and transfer turns must also execute post-turn policies before honoring transfer termination.
- Async tool approval futures need the same `AssertUnwindSafe(...).catch_unwind()` isolation as the sync approval-context hook. A panic after the approval future is polled must turn into `ToolApprovalResolved { approved: false }` plus an error tool result, not an unwound dispatch task.
- Steering interrupts cannot rely on a second `poll_steering()` call to recover messages already drained by a worker task. If a worker polls steering to trip the interrupt flag, it must hand those drained messages into shared batch state so `ToolExecOutcome::SteeringInterrupt` preserves them for the next turn.
- Parent cancellation must be observed while the batch collector waits on join handles. Child cancellation tokens do not unblock `JoinHandle::await` for cancellation-unaware tools, so the collector has to `select!` on `batch_token.cancelled()`, abort the remaining tasks, and return an aborted batch outcome immediately.
- An aborted tool batch must terminate the current turn inline. Routing that outcome through `handle_cancellation()` emits a second synthetic `TurnStart` and drops the tool-call payload/results that belong to the interrupted turn.
- `ToolExecutionStrategy::partition()` indices target the post-preprocessing `tool_calls` slice passed into the strategy, not the original LLM-emitted tool-call list. The dispatch layer must reject out-of-bounds, duplicate, or missing prepared indices with synthetic tool errors before any tool starts.
- `PreDispatchVerdict::Stop` must synthesize one terminal error result for every unresolved tool call in the batch, including calls that already passed preprocessing before a later stop fires. Stop aborts dispatch, but it does not relax tool-result parity.
- Pre-dispatch is batch-wide and two-pass. A later `PreDispatchVerdict::Stop` must prevent any earlier tool from emitting approval request/resolution side effects, so approval only starts after the entire batch clears pre-dispatch.
- Pre-dispatch snapshots `SessionState` once per tool batch and reuses that borrow for every `ToolDispatchContext`. Cloning the full state inside the per-tool loop quietly turns large sessions into avoidable hot-path overhead.
- Cancellation has to be observed in preprocessing and before each execution group/tool dispatch, not just while collecting spawned handles. Otherwise approval waits can hang forever and later tools can still launch after the batch is already cancelled.
- Overflow recovery cancellation must reuse a started-turn cancellation path. Calling the pre-turn helper after `TurnStart` has already been emitted duplicates `TurnStart` and breaks lifecycle ordering for the interrupted turn.
- `handle_stream_error()` must not emit `MessageEnd` for context overflow. Overflow recovery owns the terminal message lifecycle, and unrecoverable overflow must still surface exactly one `MessageEnd`.
- `LoopState.turn_index` must advance after every completed turn, not just tool-loop continuation. Text-only or other `BreakInner` turns still finish a turn before the outer loop polls follow-up messages.
- First-turn `PreTurn` policies must receive the initial prompt batch in `PolicyContext::new_messages`; only `agent_loop_continue()` resumes with an empty initial batch.
- Text-only turns cannot break the inner loop while `pending_messages` is non-empty. Post-turn or post-loop injections queued for the next turn must continue loop processing before follow-up polling or `AgentEnd`.