Skip to content

feat(orchestration): persist run-next messages server-side across providers - #7240

Open
DavidIlie wants to merge 4 commits into
pingdotgg:mainfrom
DavidIlie:feat/server-thread-message-queue
Open

feat(orchestration): persist run-next messages server-side across providers#7240
DavidIlie wants to merge 4 commits into
pingdotgg:mainfrom
DavidIlie:feat/server-thread-message-queue

Conversation

@DavidIlie

@DavidIlie DavidIlie commented Aug 16, 2026

Copy link
Copy Markdown

What?

Adds a mobile Steer active turns setting. When it is off, follow-up messages are stored on the T3 server and run after the active turn finishes instead of waiting in the mobile app.

Queued messages are visible and cancellable from both mobile and web, and the existing immediate steering behavior remains the default.

Why?

Mobile's local outbox previously could not hand a follow-up to the server until the current turn stopped. The app had to remain open long enough to observe that stop and send the next message, so backgrounding or disconnecting the phone could leave the follow-up stranded locally.

How?

The turn-start contract now distinguishes immediate delivery from after-current delivery. The server persists queued turn intent in the event log and a dedicated projection before the provider boundary, so the behavior applies uniformly to Codex, Claude Code, Cursor, Grok, and OpenCode.

The provider reactor dispatches the oldest queued message when the thread becomes ready. The projection tracks queued and handoff states: pre-handoff work resumes after a server restart, while a restart during the external provider handoff is reported as interrupted instead of replayed because provider CLIs do not share a durable idempotency key with orchestration. That avoids executing the same coding instruction twice.

Command reads always retain durable queued messages even when ordinary message history is windowed. Shell snapshots expose queue presence for settle and snooze decisions, and reverting a thread cancels both the queue record and its visible message.

Mobile waits for its saved preference before accepting a send, snapshots the selected delivery mode into its offline outbox, and sends online messages to the server immediately. Both mobile and web project the durable queue state and expose cancellation before provider handoff.

Verification

  • 414 focused tests across contracts, shell/detail reducers, mobile outbox/preferences, web rendering, decider, migration, command/shell queries, projection/revert behavior, provider recovery, and relay awareness
  • pnpm --filter @t3tools/contracts typecheck
  • pnpm --filter @t3tools/client-runtime typecheck
  • pnpm --filter t3 typecheck
  • pnpm --filter @t3tools/mobile typecheck
  • pnpm --filter @t3tools/web typecheck
  • isolated backend startup against migration 41 and a queued-message projection fixture
  • changed-file formatting, targeted lint, and git diff --check

The in-app preview authenticated and navigated to the isolated fixture, but its screenshot/evaluate broker timed out repeatedly, so no visual artifact is attached.

Generated with GPT-5.6 Sol via the Codex harness.

Note

Persist queued turn messages server-side with lifecycle management and cancellation UI

  • Adds a deliveryMode: 'after-current' option to thread.turn.start so messages sent during an active session are queued server-side instead of starting a new turn immediately.
  • Introduces a durable projection_thread_turn_queue table (migration 041) and a ProjectionQueuedTurnRepository to persist queued turns with queuedhandoff → deleted status transitions.
  • The orchestration projection pipeline tracks queued message state: marking messages deliveryState='queued' on enqueue, clearing it on dispatch, and deleting the message on cancellation.
  • On reactor startup, any turn stuck in handoff is treated as ambiguous and transitions the thread session to error with an activity recorded, preventing duplicate dispatch.
  • Exposes a cancelQueuedTurn command on both web and mobile UIs; queued user messages show a 'Queued' label and a Cancel button in the message feed.
  • Adds a steerActiveTurns preference on mobile (default on); when off, messages sent during active work use after-current delivery mode.
  • canSettle and hasQueuedTurnStart now block thread settling when hasQueuedTurns is true on the shell snapshot.
  • Risk: threads with a queued turn in handoff at server restart will be set to error state and require the user to retry.

Macroscope summarized 6632d44.


Note

High Risk
Touches orchestration turn delivery, durable queue/handoff recovery, and provider dispatch—incorrect replay or dispatch could double-run agent work or strand messages.

