Skip to content

feat: orchestration core and Codex plumbing#103

Merged
juliusmarminge merged 77 commits intomainfrom
codething/648ca884
Mar 6, 2026
Merged

feat: orchestration core and Codex plumbing#103
juliusmarminge merged 77 commits intomainfrom
codething/648ca884

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Feb 27, 2026

Summary

This PR is now the stack base for the provider split.

It keeps the core orchestration and Codex-facing work from the original branch:

  • thread-scoped provider runtime and session flow
  • runtime mode and interaction mode orchestration
  • proposed plan persistence and user-input handling
  • Codex model-option plumbing through orchestration and the composer
  • the shared event, persistence, and UI changes needed to support that core flow

The provider-specific adapters have been split out into follow-up PRs:

  • Cursor: codething/648ca884-cursor
  • Claude Code: codething/648ca884-claude

Why this split

The original branch mixed three concerns together: core orchestration/runtime work, the Cursor adapter, and the Claude Code adapter. Splitting them makes the core behavior reviewable on its own and leaves provider-specific integration details in smaller follow-up PRs.

Validation

  • bun lint
  • bun typecheck
  • cd apps/server && bun run test -- --run src/provider/Layers/ProviderAdapterRegistry.test.ts
  • cd apps/web && bun run test -- --run src/session-logic.test.ts

Note

Switch orchestration and provider stack to threadId-based runtime v2 and add Codex plumbing with proposed plans, interaction/runtime modes, and workspace file write API

Replace session-scoped ids with threadId across orchestration, provider service, Codex adapter/manager, contracts, and tests; introduce Provider Runtime v2 event model (content.delta, item.*, request.*, user-input.*) with sequencing; persist and project runtimeMode, interactionMode, and proposedPlans; add provider capabilities and user-input responses; add activity sequence ordering; add WebSocket projects.writeFile route and snapshot/query/projection support; migrate migrations and repositories; update web app to provider-aware model selection, plan follow-up, pending user inputs, and proposed plan timeline; remove legacy checkpoint handling and session ids.

📍Where to Start

