feat: orchestration core and Codex plumbing#103
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:
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds multi-provider support: provider-scoped model configuration and helpers, nests provider startup options under Changes
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)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes 🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
aaae1cf to
03beb30
Compare
There was a problem hiding this comment.
🧹 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
getDefaultModelandgetModelOptions, but are nested under theresolveModelSlugdescribe block. Consider moving them to a dedicateddescribe("backward compatibility", ...)ordescribe("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 innormalizeModelSlug.Line 79 casts any non-aliased trimmed string to
ModelSlug, which could return an invalid slug string. WhileresolveModelSlugsubsequently validates againstMODEL_SLUG_SET_BY_PROVIDER, direct callers ofnormalizeModelSlugreceive an uncheckedModelSlug.If this is intentional (normalize = alias expansion only, resolve = validation), consider a brief doc comment clarifying that callers should use
resolveModelSlugfor 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.
There was a problem hiding this comment.
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 | 🟡 MinorAcknowledge the fallback for active sessions, but clarify the limitation for stale sessions.
The code has a fallback path (lines 234-250): if
resumeThreadIdexists and the provider session is still active, recovery succeeds using the persistedresumeCursorfrom that session. However, sessions that are stale (no active provider session) and lack a persistedresumeCursorwill fail at the guard on line 262.If existing production data includes stale sessions with
resumeThreadIdbut noresumeCursor, 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 assertmodel: "gpt-5". That pairing can make this test semantically inconsistent with provider-aware model resolution. Prefer deriving the model from contracts forclaudeCode(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 hoistingresolveResumeCursorForSessionoutside the closure.This function is recreated on every
ensureSessionForThreadcall. Since it only depends onproviderService(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: ConstrainpermissionModeto known values usingSchema.Literals.Claude Code supports a fixed set of permission modes:
"default","acceptEdits","plan", and"bypassPermissions". UseSchema.Literalsinstead ofTrimmedNonEmptyStringSchemafor 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 fromlistSessionsto 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
📒 Files selected for processing (13)
apps/server/src/codexAppServerManager.test.tsapps/server/src/codexAppServerManager.tsapps/server/src/orchestration/Layers/ProviderCommandReactor.test.tsapps/server/src/orchestration/Layers/ProviderCommandReactor.tsapps/server/src/orchestration/decider.projectScripts.test.tsapps/server/src/orchestration/decider.tsapps/server/src/provider/Layers/ProviderService.test.tsapps/server/src/provider/Layers/ProviderService.tsapps/server/src/provider/Layers/ProviderSessionDirectory.test.tspackages/contracts/src/orchestration.test.tspackages/contracts/src/orchestration.tspackages/contracts/src/provider.test.tspackages/contracts/src/provider.ts
💤 Files with no reviewable changes (1)
- apps/server/src/codexAppServerManager.test.ts
There was a problem hiding this comment.
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.
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: Exported constant
DEFAULT_PROVIDER_KINDis never used- Removed the unused
DEFAULT_PROVIDER_KINDconstant since it was dead code never imported or referenced anywhere in the codebase.
- Removed the unused
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",There was a problem hiding this comment.
🧹 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 frompackages/contractsfor 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
emitRuntimeEventis called. Consider adding a small yield or usingEffect.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 usingassertFailurefor consistency.The session error test manually checks
result._tag === "Failure"and uses early returns, while the validation test usesassertFailure. Consider using a similar pattern orCause.matchfor 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
toSessionErrorfunction 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 >= 1and 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 inProviderPicker.Line 2829 hardcodes
"codex"/"claudeCode", so newly added providers could appear in the dropdown but never be selectable. Prefer validating against the same filteredPROVIDER_OPTIONSsource 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
📒 Files selected for processing (17)
apps/server/integration/OrchestrationEngineHarness.integration.tsapps/server/integration/TestProviderAdapter.integration.tsapps/server/integration/orchestrationEngine.integration.test.tsapps/server/integration/providerService.integration.test.tsapps/server/src/orchestration/Layers/CheckpointReactor.test.tsapps/server/src/orchestration/Layers/ProviderCommandReactor.test.tsapps/server/src/orchestration/Layers/ProviderCommandReactor.tsapps/server/src/provider/Layers/ClaudeCodeAdapter.test.tsapps/server/src/provider/Layers/ClaudeCodeAdapter.tsapps/server/src/provider/Layers/ProviderAdapterRegistry.test.tsapps/server/src/provider/Layers/ProviderAdapterRegistry.tsapps/server/src/provider/Layers/ProviderService.test.tsapps/server/src/provider/Services/ClaudeCodeAdapter.tsapps/server/src/serverLayers.tsapps/web/src/components/ChatView.tsxapps/web/src/session-logic.test.tsapps/web/src/session-logic.ts
There was a problem hiding this comment.
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.
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 {There was a problem hiding this comment.
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
📒 Files selected for processing (2)
apps/server/src/provider/Layers/ClaudeCodeAdapter.test.tsapps/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
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: Exported
ClaudeCodeAdapterLiveconstant is never imported- Removed the unused
ClaudeCodeAdapterLiveconstant export since all consumers use themakeClaudeCodeAdapterLive()factory function instead.
- Removed the unused
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);
}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: Hardcoded provider prefixes silently drop model selections
- Replaced hardcoded
startsWith("codex:")andstartsWith("claudeCode:")checks with a genericindexOf(":")split that works for any provider prefix, making the handler extensible without coordinated changes.
- Replaced hardcoded
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">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: 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.
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" {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: Detached fiber bypasses adapter scope management
- Replaced
Effect.runFork(runSdkStream(context))withyield* Effect.forkDetach(runSdkStream(context))to inherit provided services, stored the fiber inClaudeSessionContext.streamFiber, and addedFiber.interruptinstopSessionInternalfor deterministic cleanup before queue shutdown.
- Replaced
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,dc91dc5 to
36a0695
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: 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.
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 } : {}),
packages/contracts/src/model.ts
Outdated
| 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>> = { |
There was a problem hiding this comment.
🟢 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)
42ebb30 to
870096b
Compare
| ] | ||
| .toSorted((left, right) => left.createdAt.localeCompare(right.createdAt)) | ||
| .toSorted(compareThreadActivities) | ||
| .slice(-500); |
There was a problem hiding this comment.
🟡 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
compareActivitiesByOrderintroduces a logic regression where activities without asequenceproperty are strictly sorted before activities with asequenceproperty. In scenarios involving optimistic UI updates, a locally generated 'resolve' activity (which lacks a server-assignedsequence) will be sorted before the corresponding server-confirmed 'request' activity (which has asequence).
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.
scripts/cursor-acp-probe.mjs
Outdated
| channel: "client->server", | ||
| message, | ||
| }); | ||
| this.#child.stdin.write(`${JSON.stringify(message)}\n`); |
There was a problem hiding this comment.
🟢 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) => ( |
There was a problem hiding this comment.
🟢 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.
scripts/cursor-acp-probe.mjs
Outdated
| ...process.env, | ||
| NO_COLOR: "1", | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🟢 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.
3e4539f to
162d5e6
Compare
There was a problem hiding this comment.
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.tswhich was not referenced by any imports, tests, or build configurations.
- Deleted the accidentally committed debugging reproduction script
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); |
There was a problem hiding this comment.
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.
Co-authored-by: codex <[email protected]>
Co-authored-by: codex <[email protected]>
There was a problem hiding this comment.
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
adapterHarnesswhen using real Codex- Made adapterHarness nullable in the OrchestrationIntegrationHarness interface and removed the unsafe
ascast, so TypeScript now enforces null checks at compile time.
- Made adapterHarness nullable in the OrchestrationIntegrationHarness interface and removed the unsafe
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=="],
apps/server/src/main.ts
Outdated
| import { Command, Flag } from "effect/unstable/cli"; | ||
| import { NetService } from "@t3tools/shared/Net"; | ||
|
|
||
| // Dummy comment. |
There was a problem hiding this comment.
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.
| rootDir, | ||
| workspaceDir, | ||
| dbPath, | ||
| adapterHarness: adapterHarness as TestProviderAdapterHarness, |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 TestProviderAdapterHarnesscast, so null-pointer errors surface at compile time.
- Made adapterHarness nullable in the OrchestrationIntegrationHarness interface and removed the unsafe
- ✅ 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 thePendingApprovalRequest.methodtype.
- Extended the ternary to map
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, |
There was a problem hiding this comment.
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.
| if (input.interactionMode !== "plan") { | ||
| return undefined; | ||
| } | ||
| const model = normalizeCodexModelSlug(input.model) ?? "gpt-5.3-codex"; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
| export const ProviderKind = Schema.Literals(["codex", "cursor"]); |
There was a problem hiding this comment.
🟢 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<{ | |||
There was a problem hiding this comment.
🟡 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, |
There was a problem hiding this comment.
🟡 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
Co-authored-by: codex <[email protected]>
There was a problem hiding this comment.
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.
- Removed the meaningless
- ✅ Fixed: Null adapter harness unsafely cast to non-null type
- Made
adapterHarnessnullable (TestProviderAdapterHarness | null) in the interface and removed the unsafeascast, with non-null assertions added at test call sites that are guaranteed non-null.
- Made
- ✅ Fixed: File-read approval stored with wrong method value
- Extended the
PendingApprovalRequest.methodtype union to include"item/fileRead/requestApproval"and expanded the binary ternary to a three-way conditional that correctly maps the"file-read"request kind.
- Extended the
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,
apps/server/src/main.ts
Outdated
| import { Command, Flag } from "effect/unstable/cli"; | ||
| import { NetService } from "@t3tools/shared/Net"; | ||
|
|
||
| // Dummy comment. |
There was a problem hiding this comment.
| rootDir, | ||
| workspaceDir, | ||
| dbPath, | ||
| adapterHarness: adapterHarness as TestProviderAdapterHarness, |
There was a problem hiding this comment.
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)
| : "item/fileChange/requestApproval", | ||
| requestKind, | ||
| ...(route.threadId ? { threadId: route.threadId } : {}), | ||
| threadId: context.session.threadId, |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
- Removed the meaningless
- ✅ Fixed: File-read approval mapped to wrong pending method
- Extended the ternary to correctly map
file-readrequests toitem/fileRead/requestApprovaland added the new method variant to thePendingApprovalRequesttype union.
- Extended the ternary to correctly map
- ✅ Fixed: Null adapter harness unsafely cast to non-null type
- Changed the
OrchestrationIntegrationHarness.adapterHarnesstype toTestProviderAdapterHarness | null, removed the unsafeascast, and updated test call sites with explicit non-null assertions.
- Changed the
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,
apps/server/src/main.ts
Outdated
| import { Command, Flag } from "effect/unstable/cli"; | ||
| import { NetService } from "@t3tools/shared/Net"; | ||
|
|
||
| // Dummy comment. |
There was a problem hiding this comment.
| : "item/fileChange/requestApproval", | ||
| requestKind, | ||
| ...(route.threadId ? { threadId: route.threadId } : {}), | ||
| threadId: context.session.threadId, |
There was a problem hiding this comment.
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)
| rootDir, | ||
| workspaceDir, | ||
| dbPath, | ||
| adapterHarness: adapterHarness as TestProviderAdapterHarness, |
There was a problem hiding this comment.
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.
| function isAvailableProviderOption(option: (typeof PROVIDER_OPTIONS)[number]): option is { | ||
| value: ProviderKind; | ||
| label: string; | ||
| available: true; | ||
| } { | ||
| return option.available && option.value !== "claudeCode"; | ||
| } |
There was a problem hiding this comment.
🟢 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"`
There was a problem hiding this comment.
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.
- Removed the meaningless
- ✅ Fixed: File-read approval method incorrectly mapped as file-change
- Added a
file-readbranch to the ternary inhandleServerRequestto correctly map toitem/fileRead/requestApproval, and added the new method variant to thePendingApprovalRequestinterface type.
- Added a
- ✅ Fixed: Null adapter harness unsafely cast to non-null type
- Changed
adapterHarnesstoTestProviderAdapterHarness | nullin the interface, removed the unsafe cast, and updated test usages with non-null assertions.
- Changed
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,
apps/server/src/main.ts
Outdated
| import { Command, Flag } from "effect/unstable/cli"; | ||
| import { NetService } from "@t3tools/shared/Net"; | ||
|
|
||
| // Dummy comment. |
There was a problem hiding this comment.
| : "item/fileChange/requestApproval", | ||
| requestKind, | ||
| ...(route.threadId ? { threadId: route.threadId } : {}), | ||
| threadId: context.session.threadId, |
There was a problem hiding this comment.
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)
| rootDir, | ||
| workspaceDir, | ||
| dbPath, | ||
| adapterHarness: adapterHarness as TestProviderAdapterHarness, |
There was a problem hiding this comment.
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)
Co-authored-by: codex <[email protected]>
Co-authored-by: codex <[email protected]>
There was a problem hiding this comment.
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.tswhich was development scaffolding unrelated to the PR's scope.
- Deleted the debug reproduction script
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
There was a problem hiding this comment.
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
falseoverride- Removed
Option.filter(flag, Boolean)fromresolveBooleanFlagsinceFlag.optionalalready returnsNonefor absent flags, so the filter was incorrectly collapsing explicitSome(false)intoNoneand preventing CLI overrides of env-settruevalues.
- Removed
Or push these changes by commenting:
@cursor push e05cc8885c
| @@ -496,15 +1330,15 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => | |||
| }); | |||
| if (!attachmentPath) { | |||
| return yield* toRequestError( | |||
There was a problem hiding this comment.
🟢 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
| }); | ||
|
|
||
| const resolveBooleanFlag = (flag: Option.Option<boolean>, envValue: boolean) => | ||
| Option.getOrElse(Option.filter(flag, Boolean), () => envValue); |
There was a problem hiding this comment.
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.
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.
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.



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:
The provider-specific adapters have been split out into follow-up PRs:
codething/648ca884-cursorcodething/648ca884-claudeWhy 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 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
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
threadIdacross 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 projectruntimeMode,interactionMode, andproposedPlans; add provider capabilities and user-input responses; add activitysequenceordering; add WebSocketprojects.writeFileroute 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 inapps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts(https://github.com/pingdotgg/t3code/pull/103/files#diff-22a6c9818b956463b848a73a5b3e787b44a144bd003b1cc25fa2b1b3110cdea7) and provider routing inapps/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 byThreadId(instead of provider session ids), and runtime fixtures/tests are updated to the v2 canonicalProviderRuntimeEventmodel (e.g.content.delta,item.*,request.*, payload-driventurn.completed).Codex runtime controls are expanded by threading
runtimeMode/interactionModethrough session start andturn/start, adding collaboration-mode developer instruction presets (plan vs default), supporting structureduser-inputrequest/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, addsServerLoggerLive, 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.