Overview
Introduces deliveryMode: 'after-current' on thread.turn.start so follow-up user messages are persisted on the server (event log + projection_thread_turn_queue) instead of starting the provider immediately. The decider emits thread.turn-queued; dispatch/cancel paths add thread.queued-turn-dispatched and thread.queued-turn-cancelled, with messages tagged deliveryState: 'queued' and shells exposing hasQueuedTurns.

ProviderCommandReactor dispatches the oldest queued turn when a thread session is idle, recovers pre-handoff rows after restart, and surfaces ambiguous mid-handoff state as session error rather than replaying to providers.

Mobile adds Settings → Steer active turns (off → after-current + outbox deliveryMode); web and mobile show Queued / Cancel on queued bubbles via cancelQueuedTurn. Command reads always include queued messages; settle/snooze/revert logic treats queued work as blocking.

Reviewed by Cursor Bugbot for commit 6632d44. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 77925c33-a0c0-4a41-9415-c28a7df6659c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Aug 16, 2026

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

One consistency finding in the new web queued-message affordance in apps/web/src/components/chat/MessagesTimeline.tsx. See the inline comment.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/MessagesTimeline.tsx Outdated
Comment thread apps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
Comment thread apps/mobile/src/state/use-thread-composer-state.ts Outdated
command,
threadId: command.threadId,
});
const message = thread.messages.find((entry) => entry.id === command.messageId);

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.

🟠 High orchestration/decider.ts:1085

thread.queued-turn.dispatch permanently blocks FIFO processing when the oldest queued message has been evicted from thread.messages: the invariant fails, the durable queue row remains queued, and every subsequent dispatch selects the same row. Validate and remove the turn using the durable queue record rather than the capped message snapshot; thread.queued-turn.cancel needs the same treatment for evicted rows.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/decider.ts around line 1085:

`thread.queued-turn.dispatch` permanently blocks FIFO processing when the oldest queued message has been evicted from `thread.messages`: the invariant fails, the durable queue row remains `queued`, and every subsequent dispatch selects the same row. Validate and remove the turn using the durable queue record rather than the capped message snapshot; `thread.queued-turn.cancel` needs the same treatment for evicted rows.

@DavidIlie DavidIlie Aug 16, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 64c33af. The command read model now hydrates every message whose projected delivery_state is queued while still omitting ordinary message history. Dispatch/cancel invariants therefore retain durable queue entries regardless of the UI detail window, and the query behavior has focused coverage.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

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

Reviewed the new Effect service introduced by this PR (ProjectionQueuedTurnRepository) and the touched service consumers. Two convention violations in the new service module layout; the rest of the changes (namespace imports, yield* Foo.Foo dependency acquisition in ProviderCommandReactor/ProjectionPipeline, no new runtimes or errors) look consistent with the conventions.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/persistence/Services/ProjectionQueuedTurns.ts Outdated
Comment thread apps/server/src/persistence/Layers/ProjectionQueuedTurns.ts Outdated

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8e01fce. Configure here.

Comment thread apps/server/src/orchestration/Layers/ProjectionPipeline.ts
Comment thread apps/server/src/orchestration/decider.ts
@macroscopeapp

macroscopeapp Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR introduces a significant new feature for durable server-side message queuing with new database schema, event types, commands, and UI components. New capabilities of this scope warrant human review. An unresolved High severity finding about potential queue blocking also requires attention.

You can customize Macroscope's approvability policy. Learn more.

Comment thread apps/server/src/persistence/Services/ProjectionQueuedTurns.ts Outdated
Comment thread apps/server/src/persistence/Layers/ProjectionQueuedTurns.ts Outdated
Comment thread apps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
command,
threadId: command.threadId,
});
const message = thread.messages.find((entry) => entry.id === command.messageId);

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment thread apps/mobile/src/state/use-thread-composer-state.ts Outdated
Comment thread apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
@DavidIlie DavidIlie changed the title feat(orchestration): persist run-next messages on the server feat(orchestration): persist run-next messages server-side across providers Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant