authorgravatar for hauke@schnau-lilienthal.deHauke Schnau <hauke@schnau-lilienthal.de> 2026-04-17 20:27:12+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-17 11:27:12-07:00
log721b6b4c6f596bd1d1b3af585ed9c62c4b6f72ab
tree8e1ca6264b4dd9d492d019b52a500a06bc953e82
parent40009735eea88b2266cf7d112217e85a688b3e63
signature Signed by PGP key B5690EEEBB952194

Preserve provider bindings when stopping sessions (#2125)

Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com>

8 files changed, 198 insertions(+), 160 deletions(-)

apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+108-108
......@@ -750,6 +750,57 @@ describe("ProviderCommandReactor", () => {
750750 });
751751 });
752752
753 it("preserves the active session model when in-session model switching is unsupported", async () => {
754 const harness = await createHarness({ sessionModelSwitch: "unsupported" });
755 const now = new Date().toISOString();
756
757 await Effect.runPromise(
758 harness.engine.dispatch({
759 type: "thread.turn.start",
760 commandId: CommandId.make("cmd-turn-start-unsupported-1"),
761 threadId: ThreadId.make("thread-1"),
762 message: {
763 messageId: asMessageId("user-message-unsupported-1"),
764 role: "user",
765 text: "first",
766 attachments: [],
767 },
768 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
769 runtimeMode: "approval-required",
770 createdAt: now,
771 }),
772 );
773
774 await waitFor(() => harness.sendTurn.mock.calls.length === 1);
775
776 await Effect.runPromise(
777 harness.engine.dispatch({
778 type: "thread.turn.start",
779 commandId: CommandId.make("cmd-turn-start-unsupported-2"),
780 threadId: ThreadId.make("thread-1"),
781 message: {
782 messageId: asMessageId("user-message-unsupported-2"),
783 role: "user",
784 text: "second",
785 attachments: [],
786 },
787 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
788 runtimeMode: "approval-required",
789 createdAt: now,
790 }),
791 );
792
793 await waitFor(() => harness.sendTurn.mock.calls.length === 2);
794
795 expect(harness.sendTurn.mock.calls[1]?.[0]).toMatchObject({
796 threadId: ThreadId.make("thread-1"),
797 modelSelection: {
798 provider: "codex",
799 model: "gpt-5-codex",
800 },
801 });
802 });
803
753804 it("rejects a first turn when requested provider conflicts with the thread model", async () => {
754805 const harness = await createHarness({
755806 threadModelSelection: { provider: "codex", model: "gpt-5-codex" },
......@@ -802,57 +853,6 @@ describe("ProviderCommandReactor", () => {
802853 });
803854 });
804855
805 it("preserves the active session model when in-session model switching is unsupported", async () => {
806 const harness = await createHarness({ sessionModelSwitch: "unsupported" });
807 const now = new Date().toISOString();
808
809 await Effect.runPromise(
810 harness.engine.dispatch({
811 type: "thread.turn.start",
812 commandId: CommandId.make("cmd-turn-start-unsupported-1"),
813 threadId: ThreadId.make("thread-1"),
814 message: {
815 messageId: asMessageId("user-message-unsupported-1"),
816 role: "user",
817 text: "first",
818 attachments: [],
819 },
820 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
821 runtimeMode: "approval-required",
822 createdAt: now,
823 }),
824 );
825
826 await waitFor(() => harness.sendTurn.mock.calls.length === 1);
827
828 await Effect.runPromise(
829 harness.engine.dispatch({
830 type: "thread.turn.start",
831 commandId: CommandId.make("cmd-turn-start-unsupported-2"),
832 threadId: ThreadId.make("thread-1"),
833 message: {
834 messageId: asMessageId("user-message-unsupported-2"),
835 role: "user",
836 text: "second",
837 attachments: [],
838 },
839 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
840 runtimeMode: "approval-required",
841 createdAt: now,
842 }),
843 );
844
845 await waitFor(() => harness.sendTurn.mock.calls.length === 2);
846
847 expect(harness.sendTurn.mock.calls[1]?.[0]).toMatchObject({
848 threadId: ThreadId.make("thread-1"),
849 modelSelection: {
850 provider: "codex",
851 model: "gpt-5-codex",
852 },
853 });
854 });
855
856856 it("reuses the same provider session when runtime mode is unchanged", async () => {
857857 const harness = await createHarness();
858858 const now = new Date().toISOString();
......@@ -1100,23 +1100,33 @@ describe("ProviderCommandReactor", () => {
11001100 });
11011101 });
11021102
1103 it("rejects provider changes after a thread is already bound to a session provider", async () => {
1103 it("does not stop the active session when restart fails before rebind", async () => {
11041104 const harness = await createHarness();
11051105 const now = new Date().toISOString();
11061106
1107 await Effect.runPromise(
1108 harness.engine.dispatch({
1109 type: "thread.runtime-mode.set",
1110 commandId: CommandId.make("cmd-runtime-mode-set-initial-full-access-2"),
1111 threadId: ThreadId.make("thread-1"),
1112 runtimeMode: "full-access",
1113 createdAt: now,
1114 }),
1115 );
1116
11071117 await Effect.runPromise(
11081118 harness.engine.dispatch({
11091119 type: "thread.turn.start",
1110 commandId: CommandId.make("cmd-turn-start-provider-switch-1"),
1120 commandId: CommandId.make("cmd-turn-start-restart-failure-1"),
11111121 threadId: ThreadId.make("thread-1"),
11121122 message: {
1113 messageId: asMessageId("user-message-provider-switch-1"),
1123 messageId: asMessageId("user-message-restart-failure-1"),
11141124 role: "user",
11151125 text: "first",
11161126 attachments: [],
11171127 },
11181128 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
1119 runtimeMode: "approval-required",
1129 runtimeMode: "full-access",
11201130 createdAt: now,
11211131 }),
11221132 );
......@@ -1124,22 +1134,15 @@ describe("ProviderCommandReactor", () => {
11241134 await waitFor(() => harness.startSession.mock.calls.length === 1);
11251135 await waitFor(() => harness.sendTurn.mock.calls.length === 1);
11261136
1137 harness.startSession.mockImplementationOnce(
1138 (_: unknown, __: unknown) => Effect.fail(new Error("simulated restart failure")) as never,
1139 );
1140
11271141 await Effect.runPromise(
11281142 harness.engine.dispatch({
1129 type: "thread.turn.start",
1130 commandId: CommandId.make("cmd-turn-start-provider-switch-2"),
1143 type: "thread.runtime-mode.set",
1144 commandId: CommandId.make("cmd-runtime-mode-set-restart-failure"),
11311145 threadId: ThreadId.make("thread-1"),
1132 message: {
1133 messageId: asMessageId("user-message-provider-switch-2"),
1134 role: "user",
1135 text: "second",
1136 attachments: [],
1137 },
1138 modelSelection: {
1139 provider: "claudeAgent",
1140 model: "claude-opus-4-6",
1141 },
1142 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
11431146 runtimeMode: "approval-required",
11441147 createdAt: now,
11451148 }),
......@@ -1148,57 +1151,37 @@ describe("ProviderCommandReactor", () => {
11481151 await waitFor(async () => {
11491152 const readModel = await Effect.runPromise(harness.engine.getReadModel());
11501153 const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"));
1151 return (
1152 thread?.activities.some((activity) => activity.kind === "provider.turn.start.failed") ??
1153 false
1154 );
1154 return thread?.runtimeMode === "approval-required";
11551155 });
1156 await waitFor(() => harness.startSession.mock.calls.length === 2);
1157 await harness.drain();
11561158
1157 expect(harness.startSession.mock.calls.length).toBe(1);
1158 expect(harness.sendTurn.mock.calls.length).toBe(1);
11591159 expect(harness.stopSession.mock.calls.length).toBe(0);
1160 expect(harness.sendTurn.mock.calls.length).toBe(1);
11601161
11611162 const readModel = await Effect.runPromise(harness.engine.getReadModel());
11621163 const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"));
11631164 expect(thread?.session?.threadId).toBe("thread-1");
1164 expect(thread?.session?.providerName).toBe("codex");
1165 expect(thread?.session?.runtimeMode).toBe("approval-required");
1166 expect(
1167 thread?.activities.find((activity) => activity.kind === "provider.turn.start.failed"),
1168 ).toMatchObject({
1169 payload: {
1170 detail: expect.stringContaining("cannot switch to 'claudeAgent'"),
1171 },
1172 });
1165 expect(thread?.session?.runtimeMode).toBe("full-access");
11731166 });
11741167
1175 it("does not stop the active session when restart fails before rebind", async () => {
1168 it("rejects provider changes after a thread is already bound to a session provider", async () => {
11761169 const harness = await createHarness();
11771170 const now = new Date().toISOString();
11781171
1179 await Effect.runPromise(
1180 harness.engine.dispatch({
1181 type: "thread.runtime-mode.set",
1182 commandId: CommandId.make("cmd-runtime-mode-set-initial-full-access-2"),
1183 threadId: ThreadId.make("thread-1"),
1184 runtimeMode: "full-access",
1185 createdAt: now,
1186 }),
1187 );
1188
11891172 await Effect.runPromise(
11901173 harness.engine.dispatch({
11911174 type: "thread.turn.start",
1192 commandId: CommandId.make("cmd-turn-start-restart-failure-1"),
1175 commandId: CommandId.make("cmd-turn-start-provider-switch-1"),
11931176 threadId: ThreadId.make("thread-1"),
11941177 message: {
1195 messageId: asMessageId("user-message-restart-failure-1"),
1178 messageId: asMessageId("user-message-provider-switch-1"),
11961179 role: "user",
11971180 text: "first",
11981181 attachments: [],
11991182 },
12001183 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
1201 runtimeMode: "full-access",
1184 runtimeMode: "approval-required",
12021185 createdAt: now,
12031186 }),
12041187 );
......@@ -1206,15 +1189,22 @@ describe("ProviderCommandReactor", () => {
12061189 await waitFor(() => harness.startSession.mock.calls.length === 1);
12071190 await waitFor(() => harness.sendTurn.mock.calls.length === 1);
12081191
1209 harness.startSession.mockImplementationOnce(
1210 (_: unknown, __: unknown) => Effect.fail(new Error("simulated restart failure")) as never,
1211 );
1212
12131192 await Effect.runPromise(
12141193 harness.engine.dispatch({
1215 type: "thread.runtime-mode.set",
1216 commandId: CommandId.make("cmd-runtime-mode-set-restart-failure"),
1194 type: "thread.turn.start",
1195 commandId: CommandId.make("cmd-turn-start-provider-switch-2"),
12171196 threadId: ThreadId.make("thread-1"),
1197 message: {
1198 messageId: asMessageId("user-message-provider-switch-2"),
1199 role: "user",
1200 text: "second",
1201 attachments: [],
1202 },
1203 modelSelection: {
1204 provider: "claudeAgent",
1205 model: "claude-opus-4-6",
1206 },
1207 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
12181208 runtimeMode: "approval-required",
12191209 createdAt: now,
12201210 }),
......@@ -1223,18 +1213,28 @@ describe("ProviderCommandReactor", () => {
12231213 await waitFor(async () => {
12241214 const readModel = await Effect.runPromise(harness.engine.getReadModel());
12251215 const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"));
1226 return thread?.runtimeMode === "approval-required";
1216 return (
1217 thread?.activities.some((activity) => activity.kind === "provider.turn.start.failed") ??
1218 false
1219 );
12271220 });
1228 await waitFor(() => harness.startSession.mock.calls.length === 2);
1229 await harness.drain();
12301221
1231 expect(harness.stopSession.mock.calls.length).toBe(0);
1222 expect(harness.startSession.mock.calls.length).toBe(1);
12321223 expect(harness.sendTurn.mock.calls.length).toBe(1);
1224 expect(harness.stopSession.mock.calls.length).toBe(0);
12331225
12341226 const readModel = await Effect.runPromise(harness.engine.getReadModel());
12351227 const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"));
12361228 expect(thread?.session?.threadId).toBe("thread-1");
1237 expect(thread?.session?.runtimeMode).toBe("full-access");
1229 expect(thread?.session?.providerName).toBe("codex");
1230 expect(thread?.session?.runtimeMode).toBe("approval-required");
1231 expect(
1232 thread?.activities.find((activity) => activity.kind === "provider.turn.start.failed"),
1233 ).toMatchObject({
1234 payload: {
1235 detail: expect.stringContaining("cannot switch to 'claudeAgent'"),
1236 },
1237 });
12381238 });
12391239
12401240 it("reacts to thread.turn.interrupt-requested by calling provider interrupt", async () => {
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+4-10
......@@ -247,7 +247,7 @@ const make = Effect.gen(function* () {
247247 detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.provider}'.`,
248248 });
249249 }
250 const preferredProvider: ProviderKind = currentProvider ?? threadProvider;
250 const preferredProvider: ProviderKind = threadProvider;
251251 const desiredModelSelection = requestedModelSelection ?? thread.modelSelection;
252252 const effectiveCwd = resolveThreadWorkspaceCwd({
253253 thread,
......@@ -293,9 +293,6 @@ const make = Effect.gen(function* () {
293293 thread.session && thread.session.status !== "stopped" && activeSession ? thread.id : null;
294294 if (existingSessionThreadId) {
295295 const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode;
296 const providerChanged =
297 requestedModelSelection !== undefined &&
298 requestedModelSelection.provider !== currentProvider;
299296 const sessionModelSwitch =
300297 currentProvider === undefined
301298 ? "in-session"
......@@ -312,17 +309,15 @@ const make = Effect.gen(function* () {
312309
313310 if (
314311 !runtimeModeChanged &&
315 !providerChanged &&
316312 !shouldRestartForModelChange &&
317313 !shouldRestartForModelSelectionChange
318314 ) {
319315 return existingSessionThreadId;
320316 }
321317
322 const resumeCursor =
323 providerChanged || shouldRestartForModelChange
324 ? undefined
325 : (activeSession?.resumeCursor ?? undefined);
318 const resumeCursor = shouldRestartForModelChange
319 ? undefined
320 : (activeSession?.resumeCursor ?? undefined);
326321 yield* Effect.logInfo("provider command reactor restarting provider session", {
327322 threadId,
328323 existingSessionThreadId,
......@@ -331,7 +326,6 @@ const make = Effect.gen(function* () {
331326 currentRuntimeMode: thread.session?.runtimeMode,
332327 desiredRuntimeMode: thread.runtimeMode,
333328 runtimeModeChanged,
334 providerChanged,
335329 modelChanged,
336330 shouldRestartForModelChange,
337331 shouldRestartForModelSelectionChange,
apps/server/src/provider/Layers/CodexAdapter.test.ts-1
......@@ -144,7 +144,6 @@ const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory
144144 getProvider: () =>
145145 Effect.die(new Error("ProviderSessionDirectory.getProvider is not used in test")),
146146 getBinding: () => Effect.succeed(Option.none()),
147 remove: () => Effect.void,
148147 listThreadIds: () => Effect.succeed([]),
149148 listBindings: () => Effect.succeed([]),
150149});
apps/server/src/provider/Layers/ProviderService.test.ts+76-15
......@@ -18,7 +18,6 @@ import {
1818 TurnId,
1919} from "@t3tools/contracts";
2020import { it, assert, vi } from "@effect/vitest";
21import { assertFailure } from "@effect/vitest/utils";
2221
2322import { Effect, Fiber, Layer, Metric, Option, PubSub, Ref, Stream } from "effect";
2423import * as SqlClient from "effect/unstable/sql/SqlClient";
......@@ -574,20 +573,31 @@ routing.layer("ProviderServiceLive routing", (it) => {
574573 });
575574
576575 yield* provider.stopSession({ threadId: session.threadId });
577 const sendAfterStop = yield* Effect.result(
578 provider.sendTurn({
579 threadId: session.threadId,
580 input: "after-stop",
581 attachments: [],
582 }),
583 );
584 assertFailure(
585 sendAfterStop,
586 new ProviderValidationError({
587 operation: "ProviderService.sendTurn",
588 issue: `Cannot route thread '${session.threadId}' because no persisted provider binding exists.`,
589 }),
590 );
576 routing.codex.startSession.mockClear();
577 routing.codex.sendTurn.mockClear();
578
579 yield* provider.sendTurn({
580 threadId: session.threadId,
581 input: "after-stop",
582 attachments: [],
583 });
584
585 assert.equal(routing.codex.startSession.mock.calls.length, 1);
586 const resumedStartInput = routing.codex.startSession.mock.calls[0]?.[0];
587 assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true);
588 if (resumedStartInput && typeof resumedStartInput === "object") {
589 const startPayload = resumedStartInput as {
590 provider?: string;
591 cwd?: string;
592 resumeCursor?: unknown;
593 threadId?: string;
594 };
595 assert.equal(startPayload.provider, "codex");
596 assert.equal(startPayload.cwd, "/tmp/project");
597 assert.deepEqual(startPayload.resumeCursor, session.resumeCursor);
598 assert.equal(startPayload.threadId, session.threadId);
599 }
600 assert.equal(routing.codex.sendTurn.mock.calls.length, 1);
591601 }),
592602 );
593603
......@@ -631,6 +641,57 @@ routing.layer("ProviderServiceLive routing", (it) => {
631641 }),
632642 );
633643
644 it.effect("preserves the persisted binding when stopping a session", () =>
645 Effect.gen(function* () {
646 const provider = yield* ProviderService;
647 const runtimeRepository = yield* ProviderSessionRuntimeRepository;
648
649 const initial = yield* provider.startSession(asThreadId("thread-reap-preserve"), {
650 provider: "codex",
651 threadId: asThreadId("thread-reap-preserve"),
652 cwd: "/tmp/project-reap-preserve",
653 runtimeMode: "full-access",
654 });
655
656 yield* provider.stopSession({ threadId: initial.threadId });
657
658 const persistedAfterStop = yield* runtimeRepository.getByThreadId({
659 threadId: initial.threadId,
660 });
661 assert.equal(Option.isSome(persistedAfterStop), true);
662 if (Option.isSome(persistedAfterStop)) {
663 assert.equal(persistedAfterStop.value.status, "stopped");
664 assert.deepEqual(persistedAfterStop.value.resumeCursor, initial.resumeCursor);
665 }
666
667 routing.codex.startSession.mockClear();
668 routing.codex.sendTurn.mockClear();
669
670 yield* provider.sendTurn({
671 threadId: initial.threadId,
672 input: "resume after reap",
673 attachments: [],
674 });
675
676 assert.equal(routing.codex.startSession.mock.calls.length, 1);
677 const resumedStartInput = routing.codex.startSession.mock.calls[0]?.[0];
678 assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true);
679 if (resumedStartInput && typeof resumedStartInput === "object") {
680 const startPayload = resumedStartInput as {
681 provider?: string;
682 cwd?: string;
683 resumeCursor?: unknown;
684 threadId?: string;
685 };
686 assert.equal(startPayload.provider, "codex");
687 assert.equal(startPayload.cwd, "/tmp/project-reap-preserve");
688 assert.deepEqual(startPayload.resumeCursor, initial.resumeCursor);
689 assert.equal(startPayload.threadId, initial.threadId);
690 }
691 assert.equal(routing.codex.sendTurn.mock.calls.length, 1);
692 }),
693 );
694
634695 it.effect("routes explicit claudeAgent provider session starts to the claude adapter", () =>
635696 Effect.gen(function* () {
636697 const provider = yield* ProviderService;
apps/server/src/provider/Layers/ProviderService.ts+8-1
......@@ -620,7 +620,14 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
620620 if (routed.isActive) {
621621 yield* routed.adapter.stopSession(routed.threadId);
622622 }
623 yield* directory.remove(input.threadId);
623 yield* directory.upsert({
624 threadId: input.threadId,
625 provider: routed.adapter.provider,
626 status: "stopped",
627 runtimePayload: {
628 activeTurnId: null,
629 },
630 });
624631 yield* analytics.record("provider.session.stopped", {
625632 provider: routed.adapter.provider,
626633 });
apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts+2-13
......@@ -5,7 +5,7 @@ import path from "node:path";
55import * as NodeServices from "@effect/platform-node/NodeServices";
66import { ThreadId } from "@t3tools/contracts";
77import { it, assert } from "@effect/vitest";
8import { assertFailure, assertSome } from "@effect/vitest/utils";
8import { assertSome } from "@effect/vitest/utils";
99import { Effect, Layer, Option } from "effect";
1010import * as SqlClient from "effect/unstable/sql/SqlClient";
1111
......@@ -15,7 +15,6 @@ import {
1515} from "../../persistence/Layers/Sqlite.ts";
1616import { ProviderSessionRuntimeRepositoryLive } from "../../persistence/Layers/ProviderSessionRuntime.ts";
1717import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts";
18import { ProviderSessionDirectoryPersistenceError } from "../Errors.ts";
1918import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts";
2019import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts";
2120
......@@ -31,7 +30,7 @@ function makeDirectoryLayer<E, R>(persistenceLayer: Layer.Layer<SqlClient.SqlCli
3130}
3231
3332it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryLive", (it) => {
34 it("upserts, reads, and removes thread bindings", () =>
33 it("upserts and reads thread bindings", () =>
3534 Effect.gen(function* () {
3635 const directory = yield* ProviderSessionDirectory;
3736 const runtimeRepository = yield* ProviderSessionRuntimeRepository;
......@@ -76,16 +75,6 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL
7675
7776 const threadIds = yield* directory.listThreadIds();
7877 assert.deepEqual(threadIds, [nextThreadId]);
79
80 yield* directory.remove(nextThreadId);
81 const missingProvider = yield* directory.getProvider(nextThreadId).pipe(Effect.result);
82 assertFailure(
83 missingProvider,
84 new ProviderSessionDirectoryPersistenceError({
85 operation: "ProviderSessionDirectory.getProvider",
86 detail: `No persisted provider binding found for thread '${nextThreadId}'.`,
87 }),
88 );
8978 }));
9079
9180 it("persists runtime fields and merges payload updates", () =>
apps/server/src/provider/Layers/ProviderSessionDirectory.ts-8
......@@ -145,13 +145,6 @@ const makeProviderSessionDirectory = Effect.gen(function* () {
145145 ),
146146 );
147147
148 const remove: ProviderSessionDirectoryShape["remove"] = (threadId) =>
149 repository
150 .deleteByThreadId({ threadId })
151 .pipe(
152 Effect.mapError(toPersistenceError("ProviderSessionDirectory.remove:deleteByThreadId")),
153 );
154
155148 const listThreadIds: ProviderSessionDirectoryShape["listThreadIds"] = () =>
156149 repository.list().pipe(
157150 Effect.mapError(toPersistenceError("ProviderSessionDirectory.listThreadIds:list")),
......@@ -174,7 +167,6 @@ const makeProviderSessionDirectory = Effect.gen(function* () {
174167 upsert,
175168 getProvider,
176169 getBinding,
177 remove,
178170 listThreadIds,
179171 listBindings,
180172 } satisfies ProviderSessionDirectoryShape;
apps/server/src/provider/Services/ProviderSessionDirectory.ts-4
......@@ -45,10 +45,6 @@ export interface ProviderSessionDirectoryShape {
4545 threadId: ThreadId,
4646 ) => Effect.Effect<Option.Option<ProviderRuntimeBinding>, ProviderSessionDirectoryReadError>;
4747
48 readonly remove: (
49 threadId: ThreadId,
50 ) => Effect.Effect<void, ProviderSessionDirectoryPersistenceError>;
51
5248 readonly listThreadIds: () => Effect.Effect<
5349 ReadonlyArray<ThreadId>,
5450 ProviderSessionDirectoryPersistenceError