Skip to content

feat: queue persistence & recovery, queue management, streaming model/compact, collapsible panel - #335

Open
windli2018 wants to merge 5 commits into
agegr:mainfrom
windli2018:feat/queue-persistence-recovery
Open

feat: queue persistence & recovery, queue management, streaming model/compact, collapsible panel#335
windli2018 wants to merge 5 commits into
agegr:mainfrom
windli2018:feat/queue-persistence-recovery

Conversation

@windli2018

@windli2018 windli2018 commented Aug 1, 2026

Copy link
Copy Markdown

What

pi keeps steer/follow-up queues in memory only — a server restart silently loses them. This PR mirrors every queue mutation to a per-session sidecar (<session>.jsonl.queue.json) and surfaces orphaned entries after a restart through a manual recovery dialog. Nothing is ever auto-delivered: the user decides per-entry to re-queue, re-queue & continue, discard, or export.

On top of crash recovery, this PR adds a full set of queue management operations (delete/reorder/recall/requeue), imports that pop the same recovery dialog so the user chooses how to run an imported queue, lets users switch models and compact while the agent is running, and ships a collapsible bottom panel for small screens.

How — crash recovery

  • lib/queue-store.ts (new): atomic sidecar persistence (tmp+rename) + entry types.
  • lib/rpc-manager.ts: queueMirror (live queue w/ images) / queueRecovery (orphans) / pendingQueueHints (FIFO image hints — pi reports queued items by text only and template expansion can rewrite text). queue_update events reconcile the mirror. New commands:
    • resolve_recovery {keep, discard, continueRun} — re-queues via inner.steer()/followUp(). continueRun runs the whole batch: pi has no public API to resume an idle agent's queue (agent.continue() only works inside an active _runAgentPrompt loop, and the internal continuation stalls), so bootstrapQueuedRun prompts each kept entry in series — each AgentSession.prompt() awaits its _runAgentPrompt, so entries run one turn after another. (Entries are captured before clearQueue, whose queue_update would otherwise empty the mirror.)
    • stage_recovery {entries} — stage entries as pending recovery (NOT into the live queue) so the recovery dialog pops up; used by import.
    • export_queue, import_queue
    • get_state.pendingRecovery
  • api/sessions/[id]/queue-recovery (new): lightweight sidecar read that never creates an AgentSession; when a wrapper is alive its in-memory recovery list wins.
  • QueueRecoveryDialog (new): checklist with re-queue / re-queue & continue / discard / export .md / .json / import / dismiss. A mode prop ("recovery" | "import") swaps the title/description so an import shows import copy instead of crash-recovery copy. Responsive: desktop single-row footer, mobile 2×2 grid + shorter description; matches the project's existing modal conventions (fixed overlay, var(--bg), radius 10).
  • Session DELETE removes the sidecar.

How — queue operations

pi has no single-dequeue / reorder / insert API, so all edits are implemented as clear-all + re-enqueue in the target order (image hints preserved FIFO), mirroring pi's own clearQueue() semantics (allowed mid-run since the loop only pulls entries between turns). New commands:

  • move_queue {kind, fromIndex, toIndex} — reorder within a queue.
  • recall_queue_item {kind, index} — pull one entry back to the input box for editing (returns its text + images).
  • requeue_at {kind, index, text, images} — insert an edited message back at its original position. Sending from the input while a recalledRef is active re-queues instead of prompting.
  • remove_queue_item {kind, index} — delete a single entry.
  • import_queue returns the queue snapshot (steering/followUp) so the UI updates immediately without a reload.

UI: each queued row has ↑/↓ (reorder), ↩ (recall to edit), 🗑 (delete), plus HTML5 drag on desktop and touch drag on mobile (pointer: coarse auto-disables native draggable). The queue banner collapses to a first-message preview (… +N) which is clickable to expand; in the minimal collapsed state the "Queued · N" label is also clickable to expand. Export/import buttons are always reachable (incl. when the queue is empty). Importing a file stages entries via stage_recovery and pops the recovery dialog — the user then picks re-queue / re-queue & continue / discard, rather than the queue auto-executing.

