Skip to content

fix(orchestration): deliver late Steer as the next message - #528

Merged
lukemaj merged 1 commit into
mainfrom
fix/526-late-steer-sends
Oct 19, 2026
Merged

lukemaj merged 1 commit into
mainfrom
fix/526-late-steer-sends

Conversation

@lukemaj

@lukemaj lukemaj commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

What: A Steer arriving after the run ends is delivered once as an ordinary next message on mobile and web/desktop.
Why: #388 preserved target purpose intent in the mobile outbox, and shared server admission rejected it before delivery policy could start the next message.
So what: Independent review passed on 5d339575f73577c88a48e20fd09789478c39fdb8; CI is still running, and the developer owns merge approval.

Closes #526.

At server admission, a Steer with no blocking run follows ordinary message admission and uses the current selection when its original purpose intent targeted the ended run. Both current server-resolved sends and older explicit steer_active commands use the same path. Active steering, Restart, explicit purpose declarations, and receipt deduplication keep their existing behavior. Mobile receives success through its existing outbox cleanup, so it neither restores the draft nor displays the rejection. Web and desktop use the same target intent and shared server path, so this also aligns their in-flight race.

The prior idle-Steer rejection assertion was replaced because it protected the behavior #526 explicitly changes. The new test first captures a pending Steer, ends its run, then submits and replays it, asserting one message, one new run, one provider-start effect, ordinary turn_start intent and selection provenance. It covers web/mobile and both command formats. A separate idle case preserves Restart rejection. The unchanged active-Steer test and steering completion suite protect running work.

Proof

Commands below use this worktree's node_modules/.bin/vp:

  • vp test run apps/server/src/orchestration-v2/ExecutionPurpose.integration.test.ts -t 'late web/mobile Steer': failed before the production change with PurposeAdmissionError: There is no active run to steer or restart. (target_missing).
  • vp test run apps/server/src/orchestration-v2/ExecutionPurpose.integration.test.ts: 13 passed after the change.
  • vp test run apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts apps/server/src/orchestration-v2/CommandPolicy.test.ts: 72 passed.
  • vp test run apps/mobile/src/state/thread-outbox.test.ts apps/mobile/src/state/use-thread-outbox-drain.test.ts packages/client-runtime/src/operations/commands.test.ts: 102 passed.
  • cd apps/server && ../../node_modules/.bin/tsc-rs --noEmit: exit 0, no errors; Effect suggestions only.
  • vp lint apps/server/src/orchestration-v2/Orchestrator.ts apps/server/src/orchestration-v2/ExecutionPurpose.integration.test.ts apps/mobile/src/state/use-thread-composer-state.ts: exit 0; warnings on unchanged code only.
  • vp fmt --check on those three files and docs/map.md: passed. git diff --check: passed.

No repo-wide checks, simulators, browsers, apps or live data were used. No visual layout changes. No package version bump, per repository policy. Website/SEO impact: none, this changes shared message admission only. Independent review passed on the exact PR head: findings, with review/independent=success posted by lukemaj. CI is still running.

Upstream first: inspected upstream main 1451e7cb23034d29774338608368a15d8845df36's mobile outbox and orchestrator and searched open PRs/Issues for Steer. Upstream lacks this fork-only purpose rejection; no matching upstream fix found. Details and starting-state evidence: #526 (comment).

Elon record

Requirements and who asked: #526 and the user require late Steer to send once without draft restoration/error, unchanged active steering, cross-client alignment, red/green proof, scoped tests/typecheck and one PR against main.
Deleted: Rejection of a Steer with no running work; no client retry, new transport, Spec, version bump or app launch.
Bottleneck: Shared server purpose admission rejected the message before the existing delivery fallback.
Checked myself: #526, relevant #388 diff, mobile outbox/composer, shared client commands, web/desktop send path, upstream sources/searches, focused failing-before test, passing scoped suites and server typecheck.

Implemented by gpt-6.1-sol at medium effort through Codex in T3 Code.

@github-actions github-actions Bot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Oct 19, 2026
@lukemaj

lukemaj commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

