Skip to content

feat: add active-turn message delivery controls - #5396

Closed
ClapFy wants to merge 9 commits into
pingdotgg:mainfrom
ClapFy:agent/active-turn-message-controls
Closed

feat: add active-turn message delivery controls#5396
ClapFy wants to merge 9 commits into
pingdotgg:mainfrom
ClapFy:agent/active-turn-message-controls

Conversation

@ClapFy

@ClapFy ClapFy commented Aug 5, 2026

Copy link
Copy Markdown

What Changed

Adds a Messages while working setting to web and mobile with two delivery behaviors:

  • Steer (the default) sends the message into the active turn.
  • Queue stores follow-ups per thread and sends them FIFO, one turn at a time, after the current turn finishes.

The running web composer now keeps Stop generation visible beside the mode-specific send action. Mobile uses the same preference and snapshots it onto each outbox entry so existing queued work keeps its intended delivery behavior.

Codex sessions now use turn/steer when a turn is active, matching the steering behavior already supported by the other provider adapters. A successful steer reaffirms the running turn to orchestration so no stale pending-turn projection is left behind.

Why

While an agent was working, the composer only exposed Stop and did not let users choose whether a follow-up should affect the current turn or wait for the next one. That made rapid follow-ups awkward and left queue semantics inconsistent between web and mobile.

Delivery safeguards

  • Queues are durable per thread and preserve FIFO order without an artificial message cap.
  • Stable message and command IDs make retries idempotent.
  • Only one dispatcher may own a queued web message at a time.
  • The next queued message pauses after a non-interruption send failure and requires an explicit retry.
  • Mobile retains its bounded exponential retry delay and sends only the head of each thread queue.
  • Queue drain waits for a ready, connected thread, preventing overlapping turns and token-burning retry loops.

Verification

  • vp test run packages/contracts/src/settings.test.ts apps/mobile/src/state/thread-outbox.test.ts apps/server/src/provider/Layers/CodexSessionRuntime.test.ts apps/server/src/provider/Layers/CodexAdapter.test.ts apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts apps/web/src/webThreadOutbox.test.ts apps/web/src/components/settings/settingsSearch.test.ts — 137 tests passed
  • vp lint across all 23 changed TypeScript files with --report-unused-disable-directives
  • vp fmt across all 23 changed TypeScript files
  • Package type checks passed for contracts, web, mobile, and server
  • Isolated T3 Code web environment:
    • Queue mode drained two follow-ups FIFO exactly once and cleared persisted outbox storage
    • Steer mode injected the follow-up into the active Codex turn exactly once
    • Running composer exposed Stop beside Queue/Steer
  • T3 Code Dev on iOS Simulator: Queue and Steer flows verified
  • git diff --check

Model: GPT-5.6 Sol | Harness: Codex in T3 Code

Note

Add active-turn message delivery controls with 'steer' and 'queue' behavior

  • Introduces ActiveTurnMessageBehavior ('steer' or 'queue', default 'steer') to ClientSettings and mobile preferences, controlling how messages are handled when a thread is actively running.
  • When behavior is 'steer', sendTurn in the Codex session runtime attempts turn/steer against the active running turn before falling back to turn/start; concurrent calls are serialized via a semaphore.
  • On web, messages that cannot be sent immediately are queued into a per-thread browser outbox (webThreadOutbox), with a new WebThreadOutboxDrain component draining the queue in the background, synchronizing thread settings before dispatch.
  • On mobile, the outbox schema advances to version 4 to persist activeTurnMessageBehavior per queued message; shouldDeferConfirmedThreadOutboxDelivery re-checks deferral after an initial 'send' decision.
  • Composer UIs on both platforms show 'Steer active turn' or 'Queue message' labels, a queued-message count row, and a Retry button on pause; settings panels expose a 'Messages while working' selector.
  • Risk: the web outbox falls back to an in-memory store when localStorage is unavailable, meaning queued messages may not survive page reloads in that case.

Macroscope summarized 3d84cee.


Note

Medium Risk
Touches turn delivery, durable outbox state, and Codex steering/reconciliation paths across web, mobile, and server; cross-tab localStorage sync and race handling add complexity but are covered by tests.

Overview
Adds Messages while working (steer vs queue, default steer) to shared settings, web General settings, and mobile preferences so users choose whether follow-ups inject into the active turn or wait in a per-thread FIFO.

Web enqueues when queue mode applies (or the thread already has queued work), via a new localStorage-backed per-thread outbox and a root-level WebThreadOutboxDrain that delivers one message at a time, pauses on definitive failures, and supports retry. The running composer keeps Stop beside send, with labels Steer active turn / Queue message; failed sends restore draft content without overwriting text typed after send via restoreComposerContent.

Mobile mirrors the setting, waits for settled preferences before send (awaitActiveTurnMessageBehavior), snapshots behavior on each outbox entry (schema v4), and only drains steer deliveries while the thread is actually running and steerable.

Server (Codex) sendTurn tries turn/steer when a turn is running (semaphore-serialized), falls back to turn/start on rejection, maps session/error, and synthesizes turn/started so orchestration does not leave stale pending-turn rows; projection tests cover steer reconciliation.

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

@coderabbitai

coderabbitai Bot commented Aug 5, 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: ac86a13d-ec76-4645-90ea-a069ce49ce97

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:XL 500-999 changed lines (additions + deletions). labels Aug 5, 2026
Comment thread apps/mobile/src/state/use-thread-composer-state.ts
Comment thread apps/mobile/src/state/use-thread-outbox-drain.ts
Comment thread apps/web/src/webThreadOutbox.ts
@ClapFy
ClapFy marked this pull request as ready for review August 5, 2026 10:43
Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/server/src/provider/Layers/CodexSessionRuntime.ts
Comment thread apps/web/src/webThreadOutbox.ts
@macroscopeapp

macroscopeapp Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. New feature introducing active-turn message delivery controls with 'steer' and 'queue' modes. Adds significant new state management (web outbox store), server protocol changes (turn/steer API), and cross-platform behavior changes. Multiple open review comments identify potential bugs including outbox deadlock and image persistence loss.

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

@chrisdeeming

Copy link
Copy Markdown
Contributor

@ClapFy this is a great start are you in a position to continue working on this until the AI reviews are clean?

Some additional feedback:

  • 🟠 starting is treated as steerable, but codex only steers once running. concurrent sends can issue a second turn/start. mobile server
  • 🟠 a turn completing between the runtime snapshot and turn/steer loses the already-persisted prompt as a session error. code
  • 🟠 simultaneous cross-tab queue writes can overwrite each other and silently drop a prompt. the merge and full-key write are not atomic. code
  • 🟠 web defaults to steer before settings hydration. a user configured for queue can accidentally steer immediately after reload. settings send
  • 🟠 mobile and web snapshot drafts after editable awaits. content typed after tapping send can be consumed or cleared. mobile web
  • 🟠 web only drains the currently mounted thread. navigating away leaves queued messages unsent until that thread is revisited.
  • 🟠 failed-message pause state is memory-only. reload or another tab automatically retries despite the explicit retry contract. code

@ClapFy
ClapFy force-pushed the agent/active-turn-message-controls branch from 56e5d17 to 508098f Compare August 8, 2026 12:14
@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Aug 8, 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.

Effect service conventions review: one finding in apps/server/src/provider/Layers/CodexSessionRuntime.ts (tagged-failure recovery should use Effect.catchTags). Everything else in scope (new contracts schema, mobile preference atoms, web outbox store) looked consistent with the conventions.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/provider/Layers/CodexSessionRuntime.ts Outdated
Comment thread apps/web/src/webThreadOutbox.ts
Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/web/src/components/WebThreadOutboxDrain.tsx
Comment thread apps/server/src/provider/Layers/CodexSessionRuntime.ts Outdated
Comment thread apps/web/src/components/WebThreadOutboxDrain.tsx
Comment thread apps/web/src/webThreadOutbox.ts
Comment thread apps/server/src/provider/Layers/CodexSessionRuntime.ts
Comment thread apps/server/src/provider/Layers/CodexSessionRuntime.ts
@ClapFy

ClapFy commented Aug 8, 2026

Copy link
Copy Markdown
Author

@chrisdeeming i fixed the issues but the ai checks are stick on the "Pending" status.

@chrisdeeming

Copy link
Copy Markdown
Contributor

@ClapFy it looks like the functionality from this PR is actually covered by a future rewrite of the T3 Code orchestrator.

See #2829 for that work and #4245 which implemented the same feature as this PR but closed in favour of waiting for the orchestrator v2 to land.

@ClapFy

ClapFy commented Aug 9, 2026

Copy link
Copy Markdown
Author

@ClapFy it looks like the functionality from this PR is actually covered by a future rewrite of the T3 Code orchestrator.

See #2829 for that work and #4245 which implemented the same feature as this PR but closed in favour of waiting for the orchestrator v2 to land.

