fix(server): retain scheduled dispatch refusal reasons#32115
Problem
When an orchestrator refuses a bound scheduled task's fresh-session send, the task records only Failed to dispatch orchestration command message.dispatch (...). The actionable reason is present in the typed error's direct cause but is lost before lastRunError is persisted.
Related to the diagnostic symptom in #32100. This does not fix its background-work lifecycle problem or close that issue.
Change
After squashing the dispatch result's Effect cause, recognize OrchestratorDispatchError and format its direct string cause with the existing userFacingDispatchErrorMessage. Keep the existing outer-message fallback for missing, empty, generic, or non-string causes. The releaseStuckRun caller retains the unchanged formatter.
The boundary is specific to scheduled sends. The helper accepts a nongeneric direct string; it is not an allowlist or sanitizer. Unknown nested causes, including SQL details and cyclic objects, are not traversed. New-thread launch failures remain behind their existing wrapper.
Scope and approval
This is a small, focused repair of an obvious diagnostic loss: the service already receives a source-defined refusal, and the existing persisted error field drops it. It uses the focused bug-fix exception in CONTRIBUTING.md. No schedule state, bookkeeping, queue, retry, cancellation, session release, admission guard, contract, MCP schema, or client behavior changes.
Overlap checked against current main 6577c6b61851 and exact open heads of #23131 (017184de74c1) and #31366 (a037820fa42b): those PRs own terminal-outcome accounting and retain this formatter loss. This preserves #29889's stack-free diagnostics and follows the existing dispatch formatter precedent in #21027.
Verification
- Extended the existing fresh-session
ScheduledTaskService.test.tscase. Before the source change it failed at the intended assertion: expected the background-work refusal, received the generic dispatch wrapper. Failed status, retained binding, run count and next-run assertions passed first. After the fix, the returned task, direct SQLite read and a newly constructed service all retain the reason. A later successful dispatch clears it. - Added opaque-failure controls at the same service layer for defects, strings, interruption, missing/empty/generic wrapper causes, cyclic causes, nested Error text and private SQL detail. These fallback controls enter through the defect channel; the principal refusal regression exercises an actual typed failure. The existing webhook completion-write-failure test also checks that
releaseStuckRunstill records its safe outer error. vp test run apps/server/src/scheduledTasks/ScheduledTaskService.test.ts apps/server/src/scheduledTasks/ScheduledTaskService.webhook.test.ts apps/server/src/orchestration-v2/UserFacingErrors.test.ts: 73 tests passed.- Final
vp run check:changed: format, lint, both knip steps, GraphQL, affected typechecks (3 packages), and 88 tests in 5 files passed. - Independent Codex review of the exact diff found no blocking correctness or privacy findings for the bounded scheduled-send path.
Runtime evidence: macOS, Node 26.13.0, two separate synthetic file-backed SQLite databases, actual ScheduledTaskService, ThreadManagementService, orchestrator, event/projection persistence and effect worker. The repository's ProviderReplayHarness.layerWithRegistry used the test adapter pattern from ProviderSwitch.integration.test.ts, with Claude capabilities and a session-wide background-work probe returning true after the root completed. No provider CLI or paid inference ran.
- Create a thread and complete its first test-provider turn while its session's background probe remains true.
- Persist an enabled interval task bound to that thread with
threadMode: "fresh_session", then call the realrunNow. - Drain the effect worker and inspect the returned task, SQL row and new service readback. Compare against the unchanged main service, with identical production dependencies.
| 1 | main 6577c6b61851 this change |
| 2 | root run completed completed |
| 3 | live session background probe true true |
| 4 | lastRunStatus failed failed |
| 5 | returned dispatch threadId null null |
| 6 | persisted task binding retained retained |
| 7 | runCount 1 1 |
| 8 | nextRunAt completion + interval completion + interval |
| 9 | provider turn count 1 1 |
| 10 | session close count during send 0 0 |
| 11 | fresh-service / SQL readback matches returned task matches returned task |
| 12 | |
| 13 | before lastRunError: |
| 14 | Failed to dispatch orchestration command message.dispatch |
| 15 | (scheduled-task:task:scheduled-refusal-runtime:1793986433691:manual). |
| 16 | |
| 17 | after lastRunError: |
| 18 | The current session is still running background agents or commands, and a |
| 19 | fresh session would end them. Wait for them to finish, or press Stop, then |
| 20 | send the message again. |
The main comparison imported the exact main service source with only its relative imports relocated. The patched run imported the worktree service. Assertions also verified that provider sessions and root runs were unchanged; synchronization used worker draining and persisted events.
Not checked: a real Claude process, the reporter's indefinite lifecycle failure, or a UI capture. This proves diagnostic propagation through the real server services with a deterministic test provider.
Model and harness: GPT-6-based Codex; independent Codex review.
Checks
Added 1 commit
- fix(server): retain scheduled dispatch refusal reasons45fa589
Approvability
Verdict: Approved at 45fa589
Macroscope's review found this PR approvable — Scheduled-task failures now preserve readable orchestration refusal reasons instead of generic dispatch errors. The change is limited to failure reporting, with nested defects retaining existing handling to avoid exposing private details.
You can add or adjust custom eligibility rules. Learn more.
No actionable comments were generated in the recent review.
Walkthrough
-
The scheduled task service now formats orchestrator dispatch errors before recording failed-run messages. Tests cover nonblank, whitespace-only, and nested
Errorcauses.Changes: Scheduled task error reporting
Layer / File(s) Summary Dispatch error mapping and run records
apps/server/src/scheduledTasks/ScheduledTaskService.ts,apps/server/src/scheduledTasks/ScheduledTaskService.test.tsThe service uses a mapped user-facing message for qualifying dispatch errors and falls back to the error message when no mapping is available. Other failures retain the existing error-message conversion. Tests check the recorded failure message and failed status for refusal cases. Estimated code review effort: 2 (Simple) | ~10 minutes
Change: Bug fix
Suggested reviewers:
juliusmarminge
Merge Risk: External image Minimal · up to 1ea9e
-
Scheduled task failures caused by a dispatch refusal now record the specific refusal reason instead of a generic wrapper message. The change is narrow, falls back to the previous message for opaque failures, and is covered by focused tests. No merge-blocking risk was found.
Pre-merge checks: Passed 4
-
Check name Status Explanation Title check Passed The title "fix(server): retain scheduled dispatch refusal reasons" matches the main change. The service now keeps the direct string cause of an OrchestratorDispatchError in the persisted lastRunError … Description check Passed The description follows the template. It has Problem, Change, Scope, and Verification sections, with the title-level context and a conventional-commit-compatible summary. The Problem section states th… Linked Issues check Passed Check skipped because no linked issues were found for this pull request. Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request. Full details: Title check
-
Explanation
The title "fix(server): retain scheduled dispatch refusal reasons" matches the main change. The service now keeps the direct string cause of an OrchestratorDispatchError in the persisted lastRunError for bound scheduled tasks, instead of the generic dispatch wrapper. It follows the conventional commit format and names the affected component.
Full details: Description check
-
Explanation
The description follows the template. It has Problem, Change, Scope, and Verification sections, with the title-level context and a conventional-commit-compatible summary. The Problem section states the expected and actual behavior and the affected path. The Change section explains the formatter update and the unchanged releaseStuckRun caller. The Scope section explains why this is a focused bug fix and links related PRs and issue
#32100, citing the CONTRIBUTING.md bug-fix exception. The Verification section gives specific test commands, observed before-and-after results, and a runtime comparison, and it states what was not checked. The template's optional agent/model line is present at the end. Minor gap: the Scope section does not link a dedicated issue that shows the bug; it references#32100as a related diagnostic symptom and explains the reasoning for skipping a new issue.
-
Finishing Touches
-
Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
External image Recent review info
-
External image Run configuration
-
- Source mode: Auto
- Repository: pingdotgg/t3code
- Branch: main
- File: .coderabbit.config.ts
- Commit: bdf6c6e
- Layers: Repository TS config
- Review profile: CHILL
- Plan: Advanced
- Run ID:
edd1d059-92cd-4bfc-86f8-b374e71aee32
External image Commits
-
Reviewing files that changed from the base of the PR and between 45fa58992207812d240c9e430dc5891716283751 and 1ea9ec2f12f8c4084c4b41cf04ca188f821d9079.
External image Files selected for processing: 2
-
apps/server/src/scheduledTasks/ScheduledTaskService.test.tsapps/server/src/scheduledTasks/ScheduledTaskService.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
External image Recent review info
-
External image Run configuration
-
- Source mode: Auto
- Repository: pingdotgg/t3code
- Branch: main
- File: .coderabbit.config.ts
- Commit: bdf6c6e
- Layers: Repository TS config
- Review profile: CHILL
- Plan: Advanced
- Run ID:
edd1d059-92cd-4bfc-86f8-b374e71aee32
External image Commits
-
Reviewing files that changed from the base of the PR and between 45fa58992207812d240c9e430dc5891716283751 and 1ea9ec2f12f8c4084c4b41cf04ca188f821d9079.
External image Files selected for processing: 2
-
apps/server/src/scheduledTasks/ScheduledTaskService.test.tsapps/server/src/scheduledTasks/ScheduledTaskService.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
-
Added 2 commits
Evidence
Created demo / Claude idle thread on claudeAgent / claude-haiku-4-5; its first turn failed with provider 400. In Settings → Scheduled tasks, created Goal pause check targeting that continuing conversation, prompt /goal pause, interval 15 minutes, then selected Run now. Both runs used fresh databases, Chromium, 1280×800, light theme.
Proven: base displays the generic dispatch wrapper; head displays the actionable Claude refusal. scenarios:prove exited 0.
| Before | After |
|---|---|
| bdf6c6ec89 | 1ea9ec2f12 |
| Before: generic dispatch wrapper | After: actionable Claude refusal |
| Before recording | After recording |
Not shown:
- The separate chat-composer control was not repeated; this comparison captured the scheduled-task refusal on both commits.
- The original background-work scenario and blank/non-string fallback cases were not exercised.