What: Independent review found no blocking findings on 5d339575f73577c88a48e20fd09789478c39fdb8.
Why: Late Steer falls through ordinary admission under the thread lock, with current selection replacing only a late target intent, while active steering and Restart retain their existing paths.
So what: The implementation passes this review; the parent can continue delivery, and merge still needs the developer's approval.

Reviewed the full four-file diff against 39cd468f458accd327d5e18d7616be4aa1ba8e62, #526 and its acceptance criteria, repository instructions, and AgentsMD verification/code-review procedures. The live PR head and base still match those SHAs.

Cleared paths:

  • Orchestrator.ts:5261-5292: fallback requires no blocking run and an explicit Steer intent or legacy steer_active dispatch. With blocking work present, the existing target admission and dispatch policy remain in force. Idle Restart still rejects.
  • Orchestrator.ts:5348-5386: only late target intent becomes the selection resolved inside the same dispatch lock. Explicit reference, unscoped, selection, inherited, and legacy omitted intents retain their resolver behavior.
  • Orchestrator.ts:5497-5516 and the ordinary admission path: late Steer creates an ordinary message/run and provider-start effect instead of trying to steer an ended run.
  • Replay/idempotency: the persisted message keeps the original purposeIntent; accepted receipt replay checks that original intent before returning the original admission result, without creating another run/effect. The regression replays while the newly admitted run is blocking, which also guards against reclassifying a replay as active steering.
  • Reachable clients: web and desktop's shared ChatView supplies target intent for Steer; shared client commands preserve it and send server-resolved delivery intent. Mobile persists the target intent through the outbox and removes an acknowledged delivery through the existing success path, so this fallback avoids draft restoration and rejection UI. Legacy explicit steer_active is covered as well.
  • Regression assertions distinguish one persisted message, one next run, ordinary turn_start, selection-B provenance after an A-to-B selection change, and one provider-start outbox effect. The removed assertion protected the idle-Steer rejection that fix(mobile): Steer after the run ended sends the message instead of rejecting it #526 explicitly changes. The map edit is scoped to fix(mobile): Steer after the run ended sends the message instead of rejecting it #526.

Proof reused from the PR and authorized review brief: failing-before target_missing; 13 execution-purpose tests, 72 steering/restart/policy tests, and 102 mobile outbox/shared-command tests passed; server typecheck exit 0; scoped lint/format passed. I inspected the relevant test sources and reachable code, and did not rerun tests. No implementation writes, checkout changes, app/browser/simulator launches, or repo-wide checks were performed. This is a scoped code-review verdict, not a claim that all CI has completed or live UI verification occurred.

Reviewer: gpt-6.1-sol through the Codex harness in T3 Code, independent of the implementation author.

@lukemaj
lukemaj marked this pull request as ready for review October 19, 2026 12:13
@lukemaj

lukemaj commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Independent review: PASS on 5d339575f73577c88a48e20fd09789478c39fdb8.

No actionable findings. In Orchestrator.ts:5261-5289, late Steer is recognized only when no blocking run remains; while work is active, it stays a target operation and the existing active run validation and dispatch policy apply. A late target intent is rewritten to the current purpose selection inside the serialized dispatch path (Orchestrator.ts:5348-5382), and dispatch is made immediate at 5497-5505; this adds no I/O to the decision. Restart remains a target operation and still fails without active work. The older steer_active command form is handled too.

The shared client command path sends deliveryIntent for server-resolved web/desktop messages (packages/client-runtime/src/operations/commands.ts:783-786), so web and desktop reach the same server behavior as mobile. The regression test covers current and legacy command forms for web/mobile, asserts one message, one run and one provider-start effect after replay, and uses no sleeps (ExecutionPurpose.integration.test.ts:160-234). The PR records that this focused test failed before the production change with target_missing; I inspected that proof report but did not rerun tests.

@github-actions

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 20.9 KiB 20.9 KiB 0 B (0.0%) 29.3 KiB ✅
Codex Live turn messages 2 2 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.2 KiB 21.2 KiB 0 B (0.0%) 29.3 KiB ✅
Claude Live turn messages 2 2 0 (0.0%) 8 ✅

Baseline: 39cd468 · PR result: 5d33957 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 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.

fix(mobile): Steer after the run ended sends the message instead of rejecting it

1 participant