authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-07-18 13:19:52+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-18 13:19:52+02:00
logd0b9f8d40d7f269a4c77b7c0302b5889a31ba66a
tree677f12794741942faa0e2ffd0f3560a8ab0d2ca0
parentd8b12ae801f2123e9be73d2f4139f8994e4d1d19
signature Signed by PGP key B5690EEEBB952194

[codex] Route OpenCode missing-session errors through Effect (#3608)

Co-authored-by: Codex <codex@openai.com> Co-authored-by: Julius Marminge <julius0216@outlook.com>

2 files changed, 45 insertions(+), 16 deletions(-)

apps/server/src/provider/Layers/OpenCodeAdapter.test.ts+32
......@@ -248,6 +248,38 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
248248 }),
249249 );
250250
251 it.effect("fails sendTurn for missing sessions through the typed error channel", () =>
252 Effect.gen(function* () {
253 const adapter = yield* OpenCodeAdapter;
254 const result = yield* adapter
255 .sendTurn({
256 threadId: asThreadId("thread-opencode-missing-send"),
257 input: "hello",
258 attachments: [],
259 })
260 .pipe(Effect.result);
261
262 NodeAssert.equal(result._tag, "Failure");
263 NodeAssert.equal(result.failure._tag, "ProviderAdapterSessionNotFoundError");
264 NodeAssert.equal(result.failure.provider, "opencode");
265 NodeAssert.equal(result.failure.threadId, "thread-opencode-missing-send");
266 }),
267 );
268
269 it.effect("fails stopSession for missing sessions through the typed error channel", () =>
270 Effect.gen(function* () {
271 const adapter = yield* OpenCodeAdapter;
272 const result = yield* adapter
273 .stopSession(asThreadId("thread-opencode-missing-stop"))
274 .pipe(Effect.result);
275
276 NodeAssert.equal(result._tag, "Failure");
277 NodeAssert.equal(result.failure._tag, "ProviderAdapterSessionNotFoundError");
278 NodeAssert.equal(result.failure.provider, "opencode");
279 NodeAssert.equal(result.failure.threadId, "thread-opencode-missing-stop");
280 }),
281 );
282
251283 it.effect("stops a configured-server session without trying to own server lifecycle", () =>
252284 Effect.gen(function* () {
253285 const adapter = yield* OpenCodeAdapter;
apps/server/src/provider/Layers/OpenCodeAdapter.ts+13-16
......@@ -228,28 +228,25 @@ function appendTurnItem(
228228 resolveTurnSnapshot(context, turnId).items.push(item);
229229}
230230
231function ensureSessionContext(
231const ensureSessionContext = Effect.fn("ensureSessionContext")(function* (
232232 sessions: ReadonlyMap<ThreadId, OpenCodeSessionContext>,
233233 threadId: ThreadId,
234): OpenCodeSessionContext {
234) {
235235 const session = sessions.get(threadId);
236236 if (!session) {
237 throw new ProviderAdapterSessionNotFoundError({
237 return yield* new ProviderAdapterSessionNotFoundError({
238238 provider: PROVIDER,
239239 threadId,
240240 });
241241 }
242 // `ensureSessionContext` is a sync gate used from both sync helpers and
243 // Effect bodies. `Ref.getUnsafe` is an atomic read of the backing cell —
244 // no fiber suspension required, which keeps this callable everywhere.
245 if (Ref.getUnsafe(session.stopped)) {
246 throw new ProviderAdapterSessionClosedError({
242 if (yield* Ref.get(session.stopped)) {
243 return yield* new ProviderAdapterSessionClosedError({
247244 provider: PROVIDER,
248245 threadId,
249246 });
250247 }
251248 return session;
252}
249});
253250
254251function normalizeQuestionRequest(request: QuestionRequest): ReadonlyArray<UserInputQuestion> {
255252 return request.questions.map((question, index) => ({
......@@ -1167,7 +1164,7 @@ export function makeOpenCodeAdapter(
11671164 );
11681165
11691166 const sendTurn: OpenCodeAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) {
1170 const context = ensureSessionContext(sessions, input.threadId);
1167 const context = yield* ensureSessionContext(sessions, input.threadId);
11711168 // A sendTurn while a turn is active is a steer: OpenCode queues the
11721169 // prompt into the busy session and the work continues as one turn, so
11731170 // the active turn id is reused instead of opening a new turn.
......@@ -1291,7 +1288,7 @@ export function makeOpenCodeAdapter(
12911288
12921289 const interruptTurn: OpenCodeAdapterShape["interruptTurn"] = Effect.fn("interruptTurn")(
12931290 function* (threadId, turnId) {
1294 const context = ensureSessionContext(sessions, threadId);
1291 const context = yield* ensureSessionContext(sessions, threadId);
12951292 yield* runOpenCodeSdk("session.abort", () =>
12961293 context.client.session.abort({ sessionID: context.openCodeSessionId }),
12971294 ).pipe(Effect.mapError(toRequestError));
......@@ -1313,7 +1310,7 @@ export function makeOpenCodeAdapter(
13131310 const respondToRequest: OpenCodeAdapterShape["respondToRequest"] = Effect.fn(
13141311 "respondToRequest",
13151312 )(function* (threadId, requestId, decision) {
1316 const context = ensureSessionContext(sessions, threadId);
1313 const context = yield* ensureSessionContext(sessions, threadId);
13171314 if (!context.pendingPermissions.has(requestId)) {
13181315 return yield* new ProviderAdapterRequestError({
13191316 provider: PROVIDER,
......@@ -1333,7 +1330,7 @@ export function makeOpenCodeAdapter(
13331330 const respondToUserInput: OpenCodeAdapterShape["respondToUserInput"] = Effect.fn(
13341331 "respondToUserInput",
13351332 )(function* (threadId, requestId, answers) {
1336 const context = ensureSessionContext(sessions, threadId);
1333 const context = yield* ensureSessionContext(sessions, threadId);
13371334 const request = context.pendingQuestions.get(requestId);
13381335 if (!request) {
13391336 return yield* new ProviderAdapterRequestError({
......@@ -1355,7 +1352,7 @@ export function makeOpenCodeAdapter(
13551352 function* (threadId) {
13561353 const context = sessions.get(threadId);
13571354 if (!context) {
1358 throw new ProviderAdapterSessionNotFoundError({
1355 return yield* new ProviderAdapterSessionNotFoundError({
13591356 provider: PROVIDER,
13601357 threadId,
13611358 });
......@@ -1385,7 +1382,7 @@ export function makeOpenCodeAdapter(
13851382
13861383 const readThread: OpenCodeAdapterShape["readThread"] = Effect.fn("readThread")(
13871384 function* (threadId) {
1388 const context = ensureSessionContext(sessions, threadId);
1385 const context = yield* ensureSessionContext(sessions, threadId);
13891386 const messages = yield* runOpenCodeSdk("session.messages", () =>
13901387 context.client.session.messages({
13911388 sessionID: context.openCodeSessionId,
......@@ -1411,7 +1408,7 @@ export function makeOpenCodeAdapter(
14111408
14121409 const rollbackThread: OpenCodeAdapterShape["rollbackThread"] = Effect.fn("rollbackThread")(
14131410 function* (threadId, numTurns) {
1414 const context = ensureSessionContext(sessions, threadId);
1411 const context = yield* ensureSessionContext(sessions, threadId);
14151412 const messages = yield* runOpenCodeSdk("session.messages", () =>
14161413 context.client.session.messages({
14171414 sessionID: context.openCodeSessionId,