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.
…5309) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- Schema.UnknownFromJsonString -> Schema.fromJsonString(Schema.Unknown) - SchemaIssue.InvalidValue single-argument form in checkpointDiff - McpServerClient requires protocolVersion Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Required for follow-up requests by the 2025-06-18 MCP HTTP transport that effect beta.103 enforces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The thread-panel mapping change replaced the compact className on the Run and Add controls with the isPanel conditional and dropped the non-panel icon-compact classes that the responsive test asserts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the deleted v1 ProviderCommandReactor title coverage onto the v2 service: marker arming/clearing via thread.metadata.update, superseded requestId no-ops, digest-driven regeneration, the "New thread" and unchanged-title fallbacks, generation failure, and missing initial messages, plus unit tests for formatThreadTitleContext. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hoist inline Schema compiles to module scope, drop unused imports/vars, stabilize react-markdown component identities via a module-scope factory, remove useless spreads, and use data-derived keys for release-note bullets. No behavior changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The codex resume test was the only replay scenario without a runtimePolicyOverride, so its checkpoint scope cwd fell back to process.cwd() and baseline capture ran real git over the entire checkout. Locally the capture short-circuits on checkpoint refs left behind by earlier runs; on a fresh CI checkout it is a cold multi-second capture that outlives the scenario wait budget, failing await_thread_idle while the run is still mid-checkpoint. Point the fixture's turn/start frames at the <workspace> placeholder and checkpoint a throwaway git workspace like every other replay test. Scenario waits are also wall-clock-bounded now: the iteration budget counts event-loop turns, which burn at full speed while git/fixture IO is in flight, so exhaustion additionally requires a 60s real-time deadline to pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r's scope (#5406) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- Keep the git action control disabled when the branch is up to date - Omit open PR menu entries and remove their link-opening behavior - Update logic tests for the new states
…sPinned Main owns migration numbering: 036_ProjectionThreadsPinned landed on main, so the v2 migrations shift from 036-044 to 037-045. Release path runs all of main's migrations first, then the v2 stack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Port thread pinning (#5312) into the orchestration-v2 command pipeline: thread.pin/unpin commands, thread.pinned/unpinned events, pinnedAt on the v2 thread state and projected shells, promotion semantics (pin clears settle/snooze, settle clears pin) matching the v1 decider, and client pin/unpin operations in the v2 dispatch style. - Port the regenerated-title context anchoring (#5365) into ThreadTitleRegenerationService: pin the first user message ahead of the retained tail when the digest is truncated. - Re-apply the right-panel controls positioning from #5260 to the v2 ChatView title bar controls. - Repair merge artifacts: committed conflict markers in BranchToolbar, duplicate capability keys, duplicate CommandPalette import, v1 turn naming in DiffPanel's focus-refresh effect, onSend signature merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Match progress button spacing and single-line height to static git actions
rerere replayed stale resolutions during the rebase and committed nested conflict markers in several files. Restore the branch-intended v2 shapes and re-graft main's compatible additions (pending-card opacity comments, theme-editor keybinding test, mobile scroll re-arm effects from #5566). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eysetIndex Main owns migration numbering: 037_ProjectionTurnsKeysetIndex landed on main (#5493), so the v2 migrations shift from 037-045 to 038-046. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Native subagent observability (#5219), wired per its spec's v2 merge plan: - getWorkflowScript RPC re-homed onto the v2 WS surface (contracts, rpc group, ws handler, auth scope, client atom). - AgentsPanel fed by the spec's mapper swap: projectedSubagentsToRuntime maps orchestration-v2 subagent entities into the panel model; deriveAgentPanelModel's v2Projection leg is now live and the v1 fold never runs. Agents surface wired into ChatView + RightPanelTabs. Other ports and reconciliations: - Shell reconnect-loop fix (#5561) ported into the v2 shell sync (same-session resubscribes resume from the in-memory cursor), with the cursor-resume regression test adapted to v2 fixtures. - Mobile end-follow latch (#5566) ported onto the v2 ThreadFeed. - Claude ede_diagnostic interrupt classification (#5557) ported into ClaudeAdapterV2 (aborted_tools/aborted_streaming => interrupted; CLI telemetry never becomes the failure banner). #5559 needs no v2 port (unknown system subtypes are already ignored). - Plan sidebar removed from the v2 ChatView/ChatComposer per main's plans-fold-into-chat rework (#5558); rightPanelStore stays at main's surface set. - SettingsPanels rebuilt as main's refactored version plus the branch's composer-context setting; sidebar snooze respects the time format (#4438 follow-through). - v1-only leftovers deleted: zombie v1 adapters/ingestion/tests the v2 rewrite removes, the v1-bound transfer-budget CI harness (#5350, needs a v2 rebuild), and main's v1 client pagination machinery (#5493 client side; the 037 keyset migration is kept — server-side v2 windowing is a follow-up). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Thread transfer impact
This comment will update automatically after the next completed run. |
| selectedThreadLastVisitedAt, | ||
| selectedThreadUpdatedAt, | ||
| visitThread, | ||
| ]); |
There was a problem hiding this comment.
Visit spam during active turns
Medium Severity
The mobile thread.visit command dispatches excessively. It triggers on every selectedThread.updatedAt change, which occurs rapidly during active streaming. Unlike the web, mobile lacks throttling for these frequent updates, leading to a flood of visit commands.
Reviewed by Cursor Bugbot for commit 0af2a6e. Configure here.
| runtime?.status !== "starting" && | ||
| runtime?.status !== "running" | ||
| ); | ||
| } |
There was a problem hiding this comment.
Archive ignores background work
Medium Severity
threadCanArchive only blocks preparing/starting/running (and queued with an active run). When pendingBackgroundTasks is nonempty, shell runtime is intentionally parked at idle, so archive is allowed while provider background work is still active. That can detach a session mid-background work without the interrupt guard.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 0af2a6e. Configure here.
The rebase kept the LegendList 3.3.3 upgrade and patch from #5449 and the mobile end-follow latch from #5566, but the v2 MessagesTimeline/ChatView still carried the branch's blunt any-gesture-breaks-follow listeners. Port main's #5566 web mechanics onto the v2 follow architecture: - resolveTimelineIsAtEnd measures the 40px follow re-arm band from real geometry (contentLength/scroll/scrollLength minus the composer inset), keeping the isNearEnd fallback for older state shapes. - Follow now breaks only on gestures that can actually leave the live edge: upward wheel with overflowing content, touch drags that exited the end band, scrollbar drags vs content clicks, and keyboard navigation (PageUp/Home/ArrowUp) — previously keyboard scrolling never broke follow and the next stream chunk yanked the view back down. - Listener attach retries across frames so a thread switch cannot mount the list without its opt-out listeners. Deliberately not ported: #5449's shouldRestorePosition disclosure anchoring and follow-gated maintainScrollAtEnd — the v2 timeline keeps maintainScrollAtEnd={false} with its own follow scrolls and anchor system; flipping that core is a separate change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Complete the #5449 architecture on the v2 timeline, following the LegendList author's direction to lean on the list's native mechanisms instead of app-side scroll layers: - maintainScrollAtEnd is enabled and owned by LegendList, gated off only while the user reads history (liveFollowEnabled), while a sent turn anchors near the top (anchoredEndSpace), or during the two-frame settle of a fold toggle. - maintainVisibleContentPosition compensates size changes natively ({data, size, shouldRestorePosition}); fold toggles anchor compensation to the toggled row via a disclosure anchor key, so the trigger stays under the pointer instead of the viewport chasing the end. - ChatView's hand-rolled streaming follow (double-rAF scrollToEnd on every data change) is gone; the app now only owns streaming adjustments during anchored-end-space mode, mirroring main. - timelineLiveFollowEnabled state mirrors the follow refs so the render-visible gate switches native follow off when a gesture breaks follow and back on when the viewport returns to the end band. Timeline tests updated to assert the native-ownership invariants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| if (usedTokens === null || usedTokens < 0) { | ||
| continue; | ||
| } | ||
|
|
||
| const maxTokens = asFiniteNumber(payload?.maxTokens); | ||
| const maxTokens = null; |
There was a problem hiding this comment.
🟡 Medium lib/contextWindow.ts:59
deriveLatestContextWindowSnapshot returns the latest compaction entry's afterTokenCount as usedTokens, but it iterates backwards and returns the first compaction it finds, so any user or assistant turns newer than that compaction are ignored. After subsequent turns grow the context, the meter still displays the stale post-compaction count. Consider extracting token counts from newer turn items instead of treating the most recent compaction as the current snapshot.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/lib/contextWindow.ts around line 59:
`deriveLatestContextWindowSnapshot` returns the latest compaction entry's `afterTokenCount` as `usedTokens`, but it iterates backwards and returns the *first* compaction it finds, so any user or assistant turns newer than that compaction are ignored. After subsequent turns grow the context, the meter still displays the stale post-compaction count. Consider extracting token counts from newer turn items instead of treating the most recent compaction as the current snapshot.
| selectedThreadKeyRef.current = selectedThreadKey; | ||
| }, [selectedThreadKey]); | ||
|
|
||
| const visitThread = useAtomCommand(threadEnvironment.visit, { reportFailure: false }); |
There was a problem hiding this comment.
🟡 Medium threads/ThreadDetailScreen.tsx:246
The visit effect dispatches visitThread on every projection update where selectedThread.updatedAt advances, because its dedup key ${selectedThreadKey}:${selectedThreadUpdatedAt} changes with each new watermark. During a streaming run the projection updates frequently, so this fires a server command once per update — potentially several per second for the entire stream — causing unnecessary network and server load. The web flow throttles mid-turn updates; the mobile path needs equivalent coalescing or trailing throttling.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/features/threads/ThreadDetailScreen.tsx around line 246:
The visit effect dispatches `visitThread` on every projection update where `selectedThread.updatedAt` advances, because its dedup key `${selectedThreadKey}:${selectedThreadUpdatedAt}` changes with each new watermark. During a streaming run the projection updates frequently, so this fires a server command once per update — potentially several per second for the entire stream — causing unnecessary network and server load. The web flow throttles mid-turn updates; the mobile path needs equivalent coalescing or trailing throttling.
There was a problem hiding this comment.
🟢 Low src/rightPanelStore.ts:255
migratePersistedRightPanelState dereferences each surface entry via (surface as { kind?: string }).kind without first checking that the entry is a non-null object. A persisted value like surfaces: [null] (e.g. from manual edits or corrupted storage) throws a TypeError during hydration, preventing the right-panel store from loading until storage is cleared. Consider guarding each entry with a surface && typeof surface === "object" check and returning [] otherwise so malformed entries are discarded instead of crashing.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/rightPanelStore.ts around line 255:
`migratePersistedRightPanelState` dereferences each `surface` entry via `(surface as { kind?: string }).kind` without first checking that the entry is a non-null object. A persisted value like `surfaces: [null]` (e.g. from manual edits or corrupted storage) throws a `TypeError` during hydration, preventing the right-panel store from loading until storage is cleared. Consider guarding each entry with a `surface && typeof surface === "object"` check and returning `[]` otherwise so malformed entries are discarded instead of crashing.
There was a problem hiding this comment.
🟡 Medium hooks/useThreadActionMenu.ts:81
The mark-unread case calls markThreadUnread from useUiStateStore, which only updates local browser state. It never dispatches the server-side unread tracking that the shared markThreadUnread action returned by useThreadActions performs, so the authoritative unread watermark is not updated and the change does not sync across devices. Use the markThreadUnread action from useThreadActions() instead of the Zustand store method.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/hooks/useThreadActionMenu.ts around line 81:
The `mark-unread` case calls `markThreadUnread` from `useUiStateStore`, which only updates local browser state. It never dispatches the server-side unread tracking that the shared `markThreadUnread` action returned by `useThreadActions` performs, so the authoritative unread watermark is not updated and the change does not sync across devices. Use the `markThreadUnread` action from `useThreadActions()` instead of the Zustand store method.
There was a problem hiding this comment.
🟡 Medium settings/SettingsPanels.tsx:536
changedSettingLabels reads themeHalves to decide whether to include "Theme mix", but themeHalves is omitted from the useMemo dependency array. Changing only a theme half therefore re-renders with a stale label list — changedSettingLabels stays empty and the Restore defaults button remains disabled until some unrelated dependency changes, so the user cannot trigger the reset. Add themeHalves to the dependency array.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/settings/SettingsPanels.tsx around line 536:
`changedSettingLabels` reads `themeHalves` to decide whether to include "Theme mix", but `themeHalves` is omitted from the `useMemo` dependency array. Changing only a theme half therefore re-renders with a stale label list — `changedSettingLabels` stays empty and the Restore defaults button remains disabled until some unrelated dependency changes, so the user cannot trigger the reset. Add `themeHalves` to the dependency array.


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 runtime with multi-provider adapters, scheduled tasks, and thread management
orchestration-v2runtime replacing the legacy orchestrator: event-sourced projections, typed domain events, new services for session/turn/checkpoint/fork/merge-back, and a composedOrchestrationV2LayerLivefor dependency injectionapps/server/src/orchestration-v2/Adapters/, each with capabilities declarations and replay test harnessesScheduledTaskServicewith interval and fixed-time schedule logic, CRUD operations, and a new/settings/scheduled-tasksroute in the web appdelegate_task,task_status,schedule_task, etc.) and worktree (t3_worktree_handoff,t3_worktree_status) registered inMcpHttpServerorchestration_eventsandorchestration_command_receiptstables.Macroscope summarized adf1c26.
Note
High Risk
Large orchestration rewrite touching auth-adjacent provider sessions, persistence migrations, and client protocol/cache compatibility; incorrect replay or projection semantics could corrupt thread state or drop user work.
Overview
This PR lands orchestration V2 as the live agent stack—provider adapters, projections, durable effects, and client cutover—while retiring the old thread-transfer PR comment workflow and tightening CI for native test fixtures.
Backend & platform (per branch scope): V2 runtime wiring for multiple provider instances (Codex, Claude, Cursor, ACP registry, etc.), replay/integration coverage for fork, merge-back, subagents, and steering; MCP toolkits and scheduled tasks with new migrations; shared orchestration cache schema v3 so web/mobile discard stale V1-shaped offline snapshots.
Mobile (visible in diff): Persistence and home/archive flows move to V2 shell/thread snapshots and
visibleTurnItems-driven feeds; runtime summaries replace V1 session fields for archive/stop rules; thread visit watermarks, queue controls, relationship banner, and expandable activity inspectors (checkpoint rollback, file links, structured payloads); approvals/user-input use runtime request IDs and live/stale response capability.CI/docs: Removes
.github/workflows/thread-transfer-report.ymland its publisher scripts; CI still emits transfer-budget artifacts but no longer upserts PR comments. Adds build-essential on the test runner so ACP process-tree compile tests do not soft-skip. Adds/updates orchestration V2 integration plans and user appearance doc link.Reviewed by Cursor Bugbot for commit adf1c26. Bugbot is set up for automated code reviews on this repo. Configure here.