# v0.15.0 — Mid-task instructions

Send follow-up instructions while the agent is working. They join the next ReAct step's context without interrupting the current one. Queue multiple; cancel any that haven't been consumed yet.

## How it works

While the agent is streaming, the Composer stays enabled. Type a follow-up and a **Queue** button appears to the left of STOP. Tap it and your message appears in a pending list above the input, decorated with a warm-gold dot and an × to cancel.

When the current ReAct step finishes and the next LLM call is about to begin, all queued instructions drain atomically into a single user message. The agent sees them at the top of the next iteration before any tool call. Once consumed, the pending badges disappear and the bubbles become ordinary chat history.

If you tap STOP before the queue drains, the instructions are preserved. The next time you start a task in the same session, the residual queue merges into the first user message alongside your new input — so nothing you typed is silently discarded.

## Architecture

The service worker owns the pending queue (`SessionAgentState.pendingInstructions`). Every mutation follows a strict sequence: mutate → `writeAtomic` → broadcast `chat-instruction-state` to the panel. The panel is a pure consumer of broadcast state; it never writes to the queue directly.

The drain happens at the top of each loop iteration — after the abort check but before the LLM call — via a new pure helper `src/lib/agent/loop-drain.ts`. Drain is atomic: read queue + clear + persist + broadcast in one call. If the abort signal is already set, drain is skipped and the queue is left intact.

Panel and SW each store what they need. The panel keeps `DisplayMessage[]` for rendering (with an optional `id` ulid set at queue time). The SW keeps `PendingInstruction[]` with the full payload needed to reconstruct the user message at drain time. The `chatMessageId` ulid is the cross-reference key linking the two sides. Cancel is symmetric: panel deletes its `DisplayMessage`; SW deletes its `PendingInstruction`.

## Safety

All mid-task instruction content is escaped through the existing `escapeUntrustedWrappers` helper and wrapped in `<untrusted_user_message source="mid_task">` before it reaches the LLM. It never enters the system role. This is the same defense used for page snapshots and normal user messages — mid-task instructions are not a new attack surface.

## Survives

- **SW restart** (idle eviction → recovery): queue is persisted via `writeAtomic` on every mutation; `loadSession` at startup restores it to memory; drain proceeds normally on resume.
- **Panel reload / disconnect**: on reconnect, the SW broadcasts `chat-instruction-state` for every active session; the panel re-paints pending decorations from the broadcast.
- **Abort**: queue is preserved across abort; it merges into the next `chat-start`'s first user message.

## Internal

- New field `SessionAgentState.pendingInstructions: PendingInstruction[]` (per-session FIFO queue)
- New helper module `src/lib/sessions/pending-instructions.ts` (add / cancel / drain)
- New pure helper module `src/lib/agent/loop-drain.ts` (`buildMidTaskUserMessage` + `mergeCarryoverIntoMessages`)
- New SW broadcast helper `src/background/instruction-broadcast.ts`
- New UI component `src/sidepanel/components/PendingInstructionList.tsx`
- 4 new port message types: `chat-instruction-add` / `chat-instruction-cancel` / `chat-instruction-state` / `chat-instruction-rejected`
- `DisplayMessage` user variant extended with optional `id` field (ulid; absent on normal messages — backward-compatible)
- Cross-layer regression coverage in `src/__tests__/cross-layer/mid-task-instructions.test.ts` (12 tests) and `src/__tests__/cross-layer/mid-task-recovery.test.ts` (11 tests)
- ~16 existing test fixtures updated to include `pendingInstructions: []`

## 中文摘要

v0.15.0 实现了"中途插队指令"功能（issue #34）。

- **任务进行中可发指令**：streaming 期间 Composer 不再锁定，随时输入 → 点 Queue → 指令进入待处理队列，显示在输入框上方。
- **可撤回**：每条 pending 指令右侧有 × 按钮，消费前随时取消，取消后 LLM 不会看到该条内容。
- **下一轮自动注入**：当前 ReAct step 结束、下次 LLM 调用前，队列原子清空并合并为一条 `<untrusted_user_message source="mid_task">` 注入上下文，注入后 pending badge 消失，bubble 变为普通聊天历史。
- **Abort 不丢指令**：点 STOP 后队列保留，下次发起任务时与新输入合并成第一轮 user message。
- **完整持久化**：queue 随 session 写 `writeAtomic`；SW 被 Chrome evict 重启后 / panel 重连后均可恢复，panel 通过 broadcast 重绘 pending 状态。
- **安全**：内容走 `escapeUntrustedWrappers` + `<untrusted_user_message>` 包裹，从不进 system role。
