Skip to content

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

Open
windli2018 wants to merge 3 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 3 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), 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 calls agent.continue() directly (AgentSession has no public continue; events/persistence still flow via its subscriptions)
    • 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. 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 now 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. Export/import buttons are always reachable (incl. when the queue is empty) so history can be re-imported at any time, with inline green/red feedback rendered outside the banner so it shows even with an empty queue.

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.
  • 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. 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.

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
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