Skip to content

feat(chat): queue prompts sent while compaction runs - #1426

Open
vergram wants to merge 4 commits into
agegr:mainfrom
vergram:feat/compaction-message-queueing
Open

vergram wants to merge 4 commits into
agegr:mainfrom
vergram:feat/compaction-message-queueing

Conversation

@vergram

@vergram vergram commented Oct 27, 2026

Copy link
Copy Markdown

Summary

pi's SDK refuses prompts while compaction runs ("Cannot submit a prompt while
compaction is in progress"), so nothing typed into a compacting session could reach
the agent. The wrapper now parks those submissions and flushes them when the
compaction ends, as pi's TUI's compactionQueuedMessages does. This is the
compaction-queueing part of #812, the focused follow-up its closing comment asked for.

send("prompt") while isCompacting
  inner.prompt()
    rejects "Cannot submit a prompt while compaction is in progress"
-   error notice + text back in the composer
+   park { message, images, streamingBehavior }   # admission stays outstanding
+   ack { queued: true }                          # composer clears, queue row appears
+   drop the optimistic bubble                    # not a sent message until delivered

on compaction_end / auto_compaction_end
+  for entry in parked, FIFO
+    inner.prompt(entry, streamingBehavior ?? "followUp")
+      "started" → prompt_done for its client
+      "queued"  → rides the live run's prompt_done
+      error     → back to the queue + prompt_error

Shipped alongside, a pre-existing composer bug this feature cannot ship without: the
/compact builtin awaited its blocking compact POST while runBuiltinCommand holds
the composer's <fieldset disabled>, so the input box took no text for the whole
compaction and came back only after a remount.

/compact (builtin slash command)
-  await the blocking compact POST   # <fieldset disabled> held meanwhile
+  complete at once, run handleCompact detached
+    Compacting status, Stop compaction, result banner, error report the outcome
  • Parking hooks the SDK's rejection, never pre-empts inner.prompt(), so an extension
    command typed during compaction still runs (the SDK executes commands before it refuses).
  • A parked prompt keeps its admission's pendingPromptCount outstanding until delivery or
    recall, so isRunning(), the idle shutdown and the reload/clone refusals see it.
  • The flush always carries a streamingBehavior, so AgentSession.prompt() queues into a
    run still finishing the compaction or starts one when idle. It never hits
    "Agent is already processing".
  • get_state.queuedMessages and every queue_update merge pi's queue with the parked
    ones; clear_queue returns both, and Recall puts the text back in the composer.
  • The composer stays editable: the placeholder says messages queue, and the status line
    reads "Compacting… · N queued" for a manual compaction too, where agentRunning alone
    left it unlabeled.

Evidence

Before: a prompt sent during compaction bounced with "Cannot submit a prompt while
compaction is in progress." and the text returned to the composer; with /compact the
composer was dead for the compaction's duration. Neither queued-message path worked.
After: the message is accepted, appears in the queued-messages panel, and is
delivered FIFO when the compaction ends.

New tests:

✔ /compact completes at once and runs the compaction detached      # holds the compact POST open forever
✔ /compact passes its instructions to the detached compaction
✔ prompts sent while compaction runs park and flush FIFO at compaction end
✔ recall returns parked prompts to the input and settles their admissions
✔ clear_queue returns pi's own queue beside the parked prompts
✔ a failed flush puts the prompt back in the queue and reports the error
✔ parked prompts carry their images through the flush

Plus the queued-count case in lib/chat-phase-label.test.mjs and an
editable-while-compacting case in components/ChatInput.test.mjs.

Checks: tsc --noEmit clean, npm run lint clean, npm test green end to end
(2880 tests, 2859 pass, 21 skipped, 0 fail).

The branch also carries one separable commit, test: run the suite green on Windows
(28 test files, test and fixture code only): 8.3 temp-path canonicalization, CRLF
normalization at source reads, chmod semantics, ESM absolute-path imports in the hooks
fixtures, ConPTY pid timing, and EBUSY teardown of fixture MCP servers. npm test
used to fail 66 here and hang without --test-force-exit; it now exits clean. Happy to
split it into its own PR if you prefer.

Merge Danger

Door: Two-way. A revert restores today's behavior on both paths, and there is no
schema, session-file or extension-API surface change.

Blast Radius: Prompt dispatch while a compaction runs, and the /compact builtin
command. Idle sessions, runs, steer/follow-up queueing outside compaction and other
slash commands are untouched.

  • Parked text lives in the wrapper instead of the composer, so a wrapper shutdown before
    the flush drops it. The queue panel and Recall are the user's recourse while it is
    parked, and isRunning() keeps the wrapper alive meanwhile.
  • A failed flush parks the message again; it retries at the next compaction end or on
    Recall, and reports prompt_error either way.
  • isPromptRunning reads true while a prompt is parked. Intended ("a prompt is pending"),
    but it is a new state the status polling can observe.
  • /compact no longer answers "Command completed"; the compaction's own status line,
    result banner and error banner report it. Anyone keying on that toast will notice.

