Skip to content

feat: multi-provider adapter support#1

Merged
aaditagrawal merged 10 commits intomainfrom
codex/feat-upstream-prs-179-295-364
Mar 8, 2026
Merged

feat: multi-provider adapter support#1
aaditagrawal merged 10 commits intomainfrom
codex/feat-upstream-prs-179-295-364

Conversation

@aaditagrawal
Copy link
Copy Markdown
Owner

@aaditagrawal aaditagrawal commented Mar 8, 2026

Summary

  • Adds provider adapter infrastructure for OpenCode, Copilot, Amp, GeminiCli, and Kilo alongside existing Codex and Claude Code adapters
  • Enhances runtime ingestion, provider health monitoring, and session directory management to support multiple providers
  • Adds UI settings for provider selection, sidebar improvements, accent color theming, and upstream PR track sync tooling

Changes

Server:

  • New server managers: opencodeServerManager, ampServerManager, geminiCliServerManager, kiloServerManager
  • New provider adapters (Layers + Services): OpenCode, Copilot, Amp, GeminiCli, Kilo
  • Enhanced ProviderRuntimeIngestion for multi-provider event handling
  • Improved ProviderHealth monitoring and ProviderAdapterRegistry
  • Copilot text generation service and CLI path resolution

Web:

  • Provider selection in settings UI
  • Sidebar enhancements
  • Accent color theming support
  • Composer draft store and session logic updates
  • New provider React Query hooks

Contracts:

  • Extended IPC, model, orchestration, provider, and provider runtime schemas
  • New WebSocket protocol messages for multi-provider support

Tooling:

  • Upstream PR track sync script and config

Summary by CodeRabbit

  • New Features

    • Added first-class support for Copilot, Claude Code, OpenCode, Gemini CLI, AMP, and Kilo providers plus model discovery and provider usage endpoints.
  • Enhancements

    • Provider model picker: grouped models, pricing tiers, merged runtime discovery.
    • Sidebar: provider usage and per-session metrics.
    • Accent theming: global and per-provider accent colors applied across the app.
    • WS/API: provider list-models and get-usage endpoints.
  • Tests

    • Extensive adapter and CLI integration tests for new providers.
  • Documentation

    • README reorganized and PR-tracking sync script added.
  • Chores

    • Added runtime deps and build config updates.

juliusmarminge and others added 6 commits March 8, 2026 12:58
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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 8, 2026

Caution

Review failed

Pull request was closed or merged during review

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds multi-provider support (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

Cohort / File(s) Summary
Planning & Docs
\.plans/17-claude-code.md, README.md, docs/custom-alpha-workflow.md, config/upstream-pr-tracks.json
New Claude Code plan; README updated for multi-agent support; PR-tracking config and docs added.
Dependencies & Build
apps/server/package.json, apps/server/tsdown.config.ts, package.json
Adds runtime SDK deps (Anthropic, Copilot, OpenCode), expands tsdown noExternal whitelist, updates dev deps, and adds sync script.
Server — Managers
apps/server/src/ampServerManager.ts, apps/server/src/geminiCliServerManager.ts, apps/server/src/kiloServerManager.ts, apps/server/src/opencodeServerManager.ts, apps/server/src/codexAppServerManager.ts
New provider managers implementing session lifecycle, streaming JSON/CLI parsing, event mapping, usage aggregation, read/rollback APIs; Codex manager adds rate-limit read.
Server — Adapters (Services & Layers)
apps/server/src/provider/Services/*, apps/server/src/provider/Layers/* (ClaudeCode, Copilot, OpenCode, GeminiCli, Amp, Kilo) and tests
Adds service tags and live adapter layers for each provider: layer wiring, event queues/streamEvents, error translation, validations, plus adapter tests/harnesses.
Provider Registry & Health
apps/server/src/provider/Layers/ProviderAdapterRegistry.ts, apps/server/src/provider/Layers/ProviderHealth.ts, apps/server/src/provider/Layers/copilotCliPath.ts
Registry expanded to include new adapters; Gemini CLI health probe added; Copilot CLI bundled-path resolution implemented.
Orchestration & Runtime
apps/server/src/orchestration/* (ProviderRuntimeIngestion.ts, projector.ts, decider.ts, tests)
Expanded provider unions in tests and logic; assistant buffering stores {text, createdAt}; per-thread pending token aggregation added; projector settles latestTurn when session status changes; optional turnUsage propagation included.
Contracts / Types
packages/contracts/src/* (provider.ts, model.ts, ipc.ts, ws.ts, orchestration.ts, providerRuntime.ts)
Adds ProviderKind entries and provider start options, model option schemas (pricingTier), provider listModels/getUsage inputs & results, turn usage types, new runtime event sources, and IPC/WS method typings.
SDK Typings
apps/server/src/provider/claude-agent-sdk.d.ts, apps/server/src/provider/copilot-sdk.d.ts
Ambient TypeScript declarations for Claude Agent SDK and Copilot SDK.
Web — Model UI, Usage & Theme
apps/web/src/components/ChatView.tsx, apps/web/src/components/Sidebar.tsx, apps/web/src/components/ThreadTerminalDrawer.tsx, apps/web/src/components/Icons.tsx, apps/web/src/accentColor.ts, apps/web/src/index.css, apps/web/src/appSettings.ts
Model picker and icons extended for new providers; provider usage UI and React Query hooks added; accent color utilities and per-provider overrides; CSS tokens wired to accent variables.
Web — State, Queries & Routes
apps/web/src/store.ts, apps/web/src/composerDraftStore.ts, apps/web/src/session-logic.ts, apps/web/src/wsNativeApi.ts, apps/web/src/lib/providerReactQuery.ts, apps/web/src/routes/_chat.settings.tsx, apps/web/src/routes/__root.tsx, apps/web/src/main.tsx
Provider normalization and model inference extended; WebSocket native API methods provider.listModels / provider.getUsage added; React Query hooks and settings UI wiring for models and accent color.
Tests & QA
apps/server/src/*/*.test.ts, apps/server/src/geminiCliServerManager.test.ts, provider adapter tests
Extensive tests added/updated for managers, adapters, and orchestration flows (Gemini CLI, Claude adapter, Kilo/OpenCode adapter harnesses).
Utilities & Scripts
scripts/sync-upstream-pr-tracks.mjs
Adds CLI script to refresh/report upstream PR tracking branches with diffs and commit summaries.
Other
packages/shared/src/model.ts, apps/server/src/serverLayers.ts, apps/server/src/wsServer.ts
Model slug catalog and server provider layer wiring extended; WS methods providerListModels/providerGetUsage added and wired to model/usage fetchers.

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

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I hopped through adapters, one, two, three,
Copilot, Claude, Amp, and Kilo with glee.
Streams and colors, models that gleam,
Events hop by like a coder's dream.
Hooray — sessions dance in multicolored beam!

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/feat-upstream-prs-179-295-364

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Duplicate 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 | 🟠 Major

Preserve isCustom before replacing Copilot's static list.

getAppModelOptions() still returns built-in and custom entries with isCustom, but ModelOptionEntry erases that flag here. customOnly therefore 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 | 🟠 Major

Add 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:

  1. Where provider credentials are stored (environment variables, secure settings, keychain)
  2. How credentials are scoped (per-user, per-workspace, global)
  3. Whether credentials are encrypted at rest
  4. Rotation and expiration policies
  5. 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 | 🟠 Major

Expand 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:

  1. Provider switching: What happens when a user switches from Codex to Claude mid-thread?
  2. Concurrent sessions: Multiple providers active simultaneously (memory, performance, state isolation)
  3. Provider fallback: Behavior when a selected provider becomes unavailable
  4. Cross-provider resume: Whether resume cursors are strictly scoped to prevent leakage
  5. Registry edge cases: Provider discovery when some adapters fail to initialize
  6. Health monitoring: Provider-specific health checks and degradation scenarios
  7. 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 load

Add 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 | 🟠 Major

Plan 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 | 🟠 Major

Fix Schema.Union to use variadic arguments instead of an array.

Schema.Union expects 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.Literals pattern 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 | 🟠 Major

Keep terminal accent colors contrast-safe.

Using the raw accent for cursor, blue, and brightBlue can 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 from apps/web/src/accentColor.ts would 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 | 🟠 Major

Reject 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 #777777 can still fail both choices, so applyAccentColorToDocument can 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 | 🟠 Major

Forward numTurns to the manager.

Line 147 validates numTurns and then drops it. rollbackThread(threadId, 5) currently delegates the same call as rollbackThread(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 | 🟠 Major

Pass the validated rollback count through.

Line 147 ignores numTurns after 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

rollbackThread currently 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 | 🟠 Major

Scope the cached server by connection options.

providerOptions are passed per session, but ensureServer() memoizes a single SharedServerState for the whole manager. The first thread that starts wins, and later threads silently reuse its serverUrl/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 | 🟠 Major

Keep cancel distinct from decline.

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 | 🟠 Major

Avoid eager provider discovery on every chat mount.

These queries run unconditionally, and providerListModelsQueryOptions() immediately calls api.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 | 🟠 Major

Don'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 turnDiffSummaryByAssistantMessageId entry 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 | 🟠 Major

Read workspace back out of resumeCursor.

Lines 833-836 persist both sessionId and workspace, but reattach only reads sessionId. A resumed thread that omits providerOptions.workspace will call session.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 | 🟠 Major

Don'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 | 🟠 Major

Terminal turn bookkeeping is incomplete.

sendTurn() queues an orchestration turnId immediately, but terminal handling only clears currentTurnId/currentProviderTurnId. If Copilot terminates a turn before assistant.turn_start, that stale ID stays in pendingTurnIds, session.idle can still emit turn.completed, and the next assistant.turn_start can 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 | 🟠 Major

Guard startSession() against duplicate threadIds.

This path always creates a new query and then overwrites the existing sessions entry 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 | 🟠 Major

Don'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.turns is reinitialized to [] on resume, and rollbackThread() only splices that local array before rewriting the cursor. The live Claude session still retains the removed turns, and lastAssistantUuid keeps 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 | 🟠 Major

Emit resolution/exit events when a Copilot session is torn down.

stopRecord() resolves pending approvals and user-input promises, but it never emits request.resolved, user-input.resolved, or session.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 | 🟠 Major

Add 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.addFinalizer to 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 | 🟠 Major

Handle attachment validation errors through Effect error channel.

Throwing ProviderAdapterRequestError from the synchronous .map() callback escapes the Effect context and becomes an unhandled defect instead of a normal session.send failure. Within this Effect.gen generator, use Effect.forEach and propagate the error with yield* 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 | 🟡 Minor

Document 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 | 🟡 Minor

Remove the leading spaces before these headings.

markdownlint is 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 | 🟡 Minor

Fail fast on duplicate provider registrations.

new Map(adapters.map(...)) silently keeps the last adapter for a provider. With a larger built-in set and customizable options.adapters, that can hide misconfiguration and route requests to the wrong backend. Validate uniqueness while building byProvider instead 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 | 🟡 Minor

Hardcoded repository URL should use configuration.

The PR URL is hardcoded to pingdotgg/t3code, but the script reads upstreamRemote and forkRemote from configuration. Consider deriving the URL from the git remote or adding a repoUrl field 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 | 🟡 Minor

This test never proves lazy spawning.

Lines 106-108 only confirm that a ready session exists. A regression that eagerly calls spawn during startSession would still pass here unless the test asserts on spawn directly.

🤖 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 | 🟡 Minor

Drop invalid provider overrides instead of normalizing them to blue.

Line 164 uses normalizeAccentColor, which substitutes DEFAULT_ACCENT_COLOR for any non-empty invalid value. That turns a bad/stale provider override into a sticky #2563eb override 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 | 🟡 Minor

Make the live Gemini suite explicitly opt-in.

The current guard only proves that the gemini binary 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 | 🟡 Minor

Validated numTurns parameter is not passed to the manager.

The numTurns parameter is validated (must be integer >= 1) but never passed to manager.rollbackThread(). The manager's rollbackThread method doesn't accept this parameter either. If rollback functionality is intended to respect numTurns, 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 | 🟡 Minor

Content deltas receive unique itemIds, preventing event aggregation.

For content.delta events, a new random itemId is 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 | 🟡 Minor

Hardcoded runtimeMode in listSessions ignores session's actual mode.

Line 391 uses hardcoded "full-access" instead of session.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 | 🟡 Minor

Process 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_MS to 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

📥 Commits

Reviewing files that changed from the base of the PR and between e213243 and 0b25fcc.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (73)
  • .plans/17-claude-code.md
  • README.md
  • apps/server/package.json
  • apps/server/src/ampServerManager.ts
  • apps/server/src/codexAppServerManager.ts
  • apps/server/src/geminiCliServerManager.test.ts
  • apps/server/src/geminiCliServerManager.ts
  • apps/server/src/git/Layers/CopilotTextGeneration.ts
  • apps/server/src/git/Services/CopilotTextGeneration.ts
  • apps/server/src/kiloServerManager.ts
  • apps/server/src/opencodeServerManager.ts
  • apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
  • apps/server/src/orchestration/decider.ts
  • apps/server/src/orchestration/projector.ts
  • apps/server/src/provider/Layers/AmpAdapter.ts
  • apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts
  • apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
  • apps/server/src/provider/Layers/CodexAdapter.test.ts
  • apps/server/src/provider/Layers/CodexAdapter.ts
  • apps/server/src/provider/Layers/CopilotAdapter.ts
  • apps/server/src/provider/Layers/GeminiCliAdapter.ts
  • apps/server/src/provider/Layers/KiloAdapter.test.ts
  • apps/server/src/provider/Layers/KiloAdapter.ts
  • apps/server/src/provider/Layers/OpenCodeAdapter.test.ts
  • apps/server/src/provider/Layers/OpenCodeAdapter.ts
  • apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts
  • apps/server/src/provider/Layers/ProviderAdapterRegistry.ts
  • apps/server/src/provider/Layers/ProviderHealth.ts
  • apps/server/src/provider/Layers/ProviderService.test.ts
  • apps/server/src/provider/Layers/ProviderSessionDirectory.ts
  • apps/server/src/provider/Layers/copilotCliPath.ts
  • apps/server/src/provider/Services/AmpAdapter.ts
  • apps/server/src/provider/Services/ClaudeCodeAdapter.ts
  • apps/server/src/provider/Services/CopilotAdapter.ts
  • apps/server/src/provider/Services/GeminiCliAdapter.ts
  • apps/server/src/provider/Services/KiloAdapter.ts
  • apps/server/src/provider/Services/OpenCodeAdapter.ts
  • apps/server/src/provider/claude-agent-sdk.d.ts
  • apps/server/src/provider/copilot-sdk.d.ts
  • apps/server/src/serverLayers.ts
  • apps/server/src/wsServer.ts
  • apps/server/tsdown.config.ts
  • apps/web/src/accentColor.ts
  • apps/web/src/appSettings.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/Icons.tsx
  • apps/web/src/components/Sidebar.tsx
  • apps/web/src/components/ThreadTerminalDrawer.tsx
  • apps/web/src/composerDraftStore.ts
  • apps/web/src/index.css
  • apps/web/src/lib/providerReactQuery.ts
  • apps/web/src/main.tsx
  • apps/web/src/routes/__root.tsx
  • apps/web/src/routes/_chat.settings.tsx
  • apps/web/src/session-logic.test.ts
  • apps/web/src/session-logic.ts
  • apps/web/src/store.ts
  • apps/web/src/wsNativeApi.ts
  • config/upstream-pr-tracks.json
  • docs/custom-alpha-workflow.md
  • package.json
  • packages/contracts/src/ipc.ts
  • packages/contracts/src/model.ts
  • packages/contracts/src/orchestration.ts
  • packages/contracts/src/provider.ts
  • packages/contracts/src/providerRuntime.test.ts
  • packages/contracts/src/providerRuntime.ts
  • packages/contracts/src/ws.ts
  • packages/shared/src/model.ts
  • scripts/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
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

🧹 Nitpick comments (8)
apps/server/src/provider/claude-agent-sdk.d.ts (1)

63-88: Consider using unknown instead of any for consistency.

The SDKMessage interface uses any in several places (lines 72, 78, 82, 87) while other interfaces like SDKResultMessage use unknown for similar index signatures. Using unknown provides 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 through unknown may hide type mismatches.

The as unknown as ProviderRuntimeEvent double cast bypasses TypeScript's type checking. Since KiloProviderRuntimeEvent is designed to extend ProviderRuntimeEvent with "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.

readJsonData simply 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 direct client.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:

  1. Preventing credential leakage - Ensure error messages, logs, and debug output never expose API keys or tokens
  2. Secure environment propagation - When spawning child processes for CLI-based providers, explicitly document which environment variables are inherited and how to sanitize the environment
  3. Secret rotation - Note that credential rotation requires restarting affected provider sessions
  4. 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:

  1. Performance under load - Simulate multiple concurrent sessions across different providers with realistic turn rates to identify resource bottlenecks
  2. Graceful degradation - Verify that when one provider is unavailable, other providers continue to function normally
  3. 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 _claudeUsageAccumulator aggregates usage data globally across all Claude Code sessions. This design means:

  1. fetchClaudeCodeUsage() returns combined totals from all sessions
  2. State persists across session restarts
  3. No way to reset or separate per-session usage

If per-session usage tracking is needed, consider moving this state into ClaudeSessionContext and 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 using Effect.die for idiomatic Effect error handling.

Using throw inside Effect.gen produces a Cause.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

📥 Commits

Reviewing files that changed from the base of the PR and between 0b25fcc and 3057152.

📒 Files selected for processing (17)
  • .plans/17-claude-code.md
  • README.md
  • apps/server/src/ampServerManager.ts
  • apps/server/src/geminiCliServerManager.test.ts
  • apps/server/src/geminiCliServerManager.ts
  • apps/server/src/kiloServerManager.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
  • apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts
  • apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
  • apps/server/src/provider/Layers/CopilotAdapter.ts
  • apps/server/src/provider/Layers/ProviderAdapterRegistry.ts
  • apps/server/src/provider/claude-agent-sdk.d.ts
  • apps/web/src/accentColor.ts
  • apps/web/src/appSettings.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/ThreadTerminalDrawer.tsx
  • scripts/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
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

♻️ Duplicate comments (1)
apps/server/src/provider/Layers/CopilotAdapter.ts (1)

1215-1246: ⚠️ Potential issue | 🟠 Major

Validation failures still leak the started Copilot client.

validateSessionConfiguration() can call client.start() and fail before the inner client.stop() catch around createSession() / 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3057152 and d200780.

📒 Files selected for processing (12)
  • .plans/17-claude-code.md
  • apps/server/src/ampServerManager.ts
  • apps/server/src/geminiCliServerManager.test.ts
  • apps/server/src/geminiCliServerManager.ts
  • apps/server/src/kiloServerManager.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
  • apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
  • apps/server/src/provider/Layers/CopilotAdapter.ts
  • apps/server/src/provider/Layers/ProviderAdapterRegistry.ts
  • apps/server/src/provider/claude-agent-sdk.d.ts
  • apps/web/src/components/ChatView.tsx
  • scripts/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
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 | 🟠 Major

Preserve the selected service tier in the plan send flows.

The main send path passes serviceTier at Line 2727, but both of these thread.turn.start calls 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 | 🟠 Major

Don't reopen or delete the session before the owning child exits.

Lines 532-533 mark the thread ready before close, and Line 394 deletes the session immediately after kill(). Because the later close/error handlers re-fetch by threadId and 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 KiloServerManager instance for each call, which may spawn a new Kilo server if none exists. For scenarios where fetchKiloModels is called frequently, this could be inefficient.

If model discovery is expected to be called repeatedly, consider:

  1. Caching results with a TTL
  2. 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

📥 Commits

Reviewing files that changed from the base of the PR and between d200780 and b17fef4.

📒 Files selected for processing (8)
  • apps/server/src/ampServerManager.ts
  • apps/server/src/geminiCliServerManager.test.ts
  • apps/server/src/geminiCliServerManager.ts
  • apps/server/src/kiloServerManager.ts
  • apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
  • apps/server/src/provider/Layers/CopilotAdapter.ts
  • apps/web/src/components/ChatView.tsx
  • scripts/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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants