Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: SDK stream fiber is detached and unmanaged
- Replaced
Effect.runForkwithEffect.forkChildto keep the fiber in the managed runtime, stored the fiber reference in session context, and added explicitFiber.interruptinstopSessionInternalbefore queue teardown to eliminate the race window.
- Replaced
- ✅ Fixed: Identity function adds unnecessary indirection
- Removed the no-op
asCanonicalTurnIdidentity function and inlined theTurnIdvalue directly at all 10 call sites.
- Removed the no-op
Or push these changes by commenting:
@cursor push 2efc9d6c0c
Preview (2efc9d6c0c)
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
@@ -34,7 +34,7 @@
ThreadId,
TurnId,
} 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,
@@ -106,6 +106,7 @@
lastAssistantUuid: string | undefined;
lastThreadStartedId: string | undefined;
stopped: boolean;
+ streamFiber: Fiber.Fiber<void, never> | undefined;
}
interface ClaudeQueryRuntime extends AsyncIterable<SDKMessage> {
@@ -144,10 +145,6 @@
return RuntimeItemId.makeUnsafe(value);
}
-function asCanonicalTurnId(value: TurnId): TurnId {
- return value;
-}
-
function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId {
return RuntimeRequestId.makeUnsafe(value);
}
@@ -505,7 +502,7 @@
...(typeof message.session_id === "string"
? { providerThreadId: message.session_id }
: {}),
- ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
...(itemId ? { itemId: ProviderItemId.makeUnsafe(itemId) } : {}),
payload: message,
},
@@ -613,7 +610,7 @@
provider: PROVIDER,
createdAt: stamp.createdAt,
threadId: context.session.threadId,
- ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}),
+ ...(turnState ? { turnId: turnState.turnId } : {}),
payload: {
message,
class: "provider_error",
@@ -640,7 +637,7 @@
provider: PROVIDER,
createdAt: stamp.createdAt,
threadId: context.session.threadId,
- ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}),
+ ...(turnState ? { turnId: turnState.turnId } : {}),
payload: {
message,
...(detail !== undefined ? { detail } : {}),
@@ -855,7 +852,7 @@
provider: PROVIDER,
createdAt: stamp.createdAt,
threadId: context.session.threadId,
- ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
itemId: asRuntimeItemId(tool.itemId),
payload: {
itemType: tool.itemType,
@@ -896,7 +893,7 @@
provider: PROVIDER,
createdAt: stamp.createdAt,
threadId: context.session.threadId,
- ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
itemId: asRuntimeItemId(tool.itemId),
payload: {
itemType: tool.itemType,
@@ -1006,7 +1003,7 @@
provider: PROVIDER,
createdAt: stamp.createdAt,
threadId: context.session.threadId,
- ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
providerRefs: {
...providerThreadRef(context),
...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
@@ -1165,7 +1162,7 @@
provider: PROVIDER,
createdAt: stamp.createdAt,
threadId: context.session.threadId,
- ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
providerRefs: {
...providerThreadRef(context),
...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
@@ -1295,6 +1292,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();
@@ -1304,7 +1306,7 @@
provider: PROVIDER,
createdAt: stamp.createdAt,
threadId: context.session.threadId,
- ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
requestId: asRuntimeRequestId(requestId),
payload: {
requestType: pending.requestType,
@@ -1442,7 +1444,7 @@
provider: PROVIDER,
createdAt: requestedStamp.createdAt,
threadId: context.session.threadId,
- ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
requestId: asRuntimeRequestId(requestId),
payload: {
requestType,
@@ -1494,7 +1496,7 @@
provider: PROVIDER,
createdAt: resolvedStamp.createdAt,
threadId: context.session.threadId,
- ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
requestId: asRuntimeRequestId(requestId),
payload: {
requestType,
@@ -1610,6 +1612,7 @@
lastAssistantUuid: resumeState?.resumeSessionAt,
lastThreadStartedId: undefined,
stopped: false,
+ streamFiber: undefined,
};
yield* Ref.set(contextRef, context);
sessions.set(threadId, context);
@@ -1658,7 +1661,7 @@
providerRefs: {},
});
- Effect.runFork(runSdkStream(context));
+ context.streamFiber = yield* Effect.forkChild(runSdkStream(context));
return {
...session,2b53034 to
1beeff2
Compare
There was a problem hiding this comment.
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: Native event logger missing threadId in event payload
- Added
threadId: context.session.threadIdto the event object and passedcontext.session.threadIdinstead ofnullas the second argument tonativeEventLogger.write(), enabling per-thread log routing consistent with the Codex adapter.
- Added
Or push these changes by commenting:
@cursor push 8489584240
Preview (8489584240)
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
@@ -502,6 +502,7 @@
provider: PROVIDER,
createdAt: observedAt,
method: sdkNativeMethod(message),
+ threadId: context.session.threadId,
...(typeof message.session_id === "string"
? { providerThreadId: message.session_id }
: {}),
@@ -510,7 +511,7 @@
payload: message,
},
},
- null,
+ context.session.threadId,
);
});4afd04a to
3af67f5
Compare
| customClaudeModels: Schema.Array(Schema.String).pipe( | ||
| Schema.withConstructorDefault(() => Option.some([])), | ||
| ), |
There was a problem hiding this comment.
🟢 Low src/appSettings.ts:29
Adding customClaudeModels as a required field causes Schema.decodeSync to throw when decoding existing localStorage data that lacks this key. The caught error triggers fallback to DEFAULT_APP_SETTINGS, permanently discarding the user's saved configuration. Mark the field optional with a decode-time default instead of using withConstructorDefault.
- customClaudeModels: Schema.Array(Schema.String).pipe(
- Schema.withConstructorDefault(() => Option.some([])),
- ),Also found in 1 other location(s)
apps/web/src/routes/_chat.settings.tsx:65
The
getCustomModelsForProviderfunction directly returnssettings.customClaudeModels. SincecustomClaudeModelsis a new property added to the schema in this PR, the persistedsettingsobject in local storage for existing users will not contain this key. This causes the function to returnundefined. Consuming code (e.g.,SettingsRouteViewat lines 164 and 398) assumes an array and will crash with aTypeErrorwhen accessing.lengthor.includes()onundefined. The accessor should default to an empty array (e.g.,settings.customClaudeModels ?? []) to handle the schema migration for existing data.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/appSettings.ts around lines 29-31:
Adding `customClaudeModels` as a required field causes `Schema.decodeSync` to throw when decoding existing `localStorage` data that lacks this key. The caught error triggers fallback to `DEFAULT_APP_SETTINGS`, permanently discarding the user's saved configuration. Mark the field optional with a decode-time default instead of using `withConstructorDefault`.
Evidence trail:
apps/web/src/appSettings.ts lines 29-31 (customClaudeModels definition with withConstructorDefault), lines 134-143 (parsePersistedSettings using decodeSync with catch fallback to DEFAULT_APP_SETTINGS). Git diff (MERGE_BASE..REVIEWED_COMMIT) shows customClaudeModels being added. Effect-TS issue #1997 (https://github.com/Effect-TS/effect/issues/1997) confirms withConstructorDefault provides defaults at construction time only, not during decode: "I often find myself needing to provide default values at construction, but not at parsing."
Also found in 1 other location(s):
- apps/web/src/routes/_chat.settings.tsx:65 -- The `getCustomModelsForProvider` function directly returns `settings.customClaudeModels`. Since `customClaudeModels` is a new property added to the schema in this PR, the persisted `settings` object in local storage for existing users will not contain this key. This causes the function to return `undefined`. Consuming code (e.g., `SettingsRouteView` at lines 164 and 398) assumes an array and will crash with a `TypeError` when accessing `.length` or `.includes()` on `undefined`. The accessor should default to an empty array (e.g., `settings.customClaudeModels ?? []`) to handle the schema migration for existing data.
There was a problem hiding this comment.
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: Redundant duplicate
threadIdassignment in session construction- Removed the redundant
...(threadId ? { threadId } : {})spread at line 1587 sincethreadIdis already set directly at line 1581 and is always defined.
- Removed the redundant
Or push these changes by commenting:
@cursor push 1970102289
Preview (1970102289)
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
@@ -1584,7 +1584,6 @@
runtimeMode: input.runtimeMode,
...(input.cwd ? { cwd: input.cwd } : {}),
...(input.model ? { model: input.model } : {}),
- ...(threadId ? { threadId } : {}),
resumeCursor: {
...(threadId ? { threadId } : {}),
...(resumeState?.resume ? { resume: resumeState.resume } : {}),| async respondToUserInput( | ||
| threadId: ThreadId, | ||
| requestId: ApprovalRequestId, | ||
| answers: ProviderUserInputAnswers, | ||
| ): Promise<void> { | ||
| const context = this.requireSession(threadId); | ||
| const pendingRequest = context.pendingUserInputs.get(requestId); | ||
| if (!pendingRequest) { | ||
| throw new Error(`Unknown pending user input request: ${requestId}`); | ||
| } | ||
|
|
||
| context.pendingUserInputs.delete(requestId); | ||
| const codexAnswers = toCodexUserInputAnswers(answers); | ||
| this.writeMessage(context, { |
There was a problem hiding this comment.
🟡 Medium src/codexAppServerManager.ts:807
In respondToUserInput, context.pendingUserInputs.delete(requestId) is called before validating answers via toCodexUserInputAnswers. If conversion throws, the request record is permanently lost but no response reaches the provider, leaving the session corrupted and blocking retries. Move the deletion after successful validation and conversion.
async respondToUserInput(
threadId: ThreadId,
requestId: ApprovalRequestId,
answers: ProviderUserInputAnswers,
): Promise<void> {
const context = this.requireSession(threadId);
const pendingRequest = context.pendingUserInputs.get(requestId);
if (!pendingRequest) {
throw new Error(`Unknown pending user input request: ${requestId}`);
}
- context.pendingUserInputs.delete(requestId);
const codexAnswers = toCodexUserInputAnswers(answers);
+ context.pendingUserInputs.delete(requestId);
this.writeMessage(context, {
id: pendingRequest.jsonRpcId,
result: {🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/codexAppServerManager.ts around lines 807-820:
In `respondToUserInput`, `context.pendingUserInputs.delete(requestId)` is called before validating `answers` via `toCodexUserInputAnswers`. If conversion throws, the request record is permanently lost but no response reaches the provider, leaving the session corrupted and blocking retries. Move the deletion after successful validation and conversion.
Evidence trail:
apps/server/src/codexAppServerManager.ts lines 807-825 (REVIEWED_COMMIT): `respondToUserInput` method showing delete at line 818, conversion at line 819
apps/server/src/codexAppServerManager.ts lines 358-381 (REVIEWED_COMMIT): `toCodexUserInputAnswer` function that throws Error at line 370, and `toCodexUserInputAnswers` that calls it
Line 818: `context.pendingUserInputs.delete(requestId);`
Line 819: `const codexAnswers = toCodexUserInputAnswers(answers);`
Line 370: `throw new Error("User input answers must be strings or arrays of strings.");`
| this.runPromise = services ? Effect.runPromiseWith(services) : Effect.runPromise; | ||
| } | ||
|
|
||
| async startSession(input: CodexAppServerStartSessionInput): Promise<ProviderSession> { |
There was a problem hiding this comment.
🟠 High src/codexAppServerManager.ts:428
startSession overwrites this.sessions.set(threadId, context) without checking if a session already exists for that threadId. This orphans the previous process (which keeps running) and leaves its exit handler active. When the orphaned process later exits, that handler calls this.sessions.delete(threadId), which removes the new session from the map. Subsequent calls like sendTurn then fail with "Unknown session" because the entry was deleted by the stale handler.
Also found in 1 other location(s)
apps/server/src/provider/Layers/CodexAdapter.ts:1262
The
nativeEventLoggercreated whenoptions.nativeEventLogPathis provided is never closed. Whilemanageris wrapped inEffect.acquireReleasefor cleanup,nativeEventLoggeris instantiated separately and itsclose()method is never called. This causes file handles opened by the logger to leak every time theCodexAdapterlayer is released (e.g., during configuration reloads or tests).
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/codexAppServerManager.ts around line 428:
`startSession` overwrites `this.sessions.set(threadId, context)` without checking if a session already exists for that `threadId`. This orphans the previous process (which keeps running) and leaves its `exit` handler active. When the orphaned process later exits, that handler calls `this.sessions.delete(threadId)`, which removes the *new* session from the map. Subsequent calls like `sendTurn` then fail with "Unknown session" because the entry was deleted by the stale handler.
Evidence trail:
apps/server/src/codexAppServerManager.ts: lines 428-471 show `startSession` calls `this.sessions.set(threadId, context)` without checking for existing session; line 943 shows exit handler calls `this.sessions.delete(context.session.threadId)` where `context` is captured in closure; lines 890-893 show `requireSession` throws 'Unknown session' when entry not in map; lines 606-607 show `sendTurn` uses `requireSession`.
Also found in 1 other location(s):
- apps/server/src/provider/Layers/CodexAdapter.ts:1262 -- The `nativeEventLogger` created when `options.nativeEventLogPath` is provided is never closed. While `manager` is wrapped in `Effect.acquireRelease` for cleanup, `nativeEventLogger` is instantiated separately and its `close()` method is never called. This causes file handles opened by the logger to leak every time the `CodexAdapter` layer is released (e.g., during configuration reloads or tests).
| export const OpenCodeIcon: Icon = (props) => ( | ||
| <svg {...props} viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| <g clipPath="url(#opencode__clip0_1311_94969)"> | ||
| <path d="M24 32H8V16H24V32Z" fill="#BCBBBB" /> | ||
| <path d="M24 8H8V32H24V8ZM32 40H0V0H32V40Z" fill="#211E1E" /> | ||
| </g> | ||
| <defs> | ||
| <clipPath id="opencode__clip0_1311_94969"> | ||
| <rect width="32" height="40" fill="white" /> | ||
| </clipPath> | ||
| </defs> | ||
| </svg> | ||
| ); |
There was a problem hiding this comment.
🟢 Low components/Icons.tsx:329
The OpenCodeIcon component uses hardcoded fill attributes (#211E1E and #BCBBBB) on its SVG paths. In dark mode, the #211E1E dark grey fill renders the icon nearly invisible due to low contrast against dark backgrounds. Unlike GitHubIcon and OpenAI, which use currentColor to inherit the surrounding text color, this icon fails to adapt to the theme.
- <svg {...props} viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <svg {...props} viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<g clipPath="url(#opencode__clip0_1311_94969)">
- <path d="M24 32H8V16H24V32Z" fill="#BCBBBB" />
- <path d="M24 8H8V32H24V8ZM32 40H0V0H32V40Z" fill="#211E1E" />
+ <path d="M24 32H8V16H24V32Z" fill="currentColor" />
+ <path d="M24 8H8V32H24V8ZM32 40H0V0H32V40Z" fill="currentColor" />🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/components/Icons.tsx around lines 329-341:
The `OpenCodeIcon` component uses hardcoded `fill` attributes (`#211E1E` and `#BCBBBB`) on its SVG paths. In dark mode, the `#211E1E` dark grey fill renders the icon nearly invisible due to low contrast against dark backgrounds. Unlike `GitHubIcon` and `OpenAI`, which use `currentColor` to inherit the surrounding text color, this icon fails to adapt to the theme.
Evidence trail:
apps/web/src/components/Icons.tsx lines 329-342 (OpenCodeIcon with hardcoded fills #211E1E and #BCBBBB), lines 5-15 (GitHubIcon with fill="currentColor" at line 12), lines 146-150 (OpenAI with fill="currentColor" at line 147). Verified at commit REVIEWED_COMMIT.
|
I prepared a CI fix PR targeting this branch: #243\n\nIt updates stale ClaudeCodeAdapter test expectations for thread identity/providerThreadId behavior and aligns with current adapter semantics. |
Fix detached SDK stream fiber by replacing Effect.runFork with Effect.forkChild and adding explicit Fiber.interrupt on session stop. Add missing threadId to native event logger for per-thread log routing. Remove no-op asCanonicalTurnId identity function. Remove redundant threadId spread in session construction. Fix stale test expectations for thread identity behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…apters Merge the Claude Code adapter (PR pingdotgg#179) into main, resolving 45 conflicts caused by the deliberate split of provider stacks on March 5. Key additions: - ClaudeCodeAdapter with full session, turn, and resume lifecycle - Cursor provider support (model catalog, UI, routing) - ProviderKind expanded from "codex" to "codex" | "claudeCode" | "cursor" - Provider model catalogs, aliases, and slug resolution across all providers - UI support for Claude Code and Cursor in ChatView, settings, and composer Conflict resolution strategy: - Kept HEAD's refactored patterns (scoped finalizers, telemetry, serviceTier) - Added PR's new provider routing, adapters, and UI components - Fixed duplicate declarations, missing props, and type mismatches Typecheck: 7/7 packages pass Lint: 0 warnings, 0 errors Tests: 419/422 pass (3 pre-existing failures in ClaudeCodeAdapter.test.ts) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove filtro que escondia o Claude Code do seletor de providers, tornando-o selecionável após o merge do adapter (PR pingdotgg#179). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add first-class Claude Code support alongside existing Codex provider: - Add ClaudeCodeAdapter (1857 lines) backed by @anthropic-ai/claude-agent-sdk - Extend ProviderKind to accept "codex" | "claudeCode" - Add Claude model catalog (Opus 4.6, Sonnet 4.6, Haiku 4.5) - Register ClaudeCodeAdapter in provider registry and server layers - Add Claude SDK event sources to provider runtime events - Enable Claude Code in UI provider picker - Add ClaudeCodeProviderStartOptions to contracts - Update all tests for multi-provider support Based on upstream PR pingdotgg#179 by juliusmarminge, surgically applied to current main. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merges codething/648ca884-claude branch which adds full Claude Code provider adapter, orchestration enhancements, and UI surface. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
i did a lot of work making ti compatible with all the reverse engineered docs that are available. |
|
@juliusmarminge So I filled PR #1146 targeting this branch, excluding the Teams feature which is now in a seperate branch #1145 in drafts targeting main. Will update and fix the teams feature tmr. |
| if (!tool) { | ||
| return; | ||
| } | ||
| } |
There was a problem hiding this comment.
Dead code in tool content_block_stop handler
Low Severity
In handleStreamEvent, when a content_block_stop event fires for a tool-use block (not an assistant text block), the code looks up the tool via context.inFlightTools.get(index) but does absolutely nothing with the result. The if (!tool) { return; } guard exits early if the tool is missing, but when the tool IS found, execution falls through to the end of the function with no tool-specific action taken. This dead branch is confusing and suggests missing logic for handling tool stream closure.
| yield* Queue.offer(context.promptQueue, { | ||
| type: "message", | ||
| message, | ||
| }).pipe(Effect.mapError((cause) => toRequestError(input.threadId, "turn/start", cause))); |
There was a problem hiding this comment.
🟡 Medium Layers/ClaudeAdapter.ts:2409
Queue.offer returns Effect<boolean> where false means the queue was shutdown. The current code ignores this boolean and only maps the error channel, so when the session is stopping and the queue is shutdown, the message is silently dropped but sendTurn still returns a successful ProviderTurnStartResult. The caller believes the turn started when the message was never queued. Consider checking the boolean result and failing with an appropriate error when the queue is shutdown.
- yield* Queue.offer(context.promptQueue, {
- type: "message",
- message,
- }).pipe(Effect.mapError((cause) => toRequestError(input.threadId, "turn/start", cause)));
+ const offered = yield* Queue.offer(context.promptQueue, {
+ type: "message",
+ message,
+ });
+ if (!offered) {
+ return yield* new ProviderAdapterRequestError({
+ provider: PROVIDER,
+ method: "turn/start",
+ detail: "Failed to queue message: prompt queue is shutdown.",
+ });
+ }🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/ClaudeAdapter.ts around lines 2409-2412:
`Queue.offer` returns `Effect<boolean>` where `false` means the queue was shutdown. The current code ignores this boolean and only maps the error channel, so when the session is stopping and the queue is shutdown, the message is silently dropped but `sendTurn` still returns a successful `ProviderTurnStartResult`. The caller believes the turn started when the message was never queued. Consider checking the boolean result and failing with an appropriate error when the queue is shutdown.
Evidence trail:
- Reviewed code: `apps/server/src/provider/Layers/ClaudeAdapter.ts` lines 2409-2420 (REVIEWED_COMMIT)
- Same codebase pattern for handling Queue.offer boolean: `packages/shared/src/DrainableWorker.ts` lines 90-93 - shows `const accepted = yield* Queue.offer(queue, item); if (!accepted) { ... }`
- Effect-TS Queue.offer behavior is confirmed by the codebase's own usage pattern which captures and checks the boolean return value
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 7 total unresolved issues (including 6 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Approval events use orchestration ID as provider thread ID
- Replaced four direct uses of
context.session.threadIdasproviderThreadIdwith theproviderThreadRef(context)helper, consistent with all other event emissions in the file.
- Replaced four direct uses of
Or push these changes by commenting:
@cursor push 241180f045
Preview (241180f045)
diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts
--- a/apps/server/src/provider/Layers/ClaudeAdapter.ts
+++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts
@@ -1995,7 +1995,7 @@
requestId: asRuntimeRequestId(requestId),
payload: { questions },
providerRefs: {
- ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
+ ...providerThreadRef(context),
...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}),
providerRequestId: requestId,
},
@@ -2034,7 +2034,7 @@
requestId: asRuntimeRequestId(requestId),
payload: { answers },
providerRefs: {
- ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}),
+ ...providerThreadRef(context),
...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}),
providerRequestId: requestId,
},
@@ -2116,9 +2116,7 @@
},
},
providerRefs: {
- ...(context.session.threadId
- ? { providerThreadId: context.session.threadId }
- : {}),
+ ...providerThreadRef(context),
...(context.turnState
? { providerTurnId: String(context.turnState.turnId) }
: {}),
@@ -2167,9 +2165,7 @@
decision,
},
providerRefs: {
- ...(context.session.threadId
- ? { providerThreadId: context.session.threadId }
- : {}),
+ ...providerThreadRef(context),
...(context.turnState
? { providerTurnId: String(context.turnState.turnId) }
: {}),There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 7 total unresolved issues (including 6 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Codex model name passed to Claude on provider switch
- When a provider is explicitly requested and differs from the current provider (or no session exists), the code now falls back to DEFAULT_MODEL_BY_PROVIDER for the target provider instead of carrying over the thread's existing model slug.
Or push these changes by commenting:
@cursor push 5b9f3fb2e9
Preview (5b9f3fb2e9)
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
@@ -416,7 +416,7 @@
expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({
provider: "claudeAgent",
cwd: "/tmp/provider-project",
- model: "gpt-5-codex",
+ model: "claude-sonnet-4-6",
runtimeMode: "approval-required",
});
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
@@ -1,6 +1,7 @@
import {
type ChatAttachment,
CommandId,
+ DEFAULT_MODEL_BY_PROVIDER,
EventId,
type OrchestrationEvent,
type ProviderModelOptions,
@@ -211,7 +212,11 @@
? thread.session.providerName
: undefined;
const preferredProvider: ProviderKind | undefined = options?.provider ?? currentProvider;
- const desiredModel = options?.model ?? thread.model;
+ const providerIsChanging =
+ options?.provider !== undefined && options.provider !== currentProvider;
+ const desiredModel =
+ options?.model ??
+ (providerIsChanging ? DEFAULT_MODEL_BY_PROVIDER[options.provider!] : thread.model);
const effectiveCwd = resolveThreadWorkspaceCwd({
thread,
projects: readModel.projects,|
|
||
| context.stopped = true; | ||
|
|
||
| for (const [requestId, pending] of context.pendingApprovals) { |
There was a problem hiding this comment.
🟡 Medium Layers/ClaudeAdapter.ts:1795
In stopSessionInternal, pendingApprovals are resolved and cleaned up (lines 1795-1813), but pendingUserInputs is not handled. If a session is stopped while handleAskUserQuestion is waiting for user input, and the SDK's abort signal does not fire, the Deferred.await(answersDeferred) at line 1969 will never resolve, causing the fiber inside Effect.runPromise (line 2005) to hang indefinitely.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/ClaudeAdapter.ts around line 1795:
In `stopSessionInternal`, `pendingApprovals` are resolved and cleaned up (lines 1795-1813), but `pendingUserInputs` is not handled. If a session is stopped while `handleAskUserQuestion` is waiting for user input, and the SDK's abort signal does not fire, the `Deferred.await(answersDeferred)` at line 1969 will never resolve, causing the fiber inside `Effect.runPromise` (line 2005) to hang indefinitely.
Evidence trail:
apps/server/src/provider/Layers/ClaudeAdapter.ts lines 1785-1848 (`stopSessionInternal` function showing `pendingApprovals` cleanup at 1794-1813 but no `pendingUserInputs` handling); lines 120-130 (context type definition showing both `pendingApprovals` and `pendingUserInputs`); lines 1920-1975 (`handleAskUserQuestion` showing `Deferred.await(answersDeferred)` at line 1969 and abort signal handler at lines 1959-1966)
- Reject turn starts that request a provider switch from the thread-bound provider - Reject models that do not belong to the thread provider and log start failures - Add shared `inferProviderForModel` helper and tests; reuse it in web store inference
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 6 total unresolved issues (including 5 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: preferredProvider fallback chain contains dead code path
- Removed the dead
options?.provideroperand from the nullish coalescing chain, simplifying it tocurrentProvider ?? threadProviderwhich is behaviorally identical.
- Removed the dead
Or push these changes by commenting:
@cursor push a98a939d9b
Preview (a98a939d9b)
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
@@ -229,7 +229,7 @@
detail: `Model '${options.model}' does not belong to provider '${threadProvider}' for thread '${threadId}'.`,
});
}
- const preferredProvider: ProviderKind = currentProvider ?? options?.provider ?? threadProvider;
+ const preferredProvider: ProviderKind = currentProvider ?? threadProvider;
const desiredModel = options?.model ?? thread.model;
const effectiveCwd = resolveThreadWorkspaceCwd({
thread,- Use thread provider as the preferred provider during command handling - Update `ProviderModelPicker` to show direct model choices when provider is locked - Add browser tests for locked/unlocked picker behavior and widen browser test glob
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 6 total unresolved issues (including 5 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Turn start error handler catches interrupts as failures
- Added a Cause.hasInterruptsOnly check in the inner catchCause handler to re-propagate interrupt causes instead of converting them into spurious provider.turn.start.failed activities.
Or push these changes by commenting:
@cursor push ae48a85907
Preview (ae48a85907)
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
@@ -498,16 +498,19 @@
interactionMode: event.payload.interactionMode,
createdAt: event.payload.createdAt,
}).pipe(
- Effect.catchCause((cause) =>
- appendProviderFailureActivity({
+ Effect.catchCause((cause) => {
+ if (Cause.hasInterruptsOnly(cause)) {
+ return Effect.failCause(cause);
+ }
+ return appendProviderFailureActivity({
threadId: event.payload.threadId,
kind: "provider.turn.start.failed",
summary: "Provider turn start failed",
detail: Cause.pretty(cause),
turnId: null,
createdAt: event.payload.createdAt,
- }),
- ),
+ });
+ }),
);
});| createdAt: event.payload.createdAt, | ||
| }), | ||
| ), | ||
| ); |
There was a problem hiding this comment.
Turn start error handler catches interrupts as failures
Low Severity
The new Effect.catchCause wrapper around sendTurnForThread catches all causes including interrupts. When a fiber is interrupted (e.g., during shutdown), this creates a spurious provider.turn.start.failed activity instead of allowing the interrupt to propagate. The outer processDomainEventSafely handler already filters interrupts via Cause.hasInterruptsOnly, but this inner handler runs first and swallows them.
- derive `defaultModel` from `DEFAULT_MODEL_BY_PROVIDER` using harness provider - replace hardcoded `gpt-5-codex` in seeded project and thread setup
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 6 total unresolved issues (including 5 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Redundant variable always equals existing value
- Removed the redundant
preferredProvidervariable and replaced its two usages withthreadProvider, which is always equal.
- Removed the redundant
Or push these changes by commenting:
@cursor push 7b50561688
Preview (7b50561688)
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
@@ -229,7 +229,6 @@
detail: `Model '${options.model}' does not belong to provider '${threadProvider}' for thread '${threadId}'.`,
});
}
- const preferredProvider: ProviderKind = currentProvider ?? threadProvider;
const desiredModel = options?.model ?? thread.model;
const effectiveCwd = resolveThreadWorkspaceCwd({
thread,
@@ -247,8 +246,8 @@
}) =>
providerService.startSession(threadId, {
threadId,
- ...((input?.provider ?? preferredProvider)
- ? { provider: input?.provider ?? preferredProvider }
+ ...((input?.provider ?? threadProvider)
+ ? { provider: input?.provider ?? threadProvider }
: {}),
...(effectiveCwd ? { cwd: effectiveCwd } : {}),
...(desiredModel ? { model: desiredModel } : {}),…1146) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@JustYannicc should the ultrathink UI trigger here?
|
| yield* Effect.addFinalizer(() => | ||
| Effect.forEach( | ||
| sessions, | ||
| ([, context]) => | ||
| stopSessionInternal(context, { | ||
| emitExitEvent: false, | ||
| }), | ||
| { discard: true }, | ||
| ).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))), | ||
| ); |
There was a problem hiding this comment.
🟡 Medium Layers/ClaudeAdapter.ts:2502
When nativeEventLogPath is provided, makeClaudeAdapter creates an EventNdjsonLogger internally at line 644, but the finalizer (lines 2502-2511) never calls nativeEventLogger.close(). This leaves file handles held by the logger's thread writers open until process exit. Consider calling nativeEventLogger.close() in the finalizer when the logger was created internally.
yield* Effect.addFinalizer(() =>
Effect.forEach(
sessions,
([, context]) =>
stopSessionInternal(context, {
emitExitEvent: false,
}),
{ discard: true },
- ).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))),
+ ).pipe(
+ Effect.tap(() => Queue.shutdown(runtimeEventQueue)),
+ Effect.tap(() =>
+ nativeEventLogger ? nativeEventLogger.close() : Effect.void
+ )
+ ),
);🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/ClaudeAdapter.ts around lines 2502-2511:
When `nativeEventLogPath` is provided, `makeClaudeAdapter` creates an `EventNdjsonLogger` internally at line 644, but the finalizer (lines 2502-2511) never calls `nativeEventLogger.close()`. This leaves file handles held by the logger's thread writers open until process exit. Consider calling `nativeEventLogger.close()` in the finalizer when the logger was created internally.
Evidence trail:
- apps/server/src/provider/Layers/ClaudeAdapter.ts lines 641-646 (REVIEWED_COMMIT): Shows `nativeEventLogger` is created internally via `makeEventNdjsonLogger` when `nativeEventLogPath` is provided
- apps/server/src/provider/Layers/ClaudeAdapter.ts lines 2502-2511 (REVIEWED_COMMIT): Finalizer only stops sessions and shuts down queue, no `nativeEventLogger.close()` call
- apps/server/src/provider/Layers/EventNdjsonLogger.ts lines 25-29, 188-195 (REVIEWED_COMMIT): Shows `EventNdjsonLogger` interface has a `close()` method that closes all thread writers
- git_grep for `nativeEventLogger.close` in ClaudeAdapter.ts: No results, confirming `close()` is never called
There was a problem hiding this comment.
🟢 Low
The if (!isUnknownPendingApprovalRequestError(cause)) return; statement at line 596 is dead code — there are no subsequent statements in the generator, so the return has no effect and the generator completes with void either way. This appears to be incomplete logic where additional handling was intended for the unknown pending approval request case.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/orchestration/Layers/ProviderCommandReactor.ts around line 595:
The `if (!isUnknownPendingApprovalRequestError(cause)) return;` statement at line 596 is dead code — there are no subsequent statements in the generator, so the return has no effect and the generator completes with `void` either way. This appears to be incomplete logic where additional handling was intended for the unknown pending approval request case.
Evidence trail:
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts lines 585-598 at REVIEWED_COMMIT - The generator function contains `if (!isUnknownPendingApprovalRequestError(cause)) return;` at line 596, followed immediately by the closing of the generator (`}),`). No statements follow the if statement, making the return statement have no observable effect.
There was a problem hiding this comment.
🟡 Medium
In recoverSessionForThread, upsertSessionBinding is called at line 247 without passing persistedModelOptions and persistedProviderOptions, even though these values were extracted and used to start the session. After recovery, the runtime payload drops these options, so subsequent operations that read the persisted binding won't see the original model/provider configuration.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/ProviderService.ts around line 247:
In `recoverSessionForThread`, `upsertSessionBinding` is called at line 247 without passing `persistedModelOptions` and `persistedProviderOptions`, even though these values were extracted and used to start the session. After recovery, the runtime payload drops these options, so subsequent operations that read the persisted binding won't see the original model/provider configuration.
Evidence trail:
apps/server/src/provider/Layers/ProviderService.ts lines 229-230 (extraction of persistedModelOptions and persistedProviderOptions), lines 234-235 (passing them to startSession), line 247 (upsertSessionBinding called without extra parameter), lines 162-177 (upsertSessionBinding signature showing optional extra parameter), lines 88-99 (toRuntimePayloadFromSession showing options only included if extra is provided), lines 308-311 (startSession correctly passing options to upsertSessionBinding)
| props.provider === "claudeAgent" ? "text-[#d97757]" : "text-muted-foreground/70", | ||
| props.provider === "claudeAgent" && props.ultrathinkActive | ||
| ? "ultrathink-chroma" | ||
| : undefined, | ||
| )} |
There was a problem hiding this comment.
🟡 Medium chat/ProviderModelPicker.tsx:137
When props.lockedProvider differs from props.provider, the icon color styling uses the wrong provider. Lines 137–141 check props.provider === "claudeAgent" to apply the orange color and ultrathink-chroma class, but the icon rendered is from activeProvider (line 92). This causes the OpenAI icon to display with Claude's orange styling when the provider is locked to Codex mid-thread. Consider using activeProvider in the condition to match the icon being rendered.
| props.provider === "claudeAgent" ? "text-[#d97757]" : "text-muted-foreground/70", | |
| props.provider === "claudeAgent" && props.ultrathinkActive | |
| ? "ultrathink-chroma" | |
| : undefined, | |
| )} | |
| className={cn( | |
| "size-4 shrink-0", | |
| activeProvider === "claudeAgent" ? "text-[#d97757]" : "text-muted-foreground/70", | |
| activeProvider === "claudeAgent" && props.ultrathinkActive | |
| ? "ultrathink-chroma" | |
| : undefined, | |
| )} |
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/components/chat/ProviderModelPicker.tsx around lines 137-141:
When `props.lockedProvider` differs from `props.provider`, the icon color styling uses the wrong provider. Lines 137–141 check `props.provider === "claudeAgent"` to apply the orange color and `ultrathink-chroma` class, but the icon rendered is from `activeProvider` (line 92). This causes the OpenAI icon to display with Claude's orange styling when the provider is locked to Codex mid-thread. Consider using `activeProvider` in the condition to match the icon being rendered.
Evidence trail:
apps/web/src/components/chat/ProviderModelPicker.tsx lines 89-92 (REVIEWED_COMMIT): activeProvider computed as `props.lockedProvider ?? props.provider`, ProviderIcon selected using `PROVIDER_ICON_BY_PROVIDER[activeProvider]`.
apps/web/src/components/chat/ProviderModelPicker.tsx lines 135-140 (REVIEWED_COMMIT): ProviderIcon styling condition uses `props.provider === "claudeAgent"` instead of `activeProvider === "claudeAgent"`, creating a mismatch between icon selection and styling.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 10 total unresolved issues (including 9 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: New Claude unit tests broken by their own provider validation
- Each Claude test now creates a dedicated thread with model "claude-sonnet-4-6" so that inferProviderForModel returns "claudeAgent" and the provider validation passes, allowing sessions to start correctly.
Or push these changes by commenting:
@cursor push cda0e97dae
Preview (cda0e97dae)
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
@@ -357,9 +357,25 @@
await Effect.runPromise(
harness.engine.dispatch({
+ type: "thread.create",
+ commandId: CommandId.makeUnsafe("cmd-thread-create-claude"),
+ threadId: ThreadId.makeUnsafe("thread-claude"),
+ projectId: asProjectId("project-1"),
+ title: "Claude Thread",
+ model: "claude-sonnet-4-6",
+ interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
+ runtimeMode: "approval-required",
+ branch: null,
+ worktreePath: null,
+ createdAt: now,
+ }),
+ );
+
+ await Effect.runPromise(
+ harness.engine.dispatch({
type: "thread.turn.start",
commandId: CommandId.makeUnsafe("cmd-turn-start-claude-effort"),
- threadId: ThreadId.makeUnsafe("thread-1"),
+ threadId: ThreadId.makeUnsafe("thread-claude"),
message: {
messageId: asMessageId("user-message-claude-effort"),
role: "user",
@@ -391,7 +407,7 @@
},
});
expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({
- threadId: ThreadId.makeUnsafe("thread-1"),
+ threadId: ThreadId.makeUnsafe("thread-claude"),
model: "claude-sonnet-4-6",
modelOptions: {
claudeAgent: {
@@ -587,9 +603,25 @@
await Effect.runPromise(
harness.engine.dispatch({
+ type: "thread.create",
+ commandId: CommandId.makeUnsafe("cmd-thread-create-claude-restart"),
+ threadId: ThreadId.makeUnsafe("thread-claude-restart"),
+ projectId: asProjectId("project-1"),
+ title: "Claude Restart Thread",
+ model: "claude-sonnet-4-6",
+ interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
+ runtimeMode: "approval-required",
+ branch: null,
+ worktreePath: null,
+ createdAt: now,
+ }),
+ );
+
+ await Effect.runPromise(
+ harness.engine.dispatch({
type: "thread.turn.start",
commandId: CommandId.makeUnsafe("cmd-turn-start-claude-effort-1"),
- threadId: ThreadId.makeUnsafe("thread-1"),
+ threadId: ThreadId.makeUnsafe("thread-claude-restart"),
message: {
messageId: asMessageId("user-message-claude-effort-1"),
role: "user",
@@ -616,7 +648,7 @@
harness.engine.dispatch({
type: "thread.turn.start",
commandId: CommandId.makeUnsafe("cmd-turn-start-claude-effort-2"),
- threadId: ThreadId.makeUnsafe("thread-1"),
+ threadId: ThreadId.makeUnsafe("thread-claude-restart"),
message: {
messageId: asMessageId("user-message-claude-effort-2"),
role: "user",|
No you have to select Ultra think in the thinking level but can add that |
- keep Ultrathink frame styling without extra wrapper padding toggles - drop the inline Ultrathink badge row in ChatView - clean up stray whitespace in ProviderService test
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 11 total unresolved issues (including 10 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: JSON.stringify comparison triggers spurious session restarts
- Replaced JSON.stringify equality with a recursive structural deep-equal that compares objects by their defined keys and values regardless of property insertion order, preventing false negatives that would trigger unnecessary session restarts.
Or push these changes by commenting:
@cursor push a325efe38a
Preview (a325efe38a)
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
@@ -75,10 +75,23 @@
const WORKTREE_BRANCH_PREFIX = "t3code";
const TEMP_WORKTREE_BRANCH_PATTERN = new RegExp(`^${WORKTREE_BRANCH_PREFIX}\\/[0-9a-f]{8}$`);
+function structuralEqual(a: unknown, b: unknown): boolean {
+ if (a === b) return true;
+ if (a == null || b == null) return a === b;
+ if (typeof a !== typeof b) return false;
+ if (typeof a !== "object") return false;
+ const objA = a as Record<string, unknown>;
+ const objB = b as Record<string, unknown>;
+ const keysA = Object.keys(objA).filter((k) => objA[k] !== undefined);
+ const keysB = Object.keys(objB).filter((k) => objB[k] !== undefined);
+ if (keysA.length !== keysB.length) return false;
+ return keysA.every((key) => objB[key] !== undefined && structuralEqual(objA[key], objB[key]));
+}
+
const sameModelOptions = (
left: ProviderModelOptions | undefined,
right: ProviderModelOptions | undefined,
-): boolean => JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
+): boolean => structuralEqual(left ?? null, right ?? null);
function isUnknownPendingApprovalRequestError(cause: Cause.Cause<ProviderServiceError>): boolean {
const error = Cause.squash(cause);| const sameModelOptions = ( | ||
| left: ProviderModelOptions | undefined, | ||
| right: ProviderModelOptions | undefined, | ||
| ): boolean => JSON.stringify(left ?? null) === JSON.stringify(right ?? null); |
There was a problem hiding this comment.
JSON.stringify comparison triggers spurious session restarts
Medium Severity
sameModelOptions uses JSON.stringify for equality, which is sensitive to property insertion order. Two ProviderModelOptions objects that are semantically identical but constructed with keys in different orders (e.g., from separate code paths or deserialized from different sources) will serialize to different strings, causing shouldRestartForModelOptionsChange to evaluate true and trigger an unnecessary Claude session restart — discarding the resume cursor and losing all conversation context in the process.
- support `claudeAgent.fastMode` in shared/contracts, provider dispatch, and Claude adapter SDK settings - gate fast mode to supported Claude models and restart/forward options in orchestration - unify draft/UI fast mode state (not Codex-only) and add browser/server tests plus SDK probe
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 11 total unresolved issues (including 9 from previous reviews).
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Resume cursor dropped unnecessarily on effort change
- Removed shouldRestartForModelOptionsChange from the condition that drops resumeCursor, so effort/model-option changes now preserve conversation context like runtimeModeChanged does.
- ✅ Fixed: Redundant tool classification conditions after inclusive check
- Simplified the second if-block to only check normalized === "task", removing the three conditions already covered by the earlier normalized.includes("agent") check.
Or push these changes by commenting:
@cursor push 63ff52e33a
Preview (63ff52e33a)
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
@@ -311,7 +311,7 @@
}
const resumeCursor =
- providerChanged || shouldRestartForModelChange || shouldRestartForModelOptionsChange
+ providerChanged || shouldRestartForModelChange
? undefined
: (activeSession?.resumeCursor ?? undefined);
yield* Effect.logInfo("provider command reactor restarting provider session", {
diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts
--- a/apps/server/src/provider/Layers/ClaudeAdapter.ts
+++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts
@@ -238,12 +238,7 @@
if (normalized.includes("agent")) {
return "collab_agent_tool_call";
}
- if (
- normalized === "task" ||
- normalized === "agent" ||
- normalized.includes("subagent") ||
- normalized.includes("sub-agent")
- ) {
+ if (normalized === "task") {
return "collab_agent_tool_call";
}
if (|
|
||
| const resumeCursor = | ||
| providerChanged || shouldRestartForModelChange | ||
| providerChanged || shouldRestartForModelChange || shouldRestartForModelOptionsChange |
There was a problem hiding this comment.
Resume cursor dropped unnecessarily on effort change
Medium Severity
When shouldRestartForModelOptionsChange is true (e.g., Claude effort changes from "medium" to "max"), the resumeCursor is set to undefined, causing the restarted session to lose all conversation context. Compare this with runtimeModeChanged, which preserves the resume cursor so conversation history survives the restart. The Claude SDK's resume parameter works independently of effort settings, so there's no technical reason to discard it. Users changing their thinking level will unexpectedly lose their entire conversation.
Additional Locations (1)
| normalized.includes("sub-agent") | ||
| ) { | ||
| return "collab_agent_tool_call"; | ||
| } |
There was a problem hiding this comment.
Redundant tool classification conditions after inclusive check
Low Severity
In classifyToolItemType, the first if block matches any tool name containing "agent" (normalized.includes("agent")). The second if block then redundantly re-checks normalized === "agent", normalized.includes("subagent"), and normalized.includes("sub-agent") — all already covered by the first check. Only normalized === "task" in the second block adds new logic. The redundancy obscures what the second block actually contributes.
| }); | ||
| return; | ||
| } | ||
| const tool = context.inFlightTools.get(index); |
There was a problem hiding this comment.
🟢 Low Layers/ClaudeAdapter.ts:1365
In the content_block_stop handler (line 1354), when the stopped block corresponds to a tool (not an assistant text block), the tool is fetched from context.inFlightTools at line 1365 but execution silently falls through without emitting any event. This means the final accumulated tool input from input_json_delta streaming is never published as an item.updated event when the content block finishes — the UI will show stale/partial tool input until the tool result message arrives later. The input_json_delta handler only emits updates when the fingerprint changes, so any final delta that didn't alter the fingerprint is lost. Add an item.updated emission (following the same pattern as the input_json_delta handler at lines 1256–1281) after line 1368 to publish the tool's final input state.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/ClaudeAdapter.ts around line 1365:
In the `content_block_stop` handler (line 1354), when the stopped block corresponds to a tool (not an assistant text block), the tool is fetched from `context.inFlightTools` at line 1365 but execution silently falls through without emitting any event. This means the final accumulated tool input from `input_json_delta` streaming is never published as an `item.updated` event when the content block finishes — the UI will show stale/partial tool input until the tool result message arrives later. The `input_json_delta` handler only emits updates when the fingerprint changes, so any final delta that didn't alter the fingerprint is lost. Add an `item.updated` emission (following the same pattern as the `input_json_delta` handler at lines 1256–1281) after line 1368 to publish the tool's final input state.
Evidence trail:
apps/server/src/provider/Layers/ClaudeAdapter.ts lines 1354-1369: `content_block_stop` handler shows the tool is fetched at line 1365 (`const tool = context.inFlightTools.get(index);`), then lines 1366-1368 only check `if (!tool) { return; }` with no code to handle when tool exists - the handler ends at line 1369.
apps/server/src/provider/Layers/ClaudeAdapter.ts lines 1242-1249: `input_json_delta` handler shows the fingerprint check that prevents emission when fingerprint is unchanged: `if (!parsedInput || !nextFingerprint || tool.lastEmittedInputFingerprint === nextFingerprint) { return; }`
apps/server/src/provider/Layers/ClaudeAdapter.ts lines 1256-1281: Shows the `item.updated` event emission pattern in `input_json_delta` handler that is missing from the `content_block_stop` tool handling.
|
@juliusmarminge thanks for all the work you’ve put into this! Do you have an estimate for when it might be ready to land? |






Summary
This PR adds the Claude Code adapter on top of the core orchestration branch in #103.
It includes:
Stack
codething/648ca884Validation
bun lintbun typecheckcd apps/server && bun run test -- --run src/provider/Layers/ProviderAdapterRegistry.test.tscd apps/web && bun run test -- --run src/session-logic.test.tsNote
Medium Risk
Adds a new provider runtime backed by
@anthropic-ai/claude-agent-sdkand changes orchestration session-start logic to enforce provider/model compatibility and restart behavior, which could affect turn execution and recovery flows.Overview
Adds first-class
claudeAgentsupport end-to-end: provider-aware model contracts (including defaults and model options), a Claude adapter backed by@anthropic-ai/claude-agent-sdk, and new/updated tests validating canonical runtime event mapping, approvals/user-input bridging, resume/rollback behavior, and session recovery.Updates orchestration/provider reactors to support explicit provider selection on turn start, infer provider from model, reject provider/model mismatches, persist per-thread
providerOptions/modelOptions, and restart Claude sessions when ClaudemodelOptions(e.g. effort) change; also improves error reporting by appendingprovider.turn.start.failedactivities.Extends runtime ingestion to prefer
task.progress.summarywhen available, and refreshes docs/README to reflect Claude availability.Written by Cursor Bugbot for commit f98679d. This will update automatically on new commits. Configure here.
Note
Add Claude Code adapter to support claudeAgent as a live provider
ClaudeAdapterLiveimplementation inapps/server/src/provider/Layers/ClaudeAdapter.tsthat wraps@anthropic-ai/claude-agent-sdkbehind the generic provider adapter contract, supporting session start/stop, turn sending, interrupts, approvals, and event streaming.claudeAgenta first-class provider alongsidecodex.ProviderKind,ProviderStartOptions,ProviderModelOptions, model catalog) to include Claude models, effort levels (low/medium/high/max/ultrathink), and aliases.ChatView,ProviderTraitsPicker,CompactComposerControlsMenu, andProviderModelPickerto support provider-specific effort labels, fast mode gating, and model selection forclaudeAgent.ProviderHealthdetection for theclaudeCLI (version probe + auth status) so provider status banners reflect Claude availability.codexFastModetofastModeandsetCodexFastModetosetFastModethroughout the composer draft store for provider-agnostic fast mode handling.fastModerename is backward-compatible via a legacy fallback intoHydratedThreadDraft, but any persisted draft withcodexFastMode: truewill be read asfastMode: trueon upgrade.Macroscope summarized f98679d.