Start with the runtime v2 contract and id changes in packages/contracts/src/providerRuntime.ts (https://github.com/pingdotgg/t3code/pull/103/files#diff-448aaf2dfbb7547d852cc74c6bba5caea16c3c782a5f9cfbf43da71648ec61f7), then follow ingestion and routing in apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts (https://github.com/pingdotgg/t3code/pull/103/files#diff-22a6c9818b956463b848a73a5b3e787b44a144bd003b1cc25fa2b1b3110cdea7) and provider routing in apps/server/src/provider/Layers/ProviderService.ts (https://github.com/pingdotgg/t3code/pull/103/files#diff-b9b9384b78856ff56cff5dd0ccb106054fb5d2d1b7500fcd30d8938ebfec5775).

Macroscope summarized 3a5b3b6.


Note

High Risk
High risk because it rewires core provider session routing and runtime event shapes (sessionId->threadId, new canonical event types/payloads) and adds a new workspace file-write RPC, which can impact orchestration correctness and filesystem safety if any edge cases are missed.

Overview
Provider/orchestration plumbing is refactored to be threadId-centric: test harnesses, adapters, and the Codex app-server manager now key sessions by ThreadId (instead of provider session ids), and runtime fixtures/tests are updated to the v2 canonical ProviderRuntimeEvent model (e.g. content.delta, item.*, request.*, payload-driven turn.completed).

Codex runtime controls are expanded by threading runtimeMode/interactionMode through session start and turn/start, adding collaboration-mode developer instruction presets (plan vs default), supporting structured user-input request/response plumbing, and extending approval handling to include file-read approvals.

WebSocket API gains workspace writes via projects.writeFile, with server-side path resolution/validation plus new tests; server startup also tightens CLI boolean flag resolution, adds ServerLoggerLive, and logs a redacted/safe config. Ancillary cleanup removes outdated planning/spec docs and a favicon test script.

Written by Cursor Bugbot for commit 3a5b3b6. This will update automatically on new commits. Configure here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 27, 2026

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

Run ID: 6339add6-0dd3-49d9-aa9f-492551ab6816

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

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds multi-provider support: provider-scoped model configuration and helpers, nests provider startup options under providerOptions, introduces orchestration provider/runtime defaults, migrates session resumption from resumeThreadId to opaque resumeCursor, and wires a new ClaudeCode provider across server, integration, tests, and web UI.

Changes

Cohort / File(s) Summary
Model API (provider-scoped)
packages/contracts/src/model.ts, packages/contracts/src/model.test.ts
Introduce provider-partitioned constants and helpers (MODEL_OPTIONS_BY_PROVIDER, DEFAULT_MODEL_BY_PROVIDER, MODEL_SLUG_ALIASES_BY_PROVIDER, getModelOptions, getDefaultModel, resolveModelSlugForProvider) and update tests; preserve codex backward-compat aliases.
Provider contract & decoding
packages/contracts/src/provider.ts, packages/contracts/src/provider.test.ts, .plans/17-claude-code.md
Move provider-specific start fields under providerOptions (add CodexProviderStartOptions, ClaudeCodeProviderStartOptions, ProviderStartOptions), remove top-level resumeThreadId/codex fields, add provider-aware decoding tests.
Orchestration defaults & payloads
packages/contracts/src/orchestration.ts, packages/contracts/src/orchestration.test.ts
Add DEFAULT_PROVIDER_KIND, DEFAULT_PROVIDER_APPROVAL_POLICY, DEFAULT_PROVIDER_SANDBOX_MODE; make provider, approvalPolicy, sandboxMode optional with decoding defaults on session and thread start payloads; add decoders/tests.
Resume cursor migration (server & tests)
apps/server/src/codexAppServerManager.ts, apps/server/src/codexAppServerManager.test.ts, apps/server/src/provider/Layers/ProviderService.ts, apps/server/src/provider/Layers/ProviderService.test.ts, apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts
Replace resumeThreadId with opaque resumeCursor in inputs, session payloads, helpers and tests; add helpers to read/extract resumeCursor and conditionally include it in responses.
Provider session orchestration & reactor changes
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts, apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts, apps/server/src/orchestration/decider.ts, apps/server/src/orchestration/decider.projectScripts.test.ts
Thread/session flows updated to accept and propagate optional provider and resumeCursor; ensureSession/start/restart logic passes provider and resumeCursor; tests updated to assert provider and resumeCursor handling.
Provider adapter additions & registry
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts, apps/server/src/provider/Services/ClaudeCodeAdapter.ts, apps/server/src/provider/Layers/ProviderAdapterRegistry.ts, apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts, apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts
Add ClaudeCode adapter implementation, service binding and tests; register ClaudeCode alongside Codex in the adapter registry and update registry tests.
Integration harness & test harness changes
apps/server/integration/OrchestrationEngineHarness.integration.ts, apps/server/integration/TestProviderAdapter.integration.ts, apps/server/integration/orchestrationEngine.integration.test.ts, apps/server/integration/providerService.integration.test.ts
Make harnesses parameterizable by provider (codex
Server layers wiring
apps/server/src/serverLayers.ts, apps/server/src/provider/Layers/ProviderService.test.ts
Wire ClaudeCode adapter layer into server startup (merge adapter layers) and adapt provider-start option extraction tests.
Web UI & session logic
apps/web/src/components/ChatView.tsx, apps/web/src/session-logic.ts, apps/web/src/session-logic.test.ts
Add per-thread provider state and ProviderPicker UI, resolve models provider-aware using resolveModelSlugForProvider, add/export PROVIDER_OPTIONS and mark claudeCode available; update tests.
Various tests & small fixes
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts, apps/server/src/orchestration/decider.projectScripts.test.ts, other test files
Update numerous tests to include provider and resumeCursor shapes, adjust harness signatures and expectations for provider-aware behavior.

Sequence Diagram(s)

sequenceDiagram
  participant Decider as Decider
  participant Orchestration as OrchestrationService
  participant ProviderMgr as ProviderCommandReactor
  participant ProviderAdapter as ProviderAdapter

  Decider->>Orchestration: thread.turn.start (model?, provider?)
  Orchestration->>ProviderMgr: startProviderSession({ provider?, providerOptions?, resumeCursor? })
  ProviderMgr->>ProviderAdapter: listSessions()
  ProviderAdapter-->>ProviderMgr: [ProviderSession...] (each may include resumeCursor)
  ProviderMgr->>ProviderAdapter: startSession({ providerOptions, resumeCursor? })
  ProviderAdapter-->>ProviderMgr: ProviderSession (includes resumeCursor when present)
  ProviderMgr-->>Orchestration: sessionStarted (returns resumeCursor when defined)
  Orchestration-->>Decider: thread.turn-start-requested (includes provider and resumeCursor if present)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ⚠️ Warning The PR title 'feat: orchestration core and Codex plumbing' is misleading. The changeset primarily focuses on adding multi-provider support (introducing ClaudeCodeAdapter, provider-scoped models, and provider-aware session handling), not just 'Codex plumbing'. The title omits the main feature of adding Claude Code provider support. Update the title to reflect the primary change: 'feat: add multi-provider support with ClaudeCodeAdapter integration' or 'feat: introduce provider-aware orchestration and Claude Code provider'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/648ca884

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

@juliusmarminge juliusmarminge changed the title Add provider-aware model defaults and simplify dev runner behavior feat: clade code Feb 27, 2026
@juliusmarminge juliusmarminge force-pushed the codething/648ca884 branch 2 times, most recently from aaae1cf to 03beb30 Compare February 27, 2026 05:37
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/contracts/src/model.test.ts (1)

61-65: Consider relocating backward-compatibility tests to a separate describe block.

These tests verify getDefaultModel and getModelOptions, but are nested under the resolveModelSlug describe block. Consider moving them to a dedicated describe("backward compatibility", ...) or describe("getModelOptions and getDefaultModel", ...) for better organization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/contracts/src/model.test.ts` around lines 61 - 65, The three
assertions testing getDefaultModel and getModelOptions are misplaced inside the
resolveModelSlug describe block; extract them into a new describe block (e.g.,
describe("backward compatibility") or describe("getModelOptions and
getDefaultModel")) and place the it("keeps codex defaults for backward
compatibility", ...) test there so it’s grouped logically; ensure the test name
and assertions remain unchanged and that any shared setup/teardown used by
resolveModelSlug is still available or duplicated if needed.
packages/contracts/src/model.ts (1)

66-80: Note potential type unsoundness in normalizeModelSlug.

Line 79 casts any non-aliased trimmed string to ModelSlug, which could return an invalid slug string. While resolveModelSlug subsequently validates against MODEL_SLUG_SET_BY_PROVIDER, direct callers of normalizeModelSlug receive an unchecked ModelSlug.

If this is intentional (normalize = alias expansion only, resolve = validation), consider a brief doc comment clarifying that callers should use resolveModelSlug for validated slugs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/contracts/src/model.ts` around lines 66 - 80, normalizeModelSlug
currently expands aliases via MODEL_SLUG_ALIASES_BY_PROVIDER but casts any
non-aliased trimmed string to ModelSlug without validating against
MODEL_SLUG_SET_BY_PROVIDER; update normalizeModelSlug's function-level doc
comment to state it only performs alias expansion (and may return an unvalidated
ModelSlug) and explicitly instruct callers to use resolveModelSlug when a
validated slug is required, referencing normalizeModelSlug, resolveModelSlug,
MODEL_SLUG_ALIASES_BY_PROVIDER and MODEL_SLUG_SET_BY_PROVIDER so future readers
understand the validation contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/contracts/src/model.test.ts`:
- Around line 61-65: The three assertions testing getDefaultModel and
getModelOptions are misplaced inside the resolveModelSlug describe block;
extract them into a new describe block (e.g., describe("backward compatibility")
or describe("getModelOptions and getDefaultModel")) and place the it("keeps
codex defaults for backward compatibility", ...) test there so it’s grouped
logically; ensure the test name and assertions remain unchanged and that any
shared setup/teardown used by resolveModelSlug is still available or duplicated
if needed.

In `@packages/contracts/src/model.ts`:
- Around line 66-80: normalizeModelSlug currently expands aliases via
MODEL_SLUG_ALIASES_BY_PROVIDER but casts any non-aliased trimmed string to
ModelSlug without validating against MODEL_SLUG_SET_BY_PROVIDER; update
normalizeModelSlug's function-level doc comment to state it only performs alias
expansion (and may return an unvalidated ModelSlug) and explicitly instruct
callers to use resolveModelSlug when a validated slug is required, referencing
normalizeModelSlug, resolveModelSlug, MODEL_SLUG_ALIASES_BY_PROVIDER and
MODEL_SLUG_SET_BY_PROVIDER so future readers understand the validation contract.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 041acf1 and 03beb30.

📒 Files selected for processing (2)
  • packages/contracts/src/model.test.ts
  • packages/contracts/src/model.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/server/src/provider/Layers/ProviderService.ts (1)

262-272: ⚠️ Potential issue | 🟡 Minor

Acknowledge the fallback for active sessions, but clarify the limitation for stale sessions.

The code has a fallback path (lines 234-250): if resumeThreadId exists and the provider session is still active, recovery succeeds using the persisted resumeCursor from that session. However, sessions that are stale (no active provider session) and lack a persisted resumeCursor will fail at the guard on line 262.

If existing production data includes stale sessions with resumeThreadId but no resumeCursor, those sessions cannot recover. Verify this is intentional or plan a migration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ProviderService.ts` around lines 262 - 272,
The guard using hasResumeCursor causes stale sessions (those with a
resumeThreadId but no persisted resumeCursor) to fail recovery at the return in
ProviderService; update the recovery logic to handle this explicitly: if
resumeThreadId exists but resumeCursor is missing, either (A) attempt an
alternate recovery via the adapter (e.g., try a new adapter method or
adapter.startSession overload that accepts resumeThreadId) or (B) return a
clearer ValidationError that includes input.staleSessionId and an actionable
migration hint and emit telemetry/logging so ops can find affected records;
ensure changes touch the branch that checks hasResumeCursor and the call to
adapter.startSession, and add a comment noting that production data may require
a migration to populate resumeCursor for stale sessions.
🧹 Nitpick comments (4)
apps/server/src/orchestration/decider.projectScripts.test.ts (1)

153-155: Align provider/model in this test to provider-aware contracts.

Line 153 and Line 178 correctly add provider: "claudeCode", but Line 154/Line 179 still assert model: "gpt-5". That pairing can make this test semantically inconsistent with provider-aware model resolution. Prefer deriving the model from contracts for claudeCode (or use an explicit Claude slug) so the test remains stable as validation tightens.

Suggested patch
-import { CommandId, EventId, MessageId, ProjectId, ThreadId } from "@t3tools/contracts";
+import { CommandId, EventId, MessageId, ProjectId, ThreadId, getDefaultModel } from "@t3tools/contracts";
@@
 describe("decider project scripts", () => {
+  const claudeModel = getDefaultModel("claudeCode");
@@
-          model: "gpt-5",
+          model: claudeModel,
@@
-      model: "gpt-5",
+      model: claudeModel,

Also applies to: 178-180

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/orchestration/decider.projectScripts.test.ts` around lines
153 - 155, The test in decider.projectScripts.test.ts hardcodes model: "gpt-5"
while specifying provider: "claudeCode", making the assertion
provider-inconsistent; update the assertions that set model (the occurrences
currently using model: "gpt-5") to instead derive the expected model from the
provider-aware contract/resolver used by the code under test (or replace with an
explicit Claude model slug consistent with your provider mapping) so the test
asserts the provider-correct model for provider: "claudeCode".
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts (1)

161-164: Consider hoisting resolveResumeCursorForSession outside the closure.

This function is recreated on every ensureSessionForThread call. Since it only depends on providerService (already in scope), moving it outside would avoid unnecessary function allocations.

♻️ Suggested refactor
+const resolveResumeCursorForSession = (
+  providerService: typeof ProviderService.Service,
+  sessionId: ProviderSessionId
+) =>
+  providerService.listSessions().pipe(
+    Effect.map((sessions) => sessions.find((session) => session.sessionId === sessionId)?.resumeCursor),
+  );
+
 const ensureSessionForThread = Effect.fnUntraced(function* (
   threadId: ThreadId,
   createdAt: string,
   options?: { ... },
 ) {
   // ... existing code ...
-  const resolveResumeCursorForSession = (sessionId: ProviderSessionId) =>
-    providerService.listSessions().pipe(
-      Effect.map((sessions) => sessions.find((session) => session.sessionId === sessionId)?.resumeCursor),
-    );
   // ...
-  const resumeCursor = yield* resolveResumeCursorForSession(existingSessionId);
+  const resumeCursor = yield* resolveResumeCursorForSession(providerService, existingSessionId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/orchestration/Layers/ProviderCommandReactor.ts` around lines
161 - 164, Hoist the resolveResumeCursorForSession function out of the
ensureSessionForThread closure so it isn't recreated on every call: move the
declaration to module scope (keeping its signature
resolveResumeCursorForSession(sessionId: ProviderSessionId)) and keep using
providerService from the outer scope, then replace the inline arrow in
ensureSessionForThread with a reference to the hoisted
resolveResumeCursorForSession; ensure imports/types (ProviderSessionId) remain
available and tests/types compile.
packages/contracts/src/provider.ts (1)

58-62: Constrain permissionMode to known values using Schema.Literals.

Claude Code supports a fixed set of permission modes: "default", "acceptEdits", "plan", and "bypassPermissions". Use Schema.Literals instead of TrimmedNonEmptyStringSchema for better type safety and validation, consistent with the pattern already used elsewhere in the codebase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/contracts/src/provider.ts` around lines 58 - 62, The permissionMode
field in ClaudeCodeProviderStartOptions is currently using
TrimmedNonEmptyStringSchema; replace it with an optional Schema.Literals of the
allowed values to enforce the fixed set. Update ClaudeCodeProviderStartOptions
so permissionMode uses
Schema.optional(Schema.Literals("default","acceptEdits","plan","bypassPermissions")),
keeping the other fields unchanged and ensuring any necessary Schema/Literals
import is present.
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts (1)

125-125: Return a snapshot from listSessions to avoid shared mutable state leakage.

Returning the backing array directly can let callers mutate harness state implicitly.

Suggested change
-      listSessions: () => Effect.succeed(runtimeSessions),
+      listSessions: () => Effect.succeed([...runtimeSessions]),

Based on learnings: For integration tests, prefer deterministic inputs and explicit state checks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts` at line
125, The test helper currently returns the backing array directly from
listSessions, allowing callers to mutate runtimeSessions; change listSessions
(the implementation returning Effect.succeed(runtimeSessions)) to return a
snapshot copy (e.g., a shallow copy via slice/Array.from/spread) so callers get
an immutable snapshot and the harness state cannot be mutated implicitly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/server/src/provider/Layers/ProviderService.ts`:
- Around line 262-272: The guard using hasResumeCursor causes stale sessions
(those with a resumeThreadId but no persisted resumeCursor) to fail recovery at
the return in ProviderService; update the recovery logic to handle this
explicitly: if resumeThreadId exists but resumeCursor is missing, either (A)
attempt an alternate recovery via the adapter (e.g., try a new adapter method or
adapter.startSession overload that accepts resumeThreadId) or (B) return a
clearer ValidationError that includes input.staleSessionId and an actionable
migration hint and emit telemetry/logging so ops can find affected records;
ensure changes touch the branch that checks hasResumeCursor and the call to
adapter.startSession, and add a comment noting that production data may require
a migration to populate resumeCursor for stale sessions.

---

Nitpick comments:
In `@apps/server/src/orchestration/decider.projectScripts.test.ts`:
- Around line 153-155: The test in decider.projectScripts.test.ts hardcodes
model: "gpt-5" while specifying provider: "claudeCode", making the assertion
provider-inconsistent; update the assertions that set model (the occurrences
currently using model: "gpt-5") to instead derive the expected model from the
provider-aware contract/resolver used by the code under test (or replace with an
explicit Claude model slug consistent with your provider mapping) so the test
asserts the provider-correct model for provider: "claudeCode".

In `@apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts`:
- Line 125: The test helper currently returns the backing array directly from
listSessions, allowing callers to mutate runtimeSessions; change listSessions
(the implementation returning Effect.succeed(runtimeSessions)) to return a
snapshot copy (e.g., a shallow copy via slice/Array.from/spread) so callers get
an immutable snapshot and the harness state cannot be mutated implicitly.

In `@apps/server/src/orchestration/Layers/ProviderCommandReactor.ts`:
- Around line 161-164: Hoist the resolveResumeCursorForSession function out of
the ensureSessionForThread closure so it isn't recreated on every call: move the
declaration to module scope (keeping its signature
resolveResumeCursorForSession(sessionId: ProviderSessionId)) and keep using
providerService from the outer scope, then replace the inline arrow in
ensureSessionForThread with a reference to the hoisted
resolveResumeCursorForSession; ensure imports/types (ProviderSessionId) remain
available and tests/types compile.

In `@packages/contracts/src/provider.ts`:
- Around line 58-62: The permissionMode field in ClaudeCodeProviderStartOptions
is currently using TrimmedNonEmptyStringSchema; replace it with an optional
Schema.Literals of the allowed values to enforce the fixed set. Update
ClaudeCodeProviderStartOptions so permissionMode uses
Schema.optional(Schema.Literals("default","acceptEdits","plan","bypassPermissions")),
keeping the other fields unchanged and ensuring any necessary Schema/Literals
import is present.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 03beb30 and f4ce551.

📒 Files selected for processing (13)
  • apps/server/src/codexAppServerManager.test.ts
  • apps/server/src/codexAppServerManager.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
  • apps/server/src/orchestration/decider.projectScripts.test.ts
  • apps/server/src/orchestration/decider.ts
  • apps/server/src/provider/Layers/ProviderService.test.ts
  • apps/server/src/provider/Layers/ProviderService.ts
  • apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts
  • packages/contracts/src/orchestration.test.ts
  • packages/contracts/src/orchestration.ts
  • packages/contracts/src/provider.test.ts
  • packages/contracts/src/provider.ts
💤 Files with no reviewable changes (1)
  • apps/server/src/codexAppServerManager.test.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
.plans/17-claude-code.md (1)

75-82: Add a brief migration-note item for contract consumers.

Since removed top-level provider fields are a breaking payload change, add an explicit migration note/checklist bullet here (even if compatibility is preserved for Codex defaults).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.plans/17-claude-code.md around lines 75 - 82, Add an explicit
migration-note bullet under the "### 1.4 Contract tests (completed)" section
that warns contract consumers about the breaking change of removed top-level
provider fields; mention that this affects payload shape validated by
packages/contracts/src/*.test.ts, list a short checklist such as: update
consumers to move provider info into the new nested location, verify
provider-aware model resolution, and run the updated contract tests, and
indicate any Codex-default compatibility caveats.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.plans/17-claude-code.md:
- Around line 527-537: The file checklist omits lifecycle surfaces for resume
persistence and restart flows; update the list to include implementing
resume/restart handling in the new ClaudeCodeAdapter surfaces and in the
orchestration/registry/session layers (specifically touch
apps/server/src/provider/Services/ClaudeCodeAdapter.ts,
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts,
apps/server/src/provider/Layers/ProviderAdapterRegistry.ts,
apps/server/src/serverLayers.ts,
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts, and the web-side
handlers apps/web/src/session-logic.ts and apps/web/src/components/ChatView.tsx)
and add corresponding tests under the same server/web test trees to cover resume
persistence, restart paths, and lifecycle hooks so ProviderAdapterRegistry,
ProviderCommandReactor, ClaudeCodeAdapter, session-logic, and ChatView correctly
implement resume and restart behavior.

---

Nitpick comments:
In @.plans/17-claude-code.md:
- Around line 75-82: Add an explicit migration-note bullet under the "### 1.4
Contract tests (completed)" section that warns contract consumers about the
breaking change of removed top-level provider fields; mention that this affects
payload shape validated by packages/contracts/src/*.test.ts, list a short
checklist such as: update consumers to move provider info into the new nested
location, verify provider-aware model resolution, and run the updated contract
tests, and indicate any Codex-default compatibility caveats.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f4ce551 and e68c0d1.

📒 Files selected for processing (1)
  • .plans/17-claude-code.md

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Exported constant DEFAULT_PROVIDER_KIND is never used
    • Removed the unused DEFAULT_PROVIDER_KIND constant since it was dead code never imported or referenced anywhere in the codebase.

Create PR

Or push these changes by commenting:

@cursor push 1ef62e98e4
Preview (1ef62e98e4)
diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts
--- a/packages/contracts/src/orchestration.ts
+++ b/packages/contracts/src/orchestration.ts
@@ -31,7 +31,6 @@
 
 export const ProviderKind = Schema.Literals(["codex", "claudeCode"]);
 export type ProviderKind = typeof ProviderKind.Type;
-export const DEFAULT_PROVIDER_KIND: ProviderKind = "codex";
 export const ProviderApprovalPolicy = Schema.Literals([
   "untrusted",
   "on-failure",

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (8)
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts (3)

45-50: Use a contract-derived provider type instead of hardcoded unions.

Line 49 and Line 225 duplicate provider literals ("codex" | "claudeCode"). Derive this from the contract type to avoid drift when providers expand.

Proposed refactor
 import type { ProviderRuntimeEvent, ProviderSession } from "@t3tools/contracts";
+type HarnessProviderName = ProviderSession["provider"];
 ...
 function createProviderServiceHarness(
   cwd: string,
   hasSession = true,
   sessionCwd = cwd,
-  providerName: "codex" | "claudeCode" = "codex",
+  providerName: HarnessProviderName = "codex",
 ) {
 ...
   async function createHarness(options?: {
     readonly hasSession?: boolean;
     readonly seedFilesystemCheckpoints?: boolean;
     readonly projectWorkspaceRoot?: string;
     readonly threadWorktreePath?: string | null;
     readonly providerSessionCwd?: string;
-    readonly providerName?: "codex" | "claudeCode";
+    readonly providerName?: HarnessProviderName;
   }) {

As per coding guidelines: apps/**/*.ts: Use Zod schemas and TypeScript contracts from packages/contracts for provider events, WebSocket protocol, and model/session types.

Also applies to: 225-233

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/orchestration/Layers/CheckpointReactor.test.ts` around lines
45 - 50, The providerName parameter in createProviderServiceHarness (and the
duplicate literal type elsewhere) uses a hardcoded union ("codex" |
"claudeCode"); replace that with the canonical provider type exported from the
contracts package (import the provider enum/type/Zod-derived Type from
packages/contracts and use it for providerName and the other occurrence) so the
test derives allowed providers from the shared contract instead of duplicating
literals; update the function signature and any dependent variables to use that
imported contract type.

788-858: Add read-model and git-state assertions to the claudeCode revert test.

This test currently asserts rollback invocation only. It should also assert final thread/checkpoint/git state like the codex revert test to verify behavior, not just call intent.

Suggested parity assertions
     await waitForEvent(harness.engine, (event) => event.type === "thread.reverted");
+    const thread = await waitForThread(harness.engine, (entry) => entry.checkpoints.length === 1);
     expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(1);
     expect(harness.provider.rollbackConversation).toHaveBeenCalledWith({
       sessionId: asSessionId("sess-1"),
       numTurns: 1,
     });
+    expect(thread.latestTurnId).toBe("turn-claude-1");
+    expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1);
+    expect(fs.readFileSync(path.join(harness.cwd, "README.md"), "utf8")).toBe("v2\n");
+    expect(
+      gitRefExists(harness.cwd, checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 2)),
+    ).toBe(false);

Based on learnings: For integration tests, prefer deterministic inputs and explicit state checks, and assert on final state and side effects.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/orchestration/Layers/CheckpointReactor.test.ts` around lines
788 - 858, The claudeCode revert test only checks that
harness.provider.rollbackConversation was called; update it to also assert the
final read-model, checkpoint and git-state like the codex revert test does:
after waitForEvent(harness.engine, (event) => event.type === "thread.reverted"),
fetch the thread and checkpoint state from the engine/read-model (using the same
read-model accessors used in the codex test) and assert the thread's turn
count/status and that the checkpoint for
checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 1) was
removed/updated; also assert repository/git-state changes (e.g., files/commit
state) to match expected post-revert state and keep the existing
expect(harness.provider.rollbackConversation).toHaveBeenCalledWith(...)
assertion. Ensure you reference createHarness, harness.engine.dispatch,
checkpointRefForThreadTurn, and harness.provider.rollbackConversation when
locating where to add these checks.

391-455: Strengthen the claudeCode checkpoint test with full end-state assertions.

This test currently proves event flow, but it only minimally checks checkpoint persistence. Mirror codex assertions (baseline ref/content + completion ref/content) so provider parity regressions are caught.

Suggested assertion additions
     expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1);
     expect(
+      gitRefExists(harness.cwd, checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 0)),
+    ).toBe(true);
+    expect(
       gitRefExists(harness.cwd, checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 1)),
     ).toBe(true);
+    expect(
+      gitShowFileAtRef(
+        harness.cwd,
+        checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 0),
+        "README.md",
+      ),
+    ).toBe("v1\n");
+    expect(
+      gitShowFileAtRef(
+        harness.cwd,
+        checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 1),
+        "README.md",
+      ),
+    ).toBe("v2\n");

Based on learnings: For integration tests, prefer deterministic inputs and explicit state checks, and assert on final state and side effects.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/orchestration/Layers/CheckpointReactor.test.ts` around lines
391 - 455, The test "captures pre-turn and completion checkpoints for claudeCode
runtime events" currently only checks checkpoint existence/count; update it to
assert the full end-state like the codex test by verifying both the baseline
(pre-turn) and completion checkpoint refs and their contents: use
checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 0) and (..., 1) with
gitRefExists(harness.cwd, ...) and read the git tree/workdir to assert README.md
content before and after the turn (use waitForGitRefExists to wait for refs,
then read the commit or filesystem snapshot to assert expected contents), and
also strengthen the thread assertion returned by waitForThread to verify the
checkpoint entries' metadata (e.g., checkpointTurnCount and any stored snapshot
ids) so provider parity regressions are caught.
apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts (2)

239-262: Event streaming test has potential race condition.