The PR also contains a fix for a chat bug with Codex where you sometimes just can't stop the agent

Comment on lines +280 to +288
export type ComposerDraftContentSnapshot = Pick<
ComposerThreadDraftState,
| "prompt"
| "images"
| "terminalContexts"
| "elementContexts"
| "previewAnnotations"
| "reviewComments"
>;

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 src/composerDraftStore.ts:280

ComposerDraftContentSnapshot includes images but omits nonPersistedImageIds and persistedAttachments. When a send or attachment-preparation fails, the snapshot is restored via restoreComposerContent, which repopulates the images array but leaves nonPersistedImageIds and persistedAttachments empty. Those restored image objects are therefore not staged for persistence, so a page reload before retry drops the images while retaining the rest of the draft. Consider including nonPersistedImageIds and persistedAttachments in the Pick so the tracking arrays are preserved alongside the image payloads.

Suggested change
export type ComposerDraftContentSnapshot = Pick<
ComposerThreadDraftState,
| "prompt"
| "images"
| "terminalContexts"
| "elementContexts"
| "previewAnnotations"
| "reviewComments"
>;
export type ComposerDraftContentSnapshot = Pick<
ComposerThreadDraftState,
| "prompt"
| "images"
| "nonPersistedImageIds"
| "persistedAttachments"
| "terminalContexts"
| "elementContexts"
| "previewAnnotations"
| "reviewComments"
>;
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/composerDraftStore.ts around lines 280-288:

`ComposerDraftContentSnapshot` includes `images` but omits `nonPersistedImageIds` and `persistedAttachments`. When a send or attachment-preparation fails, the snapshot is restored via `restoreComposerContent`, which repopulates the `images` array but leaves `nonPersistedImageIds` and `persistedAttachments` empty. Those restored image objects are therefore not staged for persistence, so a page reload before retry drops the images while retaining the rest of the draft. Consider including `nonPersistedImageIds` and `persistedAttachments` in the `Pick` so the tracking arrays are preserved alongside the image payloads.

@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 finding: a documented invariant from the recent Codex follow-up interrupt fix was dropped while sendTurn was restructured. See the inline comment.

Posted via Macroscope — Effect Service Conventions

Comment on lines +1902 to +1906
yield* updateSession(sessionRef, {
status: "running",
activeTurnId: turnId,
...(normalizedModel ? { model: normalizedModel } : {}),
});

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.

The turn/start fallback now overwrites activeTurnId unconditionally, dropping the invariant (and its comment) added in "stop Codex threads with queued follow-ups": when a steer is rejected with CodexAppServerRequestError while a turn is still running, this path queues a follow-up turn and replaces the id that turn/interrupt accepts, so stopping the thread breaks again. Consider restoring the guarded update and the comment.

Suggested change
yield* updateSession(sessionRef, {
status: "running",
activeTurnId: turnId,
...(normalizedModel ? { model: normalizedModel } : {}),
});
yield* updateSession(sessionRef, (session) => ({
status: "running",
// Codex accepts follow-ups while the current turn is still
// running. The response contains the queued turn id, but
// turn/interrupt only accepts the id that is active now.
activeTurnId: session.activeTurnId ?? turnId,
...(normalizedModel ? { model: normalizedModel } : {}),
}));

Posted via Macroscope — Effect Service Conventions

@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 1 potential issue.

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 3d84cee. Configure here.

input.threadStarting ||
(input.activeTurnMessageBehavior === "queue" &&
(input.phase === "running" || input.isSendBusy)))
);

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.

Queued messages stuck after errors

High Severity

shouldDrainWebThreadOutbox only allows null, ready, or running steer. After a turn ends in error (and similarly interrupted / stopped / idle), queued heads never drain. Because shouldQueueWebThreadMessage forces every new send into the FIFO whenever the queue is non-empty, follow-ups cannot recover the session either, so the outbox deadlocks with no Retry affordance.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3d84cee. Configure here.

gitctor pushed a commit to gitctor/t3code that referenced this pull request Aug 12, 2026
Adapt upstream pingdotgg#5396 onto the crew-suite composer and settings surfaces.

Co-authored-by: ClapFy <clapfygames@gmail.com>

Model: GPT-5.6-Sol via Codex CLI (detached)
@t3dotgg

t3dotgg commented Aug 23, 2026

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

Closing this PR after an automated pass over open pull requests. A trusted reviewer said the planned orchestration rewrite already covers this feature.

@t3dotgg t3dotgg closed this Aug 23, 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.

3 participants