Conversation
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
…and Kilo adapters Add provider adapter infrastructure for OpenCode, Copilot, Amp, GeminiCli, and Kilo alongside the existing Codex and Claude Code adapters. Includes server managers, runtime ingestion enhancements, provider health monitoring, UI settings for provider selection, sidebar improvements, accent color theming, and upstream PR track sync tooling.
|
Caution Review failedPull request was closed or merged during review 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 (Copilot, Claude Code, OpenCode, Gemini CLI, Amp, Kilo): new server managers and adapter layers, expanded orchestration/events and contracts (models, usage, turnUsage), web UI model/usage and accent-color theming, SDK typings, tests, and tooling/scripts for PR tracking. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Adapter as ProviderAdapter
participant Manager as ServerManager
participant SDK as Provider SDK/CLI
participant Queue as RuntimeEventQueue
Client->>Adapter: startSession(startInput)
Adapter->>Manager: startSession(startInput)
Manager->>SDK: init / resume session
SDK-->>Manager: session ready / session_id
Manager-->>Adapter: session metadata
Manager->>Queue: emit session.started
Client->>Adapter: sendTurn(turnInput)
Adapter->>Manager: sendTurn(turnInput)
Manager->>SDK: stream prompt/messages
SDK-->>Manager: stream events (content_delta, tool_use, result)
Manager->>Queue: emit content.delta / item.started / item.completed
Manager-->>Adapter: turn.started / turn.completed (with usage)
Adapter-->>Client: ProviderTurnStartResult
Client->>Adapter: streamEvents()
Adapter->>Queue: Stream.fromQueue()
Queue-->>Client: ProviderRuntimeEvent...
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
✨ Finishing Touches
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 1
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts (1)
509-561:⚠️ Potential issue | 🟡 MinorDuplicate test case detected.
This test "accepts claude turn lifecycle when seeded thread id is a synthetic placeholder" (lines 509-561) is an exact duplicate of the test at lines 402-454. This appears to be a merge artifact and should be removed.
🗑️ Remove duplicate test
- it("accepts claude turn lifecycle when seeded thread id is a synthetic placeholder", async () => { - const harness = await createHarness(); - const seededAt = new Date().toISOString(); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.makeUnsafe("cmd-session-seed-claude-placeholder"), - threadId: ThreadId.makeUnsafe("thread-1"), - session: { - threadId: ThreadId.makeUnsafe("thread-1"), - status: "ready", - providerName: "claudeCode", - runtimeMode: "approval-required", - activeTurnId: null, - updatedAt: seededAt, - lastError: null, - }, - createdAt: seededAt, - }), - ); - - harness.emit({ - type: "turn.started", - eventId: asEventId("evt-turn-started-claude-placeholder"), - provider: "claudeCode", - createdAt: new Date().toISOString(), - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-claude-placeholder"), - }); - - await waitForThread( - harness.engine, - (thread) => - thread.session?.status === "running" && - thread.session?.activeTurnId === "turn-claude-placeholder", - ); - - harness.emit({ - type: "turn.completed", - eventId: asEventId("evt-turn-completed-claude-placeholder"), - provider: "claudeCode", - createdAt: new Date().toISOString(), - threadId: asThreadId("thread-1"), - turnId: asTurnId("turn-claude-placeholder"), - status: "completed", - }); - - await waitForThread( - harness.engine, - (thread) => thread.session?.status === "ready" && thread.session?.activeTurnId === null, - ); - });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts` around lines 509 - 561, Remove the duplicated test case named "accepts claude turn lifecycle when seeded thread id is a synthetic placeholder" (the whole it(...) block shown in the diff) so only the original test remains; locate the duplicate by matching the test title and the unique identifiers used inside (CommandId.makeUnsafe("cmd-session-seed-claude-placeholder"), asEventId("evt-turn-started-claude-placeholder"), asTurnId("turn-claude-placeholder")) and delete that entire duplicated it(...) block, then run the test suite to confirm no regressions.apps/web/src/components/ChatView.tsx (1)
5508-5534:⚠️ Potential issue | 🟠 MajorPreserve
isCustombefore replacing Copilot's static list.
getAppModelOptions()still returns built-in and custom entries withisCustom, butModelOptionEntryerases that flag here.customOnlytherefore keeps every base model missing from discovery, not just user-defined ones, so stale built-in Copilot slugs remain selectable.💡 Preserve the custom marker through the merge
-type ModelOptionEntry = { slug: string; name: string; pricingTier?: string }; +type ModelOptionEntry = { + slug: string; + name: string; + pricingTier?: string; + isCustom?: boolean; +}; @@ - const customOnly = (base[provider] ?? []).filter( - (m) => !models.some((d) => d.slug === m.slug), - ); + const customOnly = (base[provider] ?? []).filter( + (m) => m.isCustom && !models.some((d) => d.slug === m.slug), + );Also applies to: 5537-5564
🤖 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 5508 - 5534, getCustomModelOptionsByProvider currently loses the isCustom marker when merging built-in and discovered lists (via getAppModelOptions) so customOnly picks up stale built-ins; preserve the custom flag when composing options: keep the original ModelOptionEntry.isCustom on any merged entries (or explicitly copy option.isCustom into the resulting entries) instead of producing entries that erase it, update the cursor merge/filter (getCursorModelFamilyOptions + getAppModelOptions("cursor")) and the Copilot/other provider merges (the same pattern around the block that mirrors 5537-5564) to use the preserved isCustom when filtering/constructing the final arrays so only true user-defined models are flagged as custom.
🟠 Major comments (22)
.plans/17-claude-code.md-98-115 (1)
98-115:⚠️ Potential issue | 🟠 MajorAdd credential management and security considerations.
The plan lists Claude SDK configuration options (binary path, permission mode, max thinking tokens) but doesn't address how API keys or authentication credentials for the Claude SDK will be managed, stored, or rotated.
For a production multi-provider system, the plan should specify:
- Where provider credentials are stored (environment variables, secure settings, keychain)
- How credentials are scoped (per-user, per-workspace, global)
- Whether credentials are encrypted at rest
- Rotation and expiration policies
- How credential failures are surfaced to users
This is especially critical since the PR introduces 6 different provider integrations, each likely requiring separate authentication.
Additionally, consider documenting:
- Rate limiting strategies per provider to avoid quota exhaustion
- Concurrent session limits (memory/resource constraints when multiple providers are active)
- Observability patterns (structured logging with provider context, per-provider metrics)
🤖 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 98 - 115, Update the plan to include a clear credential and security policy for the new Claude/provider integrations: specify where credentials are stored (env vars, secrets manager, keychain), scoping rules (per-user, per-workspace, global), encryption-at-rest requirements, rotation/expiration policies and how failures surface to users (errors/alerts), and map these to the adapter options like providerOptions.claudeCode (pathToClaudeCodeExecutable, permissionMode, maxThinkingTokens) plus top-level options (env, additionalDirectories, hooks, resume/resumeSessionAt, includePartialMessages, canUseTool); also add production guidelines for per-provider rate limiting, concurrent session limits, and observability (structured logging/metrics with provider context) so each of the six providers has documented auth, rotation, and failure-handling expectations..plans/17-claude-code.md-374-417 (1)
374-417: 🛠️ Refactor suggestion | 🟠 MajorExpand testing strategy to cover multi-provider scenarios.
The testing strategy is thorough for single-provider integration but doesn't address critical multi-provider scenarios that are central to this PR's scope:
Missing test coverage areas:
- Provider switching: What happens when a user switches from Codex to Claude mid-thread?
- Concurrent sessions: Multiple providers active simultaneously (memory, performance, state isolation)
- Provider fallback: Behavior when a selected provider becomes unavailable
- Cross-provider resume: Whether resume cursors are strictly scoped to prevent leakage
- Registry edge cases: Provider discovery when some adapters fail to initialize
- Health monitoring: Provider-specific health checks and degradation scenarios
- Event ordering: Race conditions when multiple providers emit runtime events simultaneously
📋 Suggested additional test coverage
Add to Phase 6.4 Orchestration integration tests:
5. **Multi-provider scenarios**: - Provider switching mid-conversation - Concurrent active sessions across different providers - Provider availability changes during active sessions - Resume cursor isolation (Claude cursor cannot start Codex session) - Health monitoring and fallback behavior - Event ordering and concurrency under multi-provider loadAdd to Phase 6.3 Provider service integration tests:
7. **Provider registry and health**: - All providers report correct availability status - Provider initialization failures don't block healthy providers - Health probes execute correctly for each provider type🤖 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 374 - 417, The Phase 6 testing plan misses multi-provider scenarios; add explicit tests for provider switching mid-conversation, concurrent active sessions across different providers, provider fallback when a provider becomes unavailable, resume cursor isolation (ensure Claude cursors cannot resume Codex and vice versa), provider registry initialization failures not blocking healthy providers, per-provider health probes and degradation behavior, and event ordering/race conditions when multiple providers emit runtime events. Implement these as: (1) additions to ClaudeCodeAdapter.test.ts for adapter-level concurrency, resume/interrupt and rollback edge cases; (2) extensions to Phase 6.3 ProviderService integration tests to validate registry/health, start/send/restart flows across multiple providers; and (3) Phase 6.4 orchestration integration tests to cover provider switching, concurrent sessions, fallback/recovery, checkpoint revert under multi-provider load, and event ordering under concurrency. Ensure tests use SDK-facing boundaries only and assert strict provider scoping for resume cursors and session isolation..plans/17-claude-code.md-1-16 (1)
1-16:⚠️ Potential issue | 🟠 MajorPlan scope significantly misaligned with implementation: covers Claude only while PR implements 6+ providers.
The plan file is titled and scoped exclusively for Claude Code integration, with its introduction specifically stating "Claude integration must plug into this path." However, verification confirms the PR implements a full multi-provider adapter infrastructure including ClaudeCodeAdapter, CopilotAdapter, OpenCodeAdapter, GeminiCliAdapter, KiloAdapter, and AmpAdapter—each with corresponding server managers and orchestration routing.
This creates a documentation maintenance problem: future readers will find a plan that doesn't capture the multi-provider patterns, abstractions, or design decisions actually implemented across 6+ providers.
Update the plan to either:
- Generalize it as a multi-provider integration guide (rename and restructure to show how new providers plug into the orchestration path)
- Or add a prominent note explaining this documents the pattern applicable to all providers, not just Claude
🤖 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 1 - 16, The plan is scoped to "Claude Code" but the PR implements a multi-provider adapter infrastructure (ClaudeCodeAdapter, CopilotAdapter, OpenCodeAdapter, GeminiCliAdapter, KiloAdapter, AmpAdapter) and shared orchestration plumbing (orchestration.dispatchCommand, OrchestrationEngine, ProviderCommandReactor, ProviderService, ProviderRuntimeIngestion, orchestration.domainEvent); update the document to either (A) generalize and rename it to a multi-provider integration guide that explains how new adapters plug into the orchestration path and the common abstractions used across all adapters, or (B) add a prominent note at the top stating this plan describes a pattern applicable to all providers and then add sections showing how each adapter (list the six adapters) fits into the orchestration steps so readers can map implementation to design.packages/contracts/src/orchestration.ts-30-39 (1)
30-39:⚠️ Potential issue | 🟠 MajorFix
Schema.Unionto use variadic arguments instead of an array.
Schema.Unionexpects variadic arguments, not an array. Correct the call by spreading the members:export const ProviderKind = Schema.Union( Schema.Literal("codex"), Schema.Literal("copilot"), Schema.Literal("claudeCode"), Schema.Literal("cursor"), Schema.Literal("opencode"), Schema.Literal("geminiCli"), Schema.Literal("amp"), Schema.Literal("kilo"), );Or use the spread operator if the array is already defined. The
Schema.Literalspattern below (line 41+) is correct for its array-based API.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/contracts/src/orchestration.ts` around lines 30 - 39, ProviderKind is currently created by passing an array to Schema.Union but Schema.Union requires variadic arguments; fix by calling Schema.Union with each Schema.Literal(...) as separate arguments (i.e., replace Schema.Union([...]) with Schema.Union(Schema.Literal("codex"), Schema.Literal("copilot"), Schema.Literal("claudeCode"), Schema.Literal("cursor"), Schema.Literal("opencode"), Schema.Literal("geminiCli"), Schema.Literal("amp"), Schema.Literal("kilo")) so the ProviderKind declaration uses variadic literals rather than an array.apps/web/src/components/ThreadTerminalDrawer.tsx-56-61 (1)
56-61:⚠️ Potential issue | 🟠 MajorKeep terminal accent colors contrast-safe.
Using the raw accent for
cursor,blue, andbrightBluecan make the cursor and ANSI blue text effectively disappear for light/pastel accents on the light terminal theme. Since accent color is user-configurable now, derive these terminal colors against the current background instead of applying the accent verbatim. Reusing the existing contrast logic fromapps/web/src/accentColor.tswould avoid that regression.Also applies to: 67-68, 94-95, 103-111
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/ThreadTerminalDrawer.tsx` around lines 56 - 61, The terminal currently uses the raw normalized accentColor for terminalBlue, terminalBrightBlue and selection/cursor, which can make blue/cursor invisible for light/pastel accents; update the logic to compute contrast-safe terminal colors by reusing the contrast helpers from apps/web/src/accentColor.ts (e.g., the function that derives a contrast-safe accent against a background) instead of using accentColor verbatim: call that helper passing the resolved background (based on isDark) to produce terminalBlue and terminalBrightBlue and use a contrast-safe color for the cursor/selection (replacing resolveAccentColorRgba usage where appropriate); apply the same change to the other occurrences mentioned (lines around 67-68, 94-95, 103-111) so all terminal accent usages are derived against the current background.apps/web/src/accentColor.ts-63-85 (1)
63-85:⚠️ Potential issue | 🟠 MajorReject accents whose best foreground still fails contrast.
This only picks the better of white and
#111827; it never checks whether that best ratio is actually acceptable. Mid-tone inputs such as#777777can still fail both choices, soapplyAccentColorToDocumentcan make primary text unreadable.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/accentColor.ts` around lines 63 - 85, The current logic in resolveAccentForegroundColor only picks the better of white and "#111827" without ensuring that choice meets an accessibility contrast threshold; update resolveAccentForegroundColor to compute both contrast ratios (whiteContrast and darkContrast), determine the best choice and then compare that best ratio against a defined WCAG threshold (e.g., 4.5 for normal text); if the best ratio is below the threshold return a sentinel (null or undefined) instead of a color. Then update applyAccentColorToDocument to call normalizeAccentColor and resolveAccentForegroundColor and, if the resolver returns no valid foreground (or the best contrast is below threshold), avoid setting the --accent-color and --accent-color-foreground variables (and optionally log or fallback to a safe default accent), ensuring primary text color is never made unreadable.apps/server/src/provider/Layers/KiloAdapter.ts-135-149 (1)
135-149:⚠️ Potential issue | 🟠 MajorForward
numTurnsto the manager.Line 147 validates
numTurnsand then drops it.rollbackThread(threadId, 5)currently delegates the same call asrollbackThread(threadId, 1).🐛 Minimal fix
- try: () => manager.rollbackThread(threadId), + try: () => manager.rollbackThread(threadId, numTurns),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/provider/Layers/KiloAdapter.ts` around lines 135 - 149, The rollbackThread implementation validates numTurns but never forwards it to the manager; change the Effect.tryPromise call in rollbackThread to call manager.rollbackThread(threadId, numTurns) (keeping the existing validation, ProviderAdapterValidationError usage, and the toRequestError catch wrapper) so the manager receives the requested number of turns instead of always using the default.apps/server/src/provider/Layers/OpenCodeAdapter.ts-135-149 (1)
135-149:⚠️ Potential issue | 🟠 MajorPass the validated rollback count through.
Line 147 ignores
numTurnsafter validating it, so multi-turn rollback requests collapse to the manager's default behavior.🐛 Minimal fix
- try: () => manager.rollbackThread(threadId), + try: () => manager.rollbackThread(threadId, numTurns),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/provider/Layers/OpenCodeAdapter.ts` around lines 135 - 149, rollbackThread validates numTurns but never passes it to the manager, causing multi-turn rollbacks to be ignored; update the rollbackThread implementation to call manager.rollbackThread(threadId, numTurns) (and propagate the same args into the Effect.tryPromise try/catch) so the validated rollback count is actually used; keep existing error handling with toRequestError(threadId, "session/revert", cause) and ProviderAdapterValidationError/PROVIDER unchanged.apps/server/src/provider/Layers/GeminiCliAdapter.ts-135-149 (1)
135-149:⚠️ Potential issue | 🟠 Major
rollbackThreadcurrently ignores its count parameter.Line 147 drops
numTurns, so callers cannot request a rollback depth larger than the manager default.🐛 Minimal fix
- try: () => manager.rollbackThread(threadId), + try: () => manager.rollbackThread(threadId, numTurns),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/provider/Layers/GeminiCliAdapter.ts` around lines 135 - 149, The rollbackThread implementation validates numTurns but then ignores it when calling manager.rollbackThread(threadId); update the call to pass the validated count (e.g., manager.rollbackThread(threadId, numTurns)) so the manager uses the requested rollback depth, keeping the existing Effect.tryPromise/catch and toRequestError(threadId, "session/revert", cause) behavior intact.apps/server/src/opencodeServerManager.ts-39-43 (1)
39-43:⚠️ Potential issue | 🟠 MajorScope the cached server by connection options.
providerOptionsare passed per session, butensureServer()memoizes a singleSharedServerStatefor the whole manager. The first thread that starts wins, and later threads silently reuse itsserverUrl/auth/binary path, which can route sessions to the wrong backend.Also applies to: 1183-1200, 1270-1285
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/opencodeServerManager.ts` around lines 39 - 43, The manager currently memoizes a single SharedServerState in ensureServer(), causing different sessions to share the first server's serverUrl/auth/binary path; change ensureServer() to cache per-connection options by including providerOptions (specifically OpenCode providerOptions within OpenCodeSessionStartInput) in the cache key (e.g., maintain a Map keyed by a stable serialization of providerOptions or relevant fields) so each distinct set of providerOptions produces its own SharedServerState; update any other places that rely on the single memo (see the same pattern referenced around lines 1183-1200 and 1270-1285) to use the per-options cache lookup/creation instead of a single shared instance.apps/server/src/opencodeServerManager.ts-602-611 (1)
602-611:⚠️ Potential issue | 🟠 MajorKeep
canceldistinct fromdecline.The shared decision enum and the UI expose both Decline and Cancel turn, but this manager collapses both to
reply: "reject". That removes any behavioral distinction for OpenCode approval prompts.Also applies to: 1039-1051
apps/web/src/components/ChatView.tsx-886-888 (1)
886-888:⚠️ Potential issue | 🟠 MajorAvoid eager provider discovery on every chat mount.
These queries run unconditionally, and
providerListModelsQueryOptions()immediately callsapi.provider.listModels(). For providers like OpenCode, model discovery can probe or spawn a local server, so opening a chat tab now starts unrelated provider work in the background even if the picker is never opened.🤖 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 886 - 888, The three queries (copilotModelsQuery, opencodeModelsQuery, kiloModelsQuery) call providerListModelsQueryOptions(), which immediately invokes api.provider.listModels() and causes eager provider discovery on every ChatView mount; change them to be lazy by passing an enabled flag or deferring creation until the model picker is opened so api.provider.listModels() only runs when needed. Update the hooks that create copilotModelsQuery, opencodeModelsQuery and kiloModelsQuery to use useQuery(..., { enabled: isPickerOpen || isManualTrigger }) or construct the query options only when the picker is opened/triggered, referencing providerListModelsQueryOptions and api.provider.listModels so discovery is gated. Ensure any UI that expects these queries handles the disabled state and triggers a refetch when the picker opens.apps/web/src/components/ChatView.tsx-5342-5343 (1)
5342-5343:⚠️ Potential issue | 🟠 MajorDon't suppress diff-only assistant turns.
This early return runs before the changed-files block below is computed. Any assistant message with empty text but a valid
turnDiffSummaryByAssistantMessageIdentry will disappear completely, taking its diff summary with it.💡 Only hide rows that truly have nothing to render
- if (!row.message.text && !row.message.streaming) return null; - const messageText = row.message.text || ""; + const turnSummary = turnDiffSummaryByAssistantMessageId.get(row.message.id); + if (!row.message.text && !row.message.streaming && !turnSummary) return null; + const messageText = row.message.text || ""; @@ - const turnSummary = turnDiffSummaryByAssistantMessageId.get(row.message.id); if (!turnSummary) return null;Also applies to: 5362-5415
🤖 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 5342 - 5343, The current early return in ChatView that checks if (!row.message.text && !row.message.streaming) must be tightened so assistant turns that have no text but do have a diff summary are not dropped: update the condition to also check the diff-summary map (turnDiffSummaryByAssistantMessageId) for an entry keyed by the assistant message id (e.g., row.message.id or the specific assistantMessageId used elsewhere) and only return null when text is empty, streaming is false, and there is no diff summary for that message; adjust the render logic that computes the "changed-files" block to rely on the same key so valid diff-only assistant messages remain rendered.apps/server/src/opencodeServerManager.ts-504-507 (1)
504-507:⚠️ Potential issue | 🟠 MajorRead
workspaceback out ofresumeCursor.Lines 833-836 persist both
sessionIdandworkspace, but reattach only readssessionId. A resumed thread that omitsproviderOptions.workspacewill callsession.get()against the default workspace and fail to reconnect to workspace-scoped sessions.💡 Minimal fix
-function readResumeSessionId(resumeCursor: unknown): string | undefined { - const record = asRecord(resumeCursor); - return asString(record?.sessionId); +function readResumeSessionInfo(resumeCursor: unknown): { + readonly sessionId?: string; + readonly workspace?: string; +} { + const record = asRecord(resumeCursor); + return { + sessionId: asString(record?.sessionId), + workspace: asString(record?.workspace), + }; } @@ - const workspace = options?.workspace; + const resume = readResumeSessionInfo(openCodeInput.resumeCursor); + const workspace = options?.workspace ?? resume.workspace; @@ - const resumedSessionId = readResumeSessionId(openCodeInput.resumeCursor); + const resumedSessionId = resume.sessionId;Also applies to: 783-809, 833-836
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/opencodeServerManager.ts` around lines 504 - 507, readResumeSessionId only extracts sessionId from resumeCursor but the resume flow also persists workspace; update the resume parsing to also read workspace (use asString(record?.workspace)) and return both values (e.g., change readResumeSessionId to return { sessionId?: string, workspace?: string } or add a new readResumeWorkspace function), then update callers (including the reattach logic that calls session.get() and any places handling providerOptions.workspace) to use the parsed workspace when reconnecting so workspace-scoped sessions reconnect correctly. Ensure you update function signature and all call sites (e.g., reattach/resume handlers referenced around readResumeSessionId and session.get()) to accept and pass the workspace.apps/server/src/opencodeServerManager.ts-900-965 (1)
900-965:⚠️ Potential issue | 🟠 MajorDon't silently drop attachments on OpenCode turns.
The shared turn contract already supports attachments, and the web composer sends them, but this path always forwards a single text part. Image/file turns will look sent in the UI and arrive as text-only prompts here.
🛡️ Minimal guard until attachment parts are implemented
const variant = openCodeInput.modelOptions?.opencode?.variant ?? openCodeInput.modelOptions?.opencode?.reasoningEffort ?? parsedModel?.variant; const startedAt = nowIso(); + + if ((openCodeInput.attachments?.length ?? 0) > 0) { + throw new Error("OpenCode attachments are not supported yet."); + } context.activeTurnId = turnId;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/opencodeServerManager.ts` around lines 900 - 965, The sendTurn function currently builds parts as a single textPart (parts: [textPart(openCodeInput.input ?? "")]) which silently drops any attachments; change the parts construction so it forwards the incoming attachment/part data instead of overwriting it: locate sendTurn / openCodeInput and replace the static parts array with one derived from the incoming input (e.g. use openCodeInput.parts if present, otherwise build parts by including a textPart for openCodeInput.input when present plus mapped attachment parts from openCodeInput.attachments), and pass that parts array into context.client.session.promptAsync so attachments are preserved and not dropped.apps/server/src/provider/Layers/CopilotAdapter.ts-527-560 (1)
527-560:⚠️ Potential issue | 🟠 MajorTerminal turn bookkeeping is incomplete.
sendTurn()queues an orchestrationturnIdimmediately, but terminal handling only clearscurrentTurnId/currentProviderTurnId. If Copilot terminates a turn beforeassistant.turn_start, that stale ID stays inpendingTurnIds,session.idlecan still emitturn.completed, and the nextassistant.turn_startcan be matched to the wrong turn.Also applies to: 1052-1080, 1300-1303
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/provider/Layers/CopilotAdapter.ts` around lines 527 - 560, The terminal handling in the CopilotAdapter's event cases (e.g., the "session.idle" branch) only clears currentTurnId/currentProviderTurnId but leaves the corresponding orchestration turn queued in pendingTurnIds, causing stale IDs to persist and mis-match future assistant.turn_start; update the terminal handlers (including the "session.idle" case and the other terminal ranges noted) to also remove/advance the corresponding entry from pendingTurnIds (and any related mappings) when clearing currentTurnId/currentProviderTurnId so that sendTurn()'s queued orchestration turn is consumed or discarded consistently and no stale turnId remains.apps/server/src/provider/Layers/ClaudeCodeAdapter.ts-1509-1513 (1)
1509-1513:⚠️ Potential issue | 🟠 MajorGuard
startSession()against duplicatethreadIds.This path always creates a new query and then overwrites the existing
sessionsentry without stopping it first. A retry on the same thread will orphan the old Claude stream/process, and its events can still leak into the runtime with no remaining handle to stop it.Also applies to: 1728-1729
🤖 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 1509 - 1513, The startSession flow (using readClaudeResumeState and the derived threadId) currently creates a new PromptQueue and then unconditionally overwrites the sessions entry, which can orphan an existing Claude stream; update startSession (and the analogous block around the other occurrence at the lines noted) to guard against duplicate threadId by checking sessions.get(threadId) before creating a new session: if an active session exists, either return that existing session/handle or explicitly stop/cleanup the old session (call its shutdown/stop/abort method or clear its promptQueue and stream) before replacing it; ensure you reference the threadId derived from readClaudeResumeState(input.resumeCursor) and handle both branches (resume vs new) so no orphaned streams remain.apps/server/src/provider/Layers/ClaudeCodeAdapter.ts-393-411 (1)
393-411:⚠️ Potential issue | 🟠 MajorDon't silently drop attachments.
Images are reduced to a text note, every non-image attachment is ignored, and a file-only turn can collapse to an empty text message. The turn still succeeds, but Claude never receives the attached data. If this adapter can't forward attachments, it should reject unsupported attachments instead of degrading silently.
🤖 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 393 - 411, The adapter currently converts attachments into plain text notes and silently drops non-image files; update the method in ClaudeCodeAdapter that builds the SDKUserMessage (the block iterating over input.attachments and returning the message) to validate attachments up front: if any attachment.type is not "image" reject/throw an error (do not degrade silently), and if there are attachments but the resulting text would be empty also reject/throw so we don't send an empty turn; keep the existing behavior for images (produce the descriptive text fragments) but ensure unsupported attachments cause the call to fail instead of being ignored.apps/server/src/provider/Layers/ClaudeCodeAdapter.ts-1719-1725 (1)
1719-1725:⚠️ Potential issue | 🟠 Major
rollbackThread()only edits local bookkeeping.
context.turnsis reinitialized to[]on resume, androllbackThread()only splices that local array before rewriting the cursor. The live Claude session still retains the removed turns, andlastAssistantUuidkeeps pointing at the discarded tip, so the next send/resume continues from the supposedly rolled-back history.Safer interim fix
- const rollbackThread: ClaudeCodeAdapterShape["rollbackThread"] = (threadId, numTurns) => + const rollbackThread: ClaudeCodeAdapterShape["rollbackThread"] = (threadId, _numTurns) => Effect.gen(function* () { - const context = yield* requireSession(threadId); - const nextLength = Math.max(0, context.turns.length - numTurns); - context.turns.splice(nextLength); - yield* updateResumeCursor(context); - return yield* snapshotThread(context); + yield* requireSession(threadId); + return yield* new ProviderAdapterRequestError({ + provider: PROVIDER, + method: "thread.rollback", + detail: "Claude Code rollback is not supported without rewinding the underlying provider session.", + }); });Also applies to: 1866-1873
🤖 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 1719 - 1725, rollbackThread currently only mutates the local context.turns array and cursor, leaving the remote Claude session and lastAssistantUuid pointing at the discarded tip; update rollbackThread to also remove the corresponding turns from the live Claude session (via whatever session API/delete or edit method your adapter uses) and set lastAssistantUuid to the new tip's uuid (not resumeState?.resumeSessionAt), and ensure resume logic does not blindly set turns: [] but instead syncs context.turns with the live session state after resume; touch symbols: rollbackThread(), context.turns, lastAssistantUuid, and the resume initialization that reads resumeState?.resume/resumeSessionAt so the in-memory and remote session are consistent.apps/server/src/provider/Layers/CopilotAdapter.ts-1091-1110 (1)
1091-1110:⚠️ Potential issue | 🟠 MajorEmit resolution/exit events when a Copilot session is torn down.
stopRecord()resolves pending approvals and user-input promises, but it never emitsrequest.resolved,user-input.resolved, orsession.exited. Stream consumers can end up with dangling modal state and no canonical signal that the session actually ended.Also applies to: 1416-1431
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/provider/Layers/CopilotAdapter.ts` around lines 1091 - 1110, stopRecord currently resolves pendingApprovalResolvers and pendingUserInputResolvers but does not notify stream consumers; update stopRecord (and the analogous teardown block around the 1416-1431 region) to emit the proper resolution/exit events after resolving each pending: emit "request.resolved" for each approval resolved, "user-input.resolved" for each user-input resolver, and emit a single "session.exited" for the ActiveCopilotSession when teardown finishes (do this before calling sessions.delete(record.threadId)); make these emissions on the session/event emitter associated with the ActiveCopilotSession so consumers receive canonical end signals.apps/server/src/provider/Layers/CopilotAdapter.ts-415-420 (1)
415-420:⚠️ Potential issue | 🟠 MajorAdd layer cleanup for active Copilot sessions and the event queue.
This layer creates an unbounded queue and maintains a sessions map but never registers a finalizer. When the scope releases (in tests, hot reloads, or layer replacement), these resources can leak. Other adapters in the same directory (CursorAdapter, ClaudeCodeAdapter) use
Effect.addFinalizerto clean up sessions on scope release—apply the same pattern here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/provider/Layers/CopilotAdapter.ts` around lines 415 - 420, The makeCopilotAdapter layer creates runtimeEventQueue via Queue.unbounded and a sessions Map (Map<ThreadId, ActiveCopilotSession>) but never registers a finalizer; add an Effect.addFinalizer inside makeCopilotAdapter to run when the scope releases that (1) iterates sessions.values() and invokes each ActiveCopilotSession cleanup method (use the same method name used in CursorAdapter/ClaudeCodeAdapter — e.g., session.close()/dispose()/stop()) to terminate active sessions and clear the sessions Map, and (2) shuts down the runtimeEventQueue by calling the Queue shutdown method (e.g., runtimeEventQueue.shutdown()/offerShutdown()) so both the sessions map and the unbounded queue are cleaned up on scope release.apps/server/src/provider/Layers/CopilotAdapter.ts-1266-1284 (1)
1266-1284:⚠️ Potential issue | 🟠 MajorHandle attachment validation errors through Effect error channel.
Throwing
ProviderAdapterRequestErrorfrom the synchronous.map()callback escapes the Effect context and becomes an unhandled defect instead of a normalsession.sendfailure. Within thisEffect.gengenerator, useEffect.forEachand propagate the error withyield*to keep callers on the typed request-error path, consistent with other error handling in this function (see lines 1363 and 1395).Suggested shape
- const attachments = (input.attachments ?? []) - .map((attachment) => { + const attachments = yield* Effect.forEach(input.attachments ?? [], (attachment) => + Effect.gen(function* () { const attachmentPath = resolveAttachmentPath({ stateDir: serverConfig.stateDir, attachment, }); if (!attachmentPath) { - throw new ProviderAdapterRequestError({ + return yield* new ProviderAdapterRequestError({ provider: PROVIDER, method: "session.send", detail: `Invalid attachment id '${attachment.id}'.`, }); } return { type: "file" as const, path: attachmentPath, displayName: attachment.name, }; - }); + }), + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/provider/Layers/CopilotAdapter.ts` around lines 1266 - 1284, The synchronous throw inside the attachments .map escapes the surrounding Effect.gen and causes an unhandled defect; replace the inline .map with an Effect.forEach that resolves each attachment via resolveAttachmentPath and, inside the generator, use yield* to run the Effect so any ProviderAdapterRequestError is returned on the Effect error channel; update the attachments construction in CopilotAdapter (the block that currently maps attachments to {type: "file", path, displayName}) to use Effect.forEach and yield* so invalid attachment ids produce a typed session.send failure instead of a defect.
🟡 Minor comments (11)
README.md-3-3 (1)
3-3:⚠️ Potential issue | 🟡 MinorDocument Kilo support here too.
The PR adds Kilo alongside the other provider adapters, but this README’s support summary and agent list omit it. That leaves the user-facing docs out of sync with the feature set.
Also applies to: 20-28
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@README.md` at line 3, Update the README to include Kilo in the support summary and the agent/provider list: add "Kilo" to the sentence that currently lists Codex, Claude Code, Cursor, Copilot, Gemini CLI, Amp, and OpenCode, and also add Kilo to the agent list block referenced around lines 20-28 so the documentation matches the new provider adapter added in the PR.README.md-1-1 (1)
1-1:⚠️ Potential issue | 🟡 MinorRemove the leading spaces before these headings.
markdownlintis flagging every heading here with MD023, so the README will keep failing docs lint until the headings start at column 1.Suggested fix
- # T3 Code +# T3 Code ... - ## Getting started +## Getting started ... - ### CLI +### CLI ... - ### Desktop app +### Desktop app ... - ## Supported agents +## Supported agents ... - ## Notes +## Notes ... - ## Need help? +## Need help?Also applies to: 5-5, 7-7, 16-16, 20-20, 30-30, 36-36
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@README.md` at line 1, Several README headings have leading spaces causing markdownlint MD023; update any lines that start with whitespace followed by hashes (e.g., " # T3 Code") so the first character of the line is the first "#" character. Fix by trimming leading whitespace for all heading lines (pattern: /^\s+#+/), and ensure exactly one space between the final "#" and the heading text (e.g., "# T3 Code", "## ...") for the headings mentioned.apps/server/src/provider/Layers/ProviderAdapterRegistry.ts-36-46 (1)
36-46:⚠️ Potential issue | 🟡 MinorFail fast on duplicate provider registrations.
new Map(adapters.map(...))silently keeps the last adapter for a provider. With a larger built-in set and customizableoptions.adapters, that can hide misconfiguration and route requests to the wrong backend. Validate uniqueness while buildingbyProviderinstead of overwriting implicitly.🛠️ Suggested guard
- const byProvider = new Map(adapters.map((adapter) => [adapter.provider, adapter])); + const byProvider = new Map(); + for (const adapter of adapters) { + if (byProvider.has(adapter.provider)) { + throw new Error(`Duplicate provider adapter registered for "${adapter.provider}"`); + } + byProvider.set(adapter.provider, adapter); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/provider/Layers/ProviderAdapterRegistry.ts` around lines 36 - 46, The current creation of byProvider using new Map(adapters.map(...)) silently overwrites duplicates; modify the code that builds byProvider (the adapters array -> byProvider logic) to iterate adapters and check for duplicates: for each adapter (use adapter.provider) if byProvider.has(adapter.provider) throw an error (or otherwise fail fast) with a clear message identifying the duplicate provider and the conflicting adapter entries; otherwise set the map entry. This ensures ProviderAdapterRegistry (the adapters array and byProvider) validates uniqueness instead of keeping the last registration.scripts/sync-upstream-pr-tracks.mjs-96-97 (1)
96-97:⚠️ Potential issue | 🟡 MinorHardcoded repository URL should use configuration.
The PR URL is hardcoded to
pingdotgg/t3code, but the script readsupstreamRemoteandforkRemotefrom configuration. Consider deriving the URL from the git remote or adding arepoUrlfield to the config.🔧 Suggested fix
In
loadConfig(), add repo URL extraction or config field:function loadConfig() { const raw = fs.readFileSync(configPath, "utf8"); const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object") { throw new Error("Invalid upstream PR tracking config."); } if (!Array.isArray(parsed.trackedPrs) || parsed.trackedPrs.length === 0) { throw new Error("No tracked PRs configured."); } + // Derive repo URL from git remote or require in config + if (!parsed.repoUrl) { + const remoteUrl = tryRunGit(["remote", "get-url", parsed.upstreamRemote]); + parsed.repoUrl = remoteUrl?.replace(/\.git$/, "").replace(/^git@github\.com:/, "https://github.com/") ?? "https://github.com/pingdotgg/t3code"; + } return parsed; }Then update line 97:
- console.log(`URL: https://github.com/pingdotgg/t3code/pull/${pr.number}`); + console.log(`URL: ${config.repoUrl}/pull/${pr.number}`);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/sync-upstream-pr-tracks.mjs` around lines 96 - 97, The PR URL is hardcoded to pingdotgg/t3code; update script to derive the repo host/owner/name from configuration instead: add a repoUrl (or owner/repo) field when reading config in loadConfig() (or parse upstreamRemote/forkRemote) and then build the URL using that value instead of the literal string in the console.log call that uses pr.number; locate usages around formatSection and the console.log with pr.number/pr.title and replace the hardcoded URL construction with the derived repoUrl.apps/server/src/geminiCliServerManager.test.ts-97-108 (1)
97-108:⚠️ Potential issue | 🟡 MinorThis test never proves lazy spawning.
Lines 106-108 only confirm that a ready session exists. A regression that eagerly calls
spawnduringstartSessionwould still pass here unless the test asserts onspawndirectly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/geminiCliServerManager.test.ts` around lines 97 - 108, The test currently doesn't prove lazy spawning because it only checks session readiness; update the test for GeminiCliServerManager to assert that spawn is not invoked during startSession by either spying/mocking the spawn method on GeminiCliServerManager (or the underlying process launcher) and expecting it not to have been called, or by checking that no child/process entry exists before calling sendTurn; then verify that spawn is called (or a process appears) after sendTurn is invoked to confirm lazy per-turn spawning.apps/web/src/appSettings.ts-161-165 (1)
161-165:⚠️ Potential issue | 🟡 MinorDrop invalid provider overrides instead of normalizing them to blue.
Line 164 uses
normalizeAccentColor, which substitutesDEFAULT_ACCENT_COLORfor any non-empty invalid value. That turns a bad/stale provider override into a sticky#2563eboverride instead of letting the provider inherit the global accent again.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/appSettings.ts` around lines 161 - 165, The current mapping in providerAccentColors uses normalizeAccentColor which replaces invalid-but-nonempty values with DEFAULT_ACCENT_COLOR causing stale overrides to stick; instead validate each value from settings.providerAccentColors and drop entries that are not valid accents so providers inherit the global accent. Update the logic around providerAccentColors to call a validation helper (or add an isValidAccentColor check) and only map entries where isValidAccentColor(v) is true, then apply normalizeAccentColor(v) (or keep as-is) for those valid entries, removing any reference that forces DEFAULT_ACCENT_COLOR for invalid inputs.apps/server/src/geminiCliServerManager.test.ts-552-563 (1)
552-563:⚠️ Potential issue | 🟡 MinorMake the live Gemini suite explicitly opt-in.
The current guard only proves that the
geminibinary exists. On machines with the CLI installed but no auth/config/network, this suite will still run and fail unexpectedly.♻️ Safer gate
-const hasGemini = await (async () => { - try { - const { execSync } = await import("node:child_process"); - execSync("gemini --version", { stdio: "pipe" }); - return true; - } catch { - return false; - } -})(); +const hasGemini = + process.env.RUN_GEMINI_LIVE_TESTS === "1" && + (await (async () => { + try { + const { execSync } = await import("node:child_process"); + execSync("gemini --version", { stdio: "pipe" }); + return true; + } catch { + return false; + } + })());🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/geminiCliServerManager.test.ts` around lines 552 - 563, The test guard currently uses only hasGemini (which checks for the binary) and should require an explicit opt-in; update the condition used with describe.skipIf so the suite runs only when both hasGemini is true and an explicit env flag (e.g., process.env.RUN_GEMINI_LIVE === '1' or 'true') is present — change the hasGemini computation or the describe.skipIf argument to require the env var alongside hasGemini (referencing hasGemini and describe.skipIf and the "GeminiCliServerManager live integration" suite) so the integration tests are opt-in rather than running whenever the CLI binary exists.apps/server/src/provider/Layers/AmpAdapter.ts-135-150 (1)
135-150:⚠️ Potential issue | 🟡 MinorValidated
numTurnsparameter is not passed to the manager.The
numTurnsparameter is validated (must be integer >= 1) but never passed tomanager.rollbackThread(). The manager'srollbackThreadmethod doesn't accept this parameter either. If rollback functionality is intended to respectnumTurns, both the adapter and manager need updating.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/provider/Layers/AmpAdapter.ts` around lines 135 - 150, The adapter validates numTurns but never forwards it; update the rollback flow to accept and propagate numTurns: change AmpAdapter.rollbackThread to call manager.rollbackThread(threadId, numTurns) instead of manager.rollbackThread(threadId), and update the manager.rollbackThread method signature/implementation to accept (threadId, numTurns) and honor numTurns when reverting turns (adjust types, callers, and tests accordingly); keep the existing validation (ProviderAdapterValidationError) and preserve error mapping via toRequestError.apps/server/src/ampServerManager.ts-738-742 (1)
738-742:⚠️ Potential issue | 🟡 MinorContent deltas receive unique itemIds, preventing event aggregation.
For
content.deltaevents, a new randomitemIdis generated each time if not explicitly provided. This means consecutive text deltas for the same assistant message will have different itemIds, which may complicate downstream aggregation or rendering logic that groups events by itemId.Consider tracking a stable itemId for assistant messages across deltas (similar to how GeminiCliServerManager uses
activeAssistantItemId).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/ampServerManager.ts` around lines 738 - 742, The current logic generates a new random itemId for partial.type === "content.delta" when itemId isn't provided, causing deltas for the same assistant message to get different ids; add a stable activeAssistantItemId field to AmpServerManager (like GeminiCliServerManager's activeAssistantItemId), set it when an assistant message is created/started (e.g., on message.create or the first chunk) and then use that activeAssistantItemId instead of RuntimeItemId.makeUnsafe(randomUUID()) for subsequent content.delta events when itemId is absent, and clear/reset the activeAssistantItemId when the message completes or is finalized.apps/server/src/geminiCliServerManager.ts-385-400 (1)
385-400:⚠️ Potential issue | 🟡 MinorHardcoded runtimeMode in listSessions ignores session's actual mode.
Line 391 uses hardcoded
"full-access"instead ofsession.runtimeMode. This discards the actual runtime mode that was set during session creation.🐛 Fix
for (const session of this.sessions.values()) { sessions.push({ provider: PROVIDER, status: session.status === "closed" ? "closed" : "ready", - runtimeMode: "full-access", + runtimeMode: session.runtimeMode as ProviderSession["runtimeMode"], threadId: session.threadId, cwd: session.cwd, model: session.model, createdAt: session.createdAt, updatedAt: session.updatedAt, }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/geminiCliServerManager.ts` around lines 385 - 400, The listSessions() method is hardcoding runtimeMode to "full-access" which discards the session's actual mode; update listSessions() to set runtimeMode from the stored session (use session.runtimeMode) when building each ProviderSession object so the returned array reflects the real mode for each session (refer to listSessions, this.sessions, ProviderSession, and session.runtimeMode).apps/server/src/kiloServerManager.ts-1202-1245 (1)
1202-1245:⚠️ Potential issue | 🟡 MinorProcess cleanup on timeout may leave orphaned child process.
When the server start times out (line 1203-1209), the promise rejects but the spawned child process isn't explicitly killed. This could leave orphaned processes if the Kilo server takes longer than
SERVER_START_TIMEOUT_MSto start.🐛 Suggested fix
const startedBaseUrl = await new Promise<string>((resolve, reject) => { + let resolved = false; const timeout = setTimeout(() => { + if (!resolved) { + child.kill(); + } reject( new Error( `Timed out waiting for Kilo server to start after ${SERVER_START_TIMEOUT_MS}ms`, ), ); }, SERVER_START_TIMEOUT_MS); let output = ""; const onChunk = (chunk: Buffer) => { output += chunk.toString(); const url = parseServerUrl(output); if (!url) { return; } clearTimeout(timeout); + resolved = true; resolve(url); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/kiloServerManager.ts` around lines 1202 - 1245, The startup promise (startedBaseUrl) currently rejects on timeout but does not terminate the spawned child process, which can leave an orphan; update the timeout handler in the Promise that uses SERVER_START_TIMEOUT_MS so it explicitly kills the child process (e.g., call child.kill() or child.kill('SIGKILL') as appropriate), remove/cleanup the stdout/stderr/error/exit listeners, then reject with the timeout error; also ensure other rejection paths (child.once('error') and the exit path where probeServer determines failure) similarly perform child cleanup and listener removal before rejecting to avoid leaked processes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 401b5e43-b921-4c18-a94b-3b8c65a0faca
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (73)
.plans/17-claude-code.mdREADME.mdapps/server/package.jsonapps/server/src/ampServerManager.tsapps/server/src/codexAppServerManager.tsapps/server/src/geminiCliServerManager.test.tsapps/server/src/geminiCliServerManager.tsapps/server/src/git/Layers/CopilotTextGeneration.tsapps/server/src/git/Services/CopilotTextGeneration.tsapps/server/src/kiloServerManager.tsapps/server/src/opencodeServerManager.tsapps/server/src/orchestration/Layers/CheckpointReactor.test.tsapps/server/src/orchestration/Layers/ProviderCommandReactor.tsapps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.tsapps/server/src/orchestration/Layers/ProviderRuntimeIngestion.tsapps/server/src/orchestration/decider.tsapps/server/src/orchestration/projector.tsapps/server/src/provider/Layers/AmpAdapter.tsapps/server/src/provider/Layers/ClaudeCodeAdapter.test.tsapps/server/src/provider/Layers/ClaudeCodeAdapter.tsapps/server/src/provider/Layers/CodexAdapter.test.tsapps/server/src/provider/Layers/CodexAdapter.tsapps/server/src/provider/Layers/CopilotAdapter.tsapps/server/src/provider/Layers/GeminiCliAdapter.tsapps/server/src/provider/Layers/KiloAdapter.test.tsapps/server/src/provider/Layers/KiloAdapter.tsapps/server/src/provider/Layers/OpenCodeAdapter.test.tsapps/server/src/provider/Layers/OpenCodeAdapter.tsapps/server/src/provider/Layers/ProviderAdapterRegistry.test.tsapps/server/src/provider/Layers/ProviderAdapterRegistry.tsapps/server/src/provider/Layers/ProviderHealth.tsapps/server/src/provider/Layers/ProviderService.test.tsapps/server/src/provider/Layers/ProviderSessionDirectory.tsapps/server/src/provider/Layers/copilotCliPath.tsapps/server/src/provider/Services/AmpAdapter.tsapps/server/src/provider/Services/ClaudeCodeAdapter.tsapps/server/src/provider/Services/CopilotAdapter.tsapps/server/src/provider/Services/GeminiCliAdapter.tsapps/server/src/provider/Services/KiloAdapter.tsapps/server/src/provider/Services/OpenCodeAdapter.tsapps/server/src/provider/claude-agent-sdk.d.tsapps/server/src/provider/copilot-sdk.d.tsapps/server/src/serverLayers.tsapps/server/src/wsServer.tsapps/server/tsdown.config.tsapps/web/src/accentColor.tsapps/web/src/appSettings.tsapps/web/src/components/ChatView.tsxapps/web/src/components/Icons.tsxapps/web/src/components/Sidebar.tsxapps/web/src/components/ThreadTerminalDrawer.tsxapps/web/src/composerDraftStore.tsapps/web/src/index.cssapps/web/src/lib/providerReactQuery.tsapps/web/src/main.tsxapps/web/src/routes/__root.tsxapps/web/src/routes/_chat.settings.tsxapps/web/src/session-logic.test.tsapps/web/src/session-logic.tsapps/web/src/store.tsapps/web/src/wsNativeApi.tsconfig/upstream-pr-tracks.jsondocs/custom-alpha-workflow.mdpackage.jsonpackages/contracts/src/ipc.tspackages/contracts/src/model.tspackages/contracts/src/orchestration.tspackages/contracts/src/provider.tspackages/contracts/src/providerRuntime.test.tspackages/contracts/src/providerRuntime.tspackages/contracts/src/ws.tspackages/shared/src/model.tsscripts/sync-upstream-pr-tracks.mjs
- Fix claude-agent-sdk.d.ts query signature (prompt accepts string, options optional, Query return type) - Forward numTurns in rollbackThread for Kilo, OpenCode, GeminiCli, Amp adapters - Guard ClaudeCodeAdapter.startSession against duplicate threadIds - Reject unsupported attachments in ClaudeCodeAdapter instead of silently dropping - Replace ClaudeCodeAdapter rollback with explicit not-supported error - Add CopilotAdapter layer cleanup finalizer, fix turn bookkeeping, emit teardown events - Convert CopilotAdapter attachment validation to use Effect.forEach - Read workspace from OpenCode resume cursor, guard against unsupported attachments - Fix diff-only assistant turn suppression in ChatView - Preserve isCustom flag in ModelOptionEntry, add staleTime to model queries - Add WCAG contrast checking for accent colors with fallback - Derive contrast-safe terminal colors for ThreadTerminalDrawer - Drop invalid provider accent overrides instead of normalizing to default - Fail fast on duplicate provider adapter registration - Fix README headings, add Kilo to supported agents - Remove duplicate ProviderRuntimeIngestion test - Make Gemini CLI live tests explicitly opt-in - Fix hardcoded runtimeMode in GeminiCli listSessions - Add stable activeAssistantItemId for Amp content deltas - Kill child process on Kilo server start timeout - Derive repo URL from git remote in sync script - Update plan docs for multi-provider scope
There was a problem hiding this comment.
Actionable comments posted: 18
🧹 Nitpick comments (8)
apps/server/src/provider/claude-agent-sdk.d.ts (1)
63-88: Consider usingunknowninstead ofanyfor consistency.The
SDKMessageinterface usesanyin several places (lines 72, 78, 82, 87) while other interfaces likeSDKResultMessageuseunknownfor similar index signatures. Usingunknownprovides better type safety by requiring explicit type narrowing before use.♻️ Suggested change for stricter typing
export interface SDKMessage { readonly type?: string; readonly subtype?: string; readonly role?: string; readonly message?: { readonly id?: string; readonly content?: ReadonlyArray<unknown>; readonly [key: string]: unknown; }; - readonly content?: ReadonlyArray<Record<string, any>>; + readonly content?: ReadonlyArray<Record<string, unknown>>; readonly uuid?: string; readonly session_id?: string; readonly parent_tool_use_id?: string; readonly tool_use_id?: string; readonly tool_name?: string; - readonly input?: Record<string, any>; + readonly input?: Record<string, unknown>; readonly result?: string; readonly error?: string; readonly errors?: ReadonlyArray<unknown>; - readonly content_block?: Record<string, any>; + readonly content_block?: Record<string, unknown>; readonly index?: number; readonly preceding_tool_use_ids?: ReadonlyArray<string>; readonly is_error?: boolean; readonly suggestions?: ReadonlyArray<PermissionUpdate>; - readonly [key: string]: any; + readonly [key: string]: unknown; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/provider/claude-agent-sdk.d.ts` around lines 63 - 88, The SDKMessage interface uses permissive any types; change all occurrences to unknown for stricter typing: replace Record<string, any> in the fields content, input, and content_block with Record<string, unknown>, and replace the index signature [key: string]: any with [key: string]: unknown (and any other any usages inside SDKMessage) so callers must narrow types before use; update SDKMessage accordingly to mirror the stricter pattern used in SDKResultMessage.apps/server/src/kiloServerManager.ts (2)
1813-1815: Type cast throughunknownmay hide type mismatches.The
as unknown as ProviderRuntimeEventdouble cast bypasses TypeScript's type checking. SinceKiloProviderRuntimeEventis designed to extendProviderRuntimeEventwith"kilo"as an additional provider value, consider refining the type relationship or adding a runtime assertion in development to catch mismatches.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/kiloServerManager.ts` around lines 1813 - 1815, The double-cast from KiloProviderRuntimeEvent to ProviderRuntimeEvent in emitRuntimeEvent hides type errors; update the types or add a runtime check instead of using "as unknown as". Either make KiloProviderRuntimeEvent explicitly extend ProviderRuntimeEvent so the cast is unnecessary, change the emit signature to accept KiloProviderRuntimeEvent, or add a development-only type guard/assertion inside emitRuntimeEvent that validates the event shape before calling this.emit("event", event) to fail fast on mismatches (refer to emitRuntimeEvent, KiloProviderRuntimeEvent, and ProviderRuntimeEvent).
701-703: Remove or document the purpose of this pass-through function.
readJsonDatasimply returns the promise unchanged. This appears to be dead code or a placeholder. If it was intended for response parsing or transformation, that logic is missing. Otherwise, consider removing it and calling the promises directly.♻️ If the function has no purpose, remove it and replace usages
-async function readJsonData<T>(promise: Promise<T>): Promise<T> { - return promise; -}Then replace all
readJsonData(client.session.get(...))calls with directclient.session.get(...)calls.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/kiloServerManager.ts` around lines 701 - 703, The helper readJsonData currently just returns its input promise and is effectively dead code; either remove this pass-through and replace all calls like readJsonData(client.session.get(...)) with direct client.session.get(...) (or await the promise where appropriate), or implement the intended parsing/transformation inside readJsonData (e.g., await the response, validate/parse JSON and return typed T) and update callsites to await the function. Locate usages by the symbol readJsonData and the call pattern readJsonData(client.session.get(...)) and apply the chosen fix consistently..plans/17-claude-code.md (3)
118-125: Expand security guidance for credential handling.The credential management section correctly delegates authentication to provider-native mechanisms, but should explicitly address security concerns that span multiple providers:
- Preventing credential leakage - Ensure error messages, logs, and debug output never expose API keys or tokens
- Secure environment propagation - When spawning child processes for CLI-based providers, explicitly document which environment variables are inherited and how to sanitize the environment
- Secret rotation - Note that credential rotation requires restarting affected provider sessions
- Audit logging - Consider logging authentication attempts (success/failure) without logging the credentials themselves
📋 Suggested additions to credential management section
### 2.1.b Credential management and resource limits Each provider manages its own authentication externally: 1. **Environment variables and CLI auth** -- Credentials are resolved via provider-native mechanisms (e.g. `ANTHROPIC_API_KEY` for Claude, `OPENAI_API_KEY` for Codex, `gh auth` for Copilot). The adapter layer never stores or brokers credentials directly; it relies on the underlying CLI/SDK picking them up from the environment. + - **Security**: Adapters must sanitize error messages and logs to prevent credential leakage. When spawning child processes, explicitly allow-list environment variables rather than inheriting the full parent environment. + - **Rotation**: Credential rotation requires stopping and restarting affected provider sessions. Document the restart procedure for operators. 2. **Per-provider rate limiting** -- Each server manager (`codexAppServerManager`, `claudeCodeServerManager`, etc.) is responsible for honoring its provider's rate limits. Adapters should surface rate-limit errors as `ProviderAdapterProcessError` so orchestration can report them cleanly. 3. **Concurrent session limits** -- The number of simultaneous provider sessions is bounded by system resources (open processes, file descriptors, memory). `ProviderSessionDirectory` tracks active sessions but does not enforce hard caps; operators should monitor resource usage when running multiple providers concurrently.🤖 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 118 - 125, Update the "Credential management and resource limits" section to explicitly cover cross-provider security: add guidance to prevent credential leakage by ensuring error messages, logs, and debug output never print or interpolate API keys/tokens (sanitize messages in adapters and providers like codexAppServerManager and claudeCodeServerManager); document secure environment propagation when spawning child processes (use an explicit env whitelist/denylist and sanitize inherited variables rather than inheriting process.env blindly in adapter launch code); note that secret rotation requires restarting affected provider sessions and reference ProviderSessionDirectory as the registry of active sessions that must be restarted; and recommend audit logging of authentication attempts (record success/failure, timestamp, provider identifier) without storing the credentials themselves and surface rate-limit/auth errors as ProviderAdapterProcessError for orchestration to report.
429-437: Consider adding performance and chaos testing scenarios.The multi-provider test scenarios (lines 429-437) provide excellent coverage of correctness and isolation. To further harden the multi-provider system, consider adding:
- Performance under load - Simulate multiple concurrent sessions across different providers with realistic turn rates to identify resource bottlenecks
- Graceful degradation - Verify that when one provider is unavailable, other providers continue to function normally
- Resume under failure - Test resume behavior after ungraceful shutdowns (killed processes, network interruptions)
🧪 Suggested additional test scenarios
5. **Load and performance testing** -- Run 10+ concurrent sessions across multiple providers with realistic turn rates (1 turn every 5-30 seconds). Monitor memory usage, file descriptor counts, and response latency to identify resource exhaustion or contention issues. 6. **Chaos scenarios** -- Simulate provider binary crashes, network timeouts, and disk full conditions. Verify that the orchestration layer surfaces clear errors and that unaffected providers continue operating.🤖 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 429 - 437, Add performance and chaos testing to the "6.5 Multi-provider test scenarios" section: extend the list to include "Performance under load" (simulate 10+ concurrent sessions across providers with realistic turn rates, monitor memory, FD counts, and latency) and "Chaos scenarios / Graceful degradation" (simulate provider crashes, network timeouts, disk-full, and verify listProviderStatuses()/orchestration surfaces errors and other providers continue functioning), and also add "Resume under failure" to validate resume cursor behavior after ungraceful shutdowns; update the numbered list and the suggested additional test scenarios block so these cases are described with expected monitors and pass/fail criteria.
316-342: Add decision criteria for rollback strategy selection.Phase 4 presents two rollback options (provider-native rewind vs. session restart) but doesn't specify how to choose between them or when the decision should be made. This ambiguity could delay implementation.
🎯 Suggested decision framework
Add a subsection after line 319:
### 4.0 Decision criteria Choose rollback strategy during adapter implementation based on SDK capabilities: 1. **If Claude SDK exposes a rewind/undo API** → use Option A (provider-native rewind) - Verify: SDK documentation confirms conversation history can be truncated - Verify: Rewind operation is atomic and does not corrupt session state 2. **If no native rewind exists** → use Option B (session restart) - Document: Users will see "conversation reset to checkpoint" behavior - Document: Performance implications of session recreation 3. **Fallback**: If investigation timeline exceeds 2 days, proceed with Option B and file a follow-up issue to migrate to Option A if native rewind becomes available. **Decision deadline**: Before starting Phase 4 implementation (after Phase 3 is complete).🤖 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 316 - 342, Add a "Decision criteria" subsection after the Phase 4 description that specifies how to choose between provider-native rewind and session-restart: state that adapter implementers must check the Claude SDK for a rewind/undo API and atomicity (if present, implement provider-native rewind and keep CheckpointReactor unchanged), otherwise implement the session-restart + state-truncation shim in the Claude adapter (stop session, rewrite resume cursor, force session recreation) and document user-visible behavior; include a fallback policy (if investigation >2 days, default to session-restart and file an issue to migrate), require the decision before Phase 4 work begins, and ensure providerService.rollbackConversation() behavior is documented and deterministic for checkpoint revert tests.apps/server/src/provider/Layers/ClaudeCodeAdapter.ts (1)
64-99: Module-level mutable state accumulates usage across all sessions.The
_claudeUsageAccumulatoraggregates usage data globally across all Claude Code sessions. This design means:
fetchClaudeCodeUsage()returns combined totals from all sessions- State persists across session restarts
- No way to reset or separate per-session usage
If per-session usage tracking is needed, consider moving this state into
ClaudeSessionContextand aggregating on demand.🤖 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 64 - 99, The module currently uses a module-level mutable _claudeUsageAccumulator with accumulateClaudeUsage and storeClaudeRateLimits, causing global aggregation across sessions; move this state into the per-session ClaudeSessionContext (add a usage field and methods like addClaudeUsage and setClaudeRateLimits on the context), update accumulateClaudeUsage and storeClaudeRateLimits to operate on the session's context instead of the module variable, ensure fetchClaudeCodeUsage reads/aggregates from the session context (or from all session contexts only when intentional), and reset the session usage on session restart/close so usage is isolated per session.apps/server/src/provider/Layers/ProviderAdapterRegistry.ts (1)
48-55: Consider usingEffect.diefor idiomatic Effect error handling.Using
throwinsideEffect.genproduces aCause.Die(defect), which is appropriate for invariant violations. However,Effect.die()is more idiomatic and makes the intent explicit.♻️ Suggested idiomatic Effect approach
for (const adapter of adapters) { if (byProvider.has(adapter.provider)) { - throw new Error( - `Duplicate provider adapter registration for provider "${adapter.provider}"`, - ); + return yield* Effect.die( + new Error( + `Duplicate provider adapter registration for provider "${adapter.provider}"`, + ), + ); } byProvider.set(adapter.provider, adapter); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/provider/Layers/ProviderAdapterRegistry.ts` around lines 48 - 55, Replace the thrown Error inside the adapter registration loop with an explicit Effect.die to signal a defect idiomatically: when detecting a duplicate key (the check using byProvider.has(adapter.provider) inside the for (const adapter of adapters) loop), call Effect.die with a descriptive error (including adapter.provider) instead of throw, ensuring Effect is imported/used in this module and the defect intent is explicit for the ProviderAdapterRegistry registration logic.
🤖 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/ampServerManager.ts`:
- Around line 280-293: sendTurn currently overwrites AmpSession.activeTurnId and
session state even when a turn is already running; add a guard at the start of
sendTurn to reject new requests if session.status === "running" (or if
session.activeTurnId is set) by throwing an Error indicating a concurrent turn
is in progress, so a second AMP turn cannot interleave into the same AmpSession;
update any callers/tests expecting concurrent starts accordingly and reference
sendTurn, AmpSession, activeTurnId, and session.status ("running") when locating
the change.
- Around line 594-612: The start event computes itemType with
classifyToolName(block.name) but the completion event hard-codes
"dynamic_tool_call", causing mismatch; capture and reuse the same itemType
variable (from classifyToolName) when emitting the corresponding
"item.completed" event(s) so the payload.itemType matches the one used in the
"item.started" emission (update the completion branches that currently use
"dynamic_tool_call" to use the itemType variable instead); apply the same change
in the other completion location mentioned so all tool items (e.g.,
command_execution, file_change, mcp_tool_call) finish with the same type they
started with.
- Around line 237-262: When the AMP child process closes or errors the code
currently marks the session closed and emits session.exited/runtime.error but
never signals the in-flight turn, causing callers to hang; update both
child.on("close", ...) and child.on("error", ...) handlers to retrieve the
active turn id from the session (use s.activeTurnId, not the undefined session
variable), and emit a terminal turn event via emitEvent(threadId,
s.activeTurnId, { type: "turn.completed", payload: { status: "failed" (or
"aborted"), reason: <clear message like process exit code or error.message> } })
immediately before or after emitting session events so any waiter receives a
terminal turn event. Ensure you fix the undefined identifier by replacing
session.activeTurnId with s.activeTurnId in the error handler.
- Around line 417-423: The catch block in the JSON.parse of the JSONL handler
currently logs raw provider output via console.warn using the variable trimmed;
change this to avoid writing raw AMP/provider content to server logs by
replacing the console.warn call with a sanitized/log-safe message (e.g., "Failed
to parse JSONL line — content redacted" plus non-sensitive metadata such as the
length or a fixed hash of trimmed) and route the message through the service
logger used in this class (e.g., this.logger or processLogger) instead of
console; keep the rest of the catch behavior (treating as raw assistant text and
calling emitEvent/threadId/session.activeTurnId) unchanged.
- Around line 535-547: The code emits a duplicate turn.completed when both
handleAssistantMessage() and handleResultMessage() report stop_reason ===
"end_turn"; to fix, ensure only one completion path runs by guarding the
end-turn block in ampServerManager.ts (the block referencing inner?.stop_reason,
_ampUsageAccumulator.turnCount, closeAllSubagentTasks, emitEvent, and
session.activeTurnId) with a check that the turn is still active (e.g., if
session.activeTurnId is truthy and session.status !== "ready") before
incrementing, closing tasks, emitting, and clearing session fields; apply the
same guard logic to the similar block around handleResultMessage() (the section
noted at 703-737) so a second message won’t emit or clear an already-completed
turn.
In `@apps/server/src/geminiCliServerManager.test.ts`:
- Around line 129-144: The test currently removes the session with
manager.stopSession(...) so sendTurn(...) hits the "unknown session" branch;
instead simulate a closed-but-present session by leaving the session in
GeminiCliServerManager's storage and setting its status to "closed" before
calling sendTurn. Update the test to start a session, locate the stored session
(via the manager's internal sessions map or a test-accessible accessor), set
session.status = "closed", then call manager.sendTurn(...) and assert it throws
the "closed" session path; reference GeminiCliServerManager, startSession,
stopSession (remove), sendTurn, and the session.status === "closed" branch when
making the change.
In `@apps/server/src/geminiCliServerManager.ts`:
- Around line 234-249: The sendTurn method currently overwrites an in-progress
session because GeminiCliSession only tracks a single
activeTurnId/activeProcess/buffers; modify sendTurn to reject overlapping turns
by checking session.status (and/or session.activeProcess) before creating a new
turn and throw a clear error if it's "running" (e.g., "Gemini CLI session
already running: {threadId}"), leaving existing session state untouched; update
any call sites/tests that expect concurrent sends to either await completion or
handle this error. Ensure references: sendTurn, GeminiCliSession,
session.status, session.activeTurnId, session.activeProcess,
session.activeToolItems, and session.activeAssistantItemId.
- Around line 293-313: The Gemini child close handler (child.on("close"))
currently clears s.activeProcess and resets s.status to "ready" but only emits a
turn.completed when code !== 0; change this so that if s.status === "running"
and s.activeTurnId === turnId you always emit a terminal turn.completed event:
emit state "succeeded" when code === 0 (no errorMessage) and state "failed" with
errorMessage `Gemini CLI exited with code ${code}` when code !== 0; keep the
existing updates to s.status and s.updatedAt and the guard using
this.sessions.get(input.threadId)/s.activeTurnId to avoid emitting for unrelated
turns.
- Around line 250-264: The code currently builds the CLI args using
session.model and only forwards input.input as prompt, which silently ignores
input.model and any input.attachments; update the argument construction in the
block that creates args (referencing prompt, args, session.model) to: if
input.model is present, use that to set the "-m" value instead of session.model;
if both are absent, omit "-m" as before; additionally detect input.attachments
and either reject the request with a clear error/log (including the attachment
metadata) or implement forwarding support—do not drop attachments silently
(check where input.attachments is available and handle it consistently with
other inputs). Ensure the change is applied where args is built so spawned CLI
actually uses the requested model and attachments are not ignored.
In `@apps/server/src/kiloServerManager.ts`:
- Around line 926-943: The sendTurn call currently only sends text via parts:
[textPart(kiloInput.input ?? "")], which silently drops any attachments from
ProviderSendTurnInput; update the sendTurn (the code surrounding readJsonData
and context.client.session.promptAsync) to explicitly check
kiloInput.attachments (or ProviderSendTurnInput.attachments) and reject or throw
a clear error when attachments are present (e.g., "attachments not supported by
Kilo"), rather than ignoring them, so callers get immediate feedback; ensure the
validation occurs before calling context.client.session.promptAsync and
reference parts/textPart/readJsonData when implementing the guard.
- Around line 1202-1250: The promise that sets startedBaseUrl registers
listeners (child.stdout.on/child.stderr.on for onChunk and child.once for
"error" and "exit") but never removes them on resolve/reject; create a single
cleanup function inside the Promise that clears the timeout and removes the
onChunk listener from stdout/stderr and removes the "error" and "exit" handlers
from child (use off/removeListener for onChunk and removeListener/off for the
once handlers), then call that cleanup just before every resolve(url) or
reject(...) (including the reuse branch in probeServer and the timeout handler)
so listeners are always removed when parseServerUrl finds a URL, on error, exit,
or timeout.
In `@apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts`:
- Around line 456-507: The test "ignores auxiliary turn completions from a
different provider thread" currently emits both auxiliary and primary
completions for the same thread ("thread-1"), so it doesn't verify cross-thread
behavior; change the auxiliary completion to target a different thread and seed
that thread first: emit a turn.started for a second thread id (e.g.,
asThreadId("thread-2")) with the same provider (provider: "codex") before
emitting the auxiliary turn.completed (use turnId asTurnId("turn-aux") and
threadId asThreadId("thread-2")), then leave the primary completion targeting
thread-1; alternatively, if you prefer not to add a second thread, rename the
test to reflect that it only covers a non-active turn on the same thread.
In `@apps/server/src/provider/Layers/CopilotAdapter.ts`:
- Around line 1011-1041: reconfigureSession currently destroys the active
session (previousSession.destroy and previousUnsubscribe()) before calling
record.client.resumeSession, which leaves record.session pointing to a destroyed
handle if resumeSession fails; change the flow to call
record.client.resumeSession(...) first to obtain nextSession, only then call
previousUnsubscribe() and await previousSession.destroy(), and finally swap in
the new session by setting record.session = nextSession, record.unsubscribe =
nextSession.on(...), and update record.model, record.reasoningEffort and
record.updatedAt; ensure createInteractionHandlers(...) and any options remain
passed into resumeSession unchanged so the new session is fully initialized
before tearing down the old one.
- Around line 1752-1759: The percentUsed field can become NaN when limit
(entitlement) is 0; update the return construction so you only compute and
include percentUsed when both used and limit are defined and limit > 0 (e.g.,
check limit > 0 before Math.round((used/limit)*100)). In other words, in the
object returned by the function (the block that currently spreads ...(limit !==
undefined && used !== undefined ? { percentUsed: Math.round((used / limit) *
100) } : {})), change the predicate to require limit > 0 as well so percentUsed
is omitted for zero entitlements.
- Around line 1211-1246: The Copilot client started by
validateSessionConfiguration may be left running if createSession/resumeSession
fails before inserting into sessions; update the session startup logic around
validateSessionConfiguration and the Effect.tryPromise block so that on any
failure you explicitly shut down the started client (call the client's
shutdown/close/terminate API — e.g., client.shutdown() or client.close(),
adapting to the actual method name) and await that shutdown before rethrowing
the ProviderAdapterProcessError; ensure this cleanup covers errors from both
resumeSession and createSession and runs prior to returning the error so no
orphaned Copilot processes remain after failed session startup.
- Around line 1172-1185: The early-return uses the stale existing.runtimeMode
which breaks dynamic switching; before returning the ProviderSession object
(from the sessions.get(...) branch) update the live session's runtimeMode to
match input.runtimeMode (and refresh updatedAt) so createInteractionHandlers
reads the current mode; modify the retrieved existing session object (or
sessions map entry) to set existing.runtimeMode = input.runtimeMode (and
updatedAt = Date.now() or appropriate timestamp) so the returned object and the
stored session reflect the new runtimeMode.
In `@apps/web/src/components/ChatView.tsx`:
- Around line 5548-5580: mergeDiscoveredModels currently only prepends new slugs
for non-"copilot" providers so discovered entries never replace static ones
(losing discovered metadata like pricingTier), and groupModelsBySubProvider
rebuilds groups using only {slug, name} which drops pricingTier; update
mergeDiscoveredModels to merge/override base entries by slug for non-copilot
providers (e.g., build a map of discovered models by slug and when assembling
result use the discovered model if present otherwise fall back to base) and
adjust groupModelsBySubProvider to preserve additional fields (at least
pricingTier) when creating grouped entries instead of only using {slug, name},
so discovered metadata is kept for overlapping slugs.
In `@scripts/sync-upstream-pr-tracks.mjs`:
- Around line 28-40: The deriveRepoUrl regex allows the captured group to
include the ".git" suffix because it uses a non-greedy quantifier with an
optional suffix; fix by first normalizing remoteUrl to strip a trailing ".git"
(e.g., remoteUrl = remoteUrl.replace(/\.git$/, '')) and then apply the SSH and
HTTPS patterns without trying to match or optionally consume ".git"; update
deriveRepoUrl so sshMatch and httpsMatch operate on the normalized URL (preserve
detection for "git@host:owner/repo" and "https://host/owner/repo") to reliably
return URLs without the ".git" suffix.
---
Nitpick comments:
In @.plans/17-claude-code.md:
- Around line 118-125: Update the "Credential management and resource limits"
section to explicitly cover cross-provider security: add guidance to prevent
credential leakage by ensuring error messages, logs, and debug output never
print or interpolate API keys/tokens (sanitize messages in adapters and
providers like codexAppServerManager and claudeCodeServerManager); document
secure environment propagation when spawning child processes (use an explicit
env whitelist/denylist and sanitize inherited variables rather than inheriting
process.env blindly in adapter launch code); note that secret rotation requires
restarting affected provider sessions and reference ProviderSessionDirectory as
the registry of active sessions that must be restarted; and recommend audit
logging of authentication attempts (record success/failure, timestamp, provider
identifier) without storing the credentials themselves and surface
rate-limit/auth errors as ProviderAdapterProcessError for orchestration to
report.
- Around line 429-437: Add performance and chaos testing to the "6.5
Multi-provider test scenarios" section: extend the list to include "Performance
under load" (simulate 10+ concurrent sessions across providers with realistic
turn rates, monitor memory, FD counts, and latency) and "Chaos scenarios /
Graceful degradation" (simulate provider crashes, network timeouts, disk-full,
and verify listProviderStatuses()/orchestration surfaces errors and other
providers continue functioning), and also add "Resume under failure" to validate
resume cursor behavior after ungraceful shutdowns; update the numbered list and
the suggested additional test scenarios block so these cases are described with
expected monitors and pass/fail criteria.
- Around line 316-342: Add a "Decision criteria" subsection after the Phase 4
description that specifies how to choose between provider-native rewind and
session-restart: state that adapter implementers must check the Claude SDK for a
rewind/undo API and atomicity (if present, implement provider-native rewind and
keep CheckpointReactor unchanged), otherwise implement the session-restart +
state-truncation shim in the Claude adapter (stop session, rewrite resume
cursor, force session recreation) and document user-visible behavior; include a
fallback policy (if investigation >2 days, default to session-restart and file
an issue to migrate), require the decision before Phase 4 work begins, and
ensure providerService.rollbackConversation() behavior is documented and
deterministic for checkpoint revert tests.
In `@apps/server/src/kiloServerManager.ts`:
- Around line 1813-1815: The double-cast from KiloProviderRuntimeEvent to
ProviderRuntimeEvent in emitRuntimeEvent hides type errors; update the types or
add a runtime check instead of using "as unknown as". Either make
KiloProviderRuntimeEvent explicitly extend ProviderRuntimeEvent so the cast is
unnecessary, change the emit signature to accept KiloProviderRuntimeEvent, or
add a development-only type guard/assertion inside emitRuntimeEvent that
validates the event shape before calling this.emit("event", event) to fail fast
on mismatches (refer to emitRuntimeEvent, KiloProviderRuntimeEvent, and
ProviderRuntimeEvent).
- Around line 701-703: The helper readJsonData currently just returns its input
promise and is effectively dead code; either remove this pass-through and
replace all calls like readJsonData(client.session.get(...)) with direct
client.session.get(...) (or await the promise where appropriate), or implement
the intended parsing/transformation inside readJsonData (e.g., await the
response, validate/parse JSON and return typed T) and update callsites to await
the function. Locate usages by the symbol readJsonData and the call pattern
readJsonData(client.session.get(...)) and apply the chosen fix consistently.
In `@apps/server/src/provider/claude-agent-sdk.d.ts`:
- Around line 63-88: The SDKMessage interface uses permissive any types; change
all occurrences to unknown for stricter typing: replace Record<string, any> in
the fields content, input, and content_block with Record<string, unknown>, and
replace the index signature [key: string]: any with [key: string]: unknown (and
any other any usages inside SDKMessage) so callers must narrow types before use;
update SDKMessage accordingly to mirror the stricter pattern used in
SDKResultMessage.
In `@apps/server/src/provider/Layers/ClaudeCodeAdapter.ts`:
- Around line 64-99: The module currently uses a module-level mutable
_claudeUsageAccumulator with accumulateClaudeUsage and storeClaudeRateLimits,
causing global aggregation across sessions; move this state into the per-session
ClaudeSessionContext (add a usage field and methods like addClaudeUsage and
setClaudeRateLimits on the context), update accumulateClaudeUsage and
storeClaudeRateLimits to operate on the session's context instead of the module
variable, ensure fetchClaudeCodeUsage reads/aggregates from the session context
(or from all session contexts only when intentional), and reset the session
usage on session restart/close so usage is isolated per session.
In `@apps/server/src/provider/Layers/ProviderAdapterRegistry.ts`:
- Around line 48-55: Replace the thrown Error inside the adapter registration
loop with an explicit Effect.die to signal a defect idiomatically: when
detecting a duplicate key (the check using byProvider.has(adapter.provider)
inside the for (const adapter of adapters) loop), call Effect.die with a
descriptive error (including adapter.provider) instead of throw, ensuring Effect
is imported/used in this module and the defect intent is explicit for the
ProviderAdapterRegistry registration logic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9a906637-335f-4f30-a22f-61af4122d6e1
📒 Files selected for processing (17)
.plans/17-claude-code.mdREADME.mdapps/server/src/ampServerManager.tsapps/server/src/geminiCliServerManager.test.tsapps/server/src/geminiCliServerManager.tsapps/server/src/kiloServerManager.tsapps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.tsapps/server/src/provider/Layers/ClaudeCodeAdapter.test.tsapps/server/src/provider/Layers/ClaudeCodeAdapter.tsapps/server/src/provider/Layers/CopilotAdapter.tsapps/server/src/provider/Layers/ProviderAdapterRegistry.tsapps/server/src/provider/claude-agent-sdk.d.tsapps/web/src/accentColor.tsapps/web/src/appSettings.tsapps/web/src/components/ChatView.tsxapps/web/src/components/ThreadTerminalDrawer.tsxscripts/sync-upstream-pr-tracks.mjs
🚧 Files skipped from review as they are similar to previous changes (3)
- README.md
- apps/web/src/components/ThreadTerminalDrawer.tsx
- apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts
ampServerManager: - Emit turn.completed when AMP process dies with active turn - Reject concurrent turns in sendTurn - Consistent tool item types between start/completion events - Sanitize raw provider output in JSONL parse logging - Guard against duplicate turn.completed emissions geminiCliServerManager: - Reject overlapping turns in sendTurn - Always emit terminal turn.completed on child close - Respect input.model override, reject unsupported attachments - Fix test to simulate closed-but-present session path - Add tests for concurrent turn rejection and attachment rejection kiloServerManager: - Reject unsupported attachments in sendTurn - Clean up listeners on server start promise resolve/reject - Remove double-cast by using ProviderRuntimeEvent directly - Remove no-op readJsonData pass-through function CopilotAdapter: - Reorder reconfigureSession to create new before destroying old - Guard against NaN percentUsed when limit is 0 - Clean up client on createSession/resumeSession failure - Update stale runtimeMode on session reuse early return Types and registry: - Replace any with unknown in SDKMessage for stricter typing - Use Effect.die for idiomatic duplicate registration error - Add comment explaining global usage accumulator intent - Fix test to use different thread for cross-thread verification Other: - Merge discovered models by slug preserving metadata in ChatView - Fix deriveRepoUrl .git suffix handling - Expand plan docs: credential security, rollback criteria, chaos testing
There was a problem hiding this comment.
Actionable comments posted: 17
♻️ Duplicate comments (1)
apps/server/src/provider/Layers/CopilotAdapter.ts (1)
1215-1246:⚠️ Potential issue | 🟠 MajorValidation failures still leak the started Copilot client.
validateSessionConfiguration()can callclient.start()and fail before the innerclient.stop()catch aroundcreateSession()/resumeSession()runs. Invalid model or reasoning-effort inputs still leave an orphaned Copilot process behind.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/provider/Layers/CopilotAdapter.ts` around lines 1215 - 1246, validateSessionConfiguration may start the Copilot client and throw before the later try/catch that calls client.stop(), leaking a started client; ensure the client is stopped if validation fails by wrapping the validateSessionConfiguration call (or the whole block that may start the client) in a try/finally or catch that calls await client.stop(). Specifically, update the flow around validateSessionConfiguration, client.start(), and the subsequent createSession()/resumeSession() logic so that any thrown error after client.start() triggers await client.stop().catch(() => {}) before rethrowing; reference validateSessionConfiguration, client.start, client.stop, resumeSessionId, createSession to locate the code to modify.
🤖 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/ampServerManager.ts`:
- Around line 307-337: sendTurn currently ignores attachments on
ProviderSendTurnInput and proceeds to mutate session.activeTurnId and
session.status; add an upfront guard (before reading or mutating session and
before creating turnId) that checks input.attachments (or equivalent field on
ProviderSendTurnInput) and throws an error rejecting unsupported attachments
(same behavior as other CLI-backed managers) if any are present. Ensure this
validation occurs immediately after finding the session and status checks and
before assigning session.activeTurnId, session.status, or writing to
session.process.stdin so no state is mutated when attachments are unsupported.
- Around line 385-395: stopSession currently kills the child and immediately
calls this.sessions.delete(threadId), which breaks the close/error handlers that
call this.sessions.get(threadId) to emit terminal events (turn.completed and
session.exited); instead, either emit those terminal events from stopSession
before deleting the entry or defer deletion until the child-process close/error
handlers finish finalizing the session (e.g., set a "closing" flag on the
Session object in stopSession, call session.process.kill(), then let the
close/error handlers perform this.sessions.delete(threadId) after emitting
turn.completed and session.exited, or await a session.finalized promise that the
handlers resolve before deleting).
- Around line 352-360: The interruptTurn implementation is marking
session.status = "ready" and updating session.updatedAt immediately, which
prevents handleAssistantMessage()/handleResultMessage() from running the
end_turn path and leaves turn.completed/activeTurnId uncleared; stop flipping
the status to "ready" here — either keep session.status as "running" and let the
existing child.on(\"close\")/handleResultMessage() emit the terminal turn event,
or if you must finalize here, explicitly emit the aborted completion (set the
turn to completed and clear activeTurnId via the same codepath
handleResultMessage uses) before returning. Update interruptTurn to only send
SIGINT and not change session.status/updatedAt, or alternatively call the
module's turn-completion helper so turn.completed and activeTurnId are set
consistently.
In `@apps/server/src/geminiCliServerManager.test.ts`:
- Around line 314-327: The test should assert that the manager actually stored
the session_id from the init event instead of only asserting no events were
emitted; after calling feedJsonLine with the init payload, add an assertion that
the manager's stored Gemini session id equals "gemini-sess-abc" (read it from
the appropriate symbol used in the implementation, e.g., manager.geminiSessionId
or manager.getGeminiSessionId()). Keep the existing
expect(events).toHaveLength(0) but also verify the stored session id to ensure
the init branch persists the value.
In `@apps/server/src/geminiCliServerManager.ts`:
- Around line 304-325: The close handler for the child process can miss terminal
events for interrupted turns because interruptTurn() sets s.status = "ready"
before the child exits and the current child.on("close", ...) only checks exit
code; update the child process listeners to use the exit signal
(child.on("exit", (code, signal) => ...)) or read the signal parameter in close,
and treat signal === "SIGINT" (and other termination signals you use) as an
interrupted completion rather than a failure; change interruptTurn() so it does
NOT flip s.status to "ready" immediately but instead marks a flag (e.g.,
s.interrupted = true or s.pendingInterrupt = true) and let the exit handler
clear the flag and set s.status = "ready" and emitEvent(input.threadId, turnId,
{ type: "turn.completed", payload: { state: "interrupted" } }) when signal
indicates interruption; apply the same change to the other handler block
referenced (lines ~354-363) and ensure emitEvent still handles normal
completed/failed flows when signal is null.
- Around line 260-275: The event emission uses session.model instead of the
actual model used for the CLI call, so when a per-turn override is provided the
published turn.started model is wrong; update the code that builds and emits the
turn.started event to use the computed effectiveModel (the same value used when
invoking the CLI, i.e., input.model ?? session.model) instead of session.model,
and make the same change for the other occurrence referenced around the sendTurn
flow (the block at ~290-294) so both emissions reflect the actual launched
model.
- Around line 397-411: listSessions() currently maps any non-"closed" session to
"ready", hiding when a thread is busy and causing callers to think they can
sendTurn() when they cannot; update the mapping in listSessions (in
geminiCliServerManager.ts) to preserve the actual session.status (e.g., "busy"
or "ready") instead of coercing everything to "ready" so the exported
ProviderSession reflects true idle/busy state, ensuring the value matches
ProviderSession["status"] types.
In `@apps/server/src/kiloServerManager.ts`:
- Around line 980-992: interruptTurn currently clears context.activeTurnId
before the provider emits follow-up events so handleSessionStatusEvent can no
longer close that turn; update interruptTurn (and the stream-error handling code
referenced around the stream failure path) to emit a terminal turn event (e.g.,
call the same turn-completion/turn-abort emitter used by
handleSessionStatusEvent) for the current active turn before clearing
context.activeTurnId, and in the stream-error path ensure you create/emit a
turn.completed or turn.aborted event (with error details) and update
context.session/status accordingly so no orchestration waits indefinitely;
locate references to interruptTurn, handleSessionStatusEvent, and where
context.activeTurnId is manipulated to make the change.
- Around line 466-469: readResumeSessionId only extracts sessionId from
resumeCursor but the saved resume state also contains workspace, so restore
workspace too: parse record.workspace via asString (using asRecord/asString on
resumeCursor) and return or expose both sessionId and workspace (or replace
readResumeSessionId with a readResumeState that returns { sessionId, workspace
}). Then update the resume/reattach logic that sets
providerOptions.kilo.workspace to use the restored workspace when present.
Ensure all callers that assumed only sessionId are adjusted to accept the new
shape or to set providerOptions.kilo.workspace and sessionId accordingly (use
symbols: readResumeSessionId, resumeCursor, providerOptions.kilo.workspace,
asRecord, asString).
- Around line 856-877: sendTurn currently overwrites context.activeTurnId which
allows a second concurrent sendTurn to steal events from the first; add a guard
at the start of sendTurn (after obtaining context via requireSession) that
checks context.activeTurnId and whether the referenced turn is still
active/streaming and if so rejects the new request (throw an Error or return a
failure) before calling createTurnId(); update sendTurn's flow so
context.activeTurnId is only set after this check (use the existing turn
tracking/state used elsewhere to determine "active" vs terminal) to prevent
overlapping turns from clobbering events.
In `@apps/server/src/provider/Layers/ClaudeCodeAdapter.ts`:
- Around line 914-923: The session update spreads the previous session so a
stale lastError persists after recovery; modify the context.session assignment
in ClaudeCodeAdapter (where context.session is set before calling
updateResumeCursor and using nowIso) to explicitly clear lastError when status
!== "failed" (e.g., include lastError: undefined in the object for non-failed
paths) so successful turns remove prior errors; apply the same change to the
other similar block referenced (around the second occurrence).
- Around line 357-365: classifyRequestType currently maps any
non-read/non-command tool to "file_change_approval", which mislabels
mcp_tool_call and dynamic_tool_call; update classifyRequestType to call
classifyToolItemType(toolName) and if it returns "mcp_tool_call" or
"dynamic_tool_call" return an appropriate tool-call approval (e.g.,
"tool_call_approval") instead of "file_change_approval", otherwise keep the
existing branch that maps "command_execution" to "command_execution_approval"
and defaults to "file_change_approval". Ensure you update the return values
consistently where classifyRequestType is used.
In `@apps/server/src/provider/Layers/CopilotAdapter.ts`:
- Around line 506-560: The session.idle case should not emit a successful
turn.completed if the session previously errored; update the session.idle branch
in handleSessionEvent (the "session.idle" case that uses currentTurnId and
currentProviderTurnId) to check for a recorded session error (the same error
marker set in the "session.error" handling) and only produce the turn.completed
event when no error is present; if an error exists, skip emitting turn.completed
and ensure session.state.changed still transitions to "ready"/"idle" as before.
- Around line 735-748: When emitting "tool.execution_start" (case
"tool.execution_start") compute the tool item type once using
itemTypeFromToolEvent(event) and persist it in the started item's payload (e.g.,
add a stable key like payload.data.toolItemType or payload.detail.toolItemType)
so the same value can be read later; then update the "tool.execution_complete"
handling to prefer that persisted payload.data.toolItemType (if present) instead
of recomputing from result.contents (keep using itemTypeFromToolEvent only as a
fallback). Also apply the same change to the similar block around lines handling
the other tool start/complete pair (the block referenced at 771-786) so item
type remains stable across the lifecycle.
In `@apps/web/src/components/ChatView.tsx`:
- Around line 5791-5798: The grouped provider submenu is rendering only
modelOption.name in the MenuRadioItem (inside the map over group.models), which
omits the pricingTier added by groupModelsBySubProvider; update the
MenuRadioItem render to include the pricing badge next to the model name (e.g.,
render modelOption.name plus the existing PricingBadge or equivalent UI using
modelOption.pricingTier), keeping the same key/value logic and onClick handler
so the nested OpenCode/Kilo menus show pricing tiers for each modelOption.
- Around line 886-905: The queries copilotModelsQuery, opencodeModelsQuery, and
kiloModelsQuery are forcing staleTime: Infinity which prevents newly discovered
models from appearing; remove the explicit staleTime: Infinity (or set it to the
shared default/shorter TTL) when calling useQuery with
providerListModelsQueryOptions("copilot"/"opencode"/"kilo") so the queries
inherit the shared 5-minute cache behavior, then leave modelOptionsByProvider
(which uses mergeDiscoveredModels and getCustomModelOptionsByProvider with
settings) unchanged so newly discovered provider models are refetched and show
up in the picker after provider auth/readiness changes.
In `@scripts/sync-upstream-pr-tracks.mjs`:
- Around line 35-40: The deriveRepoUrl() helper currently only handles scp-style
SSH and HTTPS; update deriveRepoUrl() to also parse ssh:// URLs (e.g.,
ssh://git@github.com/owner/repo(.git) => https://github.com/owner/repo) and
strip any trailing .git, and if it cannot derive a repo URL set config.repoUrl
to null/undefined and throw or log a clear failure (fail fast) instead of
leaving it unset so callers (e.g., where config.repoUrl is used to build ping
URLs) won't fall back to the hard-coded repo; ensure both the scp-style branch
and the HTTPS branch retain the same .git stripping/normalization and add a
clear error path when deriveRepoUrl() returns nothing.
---
Duplicate comments:
In `@apps/server/src/provider/Layers/CopilotAdapter.ts`:
- Around line 1215-1246: validateSessionConfiguration may start the Copilot
client and throw before the later try/catch that calls client.stop(), leaking a
started client; ensure the client is stopped if validation fails by wrapping the
validateSessionConfiguration call (or the whole block that may start the client)
in a try/finally or catch that calls await client.stop(). Specifically, update
the flow around validateSessionConfiguration, client.start(), and the subsequent
createSession()/resumeSession() logic so that any thrown error after
client.start() triggers await client.stop().catch(() => {}) before rethrowing;
reference validateSessionConfiguration, client.start, client.stop,
resumeSessionId, createSession to locate the code to modify.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 431fce40-14ae-4251-ade1-9cce369087d2
📒 Files selected for processing (12)
.plans/17-claude-code.mdapps/server/src/ampServerManager.tsapps/server/src/geminiCliServerManager.test.tsapps/server/src/geminiCliServerManager.tsapps/server/src/kiloServerManager.tsapps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.tsapps/server/src/provider/Layers/ClaudeCodeAdapter.tsapps/server/src/provider/Layers/CopilotAdapter.tsapps/server/src/provider/Layers/ProviderAdapterRegistry.tsapps/server/src/provider/claude-agent-sdk.d.tsapps/web/src/components/ChatView.tsxscripts/sync-upstream-pr-tracks.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
- apps/server/src/provider/claude-agent-sdk.d.ts
ampServerManager: - Reject unsupported attachments before mutating session state - Don't mark interrupted turn as ready; let close/error handlers finalize - Defer session deletion until child exit handlers emit terminal events geminiCliServerManager: - Assert stored geminiSessionId in init event test - Emit effectiveModel (input.model ?? session.model) in turn.started - Use signal param in close handler; emit "interrupted" state on SIGINT - Remove premature ready status in interruptTurn - listSessions exposes actual session status instead of coercing to "ready" kiloServerManager: - Preserve workspace in resume state alongside sessionId - Reject overlapping turns before overwriting activeTurnId - Emit terminal turn events on interrupt and stream failure paths ClaudeCodeAdapter: - classifyRequestType properly routes MCP/dynamic tools via switch - Clear lastError on successful turn completion and turn start CopilotAdapter: - Emit failed turn.completed (not completed) when session.idle follows error - Store tool item type on start, reuse on complete for lifecycle stability - Widen client leak catch to cover validateSessionConfiguration failures ChatView: - Remove staleTime: Infinity so model discovery respects shared cache TTL - Render pricing tier badges in grouped provider submenus sync-upstream-pr-tracks: - Add ssh:// URL format support in deriveRepoUrl - Throw on failure instead of falling back to hardcoded URL
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/components/ChatView.tsx (1)
2993-3012:⚠️ Potential issue | 🟠 MajorPreserve the selected service tier in the plan send flows.
The main send path passes
serviceTierat Line 2727, but both of thesethread.turn.startcalls omit it. Refine/implement actions can therefore ignore the active Codex tier and run with backend defaults instead.Proposed fix
await api.orchestration.dispatchCommand({ type: "thread.turn.start", commandId: newCommandId(), threadId: threadIdForSend, @@ provider: selectedProvider, model: selectedModel || undefined, + serviceTier: selectedServiceTier, ...(selectedModelOptionsForDispatch ? { modelOptions: selectedModelOptionsForDispatch } : {}), assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", runtimeMode, @@ api.orchestration.dispatchCommand({ type: "thread.turn.start", commandId: newCommandId(), threadId: nextThreadId, @@ provider: selectedProvider, model: selectedModel || undefined, + serviceTier: selectedServiceTier, ...(selectedModelOptionsForDispatch ? { modelOptions: selectedModelOptionsForDispatch } : {}), assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", runtimeMode,}, [ activeThread, beginSendPhase, forceStickToBottom, isConnecting, isSendBusy, isServerThread, persistThreadSettingsForNextTurn, resetSendPhase, runtimeMode, selectedModel, selectedModelOptionsForDispatch, selectedProvider, + selectedServiceTier, setComposerDraftInteractionMode, setThreadError, settings.enableAssistantStreaming, ]); @@ }, [ activeProject, activeProposedPlan, activeThread, beginSendPhase, isConnecting, isSendBusy, isServerThread, navigate, resetSendPhase, runtimeMode, selectedModel, selectedModelOptionsForDispatch, selectedProvider, + selectedServiceTier, settings.enableAssistantStreaming, syncServerReadModel, ]);Also applies to: 3093-3112
🤖 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 2993 - 3012, The dispatch payload for api.orchestration.dispatchCommand (type "thread.turn.start") is missing the active service tier, so add the same serviceTier field used in the main send path to the payload (e.g., serviceTier: selectedServiceTier or the existing serviceTier variable used at the earlier send flow) alongside selectedProvider, selectedModel, modelOptions, assistantDeliveryMode, runtimeMode, interactionMode, and createdAt; make this change for both occurrences of the "thread.turn.start" dispatch so refine/implement actions preserve the active Codex tier.
♻️ Duplicate comments (1)
apps/server/src/geminiCliServerManager.ts (1)
242-243:⚠️ Potential issue | 🟠 MajorDon't reopen or delete the session before the owning child exits.
Lines 532-533 mark the thread
readybeforeclose, and Line 394 deletes the session immediately afterkill(). Because the laterclose/errorhandlers re-fetch bythreadIdand mutate state unconditionally, the old child can either lose its terminal event or clobber whichever turn/session has since reused that thread.Also applies to: 304-347, 383-395, 531-533
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/geminiCliServerManager.ts` around lines 242 - 243, The session lifecycle currently mutates or deletes a session (e.g., the block that throws when session.status === "running", the code that calls kill(), and the close/error handlers that re-fetch by threadId) while the original child process may still be exiting; to fix, ensure you do not reopen or delete a session until the owning child has fully exited by associating the session with an immutable owner identifier (e.g., capture and store session.process.pid or a unique ownerId when the child is spawned) and then, in the close/error handlers and anywhere you call kill(), verify that the live session still matches that ownerId before mutating/deleting or marking ready; update the handlers that currently unconditionally mutate state (the handlers referenced around the kill()/close/error regions) to early-return unless the ownerId/pid matches the session fetched by threadId.
🧹 Nitpick comments (1)
apps/server/src/kiloServerManager.ts (1)
1845-1852: Consider caching or server reuse for frequent model discovery.The current implementation creates a new
KiloServerManagerinstance for each call, which may spawn a new Kilo server if none exists. For scenarios wherefetchKiloModelsis called frequently, this could be inefficient.If model discovery is expected to be called repeatedly, consider:
- Caching results with a TTL
- Accepting an existing manager instance as an optional parameter
This is acceptable for infrequent calls but worth considering for performance-sensitive use cases.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/kiloServerManager.ts` around lines 1845 - 1852, fetchKiloModels currently creates a new KiloServerManager for every call (via new KiloServerManager()), which may spawn servers repeatedly; modify fetchKiloModels to accept an optional KiloServerManager parameter and reuse it when provided, and/or add an in-memory cache keyed by the options with a TTL to return cached results from listModels without instantiating a new manager; ensure you still call stopAll() only for managers you created internally (track ownership) and invalidate the cache after TTL or when listModels signals changes.
🤖 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/ampServerManager.ts`:
- Around line 179-183: startSession currently rejects if sessions.has(threadId)
even when the existing ProviderSession is terminal/closed; change the logic to
treat a terminal/dead session as removable: in startSession, inspect the
existing session object from this.sessions.get(threadId) (check its state/closed
flag or isTerminal method on ProviderSession) and if it is closed/terminal, call
this.sessions.delete(threadId) before proceeding so a new session can be
created; also update the terminal cleanup handlers referenced (the ones that
only call this.sessions.delete when closing === true) to always remove the map
entry whenever the session reaches a terminal/closed state (regardless of the
closing flag) to avoid leaving dead sessions behind.
- Around line 431-443: readThread and rollbackThread currently return a fake
empty ProviderThreadSnapshot which masks unsupported behavior; replace those
stub returns with explicit errors indicating the operations are unsupported or
unimplemented for this provider (e.g., throw or return a rejected Promise with a
clear message like "readThread not supported for AMP provider" / "rollbackThread
not supported for AMP provider"); update both readThread and rollbackThread (and
any related callers) to surface this error instead of returning { threadId,
turns: [] } so resume/rollback flows can detect unsupported operations.
- Around line 245-297: In both the child.on("close") and child.on("error")
handlers ensure you finalize subagent tasks and any in-progress tool items
before emitting events: call this.closeAllSubagentTasks(threadId) (or the
existing method that finalizes tasks) and ensure toolItemTypes for the session
are drained/marked failed (e.g., invoke a drainToolItemTypes(threadId) helper or
iterate s.toolItemTypes and emit item.completed/item.failed) prior to emitting
the "turn.completed" and "session.exited"/"runtime.error" events so no
task.started/item.started remain stuck; also clear s.activeTurnId and
s.activeAssistantItemId and delete the session when s.closing as already done.
- Around line 333-350: The state mutations in sendTurn() (setting
session.activeTurnId via TurnId.makeUnsafe, session.status = "running", and
session.updatedAt) occur before writing to session.process.stdin; move these
mutations to after session.process.stdin.write(userMessage + "\n") succeeds or
wrap the write in a try/catch and roll back the mutations on failure (clear
session.activeTurnId, set session.status back to its previous value, and restore
updatedAt). Locate the sendTurn function and update the logic around
TurnId.makeUnsafe, session.activeTurnId, session.status, session.updatedAt, and
session.process.stdin.write to ensure mutations only persist after a successful
write or are reverted on write error.
In `@apps/server/src/geminiCliServerManager.ts`:
- Around line 304-347: Ensure all terminal paths clear open item state and emit
completion events: in the child.on("close") and child.on("error") handlers (and
the other terminal branch referenced around result handling), after setting
s.activeProcess = undefined and updating s.status/updatedAt, also flush
s.activeAssistantItemId (set to undefined) and iterate any remaining
s.activeToolItems to emit item.completed events for each leftover tool/item via
this.emitEvent(input.threadId, turnId, { type: "item.completed", payload: ... })
before clearing s.activeToolItems; keep the same error/interrupted/completed
payload semantics but ensure the UI receives closing item.completed events for
both assistant and tool items on every terminal branch.
- Around line 202-223: startSession() currently stores a normalized runtimeMode
("full-access") onto the GeminiCliSession but returns
providerSession.runtimeMode using raw input.runtimeMode, causing callers to see
undefined; update the return creation for ProviderSession (the object built
after this.sessions.set) to use the same normalized value used when constructing
the session (i.e., use session.runtimeMode or the normalized value rather than
input.runtimeMode) so startSession() response matches listSessions()/stored
session state.
In `@apps/server/src/provider/Layers/ClaudeCodeAdapter.ts`:
- Around line 1555-1559: Currently the code stops and removes an existing
session (sessions.get(threadId) + stopSessionInternal) before creating the
replacement, which leaves the thread unusable if query creation fails; change
the flow in create/start logic so you first allocate and initialize the new
runtime/session (e.g., construct/init the newContext / start its query/runtime),
only after the newContext is successfully created and running replace the map
entry and then call stopSessionInternal(existingContext, { emitExitEvent: true
}) to teardown the old one; apply the same reorder in the other occurrence (the
block around lines 1728-1741) referencing sessions, threadId,
stopSessionInternal and the new session initialization routine so failure leaves
the original session intact.
- Around line 1849-1889: Move the call to buildUserMessage and its error
handling to occur before creating/modifying context.turnState and
context.session and before emitting the "turn.started" event; specifically, call
buildUserMessage(input) first (and fail fast/return an error if it rejects),
then generate turnId/turnState, update context.session, emit via
offerRuntimeEvent, and finally Queue.offer; update references in this block
(buildUserMessage, context.turnState, context.session, offerRuntimeEvent,
Queue.offer) so an invalid/empty/unsupported user message never leaves the
session in a running state with an open turn.
In `@apps/server/src/provider/Layers/CopilotAdapter.ts`:
- Around line 1336-1385: The code currently appends a new turn even if a turn is
already active; update the sendTurn path in CopilotAdapter (around
getSessionRecord and where record.pendingTurnIds/record.currentTurnId are
modified) to reject overlapping sends by checking if record.currentTurnId (or
any active pending turn) already exists and returning/throwing a
ProviderAdapterRequestError (same shape used elsewhere) instead of queuing a new
turn; mirror the guard logic used in ClaudeCodeAdapter.ts (the check at lines
~1834-1840) so that a second sendTurn for the same thread is rejected
immediately.
- Around line 1039-1049: After resumeSession succeeds, ensure the new session is
installed before attempting to clean up the old one: assign record.session =
nextSession, record.model, record.reasoningEffort, record.updatedAt and set
record.unsubscribe = nextSession.on(...) (using handleSessionEvent) immediately,
then attempt to call previousUnsubscribe() and await previousSession.destroy()
inside a try/catch so any errors from previousSession.destroy() are caught and
logged instead of preventing the new session from becoming live; this preserves
nextSession even if previousSession.destroy() fails.
In `@apps/web/src/components/ChatView.tsx`:
- Around line 886-896: The runtime model discovery currently only includes
providers "copilot", "opencode", and "kilo" in the modelOptionsByProvider
computation; add "geminiCli" and "amp" to the providerListModelsQuery hooks, to
the object passed into
mergeDiscoveredModels(getCustomModelOptionsByProvider(settings), {...}), and to
the useMemo dependency array so mergeDiscoveredModels receives discovered models
for geminiCli and amp (update const
copilotModelsQuery/opencodeModelsQuery/kiloModelsQuery usages to also create
geminiCliModelsQuery and ampModelsQuery and include their .data entries and
dependencies).
---
Outside diff comments:
In `@apps/web/src/components/ChatView.tsx`:
- Around line 2993-3012: The dispatch payload for
api.orchestration.dispatchCommand (type "thread.turn.start") is missing the
active service tier, so add the same serviceTier field used in the main send
path to the payload (e.g., serviceTier: selectedServiceTier or the existing
serviceTier variable used at the earlier send flow) alongside selectedProvider,
selectedModel, modelOptions, assistantDeliveryMode, runtimeMode,
interactionMode, and createdAt; make this change for both occurrences of the
"thread.turn.start" dispatch so refine/implement actions preserve the active
Codex tier.
---
Duplicate comments:
In `@apps/server/src/geminiCliServerManager.ts`:
- Around line 242-243: The session lifecycle currently mutates or deletes a
session (e.g., the block that throws when session.status === "running", the code
that calls kill(), and the close/error handlers that re-fetch by threadId) while
the original child process may still be exiting; to fix, ensure you do not
reopen or delete a session until the owning child has fully exited by
associating the session with an immutable owner identifier (e.g., capture and
store session.process.pid or a unique ownerId when the child is spawned) and
then, in the close/error handlers and anywhere you call kill(), verify that the
live session still matches that ownerId before mutating/deleting or marking
ready; update the handlers that currently unconditionally mutate state (the
handlers referenced around the kill()/close/error regions) to early-return
unless the ownerId/pid matches the session fetched by threadId.
---
Nitpick comments:
In `@apps/server/src/kiloServerManager.ts`:
- Around line 1845-1852: fetchKiloModels currently creates a new
KiloServerManager for every call (via new KiloServerManager()), which may spawn
servers repeatedly; modify fetchKiloModels to accept an optional
KiloServerManager parameter and reuse it when provided, and/or add an in-memory
cache keyed by the options with a TTL to return cached results from listModels
without instantiating a new manager; ensure you still call stopAll() only for
managers you created internally (track ownership) and invalidate the cache after
TTL or when listModels signals changes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0ef33041-b482-456e-ae24-353b8e095beb
📒 Files selected for processing (8)
apps/server/src/ampServerManager.tsapps/server/src/geminiCliServerManager.test.tsapps/server/src/geminiCliServerManager.tsapps/server/src/kiloServerManager.tsapps/server/src/provider/Layers/ClaudeCodeAdapter.tsapps/server/src/provider/Layers/CopilotAdapter.tsapps/web/src/components/ChatView.tsxscripts/sync-upstream-pr-tracks.mjs
ampServerManager: - Clean up dead/closed sessions in startSession instead of rejecting - Always delete session from map on terminal state - Finalize subagent tasks and drain tool items on close/error paths - Move state mutations after stdin.write with try/catch rollback - readThread/rollbackThread throw unsupported errors instead of fake snapshots geminiCliServerManager: - Return normalized runtimeMode from startSession (not raw input) - Finalize open assistant/tool items on all terminal paths ClaudeCodeAdapter: - Defer existing session teardown until replacement runtime is ready - Validate buildUserMessage before opening turn state/emitting turn.started CopilotAdapter: - Install new session before destroying old in reconfigureSession - Wrap previousSession.destroy in try/catch to keep replacement live - Reject overlapping sendTurn calls with ProviderAdapterValidationError ChatView: - Add serviceTier to plan refine/implement dispatch payloads - Add geminiCli and amp model discovery queries - Include both in mergeDiscoveredModels and useMemo deps kiloServerManager: - Accept optional existing manager in fetchKiloModels to avoid respawning
Summary
Changes
Server:
opencodeServerManager,ampServerManager,geminiCliServerManager,kiloServerManagerProviderRuntimeIngestionfor multi-provider event handlingProviderHealthmonitoring andProviderAdapterRegistryWeb:
Contracts:
Tooling:
Summary by CodeRabbit
New Features
Enhancements
Tests
Documentation
Chores