The test forks a fiber to wait for the first event, then emits the event. While this generally works due to Effect's cooperative scheduling, the fiber may not have subscribed to the queue before emitRuntimeEvent is called. Consider adding a small yield or using Effect.yieldNow() after forking to ensure the fiber has started listening.

🛡️ Suggested improvement
 it.effect("passes through canonical runtime events without remapping", () =>
   Effect.gen(function* () {
     const adapter = yield* ClaudeCodeAdapter;
     const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);
+    yield* Effect.yieldNow();
     const runtimeEvent: ProviderRuntimeEvent = {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts` around lines 239 -
262, The test for ClaudeCodeAdapter can race because the forked fiber waiting on
Stream.runHead(adapter.streamEvents) may not be subscribed before
lifecycleRuntime.emitRuntimeEvent is called; after creating firstEventFiber via
Effect.forkChild, insert an Effect.yieldNow() (or another small cooperative
yield) to ensure the fiber is scheduled and listening before calling
lifecycleRuntime.emitRuntimeEvent(runtimeEvent), so the Stream subscription in
Stream.runHead actually receives the emitted event.

162-194: Consider using assertFailure for consistency.

The session error test manually checks result._tag === "Failure" and uses early returns, while the validation test uses assertFailure. Consider using a similar pattern or Cause.match for consistency and cleaner assertions.

♻️ Optional: Refactor to use pattern matching
-      assert.equal(result._tag, "Failure");
-      if (result._tag !== "Failure") {
-        return;
-      }
-
-      assert.equal(result.failure._tag, "ProviderAdapterSessionNotFoundError");
-      if (result.failure._tag !== "ProviderAdapterSessionNotFoundError") {
-        return;
-      }
-      assert.equal(result.failure.provider, "claudeCode");
-      assert.equal(result.failure.sessionId, "claude-sess-missing");
-      assert.instanceOf(result.failure.cause, Error);
+      assert.equal(result._tag, "Failure");
+      assert.equal(result.failure._tag, "ProviderAdapterSessionNotFoundError");
+      assert.equal(result.failure.provider, "claudeCode");
+      assert.equal(result.failure.sessionId, "claude-sess-missing");
+      assert.instanceOf(result.failure.cause, Error);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts` around lines 162 -
194, Replace the manual result tag checks and early returns in the
"ClaudeCodeAdapterLive session errors" test with the shared assertFailure helper
used elsewhere: call assertFailure(result) to assert it's a Failure and receive
the failure value, then assert on failure._tag ===
"ProviderAdapterSessionNotFoundError", failure.provider === "claudeCode",
failure.sessionId === "claude-sess-missing", and that failure.cause is an Error;
update the assertions in the sessionErrorLayer test (which uses
FakeClaudeRuntime, makeClaudeCodeAdapterLive, ClaudeCodeAdapter, asSessionId) to
use assertFailure instead of manual _tag checks to keep consistency and remove
early returns.
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts (2)

71-91: String-based error detection is fragile.

The toSessionError function relies on substring matching of error messages ("unknown session", "session is closed") to determine error types. This approach is brittle and may break if the upstream runtime changes its error message format.

Consider defining error codes or structured error types in the runtime interface that can be matched more reliably. If that's not feasible now, document this coupling and consider adding the expected error messages as constants.

📝 Suggested improvement
+// Expected error message patterns from ClaudeCodeRuntime.
+// If the runtime changes these, update accordingly.
+const SESSION_NOT_FOUND_PATTERNS = ["unknown session", "unknown provider session"];
+const SESSION_CLOSED_PATTERN = "session is closed";
+
 function toSessionError(
   sessionId: ProviderSessionId,
   cause: unknown,
 ): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined {
   const normalized = toMessage(cause, "").toLowerCase();
-  if (normalized.includes("unknown session") || normalized.includes("unknown provider session")) {
+  if (SESSION_NOT_FOUND_PATTERNS.some((p) => normalized.includes(p))) {
     return new ProviderAdapterSessionNotFoundError({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ClaudeCodeAdapter.ts` around lines 71 - 91,
The toSessionError function currently uses fragile substring matching of
normalized message text from toMessage(cause, "") to decide between
ProviderAdapterSessionNotFoundError and ProviderAdapterSessionClosedError;
change this by (preferred) extending the runtime error contract to include a
stable errorCode or typed error (inspect cause for an error.code or a
discriminant and branch on that), or if changing the runtime isn't possible now,
extract the literal strings ("unknown session", "unknown provider session",
"session is closed") into named constants and add a clear comment documenting
the coupling, and update toSessionError (and any callers) to check those
constants (and error.code if present) rather than ad-hoc substring matches to
make detection reliable; reference toSessionError, toMessage, PROVIDER,
ProviderAdapterSessionNotFoundError, ProviderAdapterSessionClosedError, and
ProviderSessionId when implementing.

193-208: Rollback validation is correct but consider upper bound.

The validation ensures numTurns >= 1 and is an integer, which is correct. However, there's no upper bound validation. If the runtime doesn't handle excessively large values gracefully, consider adding a reasonable upper limit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ClaudeCodeAdapter.ts` around lines 193 - 208,
The rollbackThread implementation currently validates numTurns >= 1 but lacks an
upper bound; add a MAX_TURNS constant (e.g., MAX_TURNS = <reasonable limit>) and
update the validation in ClaudeCodeAdapter.rollbackThread to also reject values
> MAX_TURNS by returning a ProviderAdapterValidationError (same shape as
existing error, operation "rollbackThread", issue like "numTurns must be an
integer between 1 and MAX_TURNS"). Ensure the new check runs before calling
runtime.rollbackThread; leave the existing Effect.tryPromise call and
toRequestError handling unchanged so runtime.rollbackThread is only invoked for
values within the allowed range.
apps/web/src/components/ChatView.tsx (1)

2823-2830: Avoid hardcoded provider guards in ProviderPicker.

Line 2829 hardcodes "codex"/"claudeCode", so newly added providers could appear in the dropdown but never be selectable. Prefer validating against the same filtered PROVIDER_OPTIONS source you render.

♻️ Proposed refactor
 const ProviderPicker = memo(function ProviderPicker(props: {
   provider: ProviderKind;
   onProviderChange: (provider: ProviderKind) => void;
 }) {
+  const availableProviders = PROVIDER_OPTIONS.filter((option) => option.available);
+  const isAvailableProvider = (value: string): value is ProviderKind =>
+    availableProviders.some((option) => option.value === value);
+
   return (
     <Select
-      items={PROVIDER_OPTIONS.filter((option) => option.available).map((option) => ({
+      items={availableProviders.map((option) => ({
         label: option.label,
         value: option.value,
       }))}
       value={props.provider}
-      onValueChange={(value) =>
-        value === "codex" || value === "claudeCode" ? props.onProviderChange(value) : undefined
-      }
+      onValueChange={(value) => (value && isAvailableProvider(value) ? props.onProviderChange(value) : undefined)}
     >
       <SelectTrigger size="sm" variant="ghost">
         <SelectValue />
       </SelectTrigger>
       <SelectPopup alignItemWithTrigger={false}>
-        {PROVIDER_OPTIONS.filter((option) => option.available).map(({ value, label }) => (
+        {availableProviders.map(({ value, label }) => (
           <SelectItem key={value} value={value}>
             {label}
           </SelectItem>
         ))}
       </SelectPopup>
     </Select>
   );
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/ChatView.tsx` around lines 2823 - 2830, The
ProviderPicker currently prevents selection by checking hardcoded provider
values ("codex"/"claudeCode") in the onValueChange handler; update it to
validate against the same PROVIDER_OPTIONS source used for items instead: lookup
the selected value in PROVIDER_OPTIONS (or in PROVIDER_OPTIONS.filter(o =>
o.available)) and only call props.onProviderChange(value) if a matching option
with available === true exists (use the option.value and option.available checks
to locate the entry), ensuring new providers shown in the dropdown can be
selected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/server/src/orchestration/Layers/CheckpointReactor.test.ts`:
- Around line 45-50: The providerName parameter in createProviderServiceHarness
(and the duplicate literal type elsewhere) uses a hardcoded union ("codex" |
"claudeCode"); replace that with the canonical provider type exported from the
contracts package (import the provider enum/type/Zod-derived Type from
packages/contracts and use it for providerName and the other occurrence) so the
test derives allowed providers from the shared contract instead of duplicating
literals; update the function signature and any dependent variables to use that
imported contract type.
- Around line 788-858: The claudeCode revert test only checks that
harness.provider.rollbackConversation was called; update it to also assert the
final read-model, checkpoint and git-state like the codex revert test does:
after waitForEvent(harness.engine, (event) => event.type === "thread.reverted"),
fetch the thread and checkpoint state from the engine/read-model (using the same
read-model accessors used in the codex test) and assert the thread's turn
count/status and that the checkpoint for
checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 1) was
removed/updated; also assert repository/git-state changes (e.g., files/commit
state) to match expected post-revert state and keep the existing
expect(harness.provider.rollbackConversation).toHaveBeenCalledWith(...)
assertion. Ensure you reference createHarness, harness.engine.dispatch,
checkpointRefForThreadTurn, and harness.provider.rollbackConversation when
locating where to add these checks.
- Around line 391-455: The test "captures pre-turn and completion checkpoints
for claudeCode runtime events" currently only checks checkpoint existence/count;
update it to assert the full end-state like the codex test by verifying both the
baseline (pre-turn) and completion checkpoint refs and their contents: use
checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 0) and (..., 1) with
gitRefExists(harness.cwd, ...) and read the git tree/workdir to assert README.md
content before and after the turn (use waitForGitRefExists to wait for refs,
then read the commit or filesystem snapshot to assert expected contents), and
also strengthen the thread assertion returned by waitForThread to verify the
checkpoint entries' metadata (e.g., checkpointTurnCount and any stored snapshot
ids) so provider parity regressions are caught.

In `@apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts`:
- Around line 239-262: The test for ClaudeCodeAdapter can race because the
forked fiber waiting on Stream.runHead(adapter.streamEvents) may not be
subscribed before lifecycleRuntime.emitRuntimeEvent is called; after creating
firstEventFiber via Effect.forkChild, insert an Effect.yieldNow() (or another
small cooperative yield) to ensure the fiber is scheduled and listening before
calling lifecycleRuntime.emitRuntimeEvent(runtimeEvent), so the Stream
subscription in Stream.runHead actually receives the emitted event.
- Around line 162-194: Replace the manual result tag checks and early returns in
the "ClaudeCodeAdapterLive session errors" test with the shared assertFailure
helper used elsewhere: call assertFailure(result) to assert it's a Failure and
receive the failure value, then assert on failure._tag ===
"ProviderAdapterSessionNotFoundError", failure.provider === "claudeCode",
failure.sessionId === "claude-sess-missing", and that failure.cause is an Error;
update the assertions in the sessionErrorLayer test (which uses
FakeClaudeRuntime, makeClaudeCodeAdapterLive, ClaudeCodeAdapter, asSessionId) to
use assertFailure instead of manual _tag checks to keep consistency and remove
early returns.

In `@apps/server/src/provider/Layers/ClaudeCodeAdapter.ts`:
- Around line 71-91: The toSessionError function currently uses fragile
substring matching of normalized message text from toMessage(cause, "") to
decide between ProviderAdapterSessionNotFoundError and
ProviderAdapterSessionClosedError; change this by (preferred) extending the
runtime error contract to include a stable errorCode or typed error (inspect
cause for an error.code or a discriminant and branch on that), or if changing
the runtime isn't possible now, extract the literal strings ("unknown session",
"unknown provider session", "session is closed") into named constants and add a
clear comment documenting the coupling, and update toSessionError (and any
callers) to check those constants (and error.code if present) rather than ad-hoc
substring matches to make detection reliable; reference toSessionError,
toMessage, PROVIDER, ProviderAdapterSessionNotFoundError,
ProviderAdapterSessionClosedError, and ProviderSessionId when implementing.
- Around line 193-208: The rollbackThread implementation currently validates
numTurns >= 1 but lacks an upper bound; add a MAX_TURNS constant (e.g.,
MAX_TURNS = <reasonable limit>) and update the validation in
ClaudeCodeAdapter.rollbackThread to also reject values > MAX_TURNS by returning
a ProviderAdapterValidationError (same shape as existing error, operation
"rollbackThread", issue like "numTurns must be an integer between 1 and
MAX_TURNS"). Ensure the new check runs before calling runtime.rollbackThread;
leave the existing Effect.tryPromise call and toRequestError handling unchanged
so runtime.rollbackThread is only invoked for values within the allowed range.

In `@apps/web/src/components/ChatView.tsx`:
- Around line 2823-2830: The ProviderPicker currently prevents selection by
checking hardcoded provider values ("codex"/"claudeCode") in the onValueChange
handler; update it to validate against the same PROVIDER_OPTIONS source used for
items instead: lookup the selected value in PROVIDER_OPTIONS (or in
PROVIDER_OPTIONS.filter(o => o.available)) and only call
props.onProviderChange(value) if a matching option with available === true
exists (use the option.value and option.available checks to locate the entry),
ensuring new providers shown in the dropdown can be selected.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e68c0d1 and 6de3ed0.

📒 Files selected for processing (17)
  • apps/server/integration/OrchestrationEngineHarness.integration.ts
  • apps/server/integration/TestProviderAdapter.integration.ts
  • apps/server/integration/orchestrationEngine.integration.test.ts
  • apps/server/integration/providerService.integration.test.ts
  • apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
  • apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts
  • apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
  • apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts
  • apps/server/src/provider/Layers/ProviderAdapterRegistry.ts
  • apps/server/src/provider/Layers/ProviderService.test.ts
  • apps/server/src/provider/Services/ClaudeCodeAdapter.ts
  • apps/server/src/serverLayers.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/session-logic.test.ts
  • apps/web/src/session-logic.ts

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Claude Code enabled in UI but server runtime unconfigured
    • Set claudeCode to available: false with label "Claude Code (soon)" in PROVIDER_OPTIONS since makeClaudeCodeAdapterLive() has no runtime configured and falls through to makeUnavailableRuntime().
  • ✅ Fixed: Provider always sent, breaking optional field semantics
    • Changed the provider field in the turn start command to only be included when the user has explicitly selected a provider via selectedProviderByThread, preventing unnecessary session restarts for threads with null providerName.
  • ✅ Fixed: Resume cursor read from live sessions not persisted state
    • Changed resolveResumeCursorForSession to read from persisted ProviderSessionDirectory.getBinding() instead of live providerService.listSessions(), ensuring the resume cursor survives adapter-level session loss.

Create PR

Or push these changes by commenting:

@cursor push 6d4ef60128
Preview (6d4ef60128)
diff --git a/apps/server/integration/OrchestrationEngineHarness.integration.ts b/apps/server/integration/OrchestrationEngineHarness.integration.ts
--- a/apps/server/integration/OrchestrationEngineHarness.integration.ts
+++ b/apps/server/integration/OrchestrationEngineHarness.integration.ts
@@ -222,7 +222,7 @@
     Layer.provide(ProviderSessionRuntimeRepositoryLive),
   );
   const providerLayer = makeProviderServiceLive().pipe(
-    Layer.provide(providerSessionDirectoryLayer),
+    Layer.provideMerge(providerSessionDirectoryLayer),
     Layer.provide(Layer.succeed(ProviderAdapterRegistry, registry)),
   );
   const checkpointStoreLayer = CheckpointStoreLive.pipe(Layer.provide(NodeServices.layer));

diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
@@ -10,7 +10,7 @@
   ThreadId,
   TurnId,
 } from "@t3tools/contracts";
-import { Effect, Exit, Layer, ManagedRuntime, PubSub, Scope, Stream } from "effect";
+import { Effect, Exit, Layer, ManagedRuntime, Option, PubSub, Scope, Stream } from "effect";
 import { afterEach, describe, expect, it, vi } from "vitest";
 
 import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts";
@@ -20,6 +20,7 @@
   ProviderService,
   type ProviderServiceShape,
 } from "../../provider/Services/ProviderService.ts";
+import { ProviderSessionDirectory } from "../../provider/Services/ProviderSessionDirectory.ts";
 import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";
 import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";
 import { ProviderCommandReactorLive } from "./ProviderCommandReactor.ts";
@@ -141,9 +142,26 @@
       Layer.provide(OrchestrationCommandReceiptRepositoryLive),
       Layer.provide(SqlitePersistenceMemory),
     );
+    const sessionDirectoryMock = Layer.succeed(ProviderSessionDirectory, {
+      upsert: () => Effect.void,
+      getProvider: () => Effect.die(new Error("not implemented in test")),
+      getBinding: (sessionId: ProviderSessionId) => {
+        const session = runtimeSessions.find((s) => s.sessionId === sessionId);
+        if (!session) return Effect.succeed(Option.none());
+        return Effect.succeed(Option.some({
+          sessionId: session.sessionId,
+          provider: session.provider,
+          resumeCursor: session.resumeCursor,
+        }));
+      },
+      getThreadId: () => Effect.succeed(Option.none()),
+      remove: () => Effect.void,
+      listSessionIds: () => Effect.succeed([]),
+    });
     const layer = ProviderCommandReactorLive.pipe(
       Layer.provideMerge(orchestrationLayer),
       Layer.provideMerge(Layer.succeed(ProviderService, service)),
+      Layer.provideMerge(sessionDirectoryMock),
     );
     runtime = ManagedRuntime.make(layer);
     const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService));

diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
@@ -15,6 +15,7 @@
 import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Stream } from "effect";
 
 import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";
+import { ProviderSessionDirectory } from "../../provider/Services/ProviderSessionDirectory.ts";
 import { ProviderService } from "../../provider/Services/ProviderService.ts";
 import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
 import {
@@ -70,6 +71,7 @@
 const make = Effect.gen(function* () {
   const orchestrationEngine = yield* OrchestrationEngineService;
   const providerService = yield* ProviderService;
+  const sessionDirectory = yield* ProviderSessionDirectory;
   const handledTurnStartKeys = yield* Cache.make<string, true>({
     capacity: HANDLED_TURN_START_KEY_MAX,
     timeToLive: HANDLED_TURN_START_KEY_TTL,
@@ -161,8 +163,9 @@
     });
 
     const resolveResumeCursorForSession = (sessionId: ProviderSessionId) =>
-      providerService.listSessions().pipe(
-        Effect.map((sessions) => sessions.find((session) => session.sessionId === sessionId)?.resumeCursor),
+      sessionDirectory.getBinding(sessionId).pipe(
+        Effect.map((binding) => Option.getOrUndefined(binding)?.resumeCursor),
+        Effect.orElseSucceed(() => undefined),
       );
 
     const startProviderSession = (input?: {

diff --git a/apps/server/src/serverLayers.ts b/apps/server/src/serverLayers.ts
--- a/apps/server/src/serverLayers.ts
+++ b/apps/server/src/serverLayers.ts
@@ -23,6 +23,7 @@
 import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry";
 import { makeProviderServiceLive } from "./provider/Layers/ProviderService";
 import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory";
+import { ProviderSessionDirectory } from "./provider/Services/ProviderSessionDirectory";
 import { ProviderService } from "./provider/Services/ProviderService";
 
 import { TerminalManagerLive } from "./terminal/Layers/Manager";
@@ -36,7 +37,7 @@
 import { NodePtyAdapterLive } from "./terminal/Layers/NodePTY";
 
 export function makeServerProviderLayer(): Layer.Layer<
-  ProviderService,
+  ProviderService | ProviderSessionDirectory,
   ProviderUnsupportedError,
   SqlClient.SqlClient | ServerConfig
 > {
@@ -55,7 +56,7 @@
     );
     return makeProviderServiceLive({
       canonicalEventLogPath: path.join(providerLogsDir, "provider-canonical.ndjson"),
-    }).pipe(Layer.provide(adapterRegistryLayer), Layer.provide(providerSessionDirectoryLayer));
+    }).pipe(Layer.provide(adapterRegistryLayer), Layer.provideMerge(providerSessionDirectoryLayer));
   }).pipe(Layer.unwrap);
 }
 

diff --git a/apps/server/src/wsServer.test.ts b/apps/server/src/wsServer.test.ts
--- a/apps/server/src/wsServer.test.ts
+++ b/apps/server/src/wsServer.test.ts
@@ -4,7 +4,7 @@
 import path from "node:path";
 
 import * as NodeServices from "@effect/platform-node/NodeServices";
-import { Effect, Exit, Layer, PubSub, Scope, Stream } from "effect";
+import { Effect, Exit, Layer, Option, PubSub, Scope, Stream } from "effect";
 import { describe, expect, it, afterEach, vi } from "vitest";
 import { createServer } from "./wsServer";
 import WebSocket from "ws";
@@ -42,6 +42,7 @@
 import { makeSqlitePersistenceLive, SqlitePersistenceMemory } from "./persistence/Layers/Sqlite";
 import { SqlClient } from "effect/unstable/sql";
 import { ProviderService, type ProviderServiceShape } from "./provider/Services/ProviderService";
+import { ProviderSessionDirectory } from "./provider/Services/ProviderSessionDirectory";
 import { Open, type OpenShape } from "./open";
 import { GitManager, type GitManagerShape } from "./git/Services/GitManager.ts";
 import type { GitCoreShape } from "./git/Services/GitCore.ts";
@@ -331,7 +332,7 @@
       devUrl?: string;
       authToken?: string;
       stateDir?: string;
-      providerLayer?: Layer.Layer<ProviderService, never>;
+      providerLayer?: Layer.Layer<ProviderService | ProviderSessionDirectory, never>;
       open?: OpenShape;
       gitManager?: GitManagerShape;
       gitCore?: Pick<GitCoreShape, "listBranches" | "initRepo" | "pullCurrentBranch">;
@@ -1007,7 +1008,17 @@
       stopAll: () => Effect.void,
       streamEvents: Stream.fromPubSub(runtimeEventPubSub),
     };
-    const providerLayer = Layer.succeed(ProviderService, providerService);
+    const providerLayer = Layer.mergeAll(
+      Layer.succeed(ProviderService, providerService),
+      Layer.succeed(ProviderSessionDirectory, {
+        upsert: () => Effect.void,
+        getProvider: () => Effect.die(new Error("not implemented in test")),
+        getBinding: () => Effect.succeed(Option.none()),
+        getThreadId: () => Effect.succeed(Option.none()),
+        remove: () => Effect.void,
+        listSessionIds: () => Effect.succeed([]),
+      }),
+    );
 
     server = await createTestServer({
       cwd: "/test",

diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -1560,7 +1560,9 @@
           attachments: turnAttachments,
         },
         model: selectedModel || undefined,
-        provider: selectedProvider,
+        ...(selectedProviderByThread[threadIdForSend]
+          ? { provider: selectedProviderByThread[threadIdForSend] }
+          : {}),
         effort: selectedEffort || undefined,
         assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered",
         approvalPolicy,

diff --git a/apps/web/src/session-logic.test.ts b/apps/web/src/session-logic.test.ts
--- a/apps/web/src/session-logic.test.ts
+++ b/apps/web/src/session-logic.test.ts
@@ -128,12 +128,12 @@
 });
 
 describe("PROVIDER_OPTIONS", () => {
-  it("marks claudeCode as available", () => {
+  it("marks claudeCode as unavailable until runtime is configured", () => {
     const claude = PROVIDER_OPTIONS.find((option) => option.value === "claudeCode");
     expect(claude).toEqual({
       value: "claudeCode",
-      label: "Claude Code",
-      available: true,
+      label: "Claude Code (soon)",
+      available: false,
     });
   });
 });

diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts
--- a/apps/web/src/session-logic.ts
+++ b/apps/web/src/session-logic.ts
@@ -13,7 +13,7 @@
   available: boolean;
 }> = [
   { value: "codex", label: "Codex", available: true },
-  { value: "claudeCode", label: "Claude Code", available: true },
+  { value: "claudeCode", label: "Claude Code (soon)", available: false },
 ];
 
 export interface WorkLogEntry {

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/provider/Layers/ClaudeCodeAdapter.ts`:
- Around line 285-289: The Layer is created with Layer.effect but
makeClaudeCodeAdapter uses Effect.acquireRelease, so its finalizer
(runtime.stopAll()) won't run when the layer is released; change both
ClaudeCodeAdapterLive and makeClaudeCodeAdapterLive to use Layer.scoped instead
of Layer.effect so the Scope is managed and acquireRelease finalizers run on
layer finalization; update the constructors that call makeClaudeCodeAdapter(...)
(the symbols ClaudeCodeAdapterLive, makeClaudeCodeAdapterLive, and
makeClaudeCodeAdapter) to return Layer.scoped(...) wrapping the scoped effect.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6de3ed0 and d431372.

📒 Files selected for processing (2)
  • apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts
  • apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Exported ClaudeCodeAdapterLive constant is never imported
    • Removed the unused ClaudeCodeAdapterLive constant export since all consumers use the makeClaudeCodeAdapterLive() factory function instead.

Create PR

Or push these changes by commenting:

@cursor push 209b86105e
Preview (209b86105e)
diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
--- a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
+++ b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
@@ -98,8 +98,6 @@
   streamEvents: Stream.empty as Stream.Stream<ProviderRuntimeEvent>,
 } satisfies ClaudeCodeAdapterShape);
 
-export const ClaudeCodeAdapterLive = Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter);
-
 export function makeClaudeCodeAdapterLive() {
   return Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter);
 }

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Hardcoded provider prefixes silently drop model selections
    • Replaced hardcoded startsWith("codex:") and startsWith("claudeCode:") checks with a generic indexOf(":") split that works for any provider prefix, making the handler extensible without coordinated changes.

Create PR

Or push these changes by commenting:

@cursor push 7bb9501a87
Preview (7bb9501a87)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -2826,25 +2826,14 @@
     <Select
       items={items}
       value={`${props.provider}:${props.model}`}
-      onValueChange={(value) =>
-        value
-          ? (() => {
-              if (value.startsWith("codex:")) {
-                props.onProviderModelChange(
-                  "codex",
-                  resolveModelSlugForProvider("codex", value.slice("codex:".length)),
-                );
-                return;
-              }
-              if (value.startsWith("claudeCode:")) {
-                props.onProviderModelChange(
-                  "claudeCode",
-                  resolveModelSlugForProvider("claudeCode", value.slice("claudeCode:".length)),
-                );
-              }
-            })()
-          : undefined
-      }
+      onValueChange={(value) => {
+        if (!value) return;
+        const colonIdx = value.indexOf(":");
+        if (colonIdx === -1) return;
+        const provider = value.slice(0, colonIdx) as ProviderKind;
+        const slug = value.slice(colonIdx + 1);
+        props.onProviderModelChange(provider, resolveModelSlugForProvider(provider, slug));
+      }}
     >
       <SelectTrigger size="sm" variant="ghost">
         <span className="flex min-w-0 items-center gap-2">

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Provider not inferred from thread model without session
    • Exported inferProviderForThreadModel from store.ts and used it in ChatView's selectedProvider derivation to infer the correct provider from the thread's model slug before falling back to the hardcoded 'codex' default.

Create PR

Or push these changes by commenting:

@cursor push 62e136dcc8
Preview (62e136dcc8)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -51,7 +51,7 @@
   formatTimestamp,
 } from "../session-logic";
 import { isScrollContainerNearBottom } from "../chat-scroll";
-import { useStore } from "../store";
+import { inferProviderForThreadModel, useStore } from "../store";
 import { truncateTitle } from "../truncateTitle";
 import {
   DEFAULT_THREAD_TERMINAL_ID,
@@ -413,7 +413,12 @@
   const selectedProvider: ProviderKind =
     (activeThread?.id ? selectedProviderByThread[activeThread.id] : undefined) ??
     sessionProvider ??
-    "codex";
+    (activeThread?.model
+      ? inferProviderForThreadModel({
+          model: activeThread.model,
+          sessionProviderName: activeThread.session?.provider ?? null,
+        })
+      : "codex");
   const selectedModel = resolveModelSlugForProvider(
     selectedProvider,
     activeThread?.model ?? activeProject?.model ?? getDefaultModel(selectedProvider) ?? DEFAULT_MODEL,

diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts
--- a/apps/web/src/store.ts
+++ b/apps/web/src/store.ts
@@ -198,7 +198,7 @@
 const CODEX_MODEL_SLUGS = new Set(getModelOptions("codex").map((option) => option.slug));
 const CLAUDE_MODEL_SLUGS = new Set(getModelOptions("claudeCode").map((option) => option.slug));
 
-function inferProviderForThreadModel(input: {
+export function inferProviderForThreadModel(input: {
   readonly model: string;
   readonly sessionProviderName: string | null;
 }): "codex" | "claudeCode" {

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Detached fiber bypasses adapter scope management
    • Replaced Effect.runFork(runSdkStream(context)) with yield* Effect.forkDetach(runSdkStream(context)) to inherit provided services, stored the fiber in ClaudeSessionContext.streamFiber, and added Fiber.interrupt in stopSessionInternal for deterministic cleanup before queue shutdown.

Create PR

Or push these changes by commenting:

@cursor push 4e4055c7f0
Preview (4e4055c7f0)
diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
--- a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
+++ b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
@@ -31,7 +31,18 @@
   ProviderThreadId,
   ProviderTurnId,
 } from "@t3tools/contracts";
-import { Cause, DateTime, Deferred, Effect, Layer, Queue, Random, Ref, Stream } from "effect";
+import {
+  Cause,
+  DateTime,
+  Deferred,
+  Effect,
+  Fiber,
+  Layer,
+  Queue,
+  Random,
+  Ref,
+  Stream,
+} from "effect";
 
 import {
   ProviderAdapterProcessError,
@@ -99,6 +110,7 @@
   lastAssistantUuid: string | undefined;
   lastThreadStartedId: ProviderThreadId | undefined;
   stopped: boolean;
+  streamFiber: Fiber.RuntimeFiber<void> | undefined;
 }
 
 interface ClaudeQueryRuntime extends AsyncIterable<SDKMessage> {
@@ -117,9 +129,7 @@
 }
 
 function isUuid(value: string): boolean {
-  return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
-    value,
-  );
+  return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
 }
 
 function toMessage(cause: unknown, fallback: string): string {
@@ -695,6 +705,11 @@
 
         context.stopped = true;
 
+        if (context.streamFiber) {
+          yield* Fiber.interrupt(context.streamFiber);
+          context.streamFiber = undefined;
+        }
+
         for (const [requestId, pending] of context.pendingApprovals) {
           yield* Deferred.succeed(pending.decision, "cancel");
           const stamp = yield* makeEventStamp();
@@ -967,6 +982,7 @@
           lastAssistantUuid: resumeState?.resumeSessionAt,
           lastThreadStartedId: undefined,
           stopped: false,
+          streamFiber: undefined,
         };
         yield* Ref.set(contextRef, context);
         sessions.set(sessionId, context);
@@ -981,7 +997,7 @@
           threadId,
         });
 
-        Effect.runFork(runSdkStream(context));
+        context.streamFiber = yield* Effect.forkDetach(runSdkStream(context));
 
         return {
           ...session,

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Cross-provider resume cursor forwarded on provider switch
    • When providerChanged is true, resumeCursor is now set to undefined instead of resolving the old provider's opaque cursor, preventing cross-provider cursor misinterpretation.

Create PR

Or push these changes by commenting:

@cursor push 966209c2b5
Preview (966209c2b5)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
@@ -211,7 +211,7 @@
         return existingSessionId;
       }
 
-      const resumeCursor = yield* resolveResumeCursorForSession(existingSessionId);
+      const resumeCursor = providerChanged ? undefined : (yield* resolveResumeCursorForSession(existingSessionId));
       const restartedSession = yield* startProviderSession({
         ...(resumeCursor !== undefined ? { resumeCursor } : {}),
         ...(options?.provider !== undefined ? { provider: options.provider } : {}),

export const MODEL_OPTIONS = MODEL_OPTIONS_BY_PROVIDER.codex;
export const DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.codex;

export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record<ProviderKind, Record<string, ModelSlug>> = {
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/model.ts:33

Lookups into MODEL_SLUG_ALIASES_BY_PROVIDER can hit inherited keys like constructor/toString, making normalizeModelSlug return a function. Suggest guarding with Object.hasOwn(...) before access, or defining the alias maps with a null prototype.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/contracts/src/model.ts around line 33:

Lookups into `MODEL_SLUG_ALIASES_BY_PROVIDER` can hit inherited keys like `constructor`/`toString`, making `normalizeModelSlug` return a function. Suggest guarding with `Object.hasOwn(...)` before access, or defining the alias maps with a null prototype.

Evidence trail:
packages/contracts/src/model.ts lines 32-53 (MODEL_SLUG_ALIASES_BY_PROVIDER defined with object literals that inherit from Object.prototype), lines 63-76 (normalizeModelSlug function using direct bracket notation lookup without Object.hasOwn guard)

@juliusmarminge juliusmarminge force-pushed the codething/648ca884 branch 2 times, most recently from 42ebb30 to 870096b Compare February 28, 2026 05:39
]
.toSorted((left, right) => left.createdAt.localeCompare(right.createdAt))
.toSorted(compareThreadActivities)
.slice(-500);
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/projector.ts:524

The compareThreadActivities comparator sorts unsequenced activities before sequenced ones, but .slice(-500) keeps the end of the array. When the thread is saturated with 500+ sequenced activities, any new unsequenced activity gets sorted to the front and immediately discarded. Consider reversing the sort order so unsequenced items appear after sequenced ones, or use .slice(0, 500) instead.

Also found in 1 other location(s)

apps/web/src/session-logic.ts:166

The new sorting function compareActivitiesByOrder introduces a logic regression where activities without a sequence property are strictly sorted before activities with a sequence property. In scenarios involving optimistic UI updates, a locally generated 'resolve' activity (which lacks a server-assigned sequence) will be sorted before the corresponding server-confirmed 'request' activity (which has a sequence).

Inside derivePendingApprovals, this inverted order causes the 'approval.resolved' event to be processed before the 'approval.requested' event. Consequently, the resolution is ignored (as the request is not yet tracked), and the request is subsequently added and remains permanently 'pending' in the resulting state.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/orchestration/projector.ts around line 524:

The `compareThreadActivities` comparator sorts unsequenced activities *before* sequenced ones, but `.slice(-500)` keeps the *end* of the array. When the thread is saturated with 500+ sequenced activities, any new unsequenced activity gets sorted to the front and immediately discarded. Consider reversing the sort order so unsequenced items appear after sequenced ones, or use `.slice(0, 500)` instead.

Evidence trail:
apps/server/src/orchestration/projector.ts lines 127-142 (compareThreadActivities function showing unsequenced items sorted before sequenced: `else if (left.sequence !== undefined) { return 1; } else if (right.sequence !== undefined) { return -1; }`), lines 519-524 (`.toSorted(compareThreadActivities).slice(-500)` usage)

Also found in 1 other location(s):
- apps/web/src/session-logic.ts:166 -- The new sorting function `compareActivitiesByOrder` introduces a logic regression where activities without a `sequence` property are strictly sorted *before* activities with a `sequence` property. In scenarios involving optimistic UI updates, a locally generated 'resolve' activity (which lacks a server-assigned `sequence`) will be sorted before the corresponding server-confirmed 'request' activity (which has a `sequence`). 

Inside `derivePendingApprovals`, this inverted order causes the 'approval.resolved' event to be processed before the 'approval.requested' event. Consequently, the resolution is ignored (as the request is not yet tracked), and the request is subsequently added and remains permanently 'pending' in the resulting state.

@juliusmarminge juliusmarminge changed the title feat: clade code feat: Claude Code & Cursor Agent Mar 1, 2026
channel: "client->server",
message,
});
this.#child.stdin.write(`${JSON.stringify(message)}\n`);
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 scripts/cursor-acp-probe.mjs:140

Consider wrapping this.#child.stdin.write() in try/catch (lines 140 and 173) to handle EPIPE errors if the child exits between the #closed check and the write.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file scripts/cursor-acp-probe.mjs around line 140:

Consider wrapping `this.#child.stdin.write()` in try/catch (lines 140 and 173) to handle `EPIPE` errors if the child exits between the `#closed` check and the write.

Evidence trail:
scripts/cursor-acp-probe.mjs lines 121, 140 (send method with #closed check then write); lines 162, 173 (#respond method with #closed check then write); lines 88-90 (#closed is set in async 'exit' event handler); git_grep confirms no stdin error handler or EPIPE handling exists in the file.

</svg>
);

export const Gemini: Icon = (props) => (
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 components/Icons.tsx:38

Hardcoded SVG IDs (gemini__a, gemini__b, etc.) will conflict if multiple Gemini icons render on the same page. Consider using React.useId() to generate unique prefixes for each instance.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/components/Icons.tsx around line 38:

Hardcoded SVG IDs (`gemini__a`, `gemini__b`, etc.) will conflict if multiple `Gemini` icons render on the same page. Consider using `React.useId()` to generate unique prefixes for each instance.

Evidence trail:
apps/web/src/components/Icons.tsx lines 38-130 (REVIEWED_COMMIT): Gemini component contains hardcoded SVG IDs including `id="gemini__a"` (line 40), `id="gemini__b"` (line 90), `id="gemini__c"` (line 102), etc. These are referenced via `mask="url(#gemini__a)"` (line 56) and `filter="url(#gemini__b)"` (line 57), `filter="url(#gemini__c)"` (line 60), etc. HTML spec requires document-unique IDs.

...process.env,
NO_COLOR: "1",
},
});
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 scripts/cursor-acp-probe.mjs:76

Consider adding an error event handler to this.#child. If agent isn't found, Node emits an error event that will go unhandled, potentially crashing the process.

 });
+
+    this.#child.on("error", (err) => {
+      this.#closed = true;
+      for (const [id, pending] of this.#pending.entries()) {
+        this.#pending.delete(id);
+        pending.reject(err);
+      }
+      this.#onMessage({
+        ts: nowIso(),
+        channel: "lifecycle",
+        event: "error",
+        error: err.message,
+      });
+    });
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file scripts/cursor-acp-probe.mjs around line 76:

Consider adding an `error` event handler to `this.#child`. If `agent` isn't found, Node emits an `error` event that will go unhandled, potentially crashing the process.

Evidence trail:
scripts/cursor-acp-probe.mjs lines 71, 77-96 (REVIEWED_COMMIT) - shows spawn() call and event handlers (only 'exit' handler, no 'error' handler). git_grep for '#child.*error' returned no results confirming no error handler exists.

@juliusmarminge juliusmarminge force-pushed the codething/648ca884 branch 2 times, most recently from 3e4539f to 162d5e6 Compare March 2, 2026 07:54
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Debugging repro script committed to repository
    • Deleted the accidentally committed debugging reproduction script logger-scope-repro.ts which was not referenced by any imports, tests, or build configurations.

Create PR

Or push these changes by commenting:

@cursor push 05feb141ca
Preview (05feb141ca)
diff --git a/apps/server/scripts/logger-scope-repro.ts b/apps/server/scripts/logger-scope-repro.ts
deleted file mode 100644
--- a/apps/server/scripts/logger-scope-repro.ts
+++ /dev/null
@@ -1,66 +1,0 @@
-import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
-import * as NodeServices from "@effect/platform-node/NodeServices";
-import path from "node:path";
-
-import { Effect, FileSystem, Layer, Logger, ServiceMap } from "effect";
-
-import { makeEventNdjsonLogger } from "../src/provider/Layers/EventNdjsonLogger.ts";
-
-class LogDir extends ServiceMap.Service<LogDir, string>()("t3/scripts/logger-scope-repro/LogDir") {}
-
-const main = Effect.gen(function* () {
-  const logdir = yield* LogDir;
-  const providerLogPath = path.join(logdir, "provider");
-
-  yield* Effect.logInfo(`providerLogPath=${providerLogPath}`);
-
-  const providerLogger = yield* makeEventNdjsonLogger(providerLogPath, {
-    stream: "native",
-    batchWindowMs: 10,
-  });
-
-  yield* Effect.logInfo("before provider write");
-
-  if (providerLogger) {
-    yield* providerLogger.write(
-      {
-        kind: "probe",
-        message: "provider-only event",
-      },
-      "thread-123" as never,
-    );
-  }
-
-  yield* Effect.logInfo("after provider write");
-  yield* Effect.sleep("50 millis");
-
-  if (providerLogger) {
-    yield* providerLogger.close();
-  }
-  yield* Effect.logInfo("after provider close");
-
-  const fs = yield* FileSystem.FileSystem;
-  const logContents = yield* fs
-    .readDirectory(logdir, { recursive: true })
-    .pipe(
-      Effect.flatMap((entries) =>
-        Effect.all(entries.map((entry) => fs.readFileString(path.join(logdir, entry)))),
-      ),
-    );
-  yield* Effect.logInfo(`logContents=${logContents}`);
-});
-
-Effect.gen(function* () {
-  const fs = yield* FileSystem.FileSystem;
-  const logdir = path.join(process.cwd(), "logtest");
-  yield* fs.makeDirectory(logdir);
-
-  const fileLogger = yield* Logger.formatSimple.pipe(
-    Logger.toFile(path.join(logdir, "global.log")),
-  );
-  const dualLogger = Logger.layer([fileLogger, Logger.consolePretty()]);
-
-  const mainLayer = Layer.mergeAll(dualLogger, Layer.succeed(LogDir, logdir));
-
-  yield* main.pipe(Effect.provide(mainLayer));
-}).pipe(Effect.scoped, Effect.provide(NodeServices.layer), NodeRuntime.runMain);
\ No newline at end of file

const mainLayer = Layer.mergeAll(dualLogger, Layer.succeed(LogDir, logdir));

yield* main.pipe(Effect.provide(mainLayer));
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer), NodeRuntime.runMain);
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.

