Skip to content

Commit 6d4ef60

Browse files
committed
Fix provider session logic bugs: Claude Code availability, optional provider field, persisted resume cursor
- Set claudeCode available: false in PROVIDER_OPTIONS since server runtime is unconfigured - Make provider field conditional in turn start command to avoid unnecessary session restarts - Read resume cursor from persisted ProviderSessionDirectory instead of live adapter sessions - Update test mocks and layer wiring to provide ProviderSessionDirectory to ProviderCommandReactor
1 parent 6de3ed0 commit 6d4ef60

File tree

8 files changed

+49
-14
lines changed

8 files changed

+49
-14
lines changed

apps/server/integration/OrchestrationEngineHarness.integration.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ export const makeOrchestrationIntegrationHarness = (
222222
Layer.provide(ProviderSessionRuntimeRepositoryLive),
223223
);
224224
const providerLayer = makeProviderServiceLive().pipe(
225-
Layer.provide(providerSessionDirectoryLayer),
225+
Layer.provideMerge(providerSessionDirectoryLayer),
226226
Layer.provide(Layer.succeed(ProviderAdapterRegistry, registry)),
227227
);
228228
const checkpointStoreLayer = CheckpointStoreLive.pipe(Layer.provide(NodeServices.layer));

apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
ThreadId,
1111
TurnId,
1212
} from "@t3tools/contracts";
13-
import { Effect, Exit, Layer, ManagedRuntime, PubSub, Scope, Stream } from "effect";
13+
import { Effect, Exit, Layer, ManagedRuntime, Option, PubSub, Scope, Stream } from "effect";
1414
import { afterEach, describe, expect, it, vi } from "vitest";
1515

1616
import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts";
@@ -20,6 +20,7 @@ import {
2020
ProviderService,
2121
type ProviderServiceShape,
2222
} from "../../provider/Services/ProviderService.ts";
23+
import { ProviderSessionDirectory } from "../../provider/Services/ProviderSessionDirectory.ts";
2324
import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";
2425
import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";
2526
import { ProviderCommandReactorLive } from "./ProviderCommandReactor.ts";
@@ -141,9 +142,26 @@ describe("ProviderCommandReactor", () => {
141142
Layer.provide(OrchestrationCommandReceiptRepositoryLive),
142143
Layer.provide(SqlitePersistenceMemory),
143144
);
145+
const sessionDirectoryMock = Layer.succeed(ProviderSessionDirectory, {
146+
upsert: () => Effect.void,
147+
getProvider: () => Effect.die(new Error("not implemented in test")),
148+
getBinding: (sessionId: ProviderSessionId) => {
149+
const session = runtimeSessions.find((s) => s.sessionId === sessionId);
150+
if (!session) return Effect.succeed(Option.none());
151+
return Effect.succeed(Option.some({
152+
sessionId: session.sessionId,
153+
provider: session.provider,
154+
resumeCursor: session.resumeCursor,
155+
}));
156+
},
157+
getThreadId: () => Effect.succeed(Option.none()),
158+
remove: () => Effect.void,
159+
listSessionIds: () => Effect.succeed([]),
160+
});
144161
const layer = ProviderCommandReactorLive.pipe(
145162
Layer.provideMerge(orchestrationLayer),
146163
Layer.provideMerge(Layer.succeed(ProviderService, service)),
164+
Layer.provideMerge(sessionDirectoryMock),
147165
);
148166
runtime = ManagedRuntime.make(layer);
149167
const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService));

apps/server/src/orchestration/Layers/ProviderCommandReactor.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Stream } from "effect";
1616