qiyu added 4 commits October 21, 2026 21:16
pi's SDK rejects prompts during compaction ("Cannot submit a prompt while
compaction is in progress"), so every submission bounced back to the
composer with an error and the input read as dead. Park the submissions in
the wrapper and flush them FIFO at compaction end, as pi's TUI's
compactionQueuedMessages does. This is the compaction-queueing part of
agegr#812, the focused follow-up its closing comment asked for.

- Parking happens on the SDK's rejection, never before inner.prompt(), so
  an extension command typed during compaction still runs: the SDK
  executes commands before it refuses.
- A parked prompt keeps its admission's pendingPromptCount outstanding
  until delivery or recall, so isRunning(), the idle shutdown and the
  reload/clone refusals all see it.
- The flush is the TUI's flushCompactionQueue() in the wrapper: every
  delivery carries a streamingBehavior (a plain parked prompt as followUp),
  so AgentSession.prompt() queues it into a run still finishing the
  compaction or starts one when idle, never "Agent is already processing".
  A delivery that started the run emits its prompt_done, one that queued
  into another run rides that run's, and a failed delivery goes back to the
  queue with prompt_error and waits for Recall or the next compaction end.
- get_state.queuedMessages and every queue_update merge pi's queue with the
  parked ones; clear_queue returns both, and Recall puts the text back in
  the composer.
- The composer stays editable while compacting: the placeholder says
  messages queue, and the status line shows "Compacting... - N queued",
  for a manual compaction too (agentRunning alone left it unlabeled).

Tests: lib/rpc-manager-compaction-queue.test.mjs (park, FIFO flush,
recall, merge, failed flush, images), the queued-count case in
chat-phase-label, the editable-composer case in ChatInput. tsc, eslint and
the touched areas' 246 tests pass; the 66 suite failures on this Windows
machine are identical with and without the change.
…mpts out of the transcript

Two bugs from manual testing of the compaction queueing.

1. /compact disabled the composer for the whole compaction. The builtin
   slash handler awaited the blocking compact POST, and runBuiltinCommand
   holds the composer's <fieldset disabled> until the command resolves: the
   input box took no text until the session was switched away and back (a
   remount resets builtinCommandPending). The command now completes at once
   and runs the compaction detached through handleCompact, the Compact
   button's path, which reports itself (Compacting status, Stop compaction,
   result banner, error); its result is the silent action: "compact".
   handleCompact takes the optional custom instructions and promotes the
   session after its reload, as the inline path did.

2. A parked prompt rendered as a sent message. handleSend's optimistic
   bubble stayed in the transcript while the message waited in the queue.
   A queued submission is now acked { queued: true } (parked, or the SDK's
   "queued" disposition) and the client drops the bubble: the queue rows
   show it, and the transcript shows it when the flush delivers it.

Tests: hooks/compact-command.test.mjs pins the detached /compact against a
compact POST that never resolves; lib/rpc-manager-compaction-queue.test.mjs
pins the queued ack (and the extension command still answering null).
tsc, eslint and the touched areas' 258 tests pass.
28 test files fixed, test and fixture code only (plus one fixture server).
No product change. Root causes, by cluster:

- 8.3 temp paths: %TEMP% resolves to C:\Users\LINWEI~1\... while git,
  the SDK and $HOME report expanded names. Node's realpathSync keeps short
  names (only realpathSync.native expands them), so fixtures compared
  unequal paths and ancestor walks reached the real ~/.agents/skills from
  the scratch dir. Fixture roots now canonicalize with realpathSync.native.
- CRLF checkouts: source-reading tests sliced on ";\n" and regexes ended
  at \n before \r, so they extracted nothing. Repo convention
  .replace(/\r\n/g, "\n") applied at the read sites.
- chmod on Windows: 0o500/0o400 do not make directories unwritable and
  stat reports 0o666 regardless. Unwritable tests now lock the file too
  (same refusal path, different syscall); mode assertions keep POSIX.
- ESM absolute paths: hooks fixtures imported a raw G:\... path, which
  Node's ESM loader rejects (protocol 'g:'), and drive-letter-case drift
  would load a second shim instance. They now take the shim through
  jiti.import("react"), the alias route the hooks resolve react by.
- ConPTY: node-pty reports pid 0 until its worker connects and arms a 5s
  watchdog; tests now wait for the connection before asserting or ticking
  mock timers.
- EBUSY teardown: fixture MCP servers held cwd open while rm ran (Windows
  refuses); shutdown is awaited first and removal retries. A fixture's
  !touch marker wrote into the repo root (unquoted backslashes in bash -c);
  markers are quoted with forward slashes. Grandchild processes of the
  MCP fixture now exit with their parent.
- PATH assertions joined with the host delimiter for a case that tests
  another platform; PTY and skills-walk tests reached the developer's real
  home on Windows (temp under the profile): hermetic .git marker added.

npm test now passes end to end here (2880 tests, 2859 pass, 21 skipped,
0 fail) and exits without --test-force-exit; it used to hang on leaked
fixture processes and fail 66.

This branch has not been deployed

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