Debugging repro script committed to repository

Low Severity

logger-scope-repro.ts appears to be a one-off debugging reproduction script (the filename literally says "repro"). It contains hardcoded test paths like "thread-123" as never, writes to a local logtest directory relative to cwd, and is not referenced by any test or build configuration. This looks like a personal debugging artifact that was unintentionally included in the PR.

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Dummy comment accidentally committed in production code
    • Removed the dummy comments from both main.ts and ProviderAdapterRegistry.ts.
  • ✅ Fixed: Unsafe null cast of adapterHarness when using real Codex
    • Made adapterHarness nullable in the OrchestrationIntegrationHarness interface and removed the unsafe as cast, so TypeScript now enforces null checks at compile time.

Create PR

Or push these changes by commenting:

@cursor push a1a05caf71
Preview (a1a05caf71)
diff --git a/apps/server/integration/OrchestrationEngineHarness.integration.ts b/apps/server/integration/OrchestrationEngineHarness.integration.ts
--- a/apps/server/integration/OrchestrationEngineHarness.integration.ts
+++ b/apps/server/integration/OrchestrationEngineHarness.integration.ts
@@ -152,7 +152,7 @@
   readonly rootDir: string;
   readonly workspaceDir: string;
   readonly dbPath: string;
-  readonly adapterHarness: TestProviderAdapterHarness;
+  readonly adapterHarness: TestProviderAdapterHarness | null;
   readonly engine: OrchestrationEngineShape;
   readonly snapshotQuery: ProjectionSnapshotQuery["Service"];
   readonly providerService: ProviderService["Service"];