How — model switch & compact while running

  • Model: pi's setModel() only overwrites agent.state.model, so the in-flight request still finishes with the old model and the next turn picks up the new one — i.e. it already "applies next turn". This PR simply removes the UI disabled={isStreaming} and adds an amber ⏱ "next turn" badge on the model button that auto-clears when the run ends. Switching back to the model the current run started with cancels the pending switch (badge clears) — the run model is captured on the agentRunning transition and intentionally not overwritten by mid-run switches.
  • Compact: compact() rewrites context + the session file, so running it mid-loop is unsafe. handleCompact queues a pendingCompact when agentRunning is true (covers streaming and tool-call execution, where isStreaming is briefly false), and an effect drains it once the agent is truly idle (prompt_done alone is not enough — retries / auto-compaction / queued follow-ups can extend a run). The button shows a three-state cycle: idle → amber "Queued" (click to cancel) → red "Compacting".

How — small-screen bottom panel

ChatInput gains a tri-state bottomMode (fullqueueHiddenminimal, persisted in localStorage): a Windows-10-style vertical bar button in the toolbar, plus swipe gestures (swipe down to collapse one step, up to expand) with a hint overlay (chevron points in the swipe direction). Queue-row touch events are isolated (stopPropagation) so dragging to reorder never triggers mode switching.

How — recovery safety

When a wrapper is (re)created with a non-empty pi queue, sidecar restore is skipped and the mirror is rebuilt from the live queue — preventing double-enqueue if the architecture ever changes so pi-web and pi can restart independently.

Why not auto-restore?

Auto-replaying queued messages after a restart risks double-delivery (messages already delivered but still mirrored) and stale-queue resurrection. Manual recovery keeps it explicit. The same reasoning applies to imports: staging them through the dialog lets the user decide whether to run, rather than auto-executing.

Notes

  • Image-only queued messages never leave pi's _steeringMessages (falsy contentText("")) — they may surface as already-delivered in recovery; users discard manually (documented in AGENTS.md).
  • next.config.ts: dev server gets its own build dir (.next-dev) so dev and production can run on separate ports simultaneously. tsconfig.json/.gitignore updated for it.

pi keeps steer/follow-up queues in memory only; a server restart silently
loses them. Mirror every queue mutation to a per-session sidecar
(<session>.jsonl.queue.json, atomic tmp+rename) and surface orphaned
entries after a restart through a manual recovery dialog — nothing is
ever auto-delivered.

- lib/queue-store.ts: sidecar persistence + queue entry types
- lib/rpc-manager.ts: queueMirror/queueRecovery/pendingQueueHints, queue_update
  reconciliation, resolve_recovery (keep/discard/continueRun via agent.continue),
  export_queue, import_queue commands, get_state.pendingRecovery
- api/sessions/[id]/queue-recovery: lightweight sidecar read that never creates
  an AgentSession; live wrapper memory wins when present
- QueueRecoveryDialog: checklist + re-queue / re-queue & continue / discard /
  export .md/.json / import / dismiss; responsive layout (desktop single row,
  mobile 2x2 grid + short description)
- ChatInput queued banner: collapse/expand with first-message preview, export /
  import with inline success/error feedback, "queued" label on its own row on
  small screens
- session DELETE removes the sidecar
- next.config.ts: dev server uses its own build dir (.next-dev) so dev and
  production can run on separate ports simultaneously
- Queue: delete single item, reorder (drag/arrow/touch), recall-to-edit,
  requeue-at-position, import when empty, immediate display after import
- Model: allow switching while streaming (applies next turn) with badge
- Compact: queue when agent running, auto-run on idle, cancel support
- Bottom panel: tri-state collapse (full/queueHidden/minimal) with swipe
- Recovery: consistency check (skip sidecar restore if pi queue non-empty)
- Collapsible queue summary row is now clickable to expand
- i18n: add missing keys, replace hardcoded Chinese tooltips
@windli2018 windli2018 changed the title feat: persist queued messages and add manual crash recovery feat: queue persistence & recovery, queue management, streaming model/compact, collapsible panel Aug 1, 2026
- Importing a queue file now stages entries as pending recovery, popping the
  recovery dialog so the user chooses re-queue / re-queue & continue / discard
  (same flow as crash recovery) instead of auto-executing.
- Fix "re-queue & continue" not executing: runAgentContinue captured
  queueMirror AFTER clearQueue (whose queue_update emptied it), so only the
  first entry ran. Now capture entries before clearQueue and prompt each in
  series — pi's steer/followUp queues aren't drained by a prompt-bootstrapped
  loop, so sequential prompts are the robust way to run a batch.
- Recovery dialog distinguishes import vs crash-recovery copy (mode prop).
- Minimal collapsed bar's "Queued · N" label is now clickable to expand.
- Fix reversed swipe-hint chevron (collapse/expand arrows were swapped).
- Switching back to the current run's model cancels the pending switch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant