Skip to content

feat(orchestrator): introduce new orchestrator - #2829

Open
juliusmarminge wants to merge 214 commits into
mainfrom
t3code/codex-turn-mapping
Open

feat(orchestrator): introduce new orchestrator#2829
juliusmarminge wants to merge 214 commits into
mainfrom
t3code/codex-turn-mapping

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

  • wire orchestration V2 provider adapter registry/factory flow for Codex and Claude provider instances
  • add Claude replay/query primitives, native fork/rollback fixtures, subagent fixture coverage, and provider replay harness updates
  • update debugger model/provider picker and improve user-facing orchestration errors

Validation

  • bun fmt
  • bun lint
  • bun typecheck
  • bun run test -- src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts -t claudeAgent
  • bun run test -- src/orchestration-v2/testkit/ClaudeReplayFixtures.integration.test.ts
  • bun run test -- src/orchestration-v2/testkit/ThreadFork.integration.test.ts -t Claude

Notes

  • Draft PR for review of current branch state. Codex all-provider replay still needs schema alignment with latest app-server behavior before it can be treated as a full-suite signal.

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

  • Adds a complete orchestration-v2 runtime replacing the legacy orchestrator: event-sourced projections, typed domain events, new services for session/turn/checkpoint/fork/merge-back, and a composed OrchestrationV2LayerLive for dependency injection
  • Implements provider adapters for Claude, Codex, Cursor, Grok, OpenCode, and ACP Registry under apps/server/src/orchestration-v2/Adapters/, each with capabilities declarations and replay test harnesses
  • Adds database migrations 038–046 creating V2 event log, projection tables (threads, runs, attempts, nodes, subagents, provider sessions), effect outbox, scheduled tasks, and a unified application event source that migrates legacy V2 events
  • Introduces a ScheduledTaskService with interval and fixed-time schedule logic, CRUD operations, and a new /settings/scheduled-tasks route in the web app
  • Adds MCP toolkits for orchestration (delegate_task, task_status, schedule_task, etc.) and worktree (t3_worktree_handoff, t3_worktree_status) registered in McpHttpServer
  • Updates web and mobile clients to consume V2 projections, shell snapshots, and runtime fields; adds thread details panel, queue management UI, and relationship graph
  • Risk: schema version bumped to 3 for cached shell/thread snapshots; old caches are discarded on load. Migrations are irreversible and alter shared orchestration_events and orchestration_command_receipts tables.

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.yml and 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.

@coderabbitai

coderabbitai Bot commented May 27, 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: c1ca340a-fbbd-406e-b826-77c2858933de

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/codex-turn-mapping

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels May 27, 2026
Comment thread apps/server/src/orchestration-v2/EventStore.ts Outdated
Comment on lines +116 to +120
return decodeTranscript({
...metadata,
entries,
});
});

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.

🟢 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.

Comment thread apps/server/src/orchestration-v2/ProviderAdapterRegistry.ts Outdated
Comment thread packages/client-runtime/src/wsRpcClient.ts Outdated
Comment thread apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/EventSink.ts
duncan4123 pushed a commit to duncan4123/t3code that referenced this pull request Jun 1, 2026
…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.
duncan4123 pushed a commit to duncan4123/t3code that referenced this pull request Jun 2, 2026
duncan4123 pushed a commit to duncan4123/t3code that referenced this pull request Jun 2, 2026
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.
duncan4123 pushed a commit to duncan4123/t3code that referenced this pull request Jun 2, 2026
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
@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

🚀 Expo continuous deployment is ready!

  • Project → t3-code
  • Platforms → android, ios
  • Scheme → t3code-preview
  🤖 Android 🍎 iOS
Fingerprint fe5a51f2e189da69dfc4c2cd458e6cfb5fdff2ea ae3bd597809dfd7771d0898f735d172973d4c1c8
Build Details Build Permalink
DetailsDistribution: INTERNAL
Build profile: preview:dev
Runtime version: fe5a51f2e189da69dfc4c2cd458e6cfb5fdff2ea
App version: 0.1.0
Git commit: 1d5a64460414f9a2c6ff4a5e4f977932228b7f1b
Build Permalink
DetailsDistribution: INTERNAL
Build profile: preview:dev
Runtime version: ae3bd597809dfd7771d0898f735d172973d4c1c8
App version: 0.1.0
Git commit: eea0dcae4150df8341606520c074dc651ae7c00a
Update Details Update Permalink
DetailsBranch: pr-2829
Runtime version: fe5a51f2e189da69dfc4c2cd458e6cfb5fdff2ea
Git commit: 1d5a64460414f9a2c6ff4a5e4f977932228b7f1b
Update Permalink
DetailsBranch: pr-2829
Runtime version: ae3bd597809dfd7771d0898f735d172973d4c1c8
Git commit: 1d5a64460414f9a2c6ff4a5e4f977932228b7f1b
Update QR Image Image

Comment thread apps/server/src/orchestration-v2/RunExecutionService.ts
Comment thread apps/server/src/ws.ts
Comment thread apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/RunExecutionService.ts
@juliusmarminge juliusmarminge changed the title WIP: wire orchestration v2 provider adapters feat(orchestrator): introduce new orchestrator Jun 14, 2026
Comment thread apps/server/src/orchestration-v2/RunExecutionService.ts
Comment on lines +109 to +128
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,
}),
);

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.

🟡 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).

Comment thread apps/server/src/orchestration-v2/Orchestrator.ts
Comment thread apps/web/src/routes/debug.orchestration-v2.tsx Outdated
Comment thread apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
@juliusmarminge
juliusmarminge force-pushed the t3code/codex-turn-mapping branch from 79031a1 to 4e68dcb Compare June 14, 2026 23:55
@juliusmarminge
juliusmarminge force-pushed the t3code/codex-turn-mapping branch from 4e68dcb to c7539b9 Compare June 17, 2026 07:30
Comment thread apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/ProviderSessionManager.ts Outdated
Comment thread apps/server/src/orchestration-v2/Orchestrator.ts Outdated
Comment thread apps/server/src/mcp/OrchestratorMcpService.ts Outdated
Comment thread apps/server/src/ws.ts Outdated
function nativeThreadId(provider: ProviderKind, thread: OrchestrationV2ProviderThread): string {
const id = thread.nativeThreadRef?.nativeId;
if (id === null || id === undefined || id.trim().length === 0) {
throw new ProviderAdapterProtocolError({

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.

🟡 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 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.

🚀 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.

maria-rcks and others added 24 commits August 7, 2026 13:28
…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>
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for adf1c26.

This comment will update automatically after the next completed run.

selectedThreadLastVisitedAt,
selectedThreadUpdatedAt,
visitThread,
]);

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.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0af2a6e. Configure here.

runtime?.status !== "starting" &&
runtime?.status !== "running"
);
}

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.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0af2a6e. Configure here.

juliusmarminge and others added 2 commits August 7, 2026 14:21
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;

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.

🟡 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 });

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.

🟡 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.

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.

🟢 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.

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.

🟡 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.

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.

🟡 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.

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