Commit graph

2 commits

Author SHA1 Message Date
ethernet
8b1c1dfa1d fix(desktop): review fixes for the agent-side TurnQueue
Review findings on the TurnQueue PR, fixed in one pass:

1. "Send now" on an idle session was a silent no-op: session.queue.promote
   reordered the queue but never drained it, so the promoted entry (and the
   drainNextQueued rescue gesture built on it) just sat there. Promote now
   fires _drain_queued_prompt in a thread when the session is idle, same as
   an idle session.queue.add.

2. A drained entry whose dispatch raised was lost: _drain_queued_prompt
   popped the entry and emitted queue.drained (painting a user turn in the
   client transcript) before _run_prompt_submit. On exception the entry was
   gone and the transcript lied. The drain now requeues the entry at the
   head (same id, so client mirrors stay consistent) via the new
   TurnQueue.requeue_front(), and queue.drained is only emitted after a
   successful dispatch.

3. Multi-line steers never settled: settlePendingSteer split the applied
   text into a line-set, so an entry that itself contained newlines
   (Cmd+Enter on a multi-line draft) matched nothing and pinned a
   "Steering..." row forever. Now matches by whole-entry containment, plus a
   message.complete backstop sweep (a steer can't outlive its turn: applied,
   dropped, or re-queued as the next turn).

4. Speculative surface removed per the contribution rubric: QueuedTurn.mode
   (written, never read), QueuedTurn.attachments (clients resolve
   attachments to @file: refs at enqueue time), enqueue_front() (replaced by
   the requeue_front() that finding 2 actually needs), and the keep_queue
   param on session.interrupt (documented for a promote+interrupt flow that
   actually interrupts via agent.interrupt() directly, so it was dead).

Also: unused sessionId arg dropped from useComposerQueue, the steer-event
lambda no longer shadows the enclosing text parameter, and a rejected
session.queue.add now surfaces an i18n'd error toast instead of silently
no-oping (draft is kept either way).

Tests: idle-promote drains immediately, failed dispatch requeues at head
without emitting queue.drained, interrupt clears the queue, multi-line
steer settles. 16 gateway tests pass; desktop tsc/eslint/vitest clean.
2026-07-15 17:54:21 -04:00
ethernet
bd70904c26 feat(desktop): move queue/steer to the agent-side TurnQueue so queued messages fire without the tab open
Queued messages in the desktop app lived in localStorage and were drained
by a React useEffect on the busy->false edge — if the session tab wasn't
mounted, the effect never ran and queued prompts sat dormant forever.

Move the queue into the agent process where steer already lives:

- agent/turn_queue.py: TurnQueue, a thread-safe FIFO on AIAgent (wired in
  agent_init next to _pending_steer). Entries carry a `source` field
  ("queue" vs "busy_submit") so drain events tell clients whether the
  text was already echoed optimistically.
- tui_gateway: _enqueue_prompt/_drain_queued_prompt delegate to
  agent.turn_queue; new session.queue.add/list/remove/clear/promote/update
  RPCs; queue.updated + queue.drained events; queue in session.info.
  An idle-session enqueue drains immediately — the gateway owns every
  drain path. session.interrupt clears the queue (keep_queue opts out
  for the promote+interrupt "send now" gesture). A leftover pending_steer
  returned by run_conversation is re-queued as the next turn instead of
  being silently dropped.
- steer honesty: new agent._on_steer_event observer fires steer.applied
  at the two real injection sites (pre-API drain + tool-batch drain) and
  steer.dropped when an interrupt discards the pending steer. The desktop
  shows steers as pending in the queue panel and only appends the steer:
  transcript row when the model actually saw the text (both the primary
  composer and session tiles previously painted it at RPC-accept time).
- desktop: composer-queue.ts rewritten as a gateway-backed mirror
  (optimistic updates settled by queue.updated); the auto-drain effect is
  deleted; "send now" promotes on the gateway; attachments resolve to
  @file: refs at enqueue time so queued text is self-contained when the
  gateway drains it later; one-time localStorage migration. Dead code
  removed: fromQueue submit option, shouldAutoDrain, queueStuck i18n.

Tests: tests/tui_gateway/test_turn_queue.py (TurnQueue unit + RPC
integration + drain semantics), composer-queue.test.ts rewritten for the
gateway-backed store.
2026-07-15 16:50:18 -04:00