@@ -435,7 +435,7 @@
       rootDir,
       workspaceDir,
       dbPath,
-      adapterHarness: adapterHarness as TestProviderAdapterHarness,
+      adapterHarness,
       engine,
       snapshotQuery,
       providerService,

diff --git a/apps/server/integration/orchestrationEngine.integration.test.ts b/apps/server/integration/orchestrationEngine.integration.test.ts
--- a/apps/server/integration/orchestrationEngine.integration.test.ts
+++ b/apps/server/integration/orchestrationEngine.integration.test.ts
@@ -180,7 +180,7 @@
         ],
       };
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession(turnResponse);
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession(turnResponse);
       yield* startTurn({
         harness,
         commandId: "cmd-turn-start-single",
@@ -314,7 +314,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -373,7 +373,7 @@
         (entry) => entry.checkpoints.length === 1 && entry.session?.threadId === "thread-1",
       );
 
-      yield* harness.adapterHarness.queueTurnResponse(THREAD_ID, {
+      yield* harness.adapterHarness!.queueTurnResponse(THREAD_ID, {
         events: [
           {
             type: "turn.started",
@@ -473,7 +473,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -538,7 +538,7 @@
       assert.equal(resolvedRow.decision, "accept");
 
       const approvalResponses = yield* waitForSync(
-        () => harness.adapterHarness.getApprovalResponses(THREAD_ID),
+        () => harness.adapterHarness!.getApprovalResponses(THREAD_ID),
         (responses) => responses.length === 1,
         "provider approval response",
       );
@@ -554,7 +554,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -633,7 +633,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -691,7 +691,7 @@
         (entry) => entry.session?.threadId === "thread-1" && entry.checkpoints.length === 1,
       );
 
-      yield* harness.adapterHarness.queueTurnResponse(THREAD_ID, {
+      yield* harness.adapterHarness!.queueTurnResponse(THREAD_ID, {
         events: [
           {
             type: "turn.started",
@@ -796,7 +796,7 @@
         gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 2)),
         false,
       );
-      assert.deepEqual(harness.adapterHarness.getRollbackCalls(THREAD_ID), [1]);
+      assert.deepEqual(harness.adapterHarness!.getRollbackCalls(THREAD_ID), [1]);
 
       const checkpointRows = yield* harness.checkpointRepository.listByThreadId({
         threadId: THREAD_ID,

diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts
--- a/apps/server/src/main.ts
+++ b/apps/server/src/main.ts
@@ -9,8 +9,6 @@
 import { Config, Data, Effect, FileSystem, Layer, Option, Path, Schema, ServiceMap } from "effect";
 import { Command, Flag } from "effect/unstable/cli";
 import { NetService } from "@t3tools/shared/Net";
-
-// Dummy comment.
 import {
   DEFAULT_PORT,
   resolveStaticDir,

diff --git a/apps/server/src/provider/Services/ProviderAdapterRegistry.ts b/apps/server/src/provider/Services/ProviderAdapterRegistry.ts
--- a/apps/server/src/provider/Services/ProviderAdapterRegistry.ts
+++ b/apps/server/src/provider/Services/ProviderAdapterRegistry.ts
@@ -39,4 +39,3 @@
   ProviderAdapterRegistryShape
 >()("t3/provider/Services/ProviderAdapterRegistry") {}
 
-// Dummy comment for workflow testing.

diff --git a/bun.lock b/bun.lock
--- a/bun.lock
+++ b/bun.lock
@@ -37,7 +37,6 @@
         "t3": "./dist/index.mjs",
       },
       "dependencies": {
-        "@anthropic-ai/claude-agent-sdk": "^0.2.62",
         "@effect/platform-node": "catalog:",
         "@effect/sql-sqlite-bun": "catalog:",
         "@pierre/diffs": "^1.1.0-beta.16",
@@ -163,8 +162,6 @@
     "vitest": "^4.0.0",
   },
   "packages": {
-    "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/[email protected]", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-ABaB37jWt7dZXfIDHebHv99jX9GIyqc0aSjcz9nxq79eauOpa+64Cah5hx/yzhsWz7m5GEtjbMIZCClTfnRRhg=="],
-
     "@babel/code-frame": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
 
     "@babel/compat-data": ["@babel/[email protected]", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
@@ -305,38 +302,6 @@
 
     "@hapi/topo": ["@hapi/[email protected]", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg=="],
 
-    "@img/sharp-darwin-arm64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
-
-    "@img/sharp-darwin-x64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
-
-    "@img/sharp-libvips-darwin-arm64": ["@img/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
-
-    "@img/sharp-libvips-darwin-x64": ["@img/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
-
-    "@img/sharp-libvips-linux-arm": ["@img/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
-
-    "@img/sharp-libvips-linux-arm64": ["@img/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
-
-    "@img/sharp-libvips-linux-x64": ["@img/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
-
-    "@img/sharp-libvips-linuxmusl-arm64": ["@img/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
-
-    "@img/sharp-libvips-linuxmusl-x64": ["@img/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
-
-    "@img/sharp-linux-arm": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
-
-    "@img/sharp-linux-arm64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
-
-    "@img/sharp-linux-x64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
-
-    "@img/sharp-linuxmusl-arm64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
-
-    "@img/sharp-linuxmusl-x64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
-
-    "@img/sharp-win32-arm64": ["@img/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
-
-    "@img/sharp-win32-x64": ["@img/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
-
     "@inquirer/ansi": ["@inquirer/[email protected]", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="],
 
     "@inquirer/confirm": ["@inquirer/[email protected]", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="],

import { Command, Flag } from "effect/unstable/cli";
import { NetService } from "@t3tools/shared/Net";

// Dummy comment.
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.

Dummy comment accidentally committed in production code

Low Severity

// Dummy comment. on line 13 of main.ts appears to be a leftover from testing or workflow validation and was accidentally included in the commit. A second instance (// Dummy comment for workflow testing.) was also found in ProviderAdapterRegistry.ts.

Fix in Cursor Fix in Web

rootDir,
workspaceDir,
dbPath,
adapterHarness: adapterHarness as TestProviderAdapterHarness,
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.

Unsafe null cast of adapterHarness when using real Codex

Low Severity

When useRealCodex is true, adapterHarness is assigned null on line 203, but on line 438 it is unconditionally cast to TestProviderAdapterHarness via as. Any future test using the realCodex option that accesses harness.adapterHarness (e.g., queueTurnResponse, getRollbackCalls) will hit a null-reference crash at runtime with no compile-time warning.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix prepared fixes for 2 of the 3 issues found in the latest run.

  • ✅ Fixed: Harness cast hides null adapter when realCodex enabled
    • Made adapterHarness nullable in the OrchestrationIntegrationHarness interface and removed the unsafe as TestProviderAdapterHarness cast, so null-pointer errors surface at compile time.
  • ✅ Fixed: File-read approval request creates wrong pending approval method
    • Extended the ternary to map requestKind === "file-read" to "item/fileRead/requestApproval" and added the new method string to the PendingApprovalRequest.method type.

Create PR

Or push these changes by commenting:

@cursor push c10d06aba4
Preview (c10d06aba4)
diff --git a/apps/server/integration/OrchestrationEngineHarness.integration.ts b/apps/server/integration/OrchestrationEngineHarness.integration.ts
--- a/apps/server/integration/OrchestrationEngineHarness.integration.ts
+++ b/apps/server/integration/OrchestrationEngineHarness.integration.ts
@@ -152,7 +152,7 @@
   readonly rootDir: string;
   readonly workspaceDir: string;
   readonly dbPath: string;
-  readonly adapterHarness: TestProviderAdapterHarness;
+  readonly adapterHarness: TestProviderAdapterHarness | null;
   readonly engine: OrchestrationEngineShape;
   readonly snapshotQuery: ProjectionSnapshotQuery["Service"];
   readonly providerService: ProviderService["Service"];
@@ -435,7 +435,7 @@
       rootDir,
       workspaceDir,
       dbPath,
-      adapterHarness: adapterHarness as TestProviderAdapterHarness,
+      adapterHarness,
       engine,
       snapshotQuery,
       providerService,

diff --git a/apps/server/integration/orchestrationEngine.integration.test.ts b/apps/server/integration/orchestrationEngine.integration.test.ts
--- a/apps/server/integration/orchestrationEngine.integration.test.ts
+++ b/apps/server/integration/orchestrationEngine.integration.test.ts
@@ -180,7 +180,7 @@
         ],
       };
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession(turnResponse);
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession(turnResponse);
       yield* startTurn({
         harness,
         commandId: "cmd-turn-start-single",
@@ -314,7 +314,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -373,7 +373,7 @@
         (entry) => entry.checkpoints.length === 1 && entry.session?.threadId === "thread-1",
       );
 
-      yield* harness.adapterHarness.queueTurnResponse(THREAD_ID, {
+      yield* harness.adapterHarness!.queueTurnResponse(THREAD_ID, {
         events: [
           {
             type: "turn.started",
@@ -473,7 +473,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -538,7 +538,7 @@
       assert.equal(resolvedRow.decision, "accept");
 
       const approvalResponses = yield* waitForSync(
-        () => harness.adapterHarness.getApprovalResponses(THREAD_ID),
+        () => harness.adapterHarness!.getApprovalResponses(THREAD_ID),
         (responses) => responses.length === 1,
         "provider approval response",
       );
@@ -554,7 +554,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -633,7 +633,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -691,7 +691,7 @@
         (entry) => entry.session?.threadId === "thread-1" && entry.checkpoints.length === 1,
       );
 
-      yield* harness.adapterHarness.queueTurnResponse(THREAD_ID, {
+      yield* harness.adapterHarness!.queueTurnResponse(THREAD_ID, {
         events: [
           {
             type: "turn.started",
@@ -796,7 +796,7 @@
         gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 2)),
         false,
       );
-      assert.deepEqual(harness.adapterHarness.getRollbackCalls(THREAD_ID), [1]);
+      assert.deepEqual(harness.adapterHarness!.getRollbackCalls(THREAD_ID), [1]);
 
       const checkpointRows = yield* harness.checkpointRepository.listByThreadId({
         threadId: THREAD_ID,

diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts
--- a/apps/server/src/codexAppServerManager.ts
+++ b/apps/server/src/codexAppServerManager.ts
@@ -34,7 +34,10 @@
 interface PendingApprovalRequest {
   requestId: ApprovalRequestId;
   jsonRpcId: string | number;
-  method: "item/commandExecution/requestApproval" | "item/fileChange/requestApproval";
+  method:
+    | "item/commandExecution/requestApproval"
+    | "item/fileChange/requestApproval"
+    | "item/fileRead/requestApproval";
   requestKind: ProviderRequestKind;
   threadId: ThreadId;
   turnId?: TurnId;
@@ -1064,7 +1067,9 @@
         method:
           requestKind === "command"
             ? "item/commandExecution/requestApproval"
-            : "item/fileChange/requestApproval",
+            : requestKind === "file-read"
+              ? "item/fileRead/requestApproval"
+              : "item/fileChange/requestApproval",
         requestKind,
         threadId: context.session.threadId,
         ...(route.turnId ? { turnId: route.turnId } : {}),

rootDir,
workspaceDir,
dbPath,
adapterHarness: adapterHarness as TestProviderAdapterHarness,
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.

Harness cast hides null adapter when realCodex enabled

Medium Severity

When useRealCodex is true, adapterHarness is set to null (line 203), but the return value casts it to TestProviderAdapterHarness via as TestProviderAdapterHarness (line 438). Any test code that accesses harness.adapterHarness methods (like queueTurnResponse, getRollbackCalls, etc.) on a real-codex harness will get a null-pointer crash at runtime instead of a clear type error at compile time.

Fix in Cursor Fix in Web

if (input.interactionMode !== "plan") {
return undefined;
}
const model = normalizeCodexModelSlug(input.model) ?? "gpt-5.3-codex";
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.

Plan mode always uses hardcoded model ignoring session model

Medium Severity

buildCodexCollaborationMode uses normalizeCodexModelSlug(input.model) which falls back to the hardcoded "gpt-5.3-codex" when input.model is undefined. However, sendTurn already resolves the model from the session (input.model ?? context.session.model) on line 659, but buildCodexCollaborationMode is called with input.model (the raw turn input) rather than the already-resolved normalizedModel. This means the collaboration mode's settings.model may diverge from the actual turnStartParams.model.

Fix in Cursor Fix in Web

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.

Bugbot Autofix determined this is a false positive.

The code already passes normalizedModel (which includes the session model fallback from input.model ?? context.session.model) to buildCodexCollaborationMode, not the raw input.model, so the model is correctly resolved.

: "item/fileChange/requestApproval",
requestKind,
...(route.threadId ? { threadId: route.threadId } : {}),
threadId: context.session.threadId,
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.

File-read approval request creates wrong pending approval method

Medium Severity

The requestKindForMethod now returns "file-read" for "item/fileRead/requestApproval", but the PendingApprovalRequest.method is set via a ternary that only maps "command" to its method and falls through to "item/fileChange/requestApproval" for everything else, including "file-read". This means file-read approval requests are tracked with the wrong method value in their pending approval entry.

Fix in Cursor Fix in Web

export const ProviderKind = Schema.Literals(["codex", "cursor"]);
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/orchestration.ts:30

The ProviderKind schema removes "claudeCode" and replaces it with "cursor". Existing persisted events that recorded "claudeCode" will fail schema validation when deserialized via replayEvents or getSnapshot, causing application errors and preventing access to historical thread data. Consider keeping "claudeCode" as a valid literal to maintain backward compatibility with stored events.

-export const ProviderKind = Schema.Literals(["codex", "cursor"]);
+export const ProviderKind = Schema.Literals(["codex", "claudeCode", "cursor"]);
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/contracts/src/orchestration.ts around line 30:

The `ProviderKind` schema removes `"claudeCode"` and replaces it with `"cursor"`. Existing persisted events that recorded `"claudeCode"` will fail schema validation when deserialized via `replayEvents` or `getSnapshot`, causing application errors and preventing access to historical thread data. Consider keeping `"claudeCode"` as a valid literal to maintain backward compatibility with stored events.

Evidence trail:
packages/contracts/src/orchestration.ts line 30: `ProviderKind = Schema.Literals(["codex", "cursor"])` (diff shows it was `["codex", "claudeCode"]` before); packages/contracts/src/orchestration.ts line 651: `ThreadTurnStartRequestedPayload` uses `provider: Schema.optional(ProviderKind)`; packages/contracts/src/orchestration.ts line 911 (via grep result): `ThreadTurnStartRequestedPayload` is part of `OrchestrationPersistedEvent`; git_diff MERGE_BASE..REVIEWED_COMMIT on packages/contracts/src/orchestration.ts confirms the schema literal change.

@@ -33,33 +33,76 @@ const THEME_OPTIONS = [
},
] as const;

const RUNTIME_MODE_OPTIONS = [
const MODEL_PROVIDER_SETTINGS: Array<{
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 routes/_chat.settings.tsx:36

The MODEL_PROVIDER_SETTINGS array only contains the codex provider entry, missing the cursor entry. Because the Models section (lines 311–432) renders UI by iterating over this array, no configuration UI appears for Cursor custom models even though getCustomModelsForProvider and related helpers support provider: "cursor". Add a cursor entry to MODEL_PROVIDER_SETTINGS so the feature is accessible.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/routes/_chat.settings.tsx around line 36:

The `MODEL_PROVIDER_SETTINGS` array only contains the `codex` provider entry, missing the `cursor` entry. Because the Models section (lines 311–432) renders UI by iterating over this array, no configuration UI appears for Cursor custom models even though `getCustomModelsForProvider` and related helpers support `provider: "cursor"`. Add a `cursor` entry to `MODEL_PROVIDER_SETTINGS` so the feature is accessible.

Evidence trail:
apps/web/src/routes/_chat.settings.tsx lines 36-51: MODEL_PROVIDER_SETTINGS array contains only codex entry. Lines 55-85: getCustomModelsForProvider, getDefaultCustomModelsForProvider, and patchCustomModels all have explicit `case "cursor":` handling. Line 312: `MODEL_PROVIDER_SETTINGS.map((providerSettings) => {...})` drives the Models section UI rendering.

runtimeMode: RuntimeMode,
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 src/orchestration.ts:238

The OrchestrationThread schema adds runtimeMode as a required field without a decoding default, while interactionMode and proposedPlans both use withDecodingDefault. When decoding existing thread snapshots from before this change, validation fails because runtimeMode is missing from persisted data, causing the application to crash or reject legacy threads. Consider adding Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE) to runtimeMode to match the pattern used for interactionMode.

-  runtimeMode: RuntimeMode,
+  runtimeMode: RuntimeMode.pipe(
+    Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE),
+  ),
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/contracts/src/orchestration.ts around line 238:

The `OrchestrationThread` schema adds `runtimeMode` as a required field without a decoding default, while `interactionMode` and `proposedPlans` both use `withDecodingDefault`. When decoding existing thread snapshots from before this change, validation fails because `runtimeMode` is missing from persisted data, causing the application to crash or reject legacy threads. Consider adding `Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE)` to `runtimeMode` to match the pattern used for `interactionMode`.

Evidence trail:
packages/contracts/src/orchestration.ts:238 - `runtimeMode: RuntimeMode` without withDecodingDefault; packages/contracts/src/orchestration.ts:239-241 - `interactionMode` with withDecodingDefault; packages/contracts/src/orchestration.ts:249-251 - `proposedPlans` with withDecodingDefault; packages/contracts/src/orchestration.ts:166,354,654 - other schemas use `RuntimeMode.pipe(Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE))`; packages/contracts/src/orchestration.ts:34 - `DEFAULT_RUNTIME_MODE` constant exists; git_log shows runtimeMode added in commit bb9157857b on 2026-03-04

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Dummy comment accidentally left in production code
    • Removed the meaningless // Dummy comment. line from main.ts.
  • ✅ Fixed: Null adapter harness unsafely cast to non-null type
    • Made adapterHarness nullable (TestProviderAdapterHarness | null) in the interface and removed the unsafe as cast, with non-null assertions added at test call sites that are guaranteed non-null.
  • ✅ Fixed: File-read approval stored with wrong method value
    • Extended the PendingApprovalRequest.method type union to include "item/fileRead/requestApproval" and expanded the binary ternary to a three-way conditional that correctly maps the "file-read" request kind.

Create PR

Or push these changes by commenting:

@cursor push 6ee7721566
Preview (6ee7721566)
diff --git a/apps/server/integration/OrchestrationEngineHarness.integration.ts b/apps/server/integration/OrchestrationEngineHarness.integration.ts
--- a/apps/server/integration/OrchestrationEngineHarness.integration.ts
+++ b/apps/server/integration/OrchestrationEngineHarness.integration.ts
@@ -152,7 +152,7 @@
   readonly rootDir: string;
   readonly workspaceDir: string;
   readonly dbPath: string;
-  readonly adapterHarness: TestProviderAdapterHarness;
+  readonly adapterHarness: TestProviderAdapterHarness | null;
   readonly engine: OrchestrationEngineShape;
   readonly snapshotQuery: ProjectionSnapshotQuery["Service"];
   readonly providerService: ProviderService["Service"];
@@ -435,7 +435,7 @@
       rootDir,
       workspaceDir,
       dbPath,
-      adapterHarness: adapterHarness as TestProviderAdapterHarness,
+      adapterHarness,
       engine,
       snapshotQuery,
       providerService,

diff --git a/apps/server/integration/orchestrationEngine.integration.test.ts b/apps/server/integration/orchestrationEngine.integration.test.ts
--- a/apps/server/integration/orchestrationEngine.integration.test.ts
+++ b/apps/server/integration/orchestrationEngine.integration.test.ts
@@ -180,7 +180,7 @@
         ],
       };
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession(turnResponse);
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession(turnResponse);
       yield* startTurn({
         harness,
         commandId: "cmd-turn-start-single",
@@ -314,7 +314,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -373,7 +373,7 @@
         (entry) => entry.checkpoints.length === 1 && entry.session?.threadId === "thread-1",
       );
 
-      yield* harness.adapterHarness.queueTurnResponse(THREAD_ID, {
+      yield* harness.adapterHarness!.queueTurnResponse(THREAD_ID, {
         events: [
           {
             type: "turn.started",
@@ -473,7 +473,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -538,7 +538,7 @@
       assert.equal(resolvedRow.decision, "accept");
 
       const approvalResponses = yield* waitForSync(
-        () => harness.adapterHarness.getApprovalResponses(THREAD_ID),
+        () => harness.adapterHarness!.getApprovalResponses(THREAD_ID),
         (responses) => responses.length === 1,
         "provider approval response",
       );
@@ -554,7 +554,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -633,7 +633,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -691,7 +691,7 @@
         (entry) => entry.session?.threadId === "thread-1" && entry.checkpoints.length === 1,
       );
 
-      yield* harness.adapterHarness.queueTurnResponse(THREAD_ID, {
+      yield* harness.adapterHarness!.queueTurnResponse(THREAD_ID, {
         events: [
           {
             type: "turn.started",
@@ -796,7 +796,7 @@
         gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 2)),
         false,
       );
-      assert.deepEqual(harness.adapterHarness.getRollbackCalls(THREAD_ID), [1]);
+      assert.deepEqual(harness.adapterHarness!.getRollbackCalls(THREAD_ID), [1]);
 
       const checkpointRows = yield* harness.checkpointRepository.listByThreadId({
         threadId: THREAD_ID,

diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts
--- a/apps/server/src/codexAppServerManager.ts
+++ b/apps/server/src/codexAppServerManager.ts
@@ -34,7 +34,10 @@
 interface PendingApprovalRequest {
   requestId: ApprovalRequestId;
   jsonRpcId: string | number;
-  method: "item/commandExecution/requestApproval" | "item/fileChange/requestApproval";
+  method:
+    | "item/commandExecution/requestApproval"
+    | "item/fileChange/requestApproval"
+    | "item/fileRead/requestApproval";
   requestKind: ProviderRequestKind;
   threadId: ThreadId;
   turnId?: TurnId;
@@ -1064,7 +1067,9 @@
         method:
           requestKind === "command"
             ? "item/commandExecution/requestApproval"
-            : "item/fileChange/requestApproval",
+            : requestKind === "file-read"
+              ? "item/fileRead/requestApproval"
+              : "item/fileChange/requestApproval",
         requestKind,
         threadId: context.session.threadId,
         ...(route.turnId ? { turnId: route.turnId } : {}),

diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts
--- a/apps/server/src/main.ts
+++ b/apps/server/src/main.ts
@@ -10,7 +10,6 @@
 import { Command, Flag } from "effect/unstable/cli";
 import { NetService } from "@t3tools/shared/Net";
 
-// Dummy comment.
 import {
   DEFAULT_PORT,
   resolveStaticDir,

import { Command, Flag } from "effect/unstable/cli";
import { NetService } from "@t3tools/shared/Net";

// Dummy comment.
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.

Dummy comment accidentally left in production code

Low Severity

// Dummy comment. is left in the production main.ts file between the effect imports and the project imports. This appears to be a development artifact that was accidentally included in the commit.

Fix in Cursor Fix in Web

rootDir,
workspaceDir,
dbPath,
adapterHarness: adapterHarness as TestProviderAdapterHarness,
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.

Null adapter harness unsafely cast to non-null type

Medium Severity

When useRealCodex is true, adapterHarness is set to null but then cast to TestProviderAdapterHarness via adapterHarness as TestProviderAdapterHarness. Any test using withRealCodexHarness that accesses harness.adapterHarness (e.g., calling queueTurnResponse or getRollbackCalls) will throw a null reference error at runtime, with no type-level warning.

Additional Locations (1)

Fix in Cursor Fix in Web

: "item/fileChange/requestApproval",
requestKind,
...(route.threadId ? { threadId: route.threadId } : {}),
threadId: context.session.threadId,
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.

File-read approval stored with wrong method value

Low Severity

requestKindForMethod now returns "file-read" for "item/fileRead/requestApproval", but the PendingApprovalRequest constructor uses a binary ternary that maps any non-"command" kind to "item/fileChange/requestApproval". This means file-read approval requests are recorded with the wrong method, misrepresenting them as file-change approvals.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Dummy comment accidentally committed in production code
    • Removed the meaningless // Dummy comment. line from main.ts.
  • ✅ Fixed: File-read approval mapped to wrong pending method
    • Extended the ternary to correctly map file-read requests to item/fileRead/requestApproval and added the new method variant to the PendingApprovalRequest type union.
  • ✅ Fixed: Null adapter harness unsafely cast to non-null type
    • Changed the OrchestrationIntegrationHarness.adapterHarness type to TestProviderAdapterHarness | null, removed the unsafe as cast, and updated test call sites with explicit non-null assertions.

Create PR

Or push these changes by commenting:

@cursor push 145cc98f55
Preview (145cc98f55)
diff --git a/apps/server/integration/OrchestrationEngineHarness.integration.ts b/apps/server/integration/OrchestrationEngineHarness.integration.ts
--- a/apps/server/integration/OrchestrationEngineHarness.integration.ts
+++ b/apps/server/integration/OrchestrationEngineHarness.integration.ts
@@ -152,7 +152,7 @@
   readonly rootDir: string;
   readonly workspaceDir: string;
   readonly dbPath: string;
-  readonly adapterHarness: TestProviderAdapterHarness;
+  readonly adapterHarness: TestProviderAdapterHarness | null;
   readonly engine: OrchestrationEngineShape;
   readonly snapshotQuery: ProjectionSnapshotQuery["Service"];
   readonly providerService: ProviderService["Service"];
@@ -435,7 +435,7 @@
       rootDir,
       workspaceDir,
       dbPath,
-      adapterHarness: adapterHarness as TestProviderAdapterHarness,
+      adapterHarness,
       engine,
       snapshotQuery,
       providerService,

diff --git a/apps/server/integration/orchestrationEngine.integration.test.ts b/apps/server/integration/orchestrationEngine.integration.test.ts
--- a/apps/server/integration/orchestrationEngine.integration.test.ts
+++ b/apps/server/integration/orchestrationEngine.integration.test.ts
@@ -180,7 +180,7 @@
         ],
       };
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession(turnResponse);
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession(turnResponse);
       yield* startTurn({
         harness,
         commandId: "cmd-turn-start-single",
@@ -314,7 +314,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -373,7 +373,7 @@
         (entry) => entry.checkpoints.length === 1 && entry.session?.threadId === "thread-1",
       );
 
-      yield* harness.adapterHarness.queueTurnResponse(THREAD_ID, {
+      yield* harness.adapterHarness!.queueTurnResponse(THREAD_ID, {
         events: [
           {
             type: "turn.started",
@@ -473,7 +473,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -538,7 +538,7 @@
       assert.equal(resolvedRow.decision, "accept");
 
       const approvalResponses = yield* waitForSync(
-        () => harness.adapterHarness.getApprovalResponses(THREAD_ID),
+        () => harness.adapterHarness!.getApprovalResponses(THREAD_ID),
         (responses) => responses.length === 1,
         "provider approval response",
       );
@@ -554,7 +554,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -633,7 +633,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -691,7 +691,7 @@
         (entry) => entry.session?.threadId === "thread-1" && entry.checkpoints.length === 1,
       );
 
-      yield* harness.adapterHarness.queueTurnResponse(THREAD_ID, {
+      yield* harness.adapterHarness!.queueTurnResponse(THREAD_ID, {
         events: [
           {
             type: "turn.started",
@@ -796,7 +796,7 @@
         gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 2)),
         false,
       );
-      assert.deepEqual(harness.adapterHarness.getRollbackCalls(THREAD_ID), [1]);
+      assert.deepEqual(harness.adapterHarness!.getRollbackCalls(THREAD_ID), [1]);
 
       const checkpointRows = yield* harness.checkpointRepository.listByThreadId({
         threadId: THREAD_ID,

diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts
--- a/apps/server/src/codexAppServerManager.ts
+++ b/apps/server/src/codexAppServerManager.ts
@@ -34,7 +34,10 @@
 interface PendingApprovalRequest {
   requestId: ApprovalRequestId;
   jsonRpcId: string | number;
-  method: "item/commandExecution/requestApproval" | "item/fileChange/requestApproval";
+  method:
+    | "item/commandExecution/requestApproval"
+    | "item/fileChange/requestApproval"
+    | "item/fileRead/requestApproval";
   requestKind: ProviderRequestKind;
   threadId: ThreadId;
   turnId?: TurnId;
@@ -1064,7 +1067,9 @@
         method:
           requestKind === "command"
             ? "item/commandExecution/requestApproval"
-            : "item/fileChange/requestApproval",
+            : requestKind === "file-read"
+              ? "item/fileRead/requestApproval"
+              : "item/fileChange/requestApproval",
         requestKind,
         threadId: context.session.threadId,
         ...(route.turnId ? { turnId: route.turnId } : {}),

diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts
--- a/apps/server/src/main.ts
+++ b/apps/server/src/main.ts
@@ -10,7 +10,6 @@
 import { Command, Flag } from "effect/unstable/cli";
 import { NetService } from "@t3tools/shared/Net";
 
-// Dummy comment.
 import {
   DEFAULT_PORT,
   resolveStaticDir,

import { Command, Flag } from "effect/unstable/cli";
import { NetService } from "@t3tools/shared/Net";

// Dummy comment.
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.

Dummy comment accidentally committed in production code

Low Severity

A // Dummy comment. was left in the production main.ts file on line 13, immediately before the import block. This appears to be a debugging artifact or placeholder that was not intended for the final commit.

Fix in Cursor Fix in Web

: "item/fileChange/requestApproval",
requestKind,
...(route.threadId ? { threadId: route.threadId } : {}),
threadId: context.session.threadId,
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.

File-read approval mapped to wrong pending method

Low Severity

requestKindForMethod now returns "file-read" for "item/fileRead/requestApproval", but the PendingApprovalRequest method assignment uses a binary ternary that only distinguishes "command" from everything else. A file-read request kind falls into the else branch and is incorrectly labeled as "item/fileChange/requestApproval". The PendingApprovalRequest.method type union also doesn't include the new file-read variant.

Additional Locations (1)

Fix in Cursor Fix in Web

rootDir,
workspaceDir,
dbPath,
adapterHarness: adapterHarness as TestProviderAdapterHarness,
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.

Null adapter harness unsafely cast to non-null type

Low Severity

When useRealCodex is true, adapterHarness is null, but it is cast to TestProviderAdapterHarness via as. Any test that accesses harness.adapterHarness methods (like queueTurnResponse or getRollbackCalls) when realCodex is true will crash with a null dereference at runtime, with no type-system warning.

Fix in Cursor Fix in Web

Comment on lines +5130 to +5136
function isAvailableProviderOption(option: (typeof PROVIDER_OPTIONS)[number]): option is {
value: ProviderKind;
label: string;
available: true;
} {
return option.available && option.value !== "claudeCode";
}
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 components/ChatView.tsx:5130

The isAvailableProviderOption type guard only checks option.value !== "claudeCode", so if "cursor" is marked available: true in PROVIDER_OPTIONS, it passes the guard even though "cursor" is not a valid ProviderKind. This causes ProviderModelPicker to attempt props.modelOptionsByProvider["cursor"].map(...), which throws because the lookup returns undefined. The type guard's implementation doesn't match its type predicate — it should validate that option.value is actually a ProviderKind, not merely exclude one string.

-function isAvailableProviderOption(option: (typeof PROVIDER_OPTIONS)[number]): option is {
-  value: ProviderKind;
-  label: string;
-  available: true;
-} {
-  return option.available && option.value !== "claudeCode";
+function isAvailableProviderOption(option: (typeof PROVIDER_OPTIONS)[number]): option is {
+  value: ProviderKind;
+  label: string;
+  available: true;
+} {
+  return option.available && option.value !== "claudeCode" && option.value !== "cursor";
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/components/ChatView.tsx around lines 5130-5136:

The `isAvailableProviderOption` type guard only checks `option.value !== "claudeCode"`, so if `"cursor"` is marked `available: true` in `PROVIDER_OPTIONS`, it passes the guard even though `"cursor"` is not a valid `ProviderKind`. This causes `ProviderModelPicker` to attempt `props.modelOptionsByProvider["cursor"].map(...)`, which throws because the lookup returns `undefined`. The type guard's implementation doesn't match its type predicate — it should validate that `option.value` is actually a `ProviderKind`, not merely exclude one string.

Evidence trail:
1. Type guard definition: apps/web/src/components/ChatView.tsx:5130-5136 - `isAvailableProviderOption` claims `option is { value: ProviderKind; ... }` but only checks `option.available && option.value !== "claudeCode"`
2. `ProviderKind` definition: packages/contracts/src/orchestration.ts:30-31 - `Schema.Literal("codex")` means only `"codex"` is valid
3. `PROVIDER_OPTIONS` definition: apps/web/src/session-logic.ts:15-23 - includes `cursor` with `available: false`
4. `modelOptionsByProvider` type: apps/web/src/components/ChatView.tsx:5196 - `Record<ProviderKind, ...>` which is `Record<"codex", ...>`
5. Usage: apps/web/src/components/ChatView.tsx:5265 - `props.modelOptionsByProvider[option.value].map(...)` would fail if `option.value` is `"cursor"`

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Dummy comment accidentally committed in production code
    • Removed the meaningless // Dummy comment. line from main.ts.
  • ✅ Fixed: File-read approval method incorrectly mapped as file-change
    • Added a file-read branch to the ternary in handleServerRequest to correctly map to item/fileRead/requestApproval, and added the new method variant to the PendingApprovalRequest interface type.
  • ✅ Fixed: Null adapter harness unsafely cast to non-null type
    • Changed adapterHarness to TestProviderAdapterHarness | null in the interface, removed the unsafe cast, and updated test usages with non-null assertions.

Create PR

Or push these changes by commenting:

@cursor push abe6d6a3bb
Preview (abe6d6a3bb)
diff --git a/apps/server/integration/OrchestrationEngineHarness.integration.ts b/apps/server/integration/OrchestrationEngineHarness.integration.ts
--- a/apps/server/integration/OrchestrationEngineHarness.integration.ts
+++ b/apps/server/integration/OrchestrationEngineHarness.integration.ts
@@ -152,7 +152,7 @@
   readonly rootDir: string;
   readonly workspaceDir: string;
   readonly dbPath: string;
-  readonly adapterHarness: TestProviderAdapterHarness;
+  readonly adapterHarness: TestProviderAdapterHarness | null;
   readonly engine: OrchestrationEngineShape;
   readonly snapshotQuery: ProjectionSnapshotQuery["Service"];
   readonly providerService: ProviderService["Service"];
@@ -435,7 +435,7 @@
       rootDir,
       workspaceDir,
       dbPath,
-      adapterHarness: adapterHarness as TestProviderAdapterHarness,
+      adapterHarness,
       engine,
       snapshotQuery,
       providerService,

diff --git a/apps/server/integration/orchestrationEngine.integration.test.ts b/apps/server/integration/orchestrationEngine.integration.test.ts
--- a/apps/server/integration/orchestrationEngine.integration.test.ts
+++ b/apps/server/integration/orchestrationEngine.integration.test.ts
@@ -180,7 +180,7 @@
         ],
       };
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession(turnResponse);
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession(turnResponse);
       yield* startTurn({
         harness,
         commandId: "cmd-turn-start-single",
@@ -314,7 +314,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -373,7 +373,7 @@
         (entry) => entry.checkpoints.length === 1 && entry.session?.threadId === "thread-1",
       );
 
-      yield* harness.adapterHarness.queueTurnResponse(THREAD_ID, {
+      yield* harness.adapterHarness!.queueTurnResponse(THREAD_ID, {
         events: [
           {
             type: "turn.started",
@@ -473,7 +473,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -538,7 +538,7 @@
       assert.equal(resolvedRow.decision, "accept");
 
       const approvalResponses = yield* waitForSync(
-        () => harness.adapterHarness.getApprovalResponses(THREAD_ID),
+        () => harness.adapterHarness!.getApprovalResponses(THREAD_ID),
         (responses) => responses.length === 1,
         "provider approval response",
       );
@@ -554,7 +554,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -633,7 +633,7 @@
     Effect.gen(function* () {
       yield* seedProjectAndThread(harness);
 
-      yield* harness.adapterHarness.queueTurnResponseForNextSession({
+      yield* harness.adapterHarness!.queueTurnResponseForNextSession({
         events: [
           {
             type: "turn.started",
@@ -691,7 +691,7 @@
         (entry) => entry.session?.threadId === "thread-1" && entry.checkpoints.length === 1,
       );
 
-      yield* harness.adapterHarness.queueTurnResponse(THREAD_ID, {
+      yield* harness.adapterHarness!.queueTurnResponse(THREAD_ID, {
         events: [
           {
             type: "turn.started",
@@ -796,7 +796,7 @@
         gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 2)),
         false,
       );
-      assert.deepEqual(harness.adapterHarness.getRollbackCalls(THREAD_ID), [1]);
+      assert.deepEqual(harness.adapterHarness!.getRollbackCalls(THREAD_ID), [1]);
 
       const checkpointRows = yield* harness.checkpointRepository.listByThreadId({
         threadId: THREAD_ID,

diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts
--- a/apps/server/src/codexAppServerManager.ts
+++ b/apps/server/src/codexAppServerManager.ts
@@ -34,7 +34,10 @@
 interface PendingApprovalRequest {
   requestId: ApprovalRequestId;
   jsonRpcId: string | number;
-  method: "item/commandExecution/requestApproval" | "item/fileChange/requestApproval";
+  method:
+    | "item/commandExecution/requestApproval"
+    | "item/fileChange/requestApproval"
+    | "item/fileRead/requestApproval";
   requestKind: ProviderRequestKind;
   threadId: ThreadId;
   turnId?: TurnId;
@@ -1064,7 +1067,9 @@
         method:
           requestKind === "command"
             ? "item/commandExecution/requestApproval"
-            : "item/fileChange/requestApproval",
+            : requestKind === "file-read"
+              ? "item/fileRead/requestApproval"
+              : "item/fileChange/requestApproval",
         requestKind,
         threadId: context.session.threadId,
         ...(route.turnId ? { turnId: route.turnId } : {}),

diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts
--- a/apps/server/src/main.ts
+++ b/apps/server/src/main.ts
@@ -10,7 +10,6 @@
 import { Command, Flag } from "effect/unstable/cli";
 import { NetService } from "@t3tools/shared/Net";
 
-// Dummy comment.
 import {
   DEFAULT_PORT,
   resolveStaticDir,

import { Command, Flag } from "effect/unstable/cli";
import { NetService } from "@t3tools/shared/Net";

// Dummy comment.
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.

Dummy comment accidentally committed in production code

Low Severity

The line // Dummy comment. before the import block is a meaningless placeholder comment that appears to have been left in accidentally. It provides no documentation value and clutters the production server entry point.

Fix in Cursor Fix in Web

: "item/fileChange/requestApproval",
requestKind,
...(route.threadId ? { threadId: route.threadId } : {}),
threadId: context.session.threadId,
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.

File-read approval method incorrectly mapped as file-change

Low Severity

The new "file-read" request kind added in requestKindForMethod isn't handled by the ternary in handleServerRequest that assigns pendingRequest.method. The ternary only distinguishes "command" vs. everything else, so "file-read" approvals are incorrectly labeled with method "item/fileChange/requestApproval". The PendingApprovalRequest interface method type also doesn't include the new "item/fileRead/requestApproval" variant.

Additional Locations (1)

Fix in Cursor Fix in Web

rootDir,
workspaceDir,
dbPath,
adapterHarness: adapterHarness as TestProviderAdapterHarness,
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.

Null adapter harness unsafely cast to non-null type

Low Severity

When useRealCodex is true, adapterHarness is null but is unsafely cast to TestProviderAdapterHarness on the return value. Any future test using withRealCodexHarness that accesses harness.adapterHarness will get null at runtime despite TypeScript indicating it's non-null, leading to confusing runtime errors.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix prepared a fix for 1 of the 2 issues found in the latest run.

  • ✅ Fixed: Debug reproduction script appears accidentally committed
    • Deleted the debug reproduction script apps/server/scripts/logger-scope-repro.ts which was development scaffolding unrelated to the PR's scope.

Create PR

Or push these changes by commenting:

@cursor push 30a8ff0ad6
Preview (30a8ff0ad6)
diff --git a/apps/server/scripts/logger-scope-repro.ts b/apps/server/scripts/logger-scope-repro.ts
deleted file mode 100644
--- a/apps/server/scripts/logger-scope-repro.ts
+++ /dev/null
@@ -1,66 +1,0 @@
-import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
-import * as NodeServices from "@effect/platform-node/NodeServices";
-import path from "node:path";
-
-import { Effect, FileSystem, Layer, Logger, ServiceMap } from "effect";
-
-import { makeEventNdjsonLogger } from "../src/provider/Layers/EventNdjsonLogger.ts";
-
-class LogDir extends ServiceMap.Service<LogDir, string>()("t3/scripts/logger-scope-repro/LogDir") {}
-
-const main = Effect.gen(function* () {
-  const logdir = yield* LogDir;
-  const providerLogPath = path.join(logdir, "provider");
-
-  yield* Effect.logInfo(`providerLogPath=${providerLogPath}`);
-
-  const providerLogger = yield* makeEventNdjsonLogger(providerLogPath, {
-    stream: "native",
-    batchWindowMs: 10,
-  });
-
-  yield* Effect.logInfo("before provider write");
-
-  if (providerLogger) {
-    yield* providerLogger.write(
-      {
-        kind: "probe",
-        message: "provider-only event",
-      },
-      "thread-123" as never,
-    );
-  }
-
-  yield* Effect.logInfo("after provider write");
-  yield* Effect.sleep("50 millis");
-
-  if (providerLogger) {
-    yield* providerLogger.close();
-  }
-  yield* Effect.logInfo("after provider close");
-
-  const fs = yield* FileSystem.FileSystem;
-  const logContents = yield* fs
-    .readDirectory(logdir, { recursive: true })
-    .pipe(
-      Effect.flatMap((entries) =>
-        Effect.all(entries.map((entry) => fs.readFileString(path.join(logdir, entry)))),
-      ),
-    );
-  yield* Effect.logInfo(`logContents=${logContents}`);
-});
-
-Effect.gen(function* () {
-  const fs = yield* FileSystem.FileSystem;
-  const logdir = path.join(process.cwd(), "logtest");
-  yield* fs.makeDirectory(logdir);
-
-  const fileLogger = yield* Logger.formatSimple.pipe(
-    Logger.toFile(path.join(logdir, "global.log")),
-  );
-  const dualLogger = Logger.layer([fileLogger, Logger.consolePretty()]);
-
-  const mainLayer = Layer.mergeAll(dualLogger, Layer.succeed(LogDir, logdir));
-
-  yield* main.pipe(Effect.provide(mainLayer));
-}).pipe(Effect.scoped, Effect.provide(NodeServices.layer), NodeRuntime.runMain);
\ No newline at end of file

diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts
--- a/apps/server/src/main.ts
+++ b/apps/server/src/main.ts
@@ -119,6 +119,13 @@
   ),
 });
 
+/**
+ * Resolve a CLI boolean flag against an environment/mode default.
+ *
+ * Effect CLI boolean flags parse to `Some(false)` when absent (even with
+ * `Flag.optional`), so we treat `Some(false)` the same as `None` — only an
+ * explicit `--flag` (i.e. `Some(true)`) overrides the environment value.
+ */
 const resolveBooleanFlag = (flag: Option.Option<boolean>, envValue: boolean) =>
   Option.getOrElse(Option.filter(flag, Boolean), () => envValue);

- Send `collaborationMode` for both `default` and `plan` interactions
- Add default-mode developer instructions and preserve plan-mode behavior
- Update server/web tests for default collaboration mode and model constant usage
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Boolean flag resolver silently ignores explicit false override
    • Removed Option.filter(flag, Boolean) from resolveBooleanFlag since Flag.optional already returns None for absent flags, so the filter was incorrectly collapsing explicit Some(false) into None and preventing CLI overrides of env-set true values.

Create PR

Or push these changes by commenting:

@cursor push e05cc8885c

@@ -496,15 +1330,15 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) =>
});
if (!attachmentPath) {
return yield* toRequestError(
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 Layers/CodexAdapter.ts:1332

In sendTurn, yield* is called on toRequestError(...), which returns a plain Error subclass, not an Effect. This causes a runtime crash (TypeError: object is not iterable). Even if it didn't crash, returning the error object would treat it as a success value, causing codexAttachments to contain an Error instead of a valid attachment. Use yield* Effect.fail(toRequestError(...)) to properly fail the operation.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/CodexAdapter.ts around line 1332:

In `sendTurn`, `yield*` is called on `toRequestError(...)`, which returns a plain `Error` subclass, not an `Effect`. This causes a runtime crash (`TypeError: object is not iterable`). Even if it didn't crash, returning the error object would treat it as a success value, causing `codexAttachments` to contain an `Error` instead of a valid attachment. Use `yield* Effect.fail(toRequestError(...))` to properly fail the operation.

Evidence trail:
apps/server/src/provider/Layers/CodexAdapter.ts:80 - `toRequestError` function definition returns `ProviderAdapterError` (Error subclass, not Effect)
apps/server/src/provider/Layers/CodexAdapter.ts:1332 - `return yield* toRequestError(...)` incorrectly uses yield* on a non-Effect
apps/server/src/provider/Layers/CodexAdapter.ts:1339-1341 - shows correct usage pattern: `Effect.mapError((cause) => toRequestError(...))` where error is used in mapError, not yielded

- Extract follow-up submission resolution into `resolvePlanFollowUpSubmission`
- Send implementation prompt in default mode when follow-up text is empty
- Keep composer mode toggle synced when starting same-thread implementation turns
- Add tests for follow-up submission behavior
@juliusmarminge juliusmarminge merged commit 010ba70 into main Mar 6, 2026
2 checks passed
@juliusmarminge juliusmarminge deleted the codething/648ca884 branch March 6, 2026 07:00
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before autofix could start.

});

const resolveBooleanFlag = (flag: Option.Option<boolean>, envValue: boolean) =>
Option.getOrElse(Option.filter(flag, Boolean), () => envValue);
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.

Boolean flag resolution silently swallows explicit false

Low Severity

resolveBooleanFlag uses Option.filter(flag, Boolean) which discards Some(false), making an explicit CLI false indistinguishable from an absent flag. While the old code had identical behavior (with explanatory comments), replacing it with a terse helper that removes those comments obscures a non-obvious semantic: users who explicitly pass --no-browser false or --auto-bootstrap-project-from-cwd false get the environment/default value instead of false. The removed comments explained this was an Effect CLI quirk where absent boolean flags parse to false, but without that context, future maintainers may not realize the helper intentionally ignores explicit false values.

Fix in Cursor Fix in Web

grantwiley added a commit to grantwiley/t3code that referenced this pull request Mar 12, 2026
Merges the Claude Code provider adapter from upstream PR pingdotgg#179,
including the core orchestration engine from PR pingdotgg#103 and fixes
for 3 failing ClaudeCodeAdapter tests.
grantwiley added a commit to grantwiley/t3code that referenced this pull request Mar 13, 2026
Merges the Claude Code provider adapter from upstream PR pingdotgg#179,
including the core orchestration engine from PR pingdotgg#103 and fixes
for 3 failing ClaudeCodeAdapter tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant