Skip to content

fix(v2): queue messages sent while context compaction runs - #12824

Open
saphid wants to merge 5 commits into
pingdotgg:t3code/codex-turn-mappingfrom
saphid:fix/v2-queue-during-compaction
Open

saphid wants to merge 5 commits into
pingdotgg:t3code/codex-turn-mappingfrom
saphid:fix/v2-queue-during-compaction

Conversation

@saphid

@saphid saphid commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

What Changed

A message sent while a context-compaction run is in flight now queues behind it instead of failing:

  • Server (orchestration-v2): when a steer or restart decision targets a run whose user message is a native maintenance command (/compact, /logout), resolveMessageDispatchIntent downgrades it to queue_after_active instead of letting delivery fail in dispatchSteerIntoRun. The maintenance-command predicate moves to CommandPolicy.ts so the decision and its guard share one definition; the existing rejection stays as the backstop for explicit promote-to-steer.
  • Web composer: ChatView's existing isCompacting signal now reaches the composer dispatch seam. While the active turn is compacting, the primary send delivers as queue regardless of the configured follow-up behavior, the button reads "Queue message", and the tooltip drops the Ctrl/⌘-steer hint (the alternate is also queue, since steering a compaction is not possible). Shared resolver change lives in client-runtime so web and mobile agree on the meaning.

Why

Compact takes 30-60s on real threads, and the natural next move is to type the follow-up while it runs. That follow-up used to die: Enter resolved to steer (the pre-queue default, or a configured steer follow-up), and the decider rejects steering into a maintenance run with "Wait for context compaction to finish before steering the thread." With the queue follow-up default the same send already worked, so the steer path was the only dead end. Queueing behind compaction is strictly better than a rejected send, and the server-side downgrade covers every client, including ones that do not resolve dispatch context server-side.

UI Changes

Both takes: web client, Claude Fable 5.1, 1280×800, follow-up behavior set to "steer" to exercise the failing path. The user types the follow-up first, clicks the context meter, clicks "Compact context", then sends while compaction runs.

Before (base) — during compaction, send resolves to steer and bounces with an error toast; the draft is restored but nothing is sent:

Before (base): sending during compaction fails with "Wait for context compaction to finish before steering the thread."

After (candidate) — during compaction the send button reads "Queue message"; the message queues behind the compaction run and starts automatically when compaction completes:

After (candidate): sending during compaction queues the message, which runs after compaction completes

Detail crops of the two deciding moments:

Before (base) detail: the steering rejection toast

After (candidate) detail: the queued row above the composer

Verification

  • Bot follow-up on 8b755c2939: vp test run apps/server/src/orchestration-v2/CommandPolicy.test.ts apps/server/src/orchestration-v2/ThreadLaunchService.test.ts — 63 tests passed; pnpm run typecheck in apps/server passed. /logout matching is case-sensitive; /compact remains case-insensitive.

  • Restack verification on 9fcc9a4bdb: vp test run apps/server/src/orchestration-v2/ThreadLaunchService.test.ts apps/server/src/orchestration-v2/CommandPolicy.test.ts packages/client-runtime/src/state/composerDispatch.test.ts apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts apps/server/src/orchestration-v2/QueuedRunOrder.test.ts apps/web/src/session-logic.test.ts apps/web/src/composer-logic.test.ts apps/mobile/src/features/threads/composerSendPresentation.test.ts — 213 tests passed across 8 files.

  • pnpm run typecheck in apps/server, apps/web, apps/mobile, and packages/client-runtime — passed.

  • Live end-to-end pass in a real web client against both revisions (Claude, real compaction, DOM-asserted outcomes): base shows the rejection toast and keeps the draft; candidate flips the button to "Queue message", queues the run, and the queued message starts automatically after "Context compacted 27.3K → 3.60K tokens". The recordings above are those sessions; I inspected the deciding states programmatically (button labels, toast text, queued row) rather than by eye.

  • apps/mobile joins web: useThreadComposerState folds the compaction state into its steering check, so the mobile send label and dispatch mode also queue behind a compaction run.

