History

Tighten provider Effect service refactor

Julius Marminge committed 4 months ago

Commit 84207d01 parent 5f81c3a30 files · +323 −205Browse files

Unified · Split

30 files
  1. apps/server/integration/OrchestrationEngineHarness.integration.ts +15 −16
  2. apps/server/integration/providerService.integration.test.ts +1 −3
  3. apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +3 −6
  4. apps/server/src/orchestration/Layers/CheckpointReactor.ts +2 −2
  5. apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +17 −14
  6. apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +4 −4
  7. apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +3 −6
  8. apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +2 −2
  9. apps/server/src/orchestration/Layers/ThreadDeletionReactor.ts +2 −2
  10. apps/server/src/provider/Layers/OpenCodeProvider.test.ts +25 −0
  11. apps/server/src/provider/Layers/ProviderEventLoggers.ts +0 −6
  12. apps/server/src/provider/Layers/ProviderService.ts +0 −11
  13. apps/server/src/provider/Layers/ProviderSessionDirectory.ts +0 −6
  14. apps/server/src/provider/ProviderAdapterRegistry.ts +26 −9
  15. apps/server/src/provider/ProviderInstanceRegistry.ts +29 −16
  16. apps/server/src/provider/ProviderRegistry.ts +20 −22
  17. apps/server/src/provider/ProviderService.test.ts +18 −15
  18. apps/server/src/provider/ProviderService.ts +28 −15
  19. apps/server/src/provider/ProviderSessionDirectory.test.ts +1 −3
  20. apps/server/src/provider/ProviderSessionDirectory.ts +4 −1
  21. apps/server/src/provider/ProviderSessionReaper.ts +1 −0
  22. apps/server/src/provider/Services/ProviderAdapterRegistry.ts +0 −3
  23. apps/server/src/provider/Services/ProviderRegistry.ts +0 −2
  24. apps/server/src/provider/Services/ProviderService.ts +0 −5
  25. apps/server/src/provider/opencodeRuntime.ts +9 −4
  26. apps/server/src/provider/providerMaintenanceRunner.test.ts +31 −0
  27. apps/server/src/provider/providerMaintenanceRunner.ts +63 −13
  28. docs/architecture/overview.md +1 −1
  29. docs/operations/effect-fn-checklist.md +17 −17
  30. docs/reference/encyclopedia.md +1 −1
apps/server/integration/OrchestrationEngineHarness.integration.ts+15−16
@@ -33,17 +33,13 @@ import { makeSqlitePersistenceLive } from "../src/persistence/Layers/Sqlite.ts"; · 32 unmodified lines
33import { ProjectionCheckpointRepository } from "../src/persistence/Services/ProjectionCheckpoints.ts";33import { ProjectionCheckpointRepository } from "../src/persistence/Services/ProjectionCheckpoints.ts";
34import { ProjectionPendingApprovalRepository } from "../src/persistence/Services/ProjectionPendingApprovals.ts";34import { ProjectionPendingApprovalRepository } from "../src/persistence/Services/ProjectionPendingApprovals.ts";
35import { makeAdapterRegistryMock } from "../src/provider/testUtils/providerAdapterRegistryMock.ts";35import { makeAdapterRegistryMock } from "../src/provider/testUtils/providerAdapterRegistryMock.ts";
36import { ProviderAdapterRegistry } from "../src/provider/Services/ProviderAdapterRegistry.ts";36import * as ProviderAdapterRegistry from "../src/provider/ProviderAdapterRegistry.ts";
37import { makeProviderRegistryLayer } from "../src/provider/testUtils/providerRegistryMock.ts";37import { makeProviderRegistryLayer } from "../src/provider/testUtils/providerRegistryMock.ts";
38import { ProviderSessionDirectoryLive } from "../src/provider/Layers/ProviderSessionDirectory.ts";38import * as ProviderSessionDirectory from "../src/provider/ProviderSessionDirectory.ts";
39import { ServerSettingsService } from "../src/serverSettings.ts";39import { ServerSettingsService } from "../src/serverSettings.ts";
40import { makeProviderServiceLive } from "../src/provider/Layers/ProviderService.ts";40import * as ProviderService from "../src/provider/ProviderService.ts";
41import { makeCodexAdapter } from "../src/provider/Layers/CodexAdapter.ts";41import { makeCodexAdapter } from "../src/provider/Layers/CodexAdapter.ts";
42import {42import * as ProviderEventLoggers from "../src/provider/ProviderEventLoggers.ts";
43 NoOpProviderEventLoggers,
44 ProviderEventLoggers,
45} from "../src/provider/Layers/ProviderEventLoggers.ts";
46import { ProviderService } from "../src/provider/Services/ProviderService.ts";
47import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts";43import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts";
48import { CheckpointReactorLive } from "../src/orchestration/Layers/CheckpointReactor.ts";44import { CheckpointReactorLive } from "../src/orchestration/Layers/CheckpointReactor.ts";
49import * as RepositoryIdentityResolver from "../src/project/RepositoryIdentityResolver.ts";45import * as RepositoryIdentityResolver from "../src/project/RepositoryIdentityResolver.ts";
@@ -178,7 +174,7 @@ export interface OrchestrationIntegrationHarness { · 128 unmodified lines
178 readonly adapterHarness: TestProviderAdapterHarness | null;174 readonly adapterHarness: TestProviderAdapterHarness | null;
179 readonly engine: OrchestrationEngineShape;175 readonly engine: OrchestrationEngineShape;
180 readonly snapshotQuery: ProjectionSnapshotQuery["Service"];176 readonly snapshotQuery: ProjectionSnapshotQuery["Service"];
181 readonly providerService: ProviderService["Service"];177 readonly providerService: ProviderService.ProviderService["Service"];
182 readonly checkpointStore: CheckpointStore.CheckpointStore["Service"];178 readonly checkpointStore: CheckpointStore.CheckpointStore["Service"];
183 readonly checkpointRepository: ProjectionCheckpointRepository["Service"];179 readonly checkpointRepository: ProjectionCheckpointRepository["Service"];
184 readonly pendingApprovalRepository: ProjectionPendingApprovalRepository["Service"];180 readonly pendingApprovalRepository: ProjectionPendingApprovalRepository["Service"];
@@ -241,7 +237,7 @@ export const makeOrchestrationIntegrationHarness = ( · 56 unmodified lines
241 });237 });
242 const fakeRegistry = adapterHarness238 const fakeRegistry = adapterHarness
243 ? Layer.succeed(239 ? Layer.succeed(
244 ProviderAdapterRegistry,240 ProviderAdapterRegistry.ProviderAdapterRegistry,
245 makeAdapterRegistryMock({ [adapterHarness.provider]: adapterHarness.adapter }),241 makeAdapterRegistryMock({ [adapterHarness.provider]: adapterHarness.adapter }),
246 )242 )
247 : null;243 : null;
@@ -262,11 +258,11 @@ export const makeOrchestrationIntegrationHarness = ( · 14 unmodified lines
262 Layer.provide(OrchestrationEventStoreLive),258 Layer.provide(OrchestrationEventStoreLive),
263 Layer.provide(OrchestrationCommandReceiptRepositoryLive),259 Layer.provide(OrchestrationCommandReceiptRepositoryLive),
264 );260 );
265 const providerSessionDirectoryLayer = ProviderSessionDirectoryLive.pipe(261 const providerSessionDirectoryLayer = ProviderSessionDirectory.layer.pipe(
266 Layer.provide(ProviderSessionRuntimeRepositoryLive),262 Layer.provide(ProviderSessionRuntimeRepositoryLive),
267 );263 );
268 const realCodexRegistry = Layer.effect(264 const realCodexRegistry = Layer.effect(
269 ProviderAdapterRegistry,265 ProviderAdapterRegistry.ProviderAdapterRegistry,
270 Effect.gen(function* () {266 Effect.gen(function* () {
271 const codexSettings = yield* decodeCodexSettings({});267 const codexSettings = yield* decodeCodexSettings({});
272 const codexAdapter = yield* makeCodexAdapter(codexSettings);268 const codexAdapter = yield* makeCodexAdapter(codexSettings);
@@ -279,15 +275,18 @@ export const makeOrchestrationIntegrationHarness = ( · 6 unmodified lines
279 Layer.provideMerge(NodeServices.layer),275 Layer.provideMerge(NodeServices.layer),
280 Layer.provideMerge(providerSessionDirectoryLayer),276 Layer.provideMerge(providerSessionDirectoryLayer),
281 );277 );
282 const providerEventLoggersLayer = Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers);278 const providerEventLoggersLayer = Layer.succeed(
279 ProviderEventLoggers.ProviderEventLoggers,
280 ProviderEventLoggers.NoOpProviderEventLoggers,
281 );
283 const providerLayer = useRealCodex282 const providerLayer = useRealCodex
284 ? makeProviderServiceLive().pipe(283 ? ProviderService.layer.pipe(
285 Layer.provide(providerSessionDirectoryLayer),284 Layer.provide(providerSessionDirectoryLayer),
286 Layer.provide(realCodexRegistry),285 Layer.provide(realCodexRegistry),
287 Layer.provide(AnalyticsService.layerTest),286 Layer.provide(AnalyticsService.layerTest),
288 Layer.provide(providerEventLoggersLayer),287 Layer.provide(providerEventLoggersLayer),
289 )288 )
290 : makeProviderServiceLive().pipe(289 : ProviderService.layer.pipe(
291 Layer.provide(providerSessionDirectoryLayer),290 Layer.provide(providerSessionDirectoryLayer),
292 Layer.provide(fakeRegistry!),291 Layer.provide(fakeRegistry!),
293 Layer.provide(AnalyticsService.layerTest),292 Layer.provide(AnalyticsService.layerTest),
@@ -395,7 +394,7 @@ export const makeOrchestrationIntegrationHarness = ( · 101 unmodified lines
395 runtime.runPromise(Effect.service(ProjectionSnapshotQuery)),394 runtime.runPromise(Effect.service(ProjectionSnapshotQuery)),
396 ).pipe(Effect.orDie);395 ).pipe(Effect.orDie);
397 const providerService = yield* tryRuntimePromise("load ProviderService service", () =>396 const providerService = yield* tryRuntimePromise("load ProviderService service", () =>
398 runtime.runPromise(Effect.service(ProviderService)),397 runtime.runPromise(Effect.service(ProviderService.ProviderService)),
399 ).pipe(Effect.orDie);398 ).pipe(Effect.orDie);
400 const checkpointStore = yield* tryRuntimePromise("load CheckpointStore service", () =>399 const checkpointStore = yield* tryRuntimePromise("load CheckpointStore service", () =>
401 runtime.runPromise(Effect.service(CheckpointStore.CheckpointStore)),400 runtime.runPromise(Effect.service(CheckpointStore.CheckpointStore)),
apps/server/integration/providerService.integration.test.ts+1−3
@@ -70,9 +70,7 @@ const makeIntegrationFixture = Effect.gen(function* () { · 69 unmodified lines
70 ),70 ),
71 ).pipe(Layer.provide(SqlitePersistenceMemory));71 ).pipe(Layer.provide(SqlitePersistenceMemory));
72 72
73 const layer = Layer.effect(ProviderService.ProviderService, ProviderService.make()).pipe(73 const layer = ProviderService.layer.pipe(Layer.provide(shared));
74 Layer.provide(shared),
75 );
76 74
77 return {75 return {
78 cwd,76 cwd,
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+3−6
@@ -49,10 +49,7 @@ import { · 48 unmodified lines
49} from "../Services/OrchestrationEngine.ts";49} from "../Services/OrchestrationEngine.ts";
50import { CheckpointReactor } from "../Services/CheckpointReactor.ts";50import { CheckpointReactor } from "../Services/CheckpointReactor.ts";
51import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts";51import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts";
52import {52import * as ProviderService from "../../provider/ProviderService.ts";
53 ProviderService,
54 type ProviderServiceShape,
55} from "../../provider/Services/ProviderService.ts";
56import { checkpointRefForThreadTurn } from "../../checkpointing/Utils.ts";53import { checkpointRefForThreadTurn } from "../../checkpointing/Utils.ts";
57import { ServerConfig } from "../../config.ts";54import { ServerConfig } from "../../config.ts";
58import * as WorkspaceEntries from "../../workspace/WorkspaceEntries.ts";55import * as WorkspaceEntries from "../../workspace/WorkspaceEntries.ts";
@@ -102,7 +99,7 @@ function createProviderServiceHarness( · 43 unmodified lines
102 },99 },
103 ] satisfies ReadonlyArray<ProviderSession>)100 ] satisfies ReadonlyArray<ProviderSession>)
104 : Effect.succeed([] as ReadonlyArray<ProviderSession>);101 : Effect.succeed([] as ReadonlyArray<ProviderSession>);
105 const service: ProviderServiceShape = {102 const service: ProviderService.ProviderService["Service"] = {
106 startSession: () => unsupported(),103 startSession: () => unsupported(),
107 sendTurn: () => unsupported(),104 sendTurn: () => unsupported(),
108 interruptTurn: () => unsupported(),105 interruptTurn: () => unsupported(),
@@ -328,7 +325,7 @@ describe("CheckpointReactor", () => { · 219 unmodified lines
328 Layer.provideMerge(orchestrationLayer),325 Layer.provideMerge(orchestrationLayer),
329 Layer.provideMerge(projectionSnapshotLayer),326 Layer.provideMerge(projectionSnapshotLayer),
330 Layer.provideMerge(RuntimeReceiptBusLive),327 Layer.provideMerge(RuntimeReceiptBusLive),
331 Layer.provideMerge(Layer.succeed(ProviderService, provider.service)),328 Layer.provideMerge(Layer.succeed(ProviderService.ProviderService, provider.service)),
332 Layer.provideMerge(vcsStatusBroadcasterLayer),329 Layer.provideMerge(vcsStatusBroadcasterLayer),
333 Layer.provideMerge(CheckpointStore.layer.pipe(Layer.provide(VcsDriverRegistry.layer))),330 Layer.provideMerge(CheckpointStore.layer.pipe(Layer.provide(VcsDriverRegistry.layer))),
334 Layer.provideMerge(331 Layer.provideMerge(
apps/server/src/orchestration/Layers/CheckpointReactor.ts+2−2
@@ -25,7 +25,7 @@ import { · 24 unmodified lines
25 resolveThreadWorkspaceCwd,25 resolveThreadWorkspaceCwd,
26} from "../../checkpointing/Utils.ts";26} from "../../checkpointing/Utils.ts";
27import * as CheckpointStore from "../../checkpointing/CheckpointStore.ts";27import * as CheckpointStore from "../../checkpointing/CheckpointStore.ts";
28import { ProviderService } from "../../provider/Services/ProviderService.ts";28import * as ProviderService from "../../provider/ProviderService.ts";
29import { CheckpointReactor, type CheckpointReactorShape } from "../Services/CheckpointReactor.ts";29import { CheckpointReactor, type CheckpointReactorShape } from "../Services/CheckpointReactor.ts";
30import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";30import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
31import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts";31import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts";
@@ -80,7 +80,7 @@ const make = Effect.gen(function* () { · 48 unmodified lines
80 randomUUID.pipe(Effect.map((uuid) => CommandId.make(`server:${tag}:${uuid}`)));80 randomUUID.pipe(Effect.map((uuid) => CommandId.make(`server:${tag}:${uuid}`)));
81 const orchestrationEngine = yield* OrchestrationEngineService;81 const orchestrationEngine = yield* OrchestrationEngineService;
82 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;82 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
83 const providerService = yield* ProviderService;83 const providerService = yield* ProviderService.ProviderService;
84 const checkpointStore = yield* CheckpointStore.CheckpointStore;84 const checkpointStore = yield* CheckpointStore.CheckpointStore;
85 const receiptBus = yield* RuntimeReceiptBus;85 const receiptBus = yield* RuntimeReceiptBus;
86 const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;86 const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+17−14
@@ -37,10 +37,7 @@ import { ProviderAdapterRequestError } from "../../provider/Errors.ts"; · 36 unmodified lines
37import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts";37import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts";
38import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";38import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";
39import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";39import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";
40import {40import * as ProviderService from "../../provider/ProviderService.ts";
41 ProviderService,
42 type ProviderServiceShape,
43} from "../../provider/Services/ProviderService.ts";
44import { makeProviderRegistryLayer } from "../../provider/testUtils/providerRegistryMock.ts";41import { makeProviderRegistryLayer } from "../../provider/testUtils/providerRegistryMock.ts";
45import { TextGeneration, type TextGenerationShape } from "../../textGeneration/TextGeneration.ts";42import { TextGeneration, type TextGenerationShape } from "../../textGeneration/TextGeneration.ts";
46import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts";43import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts";
@@ -222,8 +219,12 @@ describe("ProviderCommandReactor", () => { · 175 unmodified lines
222 }),219 }),
223 );220 );
224 const interruptTurn = vi.fn((_: unknown) => Effect.void);221 const interruptTurn = vi.fn((_: unknown) => Effect.void);
225 const respondToRequest = vi.fn<ProviderServiceShape["respondToRequest"]>(() => Effect.void);222 const respondToRequest = vi.fn<ProviderService.ProviderService["Service"]["respondToRequest"]>(
226 const respondToUserInput = vi.fn<ProviderServiceShape["respondToUserInput"]>(() => Effect.void);223 () => Effect.void,
224 );
225 const respondToUserInput = vi.fn<
226 ProviderService.ProviderService["Service"]["respondToUserInput"]
227 >(() => Effect.void);
227 const stopSession = vi.fn((input: unknown) =>228 const stopSession = vi.fn((input: unknown) =>
228 Effect.sync(() => {229 Effect.sync(() => {
229 const threadId =230 const threadId =
@@ -294,13 +295,15 @@ describe("ProviderCommandReactor", () => { · 64 unmodified lines
294 ];295 ];
295 296
296 const unsupported = () => Effect.die(new Error("Unsupported provider call in test")) as never;297 const unsupported = () => Effect.die(new Error("Unsupported provider call in test")) as never;
297 const service: ProviderServiceShape = {298 const service: ProviderService.ProviderService["Service"] = {
298 startSession: startSession as ProviderServiceShape["startSession"],299 startSession: startSession as ProviderService.ProviderService["Service"]["startSession"],
299 sendTurn: sendTurn as ProviderServiceShape["sendTurn"],300 sendTurn: sendTurn as ProviderService.ProviderService["Service"]["sendTurn"],
300 interruptTurn: interruptTurn as ProviderServiceShape["interruptTurn"],301 interruptTurn: interruptTurn as ProviderService.ProviderService["Service"]["interruptTurn"],
301 respondToRequest: respondToRequest as ProviderServiceShape["respondToRequest"],302 respondToRequest:
302 respondToUserInput: respondToUserInput as ProviderServiceShape["respondToUserInput"],303 respondToRequest as ProviderService.ProviderService["Service"]["respondToRequest"],
303 stopSession: stopSession as ProviderServiceShape["stopSession"],304 respondToUserInput:
305 respondToUserInput as ProviderService.ProviderService["Service"]["respondToUserInput"],
306 stopSession: stopSession as ProviderService.ProviderService["Service"]["stopSession"],
304 listSessions: () => Effect.succeed(runtimeSessions),307 listSessions: () => Effect.succeed(runtimeSessions),
305 getCapabilities: (_provider) =>308 getCapabilities: (_provider) =>
306 Effect.succeed({309 Effect.succeed({
@@ -346,7 +349,7 @@ describe("ProviderCommandReactor", () => { · 39 unmodified lines
346 const layer = ProviderCommandReactorLive.pipe(349 const layer = ProviderCommandReactorLive.pipe(
347 Layer.provideMerge(orchestrationLayer),350 Layer.provideMerge(orchestrationLayer),
348 Layer.provideMerge(projectionSnapshotLayer),351 Layer.provideMerge(projectionSnapshotLayer),
349 Layer.provideMerge(Layer.succeed(ProviderService, service)),352 Layer.provideMerge(Layer.succeed(ProviderService.ProviderService, service)),
350 Layer.provideMerge(makeProviderRegistryLayer(providerSnapshots as never)),353 Layer.provideMerge(makeProviderRegistryLayer(providerSnapshots as never)),
351 Layer.provideMerge(354 Layer.provideMerge(
352 Layer.mock(GitWorkflowService.GitWorkflowService)({355 Layer.mock(GitWorkflowService.GitWorkflowService)({
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+4−4
@@ -30,8 +30,8 @@ import { increment, orchestrationEventsProcessedTotal } from "../../observabilit · 29 unmodified lines
30import { ProviderAdapterRequestError } from "../../provider/Errors.ts";30import { ProviderAdapterRequestError } from "../../provider/Errors.ts";
31import type { ProviderServiceError } from "../../provider/Errors.ts";31import type { ProviderServiceError } from "../../provider/Errors.ts";
32import { TextGeneration } from "../../textGeneration/TextGeneration.ts";32import { TextGeneration } from "../../textGeneration/TextGeneration.ts";
33import { ProviderService } from "../../provider/Services/ProviderService.ts";33import * as ProviderService from "../../provider/ProviderService.ts";
34import { ProviderRegistry } from "../../provider/Services/ProviderRegistry.ts";34import * as ProviderRegistry from "../../provider/ProviderRegistry.ts";
35import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";35import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
36import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts";36import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts";
37import {37import {
@@ -190,8 +190,8 @@ const make = Effect.gen(function* () { · 152 unmodified lines
190 const crypto = yield* Crypto.Crypto;190 const crypto = yield* Crypto.Crypto;
191 const orchestrationEngine = yield* OrchestrationEngineService;191 const orchestrationEngine = yield* OrchestrationEngineService;
192 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;192 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
193 const providerService = yield* ProviderService;193 const providerService = yield* ProviderService.ProviderService;
194 const providerRegistry = yield* ProviderRegistry;194 const providerRegistry = yield* ProviderRegistry.ProviderRegistry;
195 const gitWorkflow = yield* GitWorkflowService;195 const gitWorkflow = yield* GitWorkflowService;
196 const vcsStatusBroadcaster = yield* VcsStatusBroadcaster;196 const vcsStatusBroadcaster = yield* VcsStatusBroadcaster;
197 const textGeneration = yield* TextGeneration;197 const textGeneration = yield* TextGeneration;
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+3−6
@@ -35,10 +35,7 @@ import { afterEach, describe, expect, it } from "vite-plus/test"; · 34 unmodified lines
35import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts";35import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts";
36import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";36import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";
37import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";37import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";
38import {38import * as ProviderService from "../../provider/ProviderService.ts";
39 ProviderService,
40 type ProviderServiceShape,
41} from "../../provider/Services/ProviderService.ts";
42import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts";39import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts";
43import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";40import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";
44import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";41import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";
@@ -97,7 +94,7 @@ function createProviderServiceHarness() { · 52 unmodified lines
97 const runtimeSessions: ProviderSession[] = [];94 const runtimeSessions: ProviderSession[] = [];
98 95
99 const unsupported = () => Effect.die(new Error("Unsupported provider call in test")) as never;96 const unsupported = () => Effect.die(new Error("Unsupported provider call in test")) as never;
100 const service: ProviderServiceShape = {97 const service: ProviderService.ProviderService["Service"] = {
101 startSession: () => unsupported(),98 startSession: () => unsupported(),
102 sendTurn: () => unsupported(),99 sendTurn: () => unsupported(),
103 interruptTurn: () => unsupported(),100 interruptTurn: () => unsupported(),
@@ -237,7 +234,7 @@ describe("ProviderRuntimeIngestion", () => { · 133 unmodified lines
237 Layer.provideMerge(orchestrationLayer),234 Layer.provideMerge(orchestrationLayer),
238 Layer.provideMerge(projectionSnapshotLayer),235 Layer.provideMerge(projectionSnapshotLayer),
239 Layer.provideMerge(SqlitePersistenceMemory),236 Layer.provideMerge(SqlitePersistenceMemory),
240 Layer.provideMerge(Layer.succeed(ProviderService, provider.service)),237 Layer.provideMerge(Layer.succeed(ProviderService.ProviderService, provider.service)),
241 Layer.provideMerge(makeTestServerSettingsLayer(options?.serverSettings)),238 Layer.provideMerge(makeTestServerSettingsLayer(options?.serverSettings)),
242 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),239 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),
243 Layer.provideMerge(NodeServices.layer),240 Layer.provideMerge(NodeServices.layer),
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+2−2
@@ -27,7 +27,7 @@ import * as Option from "effect/Option"; · 26 unmodified lines
27import * as Stream from "effect/Stream";27import * as Stream from "effect/Stream";
28import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";28import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
29 29
30import { ProviderService } from "../../provider/Services/ProviderService.ts";30import * as ProviderService from "../../provider/ProviderService.ts";
31import { ProjectionTurnRepository } from "../../persistence/Services/ProjectionTurns.ts";31import { ProjectionTurnRepository } from "../../persistence/Services/ProjectionTurns.ts";
32import { ProjectionTurnRepositoryLive } from "../../persistence/Layers/ProjectionTurns.ts";32import { ProjectionTurnRepositoryLive } from "../../persistence/Layers/ProjectionTurns.ts";
33import { isGitRepository } from "../../git/Utils.ts";33import { isGitRepository } from "../../git/Utils.ts";
@@ -631,7 +631,7 @@ const make = Effect.gen(function* () { · 597 unmodified lines
631 const crypto = yield* Crypto.Crypto;631 const crypto = yield* Crypto.Crypto;
632 const orchestrationEngine = yield* OrchestrationEngineService;632 const orchestrationEngine = yield* OrchestrationEngineService;
633 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;633 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
634 const providerService = yield* ProviderService;634 const providerService = yield* ProviderService.ProviderService;
635 const projectionTurnRepository = yield* ProjectionTurnRepository;635 const projectionTurnRepository = yield* ProjectionTurnRepository;
636 const serverSettingsService = yield* ServerSettingsService;636 const serverSettingsService = yield* ServerSettingsService;
637 const providerCommandId = (event: ProviderRuntimeEvent, tag: string) =>637 const providerCommandId = (event: ProviderRuntimeEvent, tag: string) =>
apps/server/src/orchestration/Layers/ThreadDeletionReactor.ts+2−2
@@ -5,7 +5,7 @@ import * as Effect from "effect/Effect"; · 4 unmodified lines
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
6import * as Stream from "effect/Stream";6import * as Stream from "effect/Stream";
7 7
8import { ProviderService } from "../../provider/Services/ProviderService.ts";8import * as ProviderService from "../../provider/ProviderService.ts";
9import * as TerminalManager from "../../terminal/Manager.ts";9import * as TerminalManager from "../../terminal/Manager.ts";
10import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";10import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
11import {11import {
@@ -38,7 +38,7 @@ export const logCleanupCauseUnlessInterrupted = <R, E>({ · 26 unmodified lines
38 38
39const make = Effect.gen(function* () {39const make = Effect.gen(function* () {
40 const orchestrationEngine = yield* OrchestrationEngineService;40 const orchestrationEngine = yield* OrchestrationEngineService;
41 const providerService = yield* ProviderService;41 const providerService = yield* ProviderService.ProviderService;
42 const terminalManager = yield* TerminalManager.TerminalManager;42 const terminalManager = yield* TerminalManager.TerminalManager;
43 43
44 const stopProviderSession = (threadId: ThreadDeletedEvent["payload"]["threadId"]) =>44 const stopProviderSession = (threadId: ThreadDeletedEvent["payload"]["threadId"]) =>
apps/server/src/provider/Layers/OpenCodeProvider.test.ts+25−0
@@ -16,6 +16,31 @@ const decodeOpenCodeSettings = Schema.decodeSync(OpenCodeSettings); · 15 unmodified lines
16 16
17const DEFAULT_VERSION_STDOUT = "opencode 1.14.19\n";17const DEFAULT_VERSION_STDOUT = "opencode 1.14.19\n";
18 18
19it("keeps OpenCode runtime causes separate from stable messages", () => {
20 const cause = new Error("sdk response included sensitive diagnostics");
21 const error = new OpenCodeRuntime.OpenCodeRuntimeError({
22 operation: "provider.list",
23 detail: cause.message,
24 cause,
25 });
26
27 assert.equal(error.cause, cause);
28 assert.equal(error.message, "OpenCode runtime operation provider.list failed.");
29 assert.doesNotMatch(error.message, /sensitive diagnostics/u);
30
31 const processExit = new OpenCodeRuntime.OpenCodeRuntimeError({
32 operation: "startOpenCodeServerProcess",
33 detail: "OpenCode server exited before startup completed.",
34 exitCode: 1,
35 stdout: "startup output",
36 stderr: "startup failure",
37 });
38 assert.equal(processExit.cause, undefined);
39 assert.equal(processExit.exitCode, 1);
40 assert.equal(processExit.stdout, "startup output");
41 assert.equal(processExit.stderr, "startup failure");
42});
43
19/**44/**
20 * The legacy `OpenCodeProviderLive` Layer + `OpenCodeProvider` service tag45 * The legacy `OpenCodeProviderLive` Layer + `OpenCodeProvider` service tag
21 * are deleted. The snapshot-producing logic they wrapped now lives in the46 * are deleted. The snapshot-producing logic they wrapped now lives in the
apps/server/src/provider/Layers/ProviderEventLoggers.ts+0−6
@@ -1,6 +0,0 @@
1// Compatibility shim for the intentionally excluded orchestration harness.
2import * as Canonical from "../ProviderEventLoggers.ts";
3
4export const ProviderEventLoggersLive = Canonical.layer;
5export const ProviderEventLoggers = Canonical.ProviderEventLoggers;
6export const NoOpProviderEventLoggers = Canonical.NoOpProviderEventLoggers;
apps/server/src/provider/Layers/ProviderService.ts+0−11
@@ -1,11 +0,0 @@
1// Compatibility shim for the intentionally excluded orchestration harness.
2import * as Layer from "effect/Layer";
3
4import * as ProviderService from "../ProviderService.ts";
5
6export type ProviderServiceLiveOptions = ProviderService.ProviderServiceOptions;
7
8export const ProviderServiceLive = ProviderService.layer;
9
10export const makeProviderServiceLive = (options?: ProviderService.ProviderServiceOptions) =>
11 Layer.effect(ProviderService.ProviderService, ProviderService.make(options));
apps/server/src/provider/Layers/ProviderSessionDirectory.ts+0−6
@@ -1,6 +0,0 @@
1// Compatibility shim for the intentionally excluded orchestration harness.
2import * as ProviderSessionDirectory from "../ProviderSessionDirectory.ts";
3
4export const ProviderSessionDirectoryLive = ProviderSessionDirectory.layer;
5
6export const makeProviderSessionDirectoryLive = () => ProviderSessionDirectory.layer;
apps/server/src/provider/ProviderAdapterRegistry.ts+26−9
@@ -12,11 +12,16 @@ · 11 unmodified lines
12 * - `getByInstance` / `listInstances` — new per-instance routing. Callers12 * - `getByInstance` / `listInstances` — new per-instance routing. Callers
13 * that already know an `instanceId` (threads, sessions, events)13 * that already know an `instanceId` (threads, sessions, events)
14 * should prefer these.14 * should prefer these.
15 * - `listProviders` — legacy kind-keyed routing for default instances
15 * (`defaultInstanceIdForDriver(kind) === kind`), matching the pre-Slice-D16 * (`defaultInstanceIdForDriver(kind) === kind`), matching the pre-Slice-D
16 * behaviour. New code should not grow additional callers of the kind-keyed17 * behaviour. New code should not grow additional callers of the kind-keyed
17 * methods; they exist so the settings UI, WS refresh RPC, and a handful18 * methods; they exist so the settings UI, WS refresh RPC, and a handful
18 * of legacy persisted rows can still be routed during the rollout.19 * of legacy persisted rows can still be routed during the rollout.
19 *20 *
21 * Adapter lookups are resolved dynamically through `ProviderInstanceRegistry`.
22 * Settings-driven hot reload is therefore visible immediately without
23 * rebuilding this facade.
24 *
20 * @module ProviderAdapterRegistry25 * @module ProviderAdapterRegistry
21 */26 */
22import {27import {
@@ -50,7 +55,10 @@ export class ProviderAdapterRegistry extends Context.Service< · 27 unmodified lines
50 {55 {
51 /**56 /**
52 * Resolve the adapter for a specific instance id. Returns57 * Resolve the adapter for a specific instance id. Returns
53 * `ProviderUnsupportedError` if no such live instance is registered.58 * `ProviderUnsupportedError` if no such instance is currently registered
59 * (which covers "never configured" and "configured but the driver is
60 * unavailable in this build"; both surface the same failure to callers
61 * that expect a working adapter).
54 */62 */
55 readonly getByInstance: (63 readonly getByInstance: (
56 instanceId: ProviderInstanceId,64 instanceId: ProviderInstanceId,
@@ -62,28 +70,33 @@ export class ProviderAdapterRegistry extends Context.Service< · 5 unmodified lines
62 ) => Effect.Effect<ProviderInstanceRoutingInfo, ProviderUnsupportedError>;70 ) => Effect.Effect<ProviderInstanceRoutingInfo, ProviderUnsupportedError>;
63 71
64 /**72 /**
65 * List every live instance id. Unavailable shadow instances are excluded73 * List all live instance ids. Excludes unavailable/shadow instances because
66 * because callers use these ids with `getByInstance`.74 * callers of this method want something they can pass to `getByInstance`.
67 */75 */
68 readonly listInstances: () => Effect.Effect<ReadonlyArray<ProviderInstanceId>>;76 readonly listInstances: () => Effect.Effect<ReadonlyArray<ProviderInstanceId>>;
69 77
70 /**78 /**
71 * List provider kinds whose default instance is currently registered.79 * List provider kinds whose default instance is currently registered.
72 *80 *
73 * @deprecated Prefer `listInstances`; this remains for migration-era81 * @deprecated Prefer `listInstances`. Retained for migration-era call sites
74 * callers that still address providers by driver kind.82 * that iterate providers to build UI or metrics.
75 */83 */
76 readonly listProviders: () => Effect.Effect<ReadonlyArray<ProviderDriverKind>>;84 readonly listProviders: () => Effect.Effect<ReadonlyArray<ProviderDriverKind>>;
77 85
78 /**86 /**
79 * Emits whenever the live instance set changes. Consumers should re-read87 * Change notification stream mirroring
80 * `listInstances` and reconcile their per-instance subscriptions.88 * `ProviderInstanceRegistry.streamChanges`. Emits one `void` tick whenever
89 * the set of live instances changes. Consumers that fan out
90 * `adapter.streamEvents` per instance re-pull `listInstances` on each tick
91 * and fork subscriptions for instances they have not seen yet.
81 */92 */
82 readonly streamChanges: Stream.Stream<void>;93 readonly streamChanges: Stream.Stream<void>;
83 94
84 /**95 /**
85 * Acquire the change subscription synchronously in the caller's scope,96 * Acquire a change subscription synchronously in the caller's current
86 * avoiding the publish race inherent in forking `Stream.fromPubSub`.97 * fiber. Consumers that must avoid missing a publish between initial
98 * reconciliation and watcher startup should use this, then fork
99 * `Stream.fromSubscription`.
87 */100 */
88 readonly subscribeChanges: Effect.Effect<PubSub.Subscription<void>, never, Scope.Scope>;101 readonly subscribeChanges: Effect.Effect<PubSub.Subscription<void>, never, Scope.Scope>;
89 }102 }
@@ -130,6 +143,8 @@ export const make = Effect.gen(function* () { · 40 unmodified lines
130 const kinds = new Set<ProviderDriverKind>();143 const kinds = new Set<ProviderDriverKind>();
131 for (const instance of instances) {144 for (const instance of instances) {
132 if (instance.instanceId === defaultInstanceIdForDriver(instance.driverKind)) {145 if (instance.instanceId === defaultInstanceIdForDriver(instance.driverKind)) {
146 // Only default instances appear through this legacy view; custom
147 // instances such as `codex_personal` have no driver-kind identity.
133 kinds.add(instance.driverKind);148 kinds.add(instance.driverKind);
134 }149 }
135 }150 }
@@ -142,6 +157,8 @@ export const make = Effect.gen(function* () { · 6 unmodified lines
142 getInstanceInfo,157 getInstanceInfo,
143 listInstances,158 listInstances,
144 listProviders,159 listProviders,
160 // The facade owns no state; the instance registry already coalesces
161 // add, remove, and rebuild notifications into a single change channel.
145 streamChanges: registry.streamChanges,162 streamChanges: registry.streamChanges,
146 subscribeChanges: registry.subscribeChanges,163 subscribeChanges: registry.subscribeChanges,
147 });164 });
apps/server/src/provider/ProviderInstanceRegistry.ts+29−16
@@ -58,38 +58,46 @@ export class ProviderInstanceRegistry extends Context.Service< · 57 unmodified lines
58 ProviderInstanceRegistry,58 ProviderInstanceRegistry,
59 {59 {
60 /**60 /**
61 * Look up one instance by id. Unknown ids return `undefined`; callers map61 * Look up one instance by id. Returns `undefined` (not `Option`) when the
62 * that absence to the appropriate domain error.62 * id is unknown; callers branch on absence and emit the appropriate domain
63 * error.
63 */64 */
64 readonly getInstance: (65 readonly getInstance: (
65 instanceId: ProviderInstanceId,66 instanceId: ProviderInstanceId,
66 ) => Effect.Effect<ProviderInstance | undefined>;67 ) => Effect.Effect<ProviderInstance | undefined>;
67 68
68 /**69 /**
69 * Every successfully materialized instance, in stable settings-author70 * Every available (driver-registered, successfully created) instance, in
70 * order.71 * stable settings-author order.
71 */72 */
72 readonly listInstances: Effect.Effect<ReadonlyArray<ProviderInstance>>;73 readonly listInstances: Effect.Effect<ReadonlyArray<ProviderInstance>>;
73 74
74 /**75 /**
75 * Shadow snapshots for unknown drivers or invalid configurations, ready76 * Wire-shape shadow snapshots for instances whose driver is unknown to this
76 * to merge into `ProviderRegistry` output.77 * build or whose config failed to decode. Suitable for merging directly
78 * into `ProviderRegistry` output.
77 */79 */
78 readonly listUnavailable: Effect.Effect<ReadonlyArray<ServerProvider>>;80 readonly listUnavailable: Effect.Effect<ReadonlyArray<ServerProvider>>;
79 81
80 /**82 /**
81 * Emits after the registry adds, removes, or rebuilds instances. The83 * Push notification stream emitted whenever the registry's contents change.
82 * payload is `void` because consumers re-read both registry lists.84 * The payload is `void` because consumers always re-pull `listInstances`
85 * and `listUnavailable` together.
83 *86 *
84 * `Stream.fromPubSub` subscribes only when execution begins, so a newly87 * `Stream.fromPubSub` defers `PubSub.subscribe` until the stream starts
85 * forked consumer can miss a publish. Consumers that cannot tolerate that88 * running, so forking a consumer races the next publish. Hot-reload
86 * gap should acquire `subscribeChanges` first.89 * consumers that cannot miss a publish should use `subscribeChanges`
90 * instead, which acquires the subscription synchronously before the
91 * consumer loop is forked.
87 */92 */
88 readonly streamChanges: Stream.Stream<void>;93 readonly streamChanges: Stream.Stream<void>;
89 94
90 /**95 /**
91 * Acquire a scoped PubSub subscription synchronously before forking the96 * Acquire a scoped subscription to the change channel synchronously in the
92 * consumer loop, ensuring subsequent publishes cannot land in a gap.97 * caller's fiber. Consumers typically `yield*` this in the same fiber that
98 * forks their consumer loop, then drain it with `PubSub.take` or
99 * `Stream.fromSubscription`. Because the subscription is registered before
100 * this effect returns, no subsequent publish can land in a gap.
93 */101 */
94 readonly subscribeChanges: Effect.Effect<PubSub.Subscription<void>, never, Scope.Scope>;102 readonly subscribeChanges: Effect.Effect<PubSub.Subscription<void>, never, Scope.Scope>;
95 }103 }
@@ -107,9 +115,14 @@ export class ProviderInstanceRegistryMutator extends Context.Service< · 11 unmodified lines
107 ProviderInstanceRegistryMutator,115 ProviderInstanceRegistryMutator,
108 {116 {
109 /**117 /**
110 * Reconcile the live registry with a new configuration map. Individual118 * Bring the live registry in line with the supplied config map. The
111 * driver creation failures become unavailable shadow snapshots so the119 * operation closes removed or replaced scopes before creating replacements
112 * settings watcher itself never fails.120 * and publishes a single change tick after the batch. Reapplying an
121 * unchanged map is a no-op.
122 *
123 * The effect never fails: individual driver creation failures become
124 * unavailable shadow snapshots, keeping settings-watcher loops alive when
125 * one entry is invalid.
113 */126 */
114 readonly reconcile: (configMap: ProviderInstanceConfigMap) => Effect.Effect<void>;127 readonly reconcile: (configMap: ProviderInstanceConfigMap) => Effect.Effect<void>;
115 }128 }
apps/server/src/provider/ProviderRegistry.ts+20−22
@@ -64,13 +64,18 @@ export class ProviderRegistry extends Context.Service< · 63 unmodified lines
64 ProviderRegistry,64 ProviderRegistry,
65 {65 {
66 /**66 /**
67 * Read the latest snapshot for every configured instance. Multiple67 * Read the latest provider snapshots for every configured instance.
68 * snapshots can share a driver kind and are distinguished by instance id.68 * Multiple snapshots may share the same driver kind and disambiguate via
69 * `instanceId`.
69 */70 */
70 readonly getProviders: Effect.Effect<ReadonlyArray<ServerProvider>>;71 readonly getProviders: Effect.Effect<ReadonlyArray<ServerProvider>>;
71 72
72 /**73 /**
73 * Refresh every provider, or the default instance for one driver kind.74 * Refresh all providers, or the default instance of the specified kind
75 * when supplied.
76 *
77 * Retained for compatibility with legacy call sites such as the WS refresh
78 * RPC and orchestration metrics.
74 *79 *
75 * @deprecated Prefer `refreshInstance` for new call sites.80 * @deprecated Prefer `refreshInstance` for new call sites.
76 */81 */
@@ -80,15 +85,16 @@ export class ProviderRegistry extends Context.Service< · 3 unmodified lines
80 85
81 /**86 /**
82 * Refresh one configured instance. Unknown ids resolve to the current87 * Refresh one configured instance. Unknown ids resolve to the current
83 * cached list to preserve the legacy transport behavior.88 * cached list, matching the legacy `refresh` behavior so transport layers
89 * do not have to special-case unknown ids.
84 */90 */
85 readonly refreshInstance: (91 readonly refreshInstance: (
86 instanceId: ProviderInstanceId,92 instanceId: ProviderInstanceId,
87 ) => Effect.Effect<ReadonlyArray<ServerProvider>>;93 ) => Effect.Effect<ReadonlyArray<ServerProvider>>;
88 94
89 /**95 /**
90 * Resolve maintenance capabilities for a live instance, falling back to96 * Resolve maintenance capabilities for one live provider instance,
91 * manual-only capabilities when that instance is unavailable.97 * falling back to manual-only capabilities when it is unavailable.
92 */98 */
93 readonly getProviderMaintenanceCapabilitiesForInstance: (99 readonly getProviderMaintenanceCapabilitiesForInstance: (
94 instanceId: ProviderInstanceId,100 instanceId: ProviderInstanceId,
@@ -96,8 +102,10 @@ export class ProviderRegistry extends Context.Service< · 1 unmodified line
96 ) => Effect.Effect<ProviderMaintenanceCapabilities>;102 ) => Effect.Effect<ProviderMaintenanceCapabilities>;
97 103
98 /**104 /**
99 * Apply volatile maintenance state for one instance. This state is not105 * Apply volatile maintenance-action state to one configured instance. This
100 * persisted; update actions are projected onto `ServerProvider.updateState`.106 * state is never persisted. Today only update actions are projected onto
107 * `ServerProvider.updateState`; install/auth actions can extend this map
108 * without adding driver-scoped APIs.
101 */109 */
102 readonly setProviderMaintenanceActionState: (input: {110 readonly setProviderMaintenanceActionState: (input: {
103 readonly instanceId: ProviderInstanceId;111 readonly instanceId: ProviderInstanceId;
@@ -105,7 +113,10 @@ export class ProviderRegistry extends Context.Service< · 1 unmodified line
105 readonly state: ServerProviderUpdateState | null;113 readonly state: ServerProviderUpdateState | null;
106 }) => Effect.Effect<ReadonlyArray<ServerProvider>>;114 }) => Effect.Effect<ReadonlyArray<ServerProvider>>;
107 115
108 /** Emits the full materialized provider list after each aggregated change. */116 /**
117 * Stream of provider snapshot updates, one emission per aggregated change.
118 * The array contains the full current state.
119 */
109 readonly streamChanges: Stream.Stream<ReadonlyArray<ServerProvider>>;120 readonly streamChanges: Stream.Stream<ReadonlyArray<ServerProvider>>;
110 }121 }
111>()("t3/provider/ProviderRegistry") {}122>()("t3/provider/ProviderRegistry") {}
@@ -671,19 +682,6 @@ export const make = Effect.gen(function* () { · 559 unmodified lines
671 // and pending fallbacks fill the gaps.682 // and pending fallbacks fill the gaps.
672 yield* upsertProviders(fallbackProviders, { publish: false });683 yield* upsertProviders(fallbackProviders, { publish: false });
673 // Subscribe to registry mutations BEFORE running the initial sync.684 // Subscribe to registry mutations BEFORE running the initial sync.
674 // `subscribeChanges` acquires the dequeue synchronously in this
675 // fibre; the subscription is active the instant this `yield*`
676 // returns. Forking the consumer loop later cannot lose a publish
677 // because no publish can reach a not-yet-subscribed dequeue.
678 //
679 // (Contrast with the pre-fix code that did
680 // `Stream.runForEach(instanceRegistry.streamChanges, …).pipe(Effect.forkScoped)`.
681 // `Stream.fromPubSub` defers `PubSub.subscribe` to stream start,
682 // and `forkScoped` only schedules the fibre — so a reconcile that
683 // published between "fibre scheduled" and "fibre starts running"
684 // was dropped, which made any settings change that replaced an
685 // instance never propagate to the aggregator's `providersRef`.)
686 // Subscribe to registry mutations BEFORE running the initial sync.
687 // `subscribeChanges` acquires the `PubSub.Subscription` synchronously685 // `subscribeChanges` acquires the `PubSub.Subscription` synchronously
688 // in this fibre; the subscription is registered with the PubSub the686 // in this fibre; the subscription is registered with the PubSub the
689 // instant this `yield*` returns, so any subsequent publish is687 // instant this `yield*` returns, so any subsequent publish is
apps/server/src/provider/ProviderService.test.ts+18−15
@@ -55,10 +55,13 @@ import { · 54 unmodified lines
55} from "../persistence/Layers/Sqlite.ts";55} from "../persistence/Layers/Sqlite.ts";
56import * as ServerSettings from "../serverSettings.ts";56import * as ServerSettings from "../serverSettings.ts";
57import * as AnalyticsService from "../telemetry/AnalyticsService.ts";57import * as AnalyticsService from "../telemetry/AnalyticsService.ts";
58import { makeAdapterRegistryMock } from "./testUtils/providerAdapterRegistryMock.ts";
58 59
59const makeProviderServiceLive = (options?: ProviderService.ProviderServiceOptions) =>60// Several tests build two ProviderService instances in one scope to simulate a
61// process restart. Construct a fresh Layer for each build so Layer memoization
62// cannot reuse the first service instance.
63const makeFreshProviderServiceLayer = (options?: ProviderService.ProviderServiceOptions) =>
60 Layer.effect(ProviderService.ProviderService, ProviderService.make(options));64 Layer.effect(ProviderService.ProviderService, ProviderService.make(options));
61import { makeAdapterRegistryMock } from "./testUtils/providerAdapterRegistryMock.ts";
62 65
63const defaultServerSettingsLayer = ServerSettings.ServerSettingsService.layerTest();66const defaultServerSettingsLayer = ServerSettings.ServerSettingsService.layerTest();
64 67
@@ -289,7 +292,7 @@ function makeProviderServiceLayer() { · 224 unmodified lines
289 292
290 const layer = it.layer(293 const layer = it.layer(
291 Layer.mergeAll(294 Layer.mergeAll(
292 makeProviderServiceLive().pipe(295 makeFreshProviderServiceLayer().pipe(
293 Layer.provide(providerAdapterLayer),296 Layer.provide(providerAdapterLayer),
294 Layer.provide(directoryLayer),297 Layer.provide(directoryLayer),
295 Layer.provide(defaultServerSettingsLayer),298 Layer.provide(defaultServerSettingsLayer),
@@ -342,7 +345,7 @@ it.effect("ProviderServiceLive catches stopAll failures during shutdown", () => · 46 unmodified lines
342 Layer.provide(runtimeRepositoryLayer),345 Layer.provide(runtimeRepositoryLayer),
343 );346 );
344 const providerLayer = Layer.mergeAll(347 const providerLayer = Layer.mergeAll(
345 makeProviderServiceLive().pipe(348 makeFreshProviderServiceLayer().pipe(
346 Layer.provide(providerAdapterLayer),349 Layer.provide(providerAdapterLayer),
347 Layer.provide(directoryLayer),350 Layer.provide(directoryLayer),
348 Layer.provide(defaultServerSettingsLayer),351 Layer.provide(defaultServerSettingsLayer),
@@ -403,7 +406,7 @@ it.effect("ProviderServiceLive rejects new sessions for disabled providers", () · 54 unmodified lines
403 const directoryLayer = ProviderSessionDirectory.layer.pipe(406 const directoryLayer = ProviderSessionDirectory.layer.pipe(
404 Layer.provide(runtimeRepositoryLayer),407 Layer.provide(runtimeRepositoryLayer),
405 );408 );
406 const providerLayer = makeProviderServiceLive().pipe(409 const providerLayer = makeFreshProviderServiceLayer().pipe(
407 Layer.provide(providerAdapterLayer),410 Layer.provide(providerAdapterLayer),
408 Layer.provide(directoryLayer),411 Layer.provide(directoryLayer),
409 Layer.provide(defaultServerSettingsLayer),412 Layer.provide(defaultServerSettingsLayer),
@@ -487,7 +490,7 @@ it.effect( · 77 unmodified lines
487 const directoryLayer = ProviderSessionDirectory.layer.pipe(490 const directoryLayer = ProviderSessionDirectory.layer.pipe(
488 Layer.provide(runtimeRepositoryLayer),491 Layer.provide(runtimeRepositoryLayer),
489 );492 );
490 const providerLayer = makeProviderServiceLive().pipe(493 const providerLayer = makeFreshProviderServiceLayer().pipe(
491 Layer.provide(providerAdapterLayer),494 Layer.provide(providerAdapterLayer),
492 Layer.provide(directoryLayer),495 Layer.provide(directoryLayer),
493 Layer.provide(serverSettingsLayer),496 Layer.provide(serverSettingsLayer),
@@ -559,7 +562,7 @@ it.effect("ProviderServiceLive rejects new sessions for disabled custom instance · 65 unmodified lines
559 const directoryLayer = ProviderSessionDirectory.layer.pipe(562 const directoryLayer = ProviderSessionDirectory.layer.pipe(
560 Layer.provide(runtimeRepositoryLayer),563 Layer.provide(runtimeRepositoryLayer),
561 );564 );
562 const providerLayer = makeProviderServiceLive().pipe(565 const providerLayer = makeFreshProviderServiceLayer().pipe(
563 Layer.provide(providerAdapterLayer),566 Layer.provide(providerAdapterLayer),
564 Layer.provide(directoryLayer),567 Layer.provide(directoryLayer),
565 Layer.provide(defaultServerSettingsLayer),568 Layer.provide(defaultServerSettingsLayer),
@@ -606,7 +609,7 @@ it.effect("ProviderServiceLive writes canonical events to the emitting thread se · 40 unmodified lines
606 const directoryLayer = ProviderSessionDirectory.layer.pipe(609 const directoryLayer = ProviderSessionDirectory.layer.pipe(
607 Layer.provide(runtimeRepositoryLayer),610 Layer.provide(runtimeRepositoryLayer),
608 );611 );
609 const providerLayer = makeProviderServiceLive({612 const providerLayer = makeFreshProviderServiceLayer({
610 canonicalEventLogger: {613 canonicalEventLogger: {
611 filePath: "memory://provider-canonical-events",614 filePath: "memory://provider-canonical-events",
612 write: (event, threadId) => {615 write: (event, threadId) => {
@@ -678,7 +681,7 @@ it.effect("ProviderServiceLive keeps persisted resumable sessions on startup", ( · 65 unmodified lines
678 });681 });
679 }).pipe(Effect.provide(directoryLayer));682 }).pipe(Effect.provide(directoryLayer));
680 683
681 const providerLayer = makeProviderServiceLive().pipe(684 const providerLayer = makeFreshProviderServiceLayer().pipe(
682 Layer.provide(Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, registry)),685 Layer.provide(Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, registry)),
683 Layer.provide(directoryLayer),686 Layer.provide(directoryLayer),
684 Layer.provide(defaultServerSettingsLayer),687 Layer.provide(defaultServerSettingsLayer),
@@ -742,7 +745,7 @@ it.effect( · 57 unmodified lines
742 const firstDirectoryLayer = ProviderSessionDirectory.layer.pipe(745 const firstDirectoryLayer = ProviderSessionDirectory.layer.pipe(
743 Layer.provide(runtimeRepositoryLayer),746 Layer.provide(runtimeRepositoryLayer),
744 );747 );
745 const firstProviderLayer = makeProviderServiceLive().pipe(748 const firstProviderLayer = makeFreshProviderServiceLayer().pipe(
746 Layer.provide(749 Layer.provide(
747 Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, firstRegistry),750 Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, firstRegistry),
748 ),751 ),
@@ -801,7 +804,7 @@ it.effect( · 52 unmodified lines
801 const secondDirectoryLayer = ProviderSessionDirectory.layer.pipe(804 const secondDirectoryLayer = ProviderSessionDirectory.layer.pipe(
802 Layer.provide(runtimeRepositoryLayer),805 Layer.provide(runtimeRepositoryLayer),
803 );806 );
804 const secondProviderLayer = makeProviderServiceLive().pipe(807 const secondProviderLayer = makeFreshProviderServiceLayer().pipe(
805 Layer.provide(808 Layer.provide(
806 Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, secondRegistry),809 Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, secondRegistry),
807 ),810 ),
@@ -1312,7 +1315,7 @@ routing.layer("ProviderServiceLive routing", (it) => { · 504 unmodified lines
1312 const firstDirectoryLayer = ProviderSessionDirectory.layer.pipe(1315 const firstDirectoryLayer = ProviderSessionDirectory.layer.pipe(
1313 Layer.provide(runtimeRepositoryLayer),1316 Layer.provide(runtimeRepositoryLayer),
1314 );1317 );
1315 const firstProviderLayer = makeProviderServiceLive().pipe(1318 const firstProviderLayer = makeFreshProviderServiceLayer().pipe(
1316 Layer.provide(1319 Layer.provide(
1317 Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, firstRegistry),1320 Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, firstRegistry),
1318 ),1321 ),
@@ -1350,7 +1353,7 @@ routing.layer("ProviderServiceLive routing", (it) => { · 31 unmodified lines
1350 const secondDirectoryLayer = ProviderSessionDirectory.layer.pipe(1353 const secondDirectoryLayer = ProviderSessionDirectory.layer.pipe(
1351 Layer.provide(runtimeRepositoryLayer),1354 Layer.provide(runtimeRepositoryLayer),
1352 );1355 );
1353 const secondProviderLayer = makeProviderServiceLive().pipe(1356 const secondProviderLayer = makeFreshProviderServiceLayer().pipe(
1354 Layer.provide(1357 Layer.provide(
1355 Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, secondRegistry),1358 Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, secondRegistry),
1356 ),1359 ),
@@ -1418,7 +1421,7 @@ routing.layer("ProviderServiceLive routing", (it) => { · 61 unmodified lines
1418 const firstDirectoryLayer = ProviderSessionDirectory.layer.pipe(1421 const firstDirectoryLayer = ProviderSessionDirectory.layer.pipe(
1419 Layer.provide(runtimeRepositoryLayer),1422 Layer.provide(runtimeRepositoryLayer),
1420 );1423 );
1421 const firstProviderLayer = makeProviderServiceLive().pipe(1424 const firstProviderLayer = makeFreshProviderServiceLayer().pipe(
1422 Layer.provide(1425 Layer.provide(
1423 Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, firstRegistry),1426 Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, firstRegistry),
1424 ),1427 ),
@@ -1451,7 +1454,7 @@ routing.layer("ProviderServiceLive routing", (it) => { · 26 unmodified lines
1451 const secondDirectoryLayer = ProviderSessionDirectory.layer.pipe(1454 const secondDirectoryLayer = ProviderSessionDirectory.layer.pipe(
1452 Layer.provide(runtimeRepositoryLayer),1455 Layer.provide(runtimeRepositoryLayer),
1453 );1456 );
1454 const secondProviderLayer = makeProviderServiceLive().pipe(1457 const secondProviderLayer = makeFreshProviderServiceLayer().pipe(
1455 Layer.provide(1458 Layer.provide(
1456 Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, secondRegistry),1459 Layer.succeed(ProviderAdapterRegistry.ProviderAdapterRegistry, secondRegistry),
1457 ),1460 ),
apps/server/src/provider/ProviderService.ts+28−15
@@ -6,6 +6,8 @@ · 5 unmodified lines
6 * unified provider event stream for subscribers.6 * unified provider event stream for subscribers.
7 *7 *
8 * It does not implement provider protocol details (adapter concern).8 * It does not implement provider protocol details (adapter concern).
9 * Uses Effect `Context.Service` for dependency injection and returns typed
10 * domain errors for validation, session, codex, and checkpoint workflows.
9 *11 *
10 * @module ProviderService12 * @module ProviderService
11 */13 */
@@ -61,14 +63,22 @@ import * as ProviderSessionDirectory from "./ProviderSessionDirectory.ts"; · 49 unmodified lines
61import { type EventNdjsonLogger } from "./Layers/EventNdjsonLogger.ts";63import { type EventNdjsonLogger } from "./Layers/EventNdjsonLogger.ts";
62import * as ProviderEventLoggers from "./ProviderEventLoggers.ts";64import * as ProviderEventLoggers from "./ProviderEventLoggers.ts";
63import * as AnalyticsService from "../telemetry/AnalyticsService.ts";65import * as AnalyticsService from "../telemetry/AnalyticsService.ts";
64import * as McpProviderSession from "../mcp/McpProviderSession.ts";66import {
65import * as McpSessionRegistry from "../mcp/McpSessionRegistry.ts";67 clearAllMcpProviderSessions,
68 clearMcpProviderSession,
69 setMcpProviderSession,
70} from "../mcp/McpProviderSession.ts";
71import {
72 issueActiveMcpCredential,
73 revokeActiveMcpThread,
74 revokeAllActiveMcpCredentials,
75} from "../mcp/McpSessionRegistry.ts";
66const isModelSelection = Schema.is(ModelSelection);76const isModelSelection = Schema.is(ModelSelection);
67 77
68export class ProviderService extends Context.Service<78export class ProviderService extends Context.Service<
69 ProviderService,79 ProviderService,
70 {80 {
71 /** Start a provider session for a thread. */81 /** Start a provider session. */
72 readonly startSession: (82 readonly startSession: (
73 threadId: ThreadId,83 threadId: ThreadId,
74 input: ProviderSessionStartInput,84 input: ProviderSessionStartInput,
@@ -99,10 +109,14 @@ export class ProviderService extends Context.Service< · 24 unmodified lines
99 input: ProviderStopSessionInput,109 input: ProviderStopSessionInput,
100 ) => Effect.Effect<void, ProviderServiceError>;110 ) => Effect.Effect<void, ProviderServiceError>;
101 111
102 /** Aggregate the active sessions reported by all registered adapters. */112 /**
113 * List active provider sessions.
114 *
115 * Aggregates runtime session lists from all registered adapters.
116 */
103 readonly listSessions: () => Effect.Effect<ReadonlyArray<ProviderSession>>;117 readonly listSessions: () => Effect.Effect<ReadonlyArray<ProviderSession>>;
104 118
105 /** Read capabilities for the adapter bound to an instance. */119 /** Read capabilities for the adapter bound to a configured provider instance. */
106 readonly getCapabilities: (120 readonly getCapabilities: (
107 instanceId: ProviderInstanceId,121 instanceId: ProviderInstanceId,
108 ) => Effect.Effect<ProviderAdapterCapabilities, ProviderServiceError>;122 ) => Effect.Effect<ProviderAdapterCapabilities, ProviderServiceError>;
@@ -119,8 +133,9 @@ export class ProviderService extends Context.Service< · 10 unmodified lines
119 }) => Effect.Effect<void, ProviderServiceError>;133 }) => Effect.Effect<void, ProviderServiceError>;
120 134
121 /**135 /**
122 * Canonical provider runtime event stream. ProviderService owns the136 * Canonical provider runtime event stream.
123 * per-instance fan-out rather than delegating it to a separate event bus.137 *
138 * Fan-out is owned by ProviderService (not by a standalone event-bus service).
124 */139 */
125 readonly streamEvents: Stream.Stream<ProviderRuntimeEvent>;140 readonly streamEvents: Stream.Stream<ProviderRuntimeEvent>;
126 }141 }
@@ -279,16 +294,14 @@ export const make = Effect.fn("ProviderService.make")(function* (options?: Provi · 152 unmodified lines
279 const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>();294 const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>();
280 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);295 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
281 const prepareMcpSession = (threadId: ThreadId, providerInstanceId: ProviderInstanceId) =>296 const prepareMcpSession = (threadId: ThreadId, providerInstanceId: ProviderInstanceId) =>
282 McpSessionRegistry.issueActiveMcpCredential({ threadId, providerInstanceId }).pipe(297 issueActiveMcpCredential({ threadId, providerInstanceId }).pipe(
283 Effect.tap((credential) =>298 Effect.tap((credential) =>
284 credential299 credential ? Effect.sync(() => setMcpProviderSession(credential.config)) : Effect.void,
285 ? Effect.sync(() => McpProviderSession.setMcpProviderSession(credential.config))
286 : Effect.void,
287 ),300 ),
288 );301 );
289 const clearMcpSession = (threadId: ThreadId) =>302 const clearMcpSession = (threadId: ThreadId) =>
290 McpSessionRegistry.revokeActiveMcpThread(threadId).pipe(303 revokeActiveMcpThread(threadId).pipe(
291 Effect.tap(() => Effect.sync(() => McpProviderSession.clearMcpProviderSession(threadId))),304 Effect.tap(() => Effect.sync(() => clearMcpProviderSession(threadId))),
292 );305 );
293 306
294 const publishRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> =>307 const publishRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> =>
@@ -1096,8 +1109,8 @@ export const make = Effect.fn("ProviderService.make")(function* (options?: Provi · 801 unmodified lines
1096 ),1109 ),
1097 ).pipe(Effect.asVoid);1110 ).pipe(Effect.asVoid);
1098 yield* Effect.forEach(currentAdapters, ([, adapter]) => adapter.stopAll()).pipe(Effect.asVoid);1111 yield* Effect.forEach(currentAdapters, ([, adapter]) => adapter.stopAll()).pipe(Effect.asVoid);
1099 yield* McpSessionRegistry.revokeAllActiveMcpCredentials();1112 yield* revokeAllActiveMcpCredentials();
1100 McpProviderSession.clearAllMcpProviderSessions();1113 clearAllMcpProviderSessions();
1101 const bindings = yield* directory.listBindings().pipe(Effect.orElseSucceed(() => []));1114 const bindings = yield* directory.listBindings().pipe(Effect.orElseSucceed(() => []));
1102 yield* Effect.forEach(bindings, (binding) =>1115 yield* Effect.forEach(bindings, (binding) =>
1103 Effect.gen(function* () {1116 Effect.gen(function* () {
apps/server/src/provider/ProviderSessionDirectory.test.ts+1−3
@@ -20,9 +20,7 @@ import * as ProviderSessionRuntime from "../persistence/ProviderSessionRuntime.t · 19 unmodified lines
20import * as ProviderSessionDirectory from "./ProviderSessionDirectory.ts";20import * as ProviderSessionDirectory from "./ProviderSessionDirectory.ts";
21 21
22function makeDirectoryLayer<E, R>(persistenceLayer: Layer.Layer<SqlClient.SqlClient, E, R>) {22function makeDirectoryLayer<E, R>(persistenceLayer: Layer.Layer<SqlClient.SqlClient, E, R>) {
23 const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe(23 const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe(Layer.provide(persistenceLayer));
24 Layer.provide(persistenceLayer),
25 );
26 return Layer.mergeAll(24 return Layer.mergeAll(
27 runtimeRepositoryLayer,25 runtimeRepositoryLayer,
28 ProviderSessionDirectory.layer.pipe(Layer.provide(runtimeRepositoryLayer)),26 ProviderSessionDirectory.layer.pipe(Layer.provide(runtimeRepositoryLayer)),
apps/server/src/provider/ProviderSessionDirectory.ts+4−1
@@ -14,7 +14,7 @@ import * as Option from "effect/Option"; · 13 unmodified lines
14import * as Schema from "effect/Schema";14import * as Schema from "effect/Schema";
15 15
16import { ProviderSessionDirectoryPersistenceError, ProviderValidationError } from "./Errors.ts";16import { ProviderSessionDirectoryPersistenceError, ProviderValidationError } from "./Errors.ts";
17import * as ProviderSessionRuntime from "../persistence/Services/ProviderSessionRuntime.ts";17import * as ProviderSessionRuntime from "../persistence/ProviderSessionRuntime.ts";
18 18
19export interface ProviderRuntimeBinding {19export interface ProviderRuntimeBinding {
20 readonly threadId: ThreadId;20 readonly threadId: ThreadId;
@@ -119,6 +119,9 @@ function toRuntimeBinding( · 98 unmodified lines
119 ({119 ({
120 threadId: runtime.threadId,120 threadId: runtime.threadId,
121 provider,121 provider,
122 // Migration boundary: rows written before provider instances had a
123 // nullable id. Promote them here so runtime routing never has to
124 // infer an instance from its driver kind.
122 providerInstanceId: runtime.providerInstanceId ?? defaultInstanceIdForDriver(provider),125 providerInstanceId: runtime.providerInstanceId ?? defaultInstanceIdForDriver(provider),
123 adapterKey: runtime.adapterKey,126 adapterKey: runtime.adapterKey,
124 runtimeMode: runtime.runtimeMode,127 runtimeMode: runtime.runtimeMode,
apps/server/src/provider/ProviderSessionReaper.ts+1−0
@@ -14,6 +14,7 @@ import * as ProviderSessionDirectory from "./ProviderSessionDirectory.ts"; · 13 unmodified lines
14export class ProviderSessionReaper extends Context.Service<14export class ProviderSessionReaper extends Context.Service<
15 ProviderSessionReaper,15 ProviderSessionReaper,
16 {16 {
17 /** Start the background provider session reaper within the provided scope. */
17 readonly start: () => Effect.Effect<void, never, Scope.Scope>;18 readonly start: () => Effect.Effect<void, never, Scope.Scope>;
18 }19 }
19>()("t3/provider/ProviderSessionReaper") {}20>()("t3/provider/ProviderSessionReaper") {}
apps/server/src/provider/Services/ProviderAdapterRegistry.ts+0−3
@@ -1,3 +0,0 @@
1// Compatibility shim for the intentionally excluded orchestration harness.
2export { ProviderAdapterRegistry } from "../ProviderAdapterRegistry.ts";
3export type { ProviderInstanceRoutingInfo } from "../ProviderAdapterRegistry.ts";
apps/server/src/provider/Services/ProviderRegistry.ts+0−2
@@ -1,2 +0,0 @@
1// Compatibility shim for the intentionally excluded orchestration modules.
2export * from "../ProviderRegistry.ts";
apps/server/src/provider/Services/ProviderService.ts+0−5
@@ -1,5 +0,0 @@
1// Compatibility shim for the intentionally excluded orchestration modules and harness.
2export * from "../ProviderService.ts";
3
4/** @deprecated Use `ProviderService["Service"]` from the canonical module. */
5export type ProviderServiceShape = import("../ProviderService.ts").ProviderService["Service"];
apps/server/src/provider/opencodeRuntime.ts+9−4
@@ -26,7 +26,7 @@ import * as Schema from "effect/Schema"; · 25 unmodified lines
26import * as Stream from "effect/Stream";26import * as Stream from "effect/Stream";
27import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";27import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
28 28
29import * as ProcessRunner from "../processRunner.ts";29import { isWindowsCommandNotFound } from "../processRunner.ts";
30import { collectStreamAsString } from "./providerSnapshot.ts";30import { collectStreamAsString } from "./providerSnapshot.ts";
31import * as NetService from "@t3tools/shared/Net";31import * as NetService from "@t3tools/shared/Net";
32import { HostProcessPlatform } from "@t3tools/shared/hostProcess";32import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
@@ -54,12 +54,15 @@ export class OpenCodeRuntimeError extends Schema.TaggedErrorClass<OpenCodeRuntim · 21 unmodified lines
54 operation: Schema.String,54 operation: Schema.String,
55 detail: Schema.String,55 detail: Schema.String,
56 cause: Schema.optional(Schema.Defect()),56 cause: Schema.optional(Schema.Defect()),
57 exitCode: Schema.optionalKey(Schema.Number),
58 stdout: Schema.optionalKey(Schema.String),
59 stderr: Schema.optionalKey(Schema.String),
57 },60 },
58) {61) {
59 static readonly is = Schema.is(OpenCodeRuntimeError);62 static readonly is = Schema.is(OpenCodeRuntimeError);
60 63
61 override get message(): string {64 override get message(): string {
62 return `${this.operation}: ${this.detail}`;65 return `OpenCode runtime operation ${this.operation} failed.`;
63 }66 }
64}67}
65 68
@@ -302,7 +305,7 @@ export const make = Effect.gen(function* () { · 236 unmodified lines
302 { concurrency: "unbounded" },305 { concurrency: "unbounded" },
303 );306 );
304 const exitCode = Number(code);307 const exitCode = Number(code);
305 if (yield* ProcessRunner.isWindowsCommandNotFound(exitCode, stderr)) {308 if (yield* isWindowsCommandNotFound(exitCode, stderr)) {
306 return yield* new OpenCodeRuntimeError({309 return yield* new OpenCodeRuntimeError({
307 operation: "runOpenCodeCommand",310 operation: "runOpenCodeCommand",
308 detail: `spawn ${input.binaryPath} ENOENT`,311 detail: `spawn ${input.binaryPath} ENOENT`,
@@ -437,7 +440,9 @@ export const make = Effect.gen(function* () { · 128 unmodified lines
437 ]440 ]
438 .filter(Boolean)441 .filter(Boolean)
439 .join("\n\n"),442 .join("\n\n"),
440 cause: { exitCode, stdout, stderr },443 exitCode,
444 stdout,
445 stderr,
441 }),446 }),
442 ).pipe(Effect.ignore);447 ).pipe(Effect.ignore);
443 }),448 }),
apps/server/src/provider/providerMaintenanceRunner.test.ts+31−0
@@ -11,6 +11,7 @@ import * as Effect from "effect/Effect"; · 10 unmodified lines
11import * as Exit from "effect/Exit";11import * as Exit from "effect/Exit";
12import * as Fiber from "effect/Fiber";12import * as Fiber from "effect/Fiber";
13import * as Layer from "effect/Layer";13import * as Layer from "effect/Layer";
14import * as PlatformError from "effect/PlatformError";
14import * as Ref from "effect/Ref";15import * as Ref from "effect/Ref";
15import * as Schema from "effect/Schema";16import * as Schema from "effect/Schema";
16import * as Sink from "effect/Sink";17import * as Sink from "effect/Sink";
@@ -140,6 +141,13 @@ function mockSpawnerLayer( · 123 unmodified lines
140 );141 );
141}142}
142 143
144function failingSpawnerLayer(cause: PlatformError.PlatformError) {
145 return Layer.succeed(
146 ChildProcessSpawner.ChildProcessSpawner,
147 ChildProcessSpawner.make(() => Effect.fail(cause)),
148 );
149}
150
143function makeRegistry(151function makeRegistry(
144 initialProviders: ServerProvider | ReadonlyArray<ServerProvider> = baseProvider,152 initialProviders: ServerProvider | ReadonlyArray<ServerProvider> = baseProvider,
145) {153) {
@@ -320,6 +328,29 @@ describe("providerMaintenanceRunner", () => { · 174 unmodified lines
320 },328 },
321 );329 );
322 330
331 it.effect("preserves update spawn failure messages", () => {
332 const cause = PlatformError.systemError({
333 _tag: "PermissionDenied",
334 module: "ChildProcess",
335 method: "spawn",
336 pathOrDescriptor: "npm",
337 description: "",
338 });
339 return Effect.gen(function* () {
340 const { registry } = yield* makeRegistry(baseProvider);
341 const runner = yield* makeTestRunner(registry);
342
343 const result = yield* runner.updateProvider(CODEX_DRIVER);
344 assert.strictEqual(result.providers[0]?.updateState?.status, "failed");
345 assert.strictEqual(
346 result.providers[0]?.updateState?.message,
347 `Failed to run update command npm: ${cause.message}`,
348 );
349 }).pipe(
350 Effect.provide(Layer.mergeAll(latestVersionHttpClient("0.0.0"), failingSpawnerLayer(cause))),
351 );
352 });
353
323 it.effect("updates a single provider instance without touching sibling instances", () => {354 it.effect("updates a single provider instance without touching sibling instances", () => {
324 const calls: Array<{ command: string; args: ReadonlyArray<string> }> = [];355 const calls: Array<{ command: string; args: ReadonlyArray<string> }> = [];
325 return Effect.gen(function* () {356 return Effect.gen(function* () {
apps/server/src/provider/providerMaintenanceRunner.ts+63−13
@@ -14,6 +14,7 @@ import * as Duration from "effect/Duration"; · 13 unmodified lines
14import * as Effect from "effect/Effect";14import * as Effect from "effect/Effect";
15import * as Layer from "effect/Layer";15import * as Layer from "effect/Layer";
16import * as Option from "effect/Option";16import * as Option from "effect/Option";
17import * as PlatformError from "effect/PlatformError";
17import * as Ref from "effect/Ref";18import * as Ref from "effect/Ref";
18import * as Schema from "effect/Schema";19import * as Schema from "effect/Schema";
19import { HttpClient } from "effect/unstable/http";20import { HttpClient } from "effect/unstable/http";
@@ -52,20 +53,68 @@ export class ProviderMaintenanceRunner extends Context.Service< · 32 unmodified lines
52 }53 }
53>()("t3/provider/providerMaintenanceRunner") {}54>()("t3/provider/providerMaintenanceRunner") {}
54 55
55class ProviderMaintenanceCommandError extends Schema.TaggedErrorClass<ProviderMaintenanceCommandError>()(56const platformFailureFields = {
56 "ProviderMaintenanceCommandError",57 failureTag: Schema.NullOr(Schema.String),
58 failureModule: Schema.String,
59 failureMethod: Schema.String,
60 failureDescription: Schema.NullOr(Schema.String),
61 failurePathOrDescriptor: Schema.NullOr(Schema.Union([Schema.String, Schema.Number])),
62} as const;
63
64interface PlatformFailureAttributes {
65 readonly failureTag: string | null;
66 readonly failureModule: string;
67 readonly failureMethod: string;
68 readonly failureDescription: string | null;
69 readonly failurePathOrDescriptor: string | number | null;
70}
71
72function platformFailureAttributes(cause: PlatformError.PlatformError): PlatformFailureAttributes {
73 const { reason } = cause;
74 return {
75 failureTag: reason._tag === "BadArgument" ? null : reason._tag,
76 failureModule: reason.module,
77 failureMethod: reason.method,
78 failureDescription: reason.description || null,
79 failurePathOrDescriptor:
80 "pathOrDescriptor" in reason ? (reason.pathOrDescriptor ?? null) : null,
81 };
82}
83
84function formatPlatformFailure(attributes: PlatformFailureAttributes): string {
85 const tag = attributes.failureTag === null ? "" : `${attributes.failureTag}: `;
86 const path =
87 attributes.failurePathOrDescriptor === null
88 ? ""
89 : ` (${String(attributes.failurePathOrDescriptor)})`;
90 const description =
91 attributes.failureDescription === null ? "" : `: ${attributes.failureDescription}`;
92 return `${tag}${attributes.failureModule}.${attributes.failureMethod}${path}${description}`;
93}
94
95class ProviderMaintenanceCommandSpawnError extends Schema.TaggedErrorClass<ProviderMaintenanceCommandSpawnError>()(
96 "ProviderMaintenanceCommandSpawnError",
57 {97 {
58 operation: Schema.Literals(["spawn", "collect"]),98 command: Schema.String,
59 command: Schema.optional(Schema.String),99 ...platformFailureFields,
60 cause: Schema.Defect(),100 cause: Schema.Defect(),
61 },101 },
62) {102) {
63 override get message(): string {103 override get message(): string {
64 const causeMessage = this.cause instanceof Error ? this.cause.message : undefined;104 return `Failed to run update command ${this.command}: ${formatPlatformFailure(this)}`;
65 if (this.operation === "spawn") {105 }
66 return `Failed to run update command ${this.command ?? "unknown"}${causeMessage ? `: ${causeMessage}` : "."}`;106}
67 }107
68 return causeMessage ?? "Update command failed to run.";108class ProviderMaintenanceCommandCollectError extends Schema.TaggedErrorClass<ProviderMaintenanceCommandCollectError>()(
109 "ProviderMaintenanceCommandCollectError",
110 {
111 command: Schema.String,
112 ...platformFailureFields,
113 cause: Schema.Defect(),
114 },
115) {
116 override get message(): string {
117 return formatPlatformFailure(this);
69 }118 }
70}119}
71 120
@@ -89,9 +138,9 @@ const runProviderMaintenanceCommandWithSpawner = Effect.fn("ProviderMaintenanceR · 17 unmodified lines
89 .pipe(138 .pipe(
90 Effect.mapError(139 Effect.mapError(
91 (cause) =>140 (cause) =>
92 new ProviderMaintenanceCommandError({141 new ProviderMaintenanceCommandSpawnError({
93 operation: "spawn",
94 command: input.command,142 command: input.command,
143 ...platformFailureAttributes(cause),
95 cause,144 cause,
96 }),145 }),
97 ),146 ),
@@ -114,8 +163,9 @@ const runProviderMaintenanceCommandWithSpawner = Effect.fn("ProviderMaintenanceR · 16 unmodified lines
114 ).pipe(163 ).pipe(
115 Effect.mapError(164 Effect.mapError(
116 (cause) =>165 (cause) =>
117 new ProviderMaintenanceCommandError({166 new ProviderMaintenanceCommandCollectError({
118 operation: "collect",167 command: input.command,
168 ...platformFailureAttributes(cause),
119 cause,169 cause,
120 }),170 }),
121 ),171 ),
docs/architecture/overview.md+1−1
@@ -132,7 +132,7 @@ sequenceDiagram · 131 unmodified lines
132[5]: ../apps/server/src/wsServer/pushBus.ts132[5]: ../apps/server/src/wsServer/pushBus.ts
133[6]: ../packages/contracts/src/ws.ts133[6]: ../packages/contracts/src/ws.ts
134[7]: ../apps/server/src/serverLayers.ts134[7]: ../apps/server/src/serverLayers.ts
135[8]: ../apps/server/src/provider/Layers/ProviderService.ts135[8]: ../apps/server/src/provider/ProviderService.ts
136[9]: ../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts136[9]: ../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
137[10]: ../apps/server/src/orchestration/Layers/OrchestrationEngine.ts137[10]: ../apps/server/src/orchestration/Layers/OrchestrationEngine.ts
138[11]: ../packages/contracts/src/orchestration.ts138[11]: ../packages/contracts/src/orchestration.ts
docs/operations/effect-fn-checklist.md+17−17
@@ -41,7 +41,7 @@ Effect.fn("name")( · 40 unmodified lines
41 41
42## Suggested Order42## Suggested Order
43 43
44- [ ] `apps/server/src/provider/Layers/ProviderService.ts`44- [ ] `apps/server/src/provider/ProviderService.ts`
45- [x] `apps/server/src/provider/Layers/ClaudeAdapter.ts`45- [x] `apps/server/src/provider/Layers/ClaudeAdapter.ts`
46- [x] `apps/server/src/provider/Layers/CodexAdapter.ts`46- [x] `apps/server/src/provider/Layers/CodexAdapter.ts`
47- [x] `apps/server/src/git/Layers/GitCore.ts`47- [x] `apps/server/src/git/Layers/GitCore.ts`
@@ -98,20 +98,20 @@ Effect.fn("name")( · 50 unmodified lines
98- [x] `Effect.forEach(..., entry => Effect.gen(...))` callbacks around `L305`98- [x] `Effect.forEach(..., entry => Effect.gen(...))` callbacks around `L305`
99- [x] Remaining apply helpers in this file99- [x] Remaining apply helpers in this file
100 100
101### `apps/server/src/provider/Layers/ProviderService.ts` (`24`)101### `apps/server/src/provider/ProviderService.ts` (`24`)
102 102
103- [ ] [makeProviderService](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L134)103- [ ] [makeProviderService](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/ProviderService.ts#L134)
104- [ ] [recoverSessionForThread](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L196)104- [ ] [recoverSessionForThread](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/ProviderService.ts#L196)
105- [ ] [resolveRoutableSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L255)105- [ ] [resolveRoutableSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/ProviderService.ts#L255)
106- [ ] [startSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L284)106- [ ] [startSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/ProviderService.ts#L284)
107- [ ] [sendTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L347)107- [ ] [sendTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/ProviderService.ts#L347)
108- [ ] [interruptTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L393)108- [ ] [interruptTurn](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/ProviderService.ts#L393)
109- [ ] [respondToRequest](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L411)109- [ ] [respondToRequest](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/ProviderService.ts#L411)
110- [ ] [respondToUserInput](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L430)110- [ ] [respondToUserInput](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/ProviderService.ts#L430)
111- [ ] [stopSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L445)111- [ ] [stopSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/ProviderService.ts#L445)
112- [ ] [listSessions](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L466)112- [ ] [listSessions](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/ProviderService.ts#L466)
113- [ ] [rollbackConversation](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L516)113- [ ] [rollbackConversation](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/ProviderService.ts#L516)
114- [ ] [runStopAll](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderService.ts#L538)114- [ ] [runStopAll](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/ProviderService.ts#L538)
115 115
116### `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` (`14`)116### `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` (`14`)
117 117
@@ -174,7 +174,7 @@ Effect.fn("name")( · 56 unmodified lines
174- [ ] [packages/shared/src/DrainableWorker.ts](/Users/julius/Development/Work/codething-mvp/packages/shared/src/DrainableWorker.ts) (`4`)174- [ ] [packages/shared/src/DrainableWorker.ts](/Users/julius/Development/Work/codething-mvp/packages/shared/src/DrainableWorker.ts) (`4`)
175- [ ] [apps/server/src/wsServer/pushBus.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/wsServer/pushBus.ts) (`4`)175- [ ] [apps/server/src/wsServer/pushBus.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/wsServer/pushBus.ts) (`4`)
176- [ ] [apps/server/src/wsServer.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/wsServer.ts) (`4`)176- [ ] [apps/server/src/wsServer.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/wsServer.ts) (`4`)
177- [ ] [apps/server/src/provider/Layers/ProviderRegistry.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderRegistry.ts) (`4`)177- [ ] [apps/server/src/provider/ProviderRegistry.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/ProviderRegistry.ts) (`4`)
178- [ ] [apps/server/src/persistence/Layers/Sqlite.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/Layers/Sqlite.ts) (`4`)178- [ ] [apps/server/src/persistence/Layers/Sqlite.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/Layers/Sqlite.ts) (`4`)
179- [ ] [apps/server/src/orchestration/Layers/ProviderCommandReactor.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts) (`4`)179- [ ] [apps/server/src/orchestration/Layers/ProviderCommandReactor.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts) (`4`)
180- [ ] [apps/server/src/main.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/main.ts) (`4`)180- [ ] [apps/server/src/main.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/main.ts) (`4`)
@@ -183,7 +183,7 @@ Effect.fn("name")( · 2 unmodified lines
183- [ ] [apps/server/src/serverLayers.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/serverLayers.ts) (`3`)183- [ ] [apps/server/src/serverLayers.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/serverLayers.ts) (`3`)
184- [ ] [apps/server/src/telemetry/Layers/AnalyticsService.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/telemetry/Layers/AnalyticsService.ts) (`2`)184- [ ] [apps/server/src/telemetry/Layers/AnalyticsService.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/telemetry/Layers/AnalyticsService.ts) (`2`)
185- [ ] [apps/server/src/telemetry/Identify.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/telemetry/Identify.ts) (`2`)185- [ ] [apps/server/src/telemetry/Identify.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/telemetry/Identify.ts) (`2`)
186- [ ] [apps/server/src/provider/Layers/ProviderAdapterRegistry.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts) (`2`)186- [ ] [apps/server/src/provider/ProviderAdapterRegistry.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/ProviderAdapterRegistry.ts) (`2`)
187- [ ] [apps/server/src/provider/Layers/CodexProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexProvider.ts) (`2`)187- [ ] [apps/server/src/provider/Layers/CodexProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexProvider.ts) (`2`)
188- [ ] [apps/server/src/provider/Layers/ClaudeProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeProvider.ts) (`2`)188- [ ] [apps/server/src/provider/Layers/ClaudeProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/ClaudeProvider.ts) (`2`)
189- [ ] [apps/server/src/persistence/NodeSqliteClient.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/NodeSqliteClient.ts) (`2`)189- [ ] [apps/server/src/persistence/NodeSqliteClient.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/NodeSqliteClient.ts) (`2`)
docs/reference/encyclopedia.md+1−1
@@ -167,7 +167,7 @@ The file patch and changed-file summary for one turn. It is usually computed in · 166 unmodified lines
167[11]: ../apps/server/src/orchestration/Layers/ProjectionPipeline.ts167[11]: ../apps/server/src/orchestration/Layers/ProjectionPipeline.ts
168[12]: ../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts168[12]: ../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
169[13]: ../apps/server/src/orchestration/Services/RuntimeReceiptBus.ts169[13]: ../apps/server/src/orchestration/Services/RuntimeReceiptBus.ts
170[14]: ../apps/server/src/provider/Layers/ProviderService.ts170[14]: ../apps/server/src/provider/ProviderService.ts
171[15]: ../apps/server/src/provider/Services/ProviderAdapter.ts171[15]: ../apps/server/src/provider/Services/ProviderAdapter.ts
172[16]: ./provider-architecture.md172[16]: ./provider-architecture.md
173[17]: ../apps/server/src/provider/Layers/CodexAdapter.ts173[17]: ../apps/server/src/provider/Layers/CodexAdapter.ts