Skip to content

Commit 4ce54d4

Browse files
only21milclaudemaria-rcks
authored
fix(server): turns no longer start on a provider instance being replaced (#24480)
Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: maria-rcks <[email protected]>
1 parent e83021c commit 4ce54d4

2 files changed

Lines changed: 63 additions & 1 deletion

File tree

‎apps/server/src/provider/ProviderInstanceRegistry.test.ts‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import * as HostProcess from "@t3tools/shared/HostProcess";
2323
import * as DateTime from "effect/DateTime";
2424
import * as Deferred from "effect/Deferred";
2525
import * as Effect from "effect/Effect";
26+
import * as Fiber from "effect/Fiber";
2627
import * as FileSystem from "effect/FileSystem";
2728
import * as Layer from "effect/Layer";
2829
import * as Path from "effect/Path";
@@ -201,6 +202,61 @@ describe("ProviderInstanceRegistry — multi-instance codex slice", () => {
201202
Layer.provideMerge(layerBase),
202203
);
203204

205+
it.effect.each(["replacement", "removal"] as const)(
206+
"hides retired instances during reconcile (%s)",
207+
(scenario) =>
208+
Effect.gen(function* () {
209+
let closed = 0;
210+
const replacementStarted = yield* Deferred.make<void>();
211+
const releaseReplacement = yield* Deferred.make<void>();
212+
const driver: typeof CodexDriver = {
213+
...CodexDriver,
214+
create: (input) =>
215+
Effect.gen(function* () {
216+
if (input.displayName === "Second") {
217+
yield* Deferred.succeed(replacementStarted, undefined);
218+
yield* Deferred.await(releaseReplacement);
219+
}
220+
const instance = yield* CodexDriver.create(input);
221+
yield* Effect.addFinalizer(() => Effect.sync(() => closed++));
222+
return instance;
223+
}),
224+
};
225+
const retiredId = ProviderInstanceId.make("retired");
226+
const siblingId = ProviderInstanceId.make("sibling");
227+
const nextId = scenario === "replacement" ? retiredId : ProviderInstanceId.make("new");
228+
const entry = {
229+
driver: CodexDriver.driverKind,
230+
enabled: false,
231+
config: makeCodexConfig({}),
232+
};
233+
const { registry, mutator } = yield* makeProviderInstanceRegistry({
234+
drivers: [driver],
235+
configMap: { [retiredId]: entry, [siblingId]: entry },
236+
});
237+
const sibling = yield* registry.getInstance(siblingId);
238+
const reconcile = yield* mutator
239+
.reconcile({
240+
[siblingId]: entry,
241+
[nextId]: {
242+
...entry,
243+
displayName: "Second",
244+
config: makeCodexConfig({ customModels: ["replacement-model"] }),
245+
},
246+
})
247+
.pipe(Effect.forkChild({ startImmediately: true }));
248+
yield* Deferred.await(replacementStarted);
249+
expect(closed).toBe(1);
250+
expect(yield* registry.getInstance(retiredId)).toBeUndefined();
251+
expect(yield* registry.listInstances).toEqual([sibling]);
252+
yield* Deferred.succeed(releaseReplacement, undefined);
253+
yield* Fiber.join(reconcile);
254+
expect((yield* registry.getInstance(nextId))?.displayName).toBe("Second");
255+
expect(yield* registry.getInstance(siblingId)).toBe(sibling);
256+
expect(closed).toBe(1);
257+
}).pipe(Effect.provide(layerTest)),
258+
);
259+
204260
it.live("treats an explicit in-config enabled:false as disabling despite the envelope", () =>
205261
Effect.gen(function* () {
206262
const staleId = ProviderInstanceId.make("codex_stale");

‎apps/server/src/provider/ProviderInstanceRegistry.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,13 @@ const makeReconcile = <R>(input: {
272272
replacedIds.add(instanceId);
273273
}
274274
}
275-
for (const id of [...removedIds, ...replacedIds]) {
275+
const retiredIds = [...removedIds, ...replacedIds];
276+
if (retiredIds.length > 0) {
277+
const remaining = new Map(previousEntries);
278+
for (const id of retiredIds) remaining.delete(id);
279+
yield* Ref.set(state.entries, remaining);
280+
}
281+
for (const id of retiredIds) {
276282
const live = previousEntries.get(id);
277283
if (live) {
278284
yield* Scope.close(live.scope, Exit.void).pipe(Effect.ignore);

0 commit comments

Comments
 (0)