1717
import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";
18+
import { ProviderSessionDirectory } from "../../provider/Services/ProviderSessionDirectory.ts";
1819
import { ProviderService } from "../../provider/Services/ProviderService.ts";
1920
import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
2021
import {
@@ -70,6 +71,7 @@ const DEFAULT_SANDBOX_MODE: ProviderSandboxMode = "workspace-write";
7071
const make = Effect.gen(function* () {
7172
const orchestrationEngine = yield* OrchestrationEngineService;
7273
const providerService = yield* ProviderService;
74+
const sessionDirectory = yield* ProviderSessionDirectory;
7375
const handledTurnStartKeys = yield* Cache.make<string, true>({
7476
capacity: HANDLED_TURN_START_KEY_MAX,
7577
timeToLive: HANDLED_TURN_START_KEY_TTL,
@@ -161,8 +163,9 @@ const make = Effect.gen(function* () {
161163
});
162164

163165
const resolveResumeCursorForSession = (sessionId: ProviderSessionId) =>
164-
providerService.listSessions().pipe(
165-
Effect.map((sessions) => sessions.find((session) => session.sessionId === sessionId)?.resumeCursor),
166+
sessionDirectory.getBinding(sessionId).pipe(
167+
Effect.map((binding) => Option.getOrUndefined(binding)?.resumeCursor),
168+
Effect.orElseSucceed(() => undefined),
166169
);
167170

168171
const startProviderSession = (input?: {

apps/server/src/serverLayers.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { makeCodexAdapterLive } from "./provider/Layers/CodexAdapter";
2323
import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry";
2424
import { makeProviderServiceLive } from "./provider/Layers/ProviderService";
2525
import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory";
26+
import { ProviderSessionDirectory } from "./provider/Services/ProviderSessionDirectory";
2627
import { ProviderService } from "./provider/Services/ProviderService";
2728

2829
import { TerminalManagerLive } from "./terminal/Layers/Manager";
@@ -36,7 +37,7 @@ import { BunPtyAdapterLive } from "./terminal/Layers/BunPTY";
3637
import { NodePtyAdapterLive } from "./terminal/Layers/NodePTY";
3738

3839
export function makeServerProviderLayer(): Layer.Layer<
39-
ProviderService,
40+
ProviderService | ProviderSessionDirectory,
4041
ProviderUnsupportedError,
4142
SqlClient.SqlClient | ServerConfig
4243
> {
@@ -55,7 +56,7 @@ export function makeServerProviderLayer(): Layer.Layer<
5556
);
5657
return makeProviderServiceLive({
5758
canonicalEventLogPath: path.join(providerLogsDir, "provider-canonical.ndjson"),
58-
}).pipe(Layer.provide(adapterRegistryLayer), Layer.provide(providerSessionDirectoryLayer));
59+
}).pipe(Layer.provide(adapterRegistryLayer), Layer.provideMerge(providerSessionDirectoryLayer));
5960
}).pipe(Layer.unwrap);
6061
}
6162

apps/server/src/wsServer.test.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import os from "node:os";
44
import path from "node:path";
55

66
import * as NodeServices from "@effect/platform-node/NodeServices";
7-
import { Effect, Exit, Layer, PubSub, Scope, Stream } from "effect";
7+
import { Effect, Exit, Layer, Option, PubSub, Scope, Stream } from "effect";
88
import { describe, expect, it, afterEach, vi } from "vitest";
99
import { createServer } from "./wsServer";
1010
import WebSocket from "ws";
@@ -42,6 +42,7 @@ import { TerminalManager, type TerminalManagerShape } from "./terminal/Services/
4242
import { makeSqlitePersistenceLive, SqlitePersistenceMemory } from "./persistence/Layers/Sqlite";
4343
import { SqlClient } from "effect/unstable/sql";
4444
import { ProviderService, type ProviderServiceShape } from "./provider/Services/ProviderService";
45+
import { ProviderSessionDirectory } from "./provider/Services/ProviderSessionDirectory";
4546
import { Open, type OpenShape } from "./open";
4647
import { GitManager, type GitManagerShape } from "./git/Services/GitManager.ts";
4748
import type { GitCoreShape } from "./git/Services/GitCore.ts";
@@ -331,7 +332,7 @@ describe("WebSocket Server", () => {
331332
devUrl?: string;
332333
authToken?: string;
333334
stateDir?: string;
334-
providerLayer?: Layer.Layer<ProviderService, never>;
335+
providerLayer?: Layer.Layer<ProviderService | ProviderSessionDirectory, never>;
335336
open?: OpenShape;
336337
gitManager?: GitManagerShape;
337338
gitCore?: Pick<GitCoreShape, "listBranches" | "initRepo" | "pullCurrentBranch">;
@@ -1007,7 +1008,17 @@ describe("WebSocket Server", () => {
10071008
stopAll: () => Effect.void,
10081009
streamEvents: Stream.fromPubSub(runtimeEventPubSub),
10091010
};
1010-
const providerLayer = Layer.succeed(ProviderService, providerService);
1011+
const providerLayer = Layer.mergeAll(
1012+
Layer.succeed(ProviderService, providerService),
1013+
Layer.succeed(ProviderSessionDirectory, {
1014+
upsert: () => Effect.void,
1015+
getProvider: () => Effect.die(new Error("not implemented in test")),
1016+
getBinding: () => Effect.succeed(Option.none()),
1017+
getThreadId: () => Effect.succeed(Option.none()),
1018+
remove: () => Effect.void,
1019+
listSessionIds: () => Effect.succeed([]),
1020+
}),
1021+
);
10111022

10121023
server = await createTestServer({
10131024
cwd: "/test",

apps/web/src/components/ChatView.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1560,7 +1560,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
15601560
attachments: turnAttachments,
15611561
},
15621562
model: selectedModel || undefined,
1563-
provider: selectedProvider,
1563+
...(selectedProviderByThread[threadIdForSend]
1564+
? { provider: selectedProviderByThread[threadIdForSend] }
1565+
: {}),
15641566
effort: selectedEffort || undefined,
15651567
assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered",
15661568
approvalPolicy,

apps/web/src/session-logic.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,12 @@ describe("deriveWorkLogEntries", () => {
128128
});
129129

130130
describe("PROVIDER_OPTIONS", () => {
131-
it("marks claudeCode as available", () => {
131+
it("marks claudeCode as unavailable until runtime is configured", () => {
132132
const claude = PROVIDER_OPTIONS.find((option) => option.value === "claudeCode");
133133
expect(claude).toEqual({
134134
value: "claudeCode",
135-
label: "Claude Code",
136-
available: true,
135+
label: "Claude Code (soon)",
136+
available: false,
137137
});
138138
});
139139
});

apps/web/src/session-logic.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const PROVIDER_OPTIONS: Array<{
1313
available: boolean;
1414
}> = [
1515
{ value: "codex", label: "Codex", available: true },
16-
{ value: "claudeCode", label: "Claude Code", available: true },
16+
{ value: "claudeCode", label: "Claude Code (soon)", available: false },
1717
];
1818

1919
export interface WorkLogEntry {

0 commit comments

Comments
 (0)