Boundaries: promoting a queued message with "Steer now" while compaction runs still reports the wait error; the queued message delivers when compaction finishes either way.

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Sep 21, 2026
@macroscopeapp

macroscopeapp Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 8b755c2

Macroscope's review found this PR approvable — This is a focused fix that queues follow-up messages only when compaction or another native maintenance run cannot accept steering, with corresponding web and mobile UI updates. Normal dispatch behavior and configured follow-up defaults remain unchanged, and the server and client paths are covered by targeted tests.

You can add or adjust custom eligibility rules. Learn more.

@saphid

saphid commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Triaged the four failing checks; none are caused by this diff. Both reproduce identically on the pristine base commit (upstream/t3code/codex-turn-mapping at 37671bd), verified locally:

  • Check (typecheck): src/orchestration-v2/ProjectionStore.ts(3413,94): error TS377026 — pre-existing at the base tip; tsc --noEmit produces the identical single error on base and on this branch (no new diagnostics from the diff).
  • Test Server 1/2/3: the six OrchestratorReplayFixtures.integration.test.ts timeouts (queued_turn/*, queued_cancelled_while_active/codex, each 60s at await_run_status … actual=starting) fail with the same names and timeouts on the base commit with this change reverted.

Everything else is green, including the Macroscope correctness check. Happy to help port a fix for either pre-existing failure if useful.

@saphid

saphid commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

On the Macroscope approvability note about /logout: the shared predicate treats sign-out runs the same way deliberately. A /logout run is a short synthetic run (ProviderTurnStartService writes a "Provider signed out" command-execution item and completes); it cannot take a steer today, so the only choices for a message sent during it are the rejection error or queueing behind it. Queueing means the message starts as a normal run after sign-out instead of bouncing back to the draft — same treatment the queue-follow-up default already gives it. The PR body names both commands so this is visible at review.

@juliusmarminge
juliusmarminge force-pushed the t3code/codex-turn-mapping branch from 61a3501 to 619623f Compare September 21, 2026 03:05
@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Sep 21, 2026
@macroscopeapp

macroscopeapp Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Macroscope has since reviewed this pull request. An earlier review was skipped by a cost limit; a review has now completed, so that notice no longer applies.

@saphid saphid closed this Sep 21, 2026
@saphid saphid reopened this Sep 21, 2026
@saphid
saphid force-pushed the fix/v2-queue-during-compaction branch from f4f6dcf to 0811501 Compare September 21, 2026 04:42
@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). and removed size:XXL 1,000+ changed lines (additions + deletions). labels Sep 21, 2026
@saphid

saphid commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Status after rebasing onto the canonical tip (081150145c7 + b37034d563f):

  • Test Server 2 and 3: green. The shards carrying the replay fixtures and orchestration suites pass with the feature — the six 60s fixture hangs from the earlier (stale-base) runs are gone, since the stale side lineage I initially built on carried that regression while the canonical branch already had the scope-lookup fix. Thanks for the correction that this should have been a no-repairs rebase.
  • Test Server 1: fixed on the current head (verified locally, 40/40): ThreadLaunchService.test.ts codified the old contract that steering into an active maintenance run fails; its follow-up scenarios now assert the new queueing behavior. This new head's CI run is waiting on the usual fork-workflow approval.
  • Check and Test: red from the base branch itself. V2LifecycleRow.tsx (landed with feat(web): give lineage subagent hovers the thread hover card #12842) still imports AgentElapsed from ../AgentsPanel, which refactor(web): remove the agents right panel #12835 deleted — the canonical tip fails typecheck and MessagesTimeline.test.tsx for every branch, including its own. Not touching it here to keep this PR scoped.

@juliusmarminge

Copy link
Copy Markdown
Member

Rebased onto t3code/codex-turn-mapping at 9d6feae1ff. Both commits applied cleanly with no conflicts.

One follow-up commit on top (65d0637888):

  • Dropped the ?. on projection.messages?.find. messages is a required array on OrchestrationV2ThreadProjection; the optional chain was only there because the CommandPolicy test fixture builds a partial projection with as unknown as. Gave that fixture a messages: [] instead of hiding it in the source.
  • Mobile now matches web. Mobile already computes isCompacting in use-thread-composer-state.ts and already feeds canSteerActiveTurn into both the send-button label (resolveComposerSendPresentation) and the dispatch mode. Folding !isCompacting into canSteerActiveTurn was the smallest change that makes the button read "Queue" and send with dispatchMode: queue during a compaction run, with no new props on the screen or composer components.
  • Comment on the server policy now says that keeping the steer/restart-to-queue as a silent downgrade versus rejecting it with an error is a maintainer decision. The behavior itself is unchanged from your PR.

Verified: apps/server vp test run on CommandPolicy and ThreadLaunchService (60 passed) and vpr typecheck; packages/client-runtime composerDispatch test (8 passed) and typecheck; apps/mobile composerSendPresentation test (5 passed) and typecheck; apps/web typecheck. All clean.

Left for a maintainer: the silent-downgrade question above.

Rebased and touched up by a maintainer's agent; a human will re-review.

Comment thread apps/server/src/orchestration-v2/CommandPolicy.ts Outdated
@saphid

saphid commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Answering the flagged silent-downgrade question with a concrete recommendation: keep it, for four reasons.

  1. The error path is a dead end. The rejection left the message in the draft with a toast; the user's only move was to wait and re-send. Queueing is the same intent, expressed in a way the thread can act on.
  2. The default already behaves this way. With the queue follow-up default, the identical send during compaction already queues today. The downgrade only changes the steer-configured (or legacy steer-default) path, so it converges two configs on one behavior rather than inventing a new one.
  3. Queued is recoverable; the rejection wasn't. A queued run can be reordered, cancelled, or promoted once the maintenance run finishes. The old failure gave the user nothing to act on.
  4. Logout parity holds. A message sent during a sign-out run previously bounced; queueing starts it after sign-out completes, which is the same outcome the queue default already produces.

If a maintainer would rather see the explicit error preserved for structured steer requests, the narrow alternative is to downgrade only deliveryIntent: auto — but we'd argue that reintroduces the exact dead end this PR exists to remove, just for a rarer config.

@saphid

saphid commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

@juliusmarminge we're making the call rather than leaving it open: the silent downgrade stays. The four reasons are in my previous comment (dead-end rejection, the queue default already delivers this send as a queued run, queued runs stay reorderable/cancellable/promotable, logout parity), and the policy comment now states the decision instead of deferring it — that's commit 8039d4b8, comment-only, no behavior change from your touch-up.

Rereview when you get a chance? Happy to switch to the deliveryIntent: auto-only downgrade if Theo weighs in the other way.

@saphid
saphid force-pushed the fix/v2-queue-during-compaction branch from 8039d4b to c361947 Compare September 22, 2026 01:41
@juliusmarminge
juliusmarminge force-pushed the t3code/codex-turn-mapping branch 6 times, most recently from 6ca6a24 to 3e4ca4c Compare September 23, 2026 23:24
@juliusmarminge
juliusmarminge force-pushed the t3code/codex-turn-mapping branch from 1bd44f2 to 3b9c885 Compare September 24, 2026 04:06
@saphid
saphid force-pushed the fix/v2-queue-during-compaction branch from c361947 to 5353024 Compare September 24, 2026 12:24

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All clear

Posted via Macroscope — UI Consistency

@macroscopeapp

This comment has been minimized.

@juliusmarminge
juliusmarminge force-pushed the t3code/codex-turn-mapping branch 3 times, most recently from fe4f6ad to 87c67bd Compare September 25, 2026 05:55
@saphid
saphid force-pushed the fix/v2-queue-during-compaction branch 2 times, most recently from 91516fc to 84baf27 Compare September 26, 2026 23:18
github-actions Bot and others added 5 commits September 27, 2026 10:58
…mpaction

messages is non-optional on the thread projection, so drop the optional
chain and give the CommandPolicy test fixture a messages array instead.
Mobile now folds isCompacting into canSteerActiveTurn so its send label
and dispatch mode queue behind a compaction run, matching web.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@saphid
saphid force-pushed the fix/v2-queue-during-compaction branch from 8b755c2 to e4fd9d0 Compare September 27, 2026 01:06

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

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants