feat(orchestrator): introduce new orchestrator - #2829
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
| return decodeTranscript({ | ||
| ...metadata, | ||
| entries, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
🟢 Low testkit/ReplayTranscriptNdjson.ts:116
The call to decodeTranscript at line 116 invokes Schema.decodeUnknownSync, which throws on validation failure. Since this isn't wrapped in Effect.try, any validation error becomes an uncaught exception (defect) instead of a typed ProviderReplayNdjsonParseError. This breaks the function's declared error contract. Consider wrapping the call in Effect.try to catch the exception and convert it to the declared error type.
- return decodeTranscript({
- ...metadata,
- entries,
- });
+ return yield* Effect.try({
+ try: () =>
+ decodeTranscript({
+ ...metadata,
+ entries,
+ }),
+ catch: (cause) =>
+ new ProviderReplayNdjsonLineParseError({
+ lineNumber: lines.length,
+ line: "<transcript validation>",
+ cause,
+ }),
+ });🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/orchestration-v2/testkit/ReplayTranscriptNdjson.ts around lines 116-120:
The call to `decodeTranscript` at line 116 invokes `Schema.decodeUnknownSync`, which throws on validation failure. Since this isn't wrapped in `Effect.try`, any validation error becomes an uncaught exception (defect) instead of a typed `ProviderReplayNdjsonParseError`. This breaks the function's declared error contract. Consider wrapping the call in `Effect.try` to catch the exception and convert it to the declared error type.
Evidence trail:
apps/server/src/orchestration-v2/testkit/ReplayTranscriptNdjson.ts lines 50-53: `decodeTranscript = Schema.decodeUnknownSync(ProviderReplayTranscript)` — throws on failure.
Line 116-118: `return decodeTranscript({...metadata, entries})` — called directly inside Effect.gen without Effect.try wrapper.
Lines 55-68 (`parseReplayRecord`): same pattern but correctly wrapped in `Effect.try`.
Line 80: function declares error type `ProviderReplayNdjsonParseError`.
packages/contracts/src/orchestrationV2.ts lines 1561-1568: `ProviderReplayTranscript` schema with `TrimmedNonEmptyString` fields that can fail validation.
…der adapters (t3-29f.6) Assessment of upstream PR pingdotgg#2829 (pingdotgg/t3code) from juliusmarminge: WIP wire orchestration v2 provider adapters with Codex and Claude adapters, event sourcing, provider session management, and replay testkit. Relevance to target issues: - pingdotgg#2838 (session resume): HIGH — ProviderSessionManager persists session IDs and separates startSession/resumeSession operations - pingdotgg#2778 (subagent hang): MEDIUM — ProviderEventIngestor provides infrastructure to forward permission events, but UI plumbing not yet wired - pingdotgg#2886 (thread stuck working): HIGH — event-sourced projections replace mutable state flags, eliminating sticky "working" states The PR is a draft (34 commits, not merged). No OpenCode ACP adapter exists yet in v2 — OpenCode would need its own adapter wired into the ProviderAdapterRegistry. Recommend watching for merge and adding an OpenCode adapter post-merge.
…n v2 provider adapters)
The upstream PR pingdotgg#2829 added orchestrationV2 methods to the WsRpcClient interface. The test mock in service.threadSubscriptions.test.ts was missing the orchestrationV2 property, causing a typecheck failure: 'Property orchestrationV2 is missing in type...' Added orchestrationV2 mock with dispatchCommand, getThreadProjection, subscribeShell, and subscribeThread as vi.fn() stubs.
The upstream PR pingdotgg#2829 targets a newer Effect version than our fork's pinned effect@4.0.0-beta.73. Fixes: - Replace Random.nextUUIDv4 with Crypto.randomUUIDv4 (beta.73 API) - Fix deterministic Service tag keys to match fork convention (include file path segments; e.g. Adapters/ClaudeAdapterV2/...) - Replace Schema.decodeSync with Schema.decodeUnknownEffect inside Effect.gen generators (tsgo schemaSyncInEffect rule) - Replace inline Schema.encodeUnknownSync with module-level wrappers to avoid schemaSyncInEffect rule inside generators
|
🚀 Expo continuous deployment is ready!
|
| Effect.gen(function* () { | ||
| const threadId = payloadInput.threadId ?? input.threadId; | ||
| const eventId = yield* idAllocator.allocate.event({ | ||
| threadId, | ||
| providerSessionId: input.providerSessionId, | ||
| }); | ||
| const occurredAt = yield* DateTime.now; | ||
| return yield* Schema.decodeUnknownEffect(OrchestrationV2DomainEvent)( | ||
| compactUndefined({ | ||
| id: eventId, | ||
| type: payloadInput.type, | ||
| threadId, | ||
| runId: payloadInput.runId ?? input.runId, | ||
| nodeId: payloadInput.nodeId ?? input.nodeId, | ||
| provider: input.event.provider, | ||
| rawEventId: input.rawEventId, | ||
| occurredAt, | ||
| payload: payloadInput.payload, | ||
| }), | ||
| ); |
There was a problem hiding this comment.
🟡 Medium orchestration-v2/ProviderEventIngestor.ts:109
In makeDomainEvent, the ?? operator on lines 121 and 123 treats explicit null as equivalent to undefined, causing payloadInput.runId ?? input.runId to fall back to input.runId when payloadInput.runId is explicitly null. Since the type is readonly runId?: RunId | null, this means explicit null values from the caller (e.g., input.event.node.runId being null on line 172) are incorrectly overwritten instead of preserved. Consider using === undefined checks like lines 161-162 and 210-211, or use payloadInput.runId === undefined ? input.runId : payloadInput.runId.
const threadId = payloadInput.threadId ?? input.threadId;
- const runId = payloadInput.runId ?? input.runId;
- const nodeId = payloadInput.nodeId ?? input.nodeId;
+ const runId = payloadInput.runId === undefined ? input.runId : payloadInput.runId;
+ const nodeId = payloadInput.nodeId === undefined ? input.nodeId : payloadInput.nodeId;🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration-v2/ProviderEventIngestor.ts around lines 109-128:
In `makeDomainEvent`, the `??` operator on lines 121 and 123 treats explicit `null` as equivalent to `undefined`, causing `payloadInput.runId ?? input.runId` to fall back to `input.runId` when `payloadInput.runId` is explicitly `null`. Since the type is `readonly runId?: RunId | null`, this means explicit `null` values from the caller (e.g., `input.event.node.runId` being `null` on line 172) are incorrectly overwritten instead of preserved. Consider using `=== undefined` checks like lines 161-162 and 210-211, or use `payloadInput.runId === undefined ? input.runId : payloadInput.runId`.
Evidence trail:
apps/server/src/orchestration-v2/ProviderEventIngestor.ts lines 105-106 (payloadInput type with `RunId | null`), line 121 (`runId: payloadInput.runId ?? input.runId`), line 122 (`nodeId: payloadInput.nodeId ?? input.nodeId`), lines 161-162 and 210-211 (codebase uses `=== undefined` pattern elsewhere). packages/contracts/src/orchestrationV2.ts line 358 (`runId: Schema.NullOr(RunId)` on ExecutionNode - confirms null is a valid value), line 857 (`runId: Schema.optional(RunId)` on EventBase - domain event uses optional/undefined, not null). apps/server/src/orchestration-v2/ProviderEventIngestor.ts lines 80-81 (compactUndefined only strips undefined, not null).
79031a1 to
4e68dcb
Compare
4e68dcb to
c7539b9
Compare
| function nativeThreadId(provider: ProviderKind, thread: OrchestrationV2ProviderThread): string { | ||
| const id = thread.nativeThreadRef?.nativeId; | ||
| if (id === null || id === undefined || id.trim().length === 0) { | ||
| throw new ProviderAdapterProtocolError({ |
There was a problem hiding this comment.
🟡 Medium Adapters/AcpAdapterV2.ts:271
When nativeThreadId is called inside Effect.gen generators (e.g., lines 899, 1813), the thrown ProviderAdapterProtocolError becomes an untyped defect instead of a typed failure. This bypasses Effect.mapError and other typed error handlers, causing the error to propagate as an unexpected defect. Consider converting nativeThreadId to return Effect<string, ProviderAdapterProtocolError> and yielding it at each call site, or inlining the validation with yield* new ProviderAdapterProtocolError(...) so the failure is properly typed.
Also found in 1 other location(s)
apps/server/src/orchestration-v2/ThreadManagementService.ts:278
The statement
return yield* managementError(...)cannot work correctly becausemanagementError()returns aThreadManagementErrorinstance, not anEffect. Theyield*operator inEffect.genexpects an Effect value. This should bereturn yield* Effect.fail(managementError(...)). The correct pattern is demonstrated elsewhere in this file (lines 241-246) whereEffect.fail(managementError(...))is properly used. This same bug pattern repeats at lines 291, 333, 357, 374, 390, 406, and 427.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts around line 271:
When `nativeThreadId` is called inside `Effect.gen` generators (e.g., lines 899, 1813), the thrown `ProviderAdapterProtocolError` becomes an untyped defect instead of a typed failure. This bypasses `Effect.mapError` and other typed error handlers, causing the error to propagate as an unexpected defect. Consider converting `nativeThreadId` to return `Effect<string, ProviderAdapterProtocolError>` and yielding it at each call site, or inlining the validation with `yield* new ProviderAdapterProtocolError(...)` so the failure is properly typed.
Evidence trail:
1. nativeThreadId function with throw: apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts lines 268-277
2. Call site inside Effect.gen: apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts line 899
3. Call site inside Effect.gen: apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts line 1813
4. Correct yield* pattern for comparison: apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts line 1808
5. ProviderAdapterProtocolError class definition: apps/server/src/orchestration-v2/ProviderAdapter.ts lines 316-327
6. Effect.gen implementation delegating to fromIteratorUnsafe: https://github.com/Effect-TS/effect-smol packages/effect/src/internal/effect.ts lines 1104-1125
7. fromIteratorUnsafe calling iter.next() without try/catch: https://github.com/Effect-TS/effect-smol packages/effect/src/internal/effect.ts lines 1285-1307
8. FiberImpl.runLoop catch block converting thrown errors to exitDie: https://github.com/Effect-TS/effect-smol packages/effect/src/internal/effect.ts lines 646-650
9. die = exitDie producing Effect<never> (untyped): https://github.com/Effect-TS/effect-smol packages/effect/src/internal/effect.ts line 947
Also found in 1 other location(s):
- apps/server/src/orchestration-v2/ThreadManagementService.ts:278 -- The statement `return yield* managementError(...)` cannot work correctly because `managementError()` returns a `ThreadManagementError` instance, not an `Effect`. The `yield*` operator in `Effect.gen` expects an Effect value. This should be `return yield* Effect.fail(managementError(...))`. The correct pattern is demonstrated elsewhere in this file (lines 241-246) where `Effect.fail(managementError(...))` is properly used. This same bug pattern repeats at lines 291, 333, 357, 374, 390, 406, and 427.
Cancel pending SDK requests before aborting the native session. Preserve per-admission cancellation state and treat stopped initial prompts as interruption instead of provider failure. Finding: R14 prompt cancellation Model: GPT-5.6 Sol via Codex
Keep the original cursor owner through ancestor traversal and preserve the history budget across empty intermediate forks. Verify exact paged history against the complete nested projection. Finding: P01 nested lineage Model: GPT-5.6 Sol via Codex
Retain pending admission after transient status failures and use one generation-owned retry worker. Ignore stale timers and duplicate evidence so older prompts cannot finish newer steering. Finding: R14 status reconciliation Model: GPT-5.6 Sol via Codex
Keep hidden local and inherited rows from consuming history pages. Preserve stop-request dependencies, source-run cutoffs, and imported history while loading related metadata from the selected cohort and using indexed watermark lookups. Finding: P01 bounded history visibility Model: GPT-5.6 Sol via Codex
| The provider process for this request is no longer available. Interrupt or restart the run | ||
| to continue. | ||
| </Text> | ||
| ) : null} |
There was a problem hiding this comment.
Dead-provider approvals stay tappable
Medium Severity
PendingApprovalCard computes disabled from responseCapability !== "live" but the action buttons still key only off respondingApprovalId. When the provider process is gone, Allow/Decline stay tappable and can dispatch a doomed respond call, unlike PendingUserInputCard which actually gates on canRespond.
Reviewed by Cursor Bugbot for commit 373612e. Configure here.
There was a problem hiding this comment.
🟠 High
Queued-message editing exposes the generic attachment picker, but the edit save request serializes only composerImages and never includes composerFiles. As a result, adding a file such as a PDF is silently dropped (or leaves a file-only edit with nothing to save). Disable generic file attachments in this mode or include and upload composerFiles in the queued-edit save request.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/chat/ChatComposer.tsx around line 4214:
Queued-message editing exposes the generic attachment picker, but the edit save request serializes only `composerImages` and never includes `composerFiles`. As a result, adding a file such as a PDF is silently dropped (or leaves a file-only edit with nothing to save). Disable generic file attachments in this mode or include and upload `composerFiles` in the queued-edit save request.
There was a problem hiding this comment.
🟡 Medium
On mobile, editing a queued message during another running turn leaves the collapsed primary Save action disabled, so the edit cannot be saved without expanding the composer. collapsedComposerPrimaryActionDisabled still treats every phase === "running" state as blocked; apply the same !isEditingQueuedMessage exception used by the footer action.
- phase === "running" ||
+ (phase === "running" && !isEditingQueuedMessage) ||🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/chat/ChatComposer.tsx around line 1522:
On mobile, editing a queued message during another running turn leaves the collapsed primary Save action disabled, so the edit cannot be saved without expanding the composer. `collapsedComposerPrimaryActionDisabled` still treats every `phase === "running"` state as blocked; apply the same `!isEditingQueuedMessage` exception used by the footer action.
| </ComposerBanner.Dock> | ||
| ) : null} | ||
| </> | ||
| )} |
There was a problem hiding this comment.
Stash menu hides blocking composer UI
Medium Severity
Opening the in-flow stash attachment unmounts the rest of the composer chrome, including pending user-input questions, plan follow-up, urgent banners, and the interrupt/activity row. Approvals stay visible via isComposerApprovalState, but pending questions do not, even though ⌘S and the inline stash badge still open stash while those questions are up. Previously the stash overlay left that UI in place.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit cef1028. Configure here.
| <ComposerBannerStack key={activeThreadId} className="relative z-0" items={bannerStackItems} /> | ||
| {!activityStackItem && | ||
| (inlineTasksBadge || (standaloneActivityStatus && !showShoulderTabs)) ? ( | ||
| {isStashMenuOpen && |
There was a problem hiding this comment.
Stash state survives thread and collapse
Medium Severity
isStashMenuOpen now replaces the banner stack, but it is not cleared on activeThreadId change, mobile collapse, or when an approval/pending-input drawer appears. Tasks already reset on thread change. A leftover open flag hides the new thread’s banners, or brings stash back the moment an approval resolves or the mobile composer re-expands.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit cef1028. Configure here.
| const promote = useAtomCommand(threadEnvironment.promoteQueuedRun); | ||
| const cancel = useAtomCommand(threadEnvironment.cancelQueuedRun); | ||
| const [expanded, setExpanded] = useState(true); | ||
| const queueListId = useId(); |
There was a problem hiding this comment.
Queue collapse persists across threads
Low Severity
expanded is local useState(true) and QueuedRunsControl is not remounted or reset when threadId changes. Collapsing the queue on one thread leaves the next thread’s list hidden behind the header, even though that thread’s queued runs are different.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit cef1028. Configure here.
|
Fixed in 84ef39d. Stash is a separate tab again. The queue and activity share a column that stops 4px before Stash. Opening Stash shows its own drawer and preserves the queue's expanded or collapsed state when closed. Waiting threads keep the Stash tab too.
Stash open: Checked the queue, waiting state, expanded tasks, and Stash at desktop and narrow widths. Targeted lint, web typecheck, and all six existing queue/stash tests passed. No new tests or stylesheets. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix is ON, but a cloud agent failed to start.
Reviewed by Cursor Bugbot for commit 84ef39d. Configure here.
| data-chat-composer-collapsed-controls="true" | ||
| <ComposerBanner.Dock> | ||
| <ComposerBanner.Column className={showStashMenu ? "hidden" : undefined}> | ||
| {props.queuedRunsControl} |
There was a problem hiding this comment.
Stash menu hides queued runs
Medium Severity
Opening the stash menu now applies hidden to the whole banner column, including queuedRunsControl. That control previously rendered above the stash/banner swap, so queued messages stayed visible and actionable. Users can no longer see or manage the queue while browsing stash.
Reviewed by Cursor Bugbot for commit 84ef39d. Configure here.
|
Queued message rows now use the shared banner list, row, icon, content, count, action, and dismiss components in 6b73b65. Their separate padding, divider lines, and button size overrides are gone.
The edit control was incorrectly treated as a peeking notice, which split the queue card and overlapped its rows. In bc5ac65, it stays inside the queue panel with Cancel visible, including when the queue is collapsed or its last message is being edited. Working stays attached and Stash stays separate.
Verified mouse and keyboard reordering, editing and saving, cancellation, steering, removal, and narrow layouts in the mock environment. Web typecheck and the four existing queue tests passed. No new tests or stylesheets. |
|
Queued-message editing is fixed in a794ced. These screenshots replace the earlier edit-row examples.
Verified in the shared preview: typing and Cancel leave the saved text unchanged; Save updates the message in place. Queue and composer dimensions stay unchanged when entering and leaving edit mode at desktop and narrow widths. Targeted lint, web typecheck, and 21 existing focused tests pass. No new tests were added. This covers the shared web/desktop composer and narrow web layouts. Provider adapters, wire contracts, connection modes, and the native mobile client are unchanged. The composer documentation now describes saving with the checkmark. Before: After: |
| @@ -245,8 +247,12 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ | |||
| : isPreparingWorktree | |||
| ? "Preparing worktree" | |||
| : isSendBusy | |||
| ? "Sending" | |||
| : "Send message" | |||
| ? isEditingQueuedMessage | |||
There was a problem hiding this comment.
🟠 High chat/ComposerPrimaryActions.tsx:250
After clicking Save on a queued-message edit, the button remains enabled and continues showing the checkmark while editQueuedRun is still pending, so text typed during that request is cleared when the save succeeds. The saving indicator is gated by isSendBusy, but this path does not call beginLocalDispatch; track the queued-edit save request separately and disable the editor/actions until it completes.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/chat/ComposerPrimaryActions.tsx around line 250:
After clicking Save on a queued-message edit, the button remains enabled and continues showing the checkmark while `editQueuedRun` is still pending, so text typed during that request is cleared when the save succeeds. The saving indicator is gated by `isSendBusy`, but this path does not call `beginLocalDispatch`; track the queued-edit save request separately and disable the editor/actions until it completes.
|
Updated the composer actions in a41ba60.
Verified click and keyboard dispatch, edit save/cancel, modifier release, and the narrow web layout in the mock preview. Targeted lint, the web typecheck, and 30 existing tests passed. No new tests or CSS files.
|
| @@ -4146,7 +4277,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |||
| isPreparingWorktree={isPreparingWorktree} | |||
| hasSendableContent={composerSendState.hasSendableContent} | |||
There was a problem hiding this comment.
🟡 Medium chat/ChatComposer.tsx:4278
ComposerPrimaryActions disables the queued-message save button when hasSendableContent is false, but that value only reflects the edit draft and omits editingQueuedAttachments. Therefore, deleting all text while retaining an existing attachment makes Update queued message unavailable even though onSend accepts attachment-only edits. Include the retained queued attachments in the value passed here.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/chat/ChatComposer.tsx around line 4278:
`ComposerPrimaryActions` disables the queued-message save button when `hasSendableContent` is false, but that value only reflects the edit draft and omits `editingQueuedAttachments`. Therefore, deleting all text while retaining an existing attachment makes `Update queued message` unavailable even though `onSend` accepts attachment-only edits. Include the retained queued attachments in the value passed here.
|
Fixed the tooltip wrapping on Mod in 330c44f. Changing the order of the shortcut text triggered the tooltip's content resize. The text now stays Verified repeated Mod press/release cycles at desktop and narrow widths. The tooltip stayed 26px high throughout, compared with 42px when the old text wrapped. Targeted lint and the web typecheck passed.
|
Bug: delegated-task completion wakes are silently starved after two deliveries per cohort — parent waits forever on the third waveTL;DR. Observed on the #7211 stack (V2 snapshot of 2026-08-28); everything cited below is The two contracts that collide
// Async delegations wake the parent on every child terminal; wait
// delegations deliver through the blocking tool call, so a wake is
// only needed if the parent settled first (timeout, disconnect).
completionWake: input.mode === "wait" ? "settled_only" : "always",
const settledDeliveryCount = cohort?.settledDeliveryCount ?? 0;
if (settledDeliveryCount >= 2) {
// A cohort permits one initial delivery and one successor. Keep the
// result pending and inspectable instead of recursively re-arming the
// parent for every child that finishes after that bounded handoff.
...state: "pending"..., offer: false…and the same Forensic timeline (real thread, from
|
| time (UTC) | event | cohort state |
|---|---|---|
| 01:01:52 | run 7 delegates t10, t11, t13, t15, t17 | — |
| 01:05:49 | t10 completes → delivery 1, wake run 8 starts | settled=0, delivery gen 1 |
| 01:05:56–01:07:01 | t17, t13, t15 complete during run 8 → "pending" |
|
| 01:07:11 | run 8 settles → sweep reserves delivery 2 = {t15, t17, t13} → wake run 9 starts 01:07:12 | settled=1, delivery gen 2 |
| 01:07:20 | t11 completes, 8s into run 9 → "pending" |
|
| 01:09:12 | run 9 settles → sweep: settledDeliveryCount → 2, pendingTaskIds = [t11], but canReserveFollowUp requires < 2 → refused |
settled=2, delivery null, t11 pending |
| 01:09–01:29 | parent idle; t11's result queryable but no wake exists or ever will | |
| 01:29:48 | human notices the stall and nudges; that run reads task_status → t11 finally acknowledged |
Final cohort row on run 7: settledDeliveryCount=2, nextGeneration=3, disposition=open, delivery=null — with a terminal, never-delivered task. Children delegated during later wake turns (t12 on run 8, t14 on run 9, t16 on run 10…) each started fresh cohorts with reset counters, so their wakes worked — which is exactly why moderate parallelism masks the bug until one dispatch wave is large enough to stagger across ≥3 waves.
(The queued-delivery merging worked beautifully, for the record — t13/t15/t17 amending the queued wake into one bundled message is the right behavior. The problem is only the lifetime cap.)
Suggested fixes (any of these)
- Make the cap a concurrency bound, not a lifetime bound. The recursion the comment fears is "one delivery in flight + one reserved successor" — that's already bounded without a lifetime counter. Reset
settledDeliveryCount(or simply drop it) whenever a sweep findspendingTaskIds.length > 0after the delivery drains; the wake-per-terminal contract then holds and the parent still never sees more than one queued wake at a time. - Sweep pendings on any parent-run settle. Extend
handleTerminalRunto arm a delivery for terminal app-owned tasks left"pending"/unarmed whenever the parent thread goes non-live. The existing generation +alreadyDispatcheddedupe inProviderContinuationService.currentDelegatedCompletionDeliveryalready guards against the duplicate-wake concern. - At minimum, don't be silent. If the cap stays, surface refused deliveries (system line in the thread: "N delegated results pending — read with task_status") and soften the tool description's "end the turn instead of polling," which is currently a trap.
Related smaller hole in the same family: for mode="wait" children (settled_only), planDelegatedCompletionDelivery returns with the task untouched (not even "pending") when the parent has a live run — that branch precedes the pending-marking branches — and the post-timeout wake-policy upgrade knowingly skips re-planning when the parent settled in between ("a missed wake is cheaper than a duplicate one"). Same silent-starvation class, just harder to hit because the blocking wait usually owns delivery.
Happy to provide the raw projection rows or test this against a patched build — we run this stack daily.
|
@juliusmarminge one addition I'd really like here: scheduled messages. I think this should be separate from normal queued messages. Queued messages mean “send after the current run finishes”; a scheduled message means “send this message at a specific date/time.” The important architectural requirement is that the schedule must be persisted and executed by the T3 server/environment that actually runs the agent, not by whichever web/mobile/desktop client created it. If I schedule a message from my laptop or phone against an agent running on another machine, I should be able to close/disconnect that client and the message should still fire at the selected time. In other words, the client should only create/edit/cancel the schedule. The host environment owns the durable record and dispatch timer. A simple UX could be a “Send later” action from the composer that creates a one-shot scheduled message for the current thread. It should be visible/cancellable/editable before it fires. I noticed this PR already adds server-persisted scheduled tasks, but those schedules appear recurring ( |
|
|
Implemented T3 action summaries in c01ff20. The same groups now describe orchestration work instead of reducing it to “used tools”:
Mixed groups show two specific categories and a count of the remaining actions. Commands, file changes, and orchestration changes take priority over reads and status checks. Failed calls do not inflate successful sends or creations; failure counts appear first so narrow layouts cannot truncate them away. A reported child failure is separate from failure of the status-check tool itself. Grouping boundaries, expansion, individual call details, and icons are unchanged. Native mobile layout is unchanged. Preview/worktree tools and unknown tools retain the generic fallback. Verified 53 focused tests across the timeline, T3 counting, and tool presentation, plus scoped lint and web/shared/client-runtime typechecks. Checked the scripted preview at desktop and narrow widths, including expanding back to the original calls and their input/output.
|


























Summary
Validation
Notes
Closes
Verified against the branch with code/commit evidence.
High confidence
Closes #4952
Closes #4873
Closes #4775
Closes #4795
Closes #4710
Closes #4668
Closes #4619
Closes #4584
Closes #4561
Closes #4713
Closes #4198
Closes #4452
Closes #3797
Closes #4232
Closes #3666
Closes #3580
Closes #2785
Closes #2789
Closes #3138
Closes #1404
Closes #231
Closes #216
Medium confidence (under review)
Closes #4568
Closes #4766
Closes #4495
Closes #4456
Closes #4399
Closes #3744
Closes #2921
Closes #3624
Closes #3149
Closes #2336
Closes #538
Closes #2173
Closes #2065
Note
Introduce orchestration V2 system with event sourcing, provider adapters, and thread management
EventStore,ProjectionStore,EffectOutbox,EffectWorker,EventSink, and supporting services (ThreadLaunchService,ThreadManagementService,ProviderSessionManager,CheckpointCapture/RollbackService,RuntimeRequestService,ContextHandoffService,ProviderRuntimeRecoveryService,LegacyV1ThreadImporter)EnvironmentApireplaces theorchestrationnamespace withorchestrationV2; WS methods and RPC scopes now target V2 endpoints;ModelSelectiondecodes both legacy provider-based and canonical instance-based forms; corrupt orchestration cache entries are deleted on decode failure;CursorSettingsremoves obsoletebinaryPath/apiEndpointfields;diffPanelStorepersisted version bumps to 2 and usesRunIdinstead ofTurnIdMacroscope summarized c01ff20.
Note
High Risk
Mobile cache schema and thread UX now depend on V2 projections and runtime semantics; mis-synced decode or archive/stop guards could corrupt offline cache or interrupt live work. Removing the thread-transfer PR reporter reduces CI visibility into transfer regressions.
Overview
This slice of the orchestration V2 work removes the automated “thread transfer impact” PR comment pipeline (trusted publisher script, tests, and
workflow_runworkflow) while CI can still uploadthread-transfer-resultsartifacts to the job summary path.Mobile is brought onto the shared V2 client stack: SQLite cache uses
ORCHESTRATION_CACHE_SCHEMA_VERSIONand stored shell/thread projection shapes, runtime wiring swaps in bounded thread snapshot loading and history controls, and thread/archive/review/approval flows move from V1session/TurnIdconcepts to runtime status,RuntimeRequestId, and checkpoint summaries derived from projections. Thread detail gains visit watermarking, a relationships banner, queue control, richer pending-request handling when the provider is dead, and a new activity inspector; archive guards usethreadCanArchiveso queued work can be discarded but live provider turns cannot.Smaller changes: CI installs
build-essentialso ACP process-tree fixtures compile instead of soft-skipping; Uniwind adds adaptive color tokens; brand mark assets are centralized; README links appearance docs; desktop tests assert user-data dir names; marketing copy updates the Cursor harness label.Reviewed by Cursor Bugbot for commit c01ff20. Bugbot is set up for automated code reviews on this repo. Configure here.