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) => {...@@ -248,6 +248,38 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
248 }),248 }),
249 );249 );
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
251 it.effect("stops a configured-server session without trying to own server lifecycle", () =>283 it.effect("stops a configured-server session without trying to own server lifecycle", () =>
252 Effect.gen(function* () {284 Effect.gen(function* () {
253 const adapter = yield* OpenCodeAdapter;285 const adapter = yield* OpenCodeAdapter;
apps/server/src/provider/Layers/OpenCodeAdapter.ts+13-16
...@@ -228,28 +228,25 @@ function appendTurnItem(...@@ -228,28 +228,25 @@ function appendTurnItem(
228 resolveTurnSnapshot(context, turnId).items.push(item);228 resolveTurnSnapshot(context, turnId).items.push(item);
229}229}
230230
231function ensureSessionContext(231const ensureSessionContext = Effect.fn("ensureSessionContext")(function* (
232 sessions: ReadonlyMap<ThreadId, OpenCodeSessionContext>,232 sessions: ReadonlyMap<ThreadId, OpenCodeSessionContext>,
233 threadId: ThreadId,233 threadId: ThreadId,
234): OpenCodeSessionContext {234) {
235 const session = sessions.get(threadId);235 const session = sessions.get(threadId);
236 if (!session) {236 if (!session) {
237 throw new ProviderAdapterSessionNotFoundError({237 return yield* new ProviderAdapterSessionNotFoundError({
238 provider: PROVIDER,238 provider: PROVIDER,
239 threadId,239 threadId,
240 });240 });
241 }241 }
242 // `ensureSessionContext` is a sync gate used from both sync helpers and242 if (yield* Ref.get(session.stopped)) {
243 // Effect bodies. `Ref.getUnsafe` is an atomic read of the backing cell —243 return yield* new ProviderAdapterSessionClosedError({
244 // no fiber suspension required, which keeps this callable everywhere.
245 if (Ref.getUnsafe(session.stopped)) {
246 throw new ProviderAdapterSessionClosedError({
247 provider: PROVIDER,244 provider: PROVIDER,
248 threadId,245 threadId,
249 });246 });
250 }247 }
251 return session;248 return session;
252}249});
253250
254function normalizeQuestionRequest(request: QuestionRequest): ReadonlyArray<UserInputQuestion> {251function normalizeQuestionRequest(request: QuestionRequest): ReadonlyArray<UserInputQuestion> {
255 return request.questions.map((question, index) => ({252 return request.questions.map((question, index) => ({
...@@ -1167,7 +1164,7 @@ export function makeOpenCodeAdapter(...@@ -1167,7 +1164,7 @@ export function makeOpenCodeAdapter(
1167 );1164 );
11681165
1169 const sendTurn: OpenCodeAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) {1166 const sendTurn: OpenCodeAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) {
1170 const context = ensureSessionContext(sessions, input.threadId);1167 const context = yield* ensureSessionContext(sessions, input.threadId);
1171 // A sendTurn while a turn is active is a steer: OpenCode queues the1168 // A sendTurn while a turn is active is a steer: OpenCode queues the
1172 // prompt into the busy session and the work continues as one turn, so1169 // prompt into the busy session and the work continues as one turn, so
1173 // the active turn id is reused instead of opening a new turn.1170 // the active turn id is reused instead of opening a new turn.
...@@ -1291,7 +1288,7 @@ export function makeOpenCodeAdapter(...@@ -1291,7 +1288,7 @@ export function makeOpenCodeAdapter(
12911288
1292 const interruptTurn: OpenCodeAdapterShape["interruptTurn"] = Effect.fn("interruptTurn")(1289 const interruptTurn: OpenCodeAdapterShape["interruptTurn"] = Effect.fn("interruptTurn")(
1293 function* (threadId, turnId) {1290 function* (threadId, turnId) {
1294 const context = ensureSessionContext(sessions, threadId);1291 const context = yield* ensureSessionContext(sessions, threadId);
1295 yield* runOpenCodeSdk("session.abort", () =>1292 yield* runOpenCodeSdk("session.abort", () =>
1296 context.client.session.abort({ sessionID: context.openCodeSessionId }),1293 context.client.session.abort({ sessionID: context.openCodeSessionId }),
1297 ).pipe(Effect.mapError(toRequestError));1294 ).pipe(Effect.mapError(toRequestError));
...@@ -1313,7 +1310,7 @@ export function makeOpenCodeAdapter(...@@ -1313,7 +1310,7 @@ export function makeOpenCodeAdapter(
1313 const respondToRequest: OpenCodeAdapterShape["respondToRequest"] = Effect.fn(1310 const respondToRequest: OpenCodeAdapterShape["respondToRequest"] = Effect.fn(
1314 "respondToRequest",1311 "respondToRequest",
1315 )(function* (threadId, requestId, decision) {1312 )(function* (threadId, requestId, decision) {
1316 const context = ensureSessionContext(sessions, threadId);1313 const context = yield* ensureSessionContext(sessions, threadId);
1317 if (!context.pendingPermissions.has(requestId)) {1314 if (!context.pendingPermissions.has(requestId)) {
1318 return yield* new ProviderAdapterRequestError({1315 return yield* new ProviderAdapterRequestError({
1319 provider: PROVIDER,1316 provider: PROVIDER,
...@@ -1333,7 +1330,7 @@ export function makeOpenCodeAdapter(...@@ -1333,7 +1330,7 @@ export function makeOpenCodeAdapter(
1333 const respondToUserInput: OpenCodeAdapterShape["respondToUserInput"] = Effect.fn(1330 const respondToUserInput: OpenCodeAdapterShape["respondToUserInput"] = Effect.fn(
1334 "respondToUserInput",1331 "respondToUserInput",
1335 )(function* (threadId, requestId, answers) {1332 )(function* (threadId, requestId, answers) {
1336 const context = ensureSessionContext(sessions, threadId);1333 const context = yield* ensureSessionContext(sessions, threadId);
1337 const request = context.pendingQuestions.get(requestId);1334 const request = context.pendingQuestions.get(requestId);
1338 if (!request) {1335 if (!request) {
1339 return yield* new ProviderAdapterRequestError({1336 return yield* new ProviderAdapterRequestError({
...@@ -1355,7 +1352,7 @@ export function makeOpenCodeAdapter(...@@ -1355,7 +1352,7 @@ export function makeOpenCodeAdapter(
1355 function* (threadId) {1352 function* (threadId) {
1356 const context = sessions.get(threadId);1353 const context = sessions.get(threadId);
1357 if (!context) {1354 if (!context) {
1358 throw new ProviderAdapterSessionNotFoundError({1355 return yield* new ProviderAdapterSessionNotFoundError({
1359 provider: PROVIDER,1356 provider: PROVIDER,
1360 threadId,1357 threadId,
1361 });1358 });
...@@ -1385,7 +1382,7 @@ export function makeOpenCodeAdapter(...@@ -1385,7 +1382,7 @@ export function makeOpenCodeAdapter(
13851382
1386 const readThread: OpenCodeAdapterShape["readThread"] = Effect.fn("readThread")(1383 const readThread: OpenCodeAdapterShape["readThread"] = Effect.fn("readThread")(
1387 function* (threadId) {1384 function* (threadId) {
1388 const context = ensureSessionContext(sessions, threadId);1385 const context = yield* ensureSessionContext(sessions, threadId);
1389 const messages = yield* runOpenCodeSdk("session.messages", () =>1386 const messages = yield* runOpenCodeSdk("session.messages", () =>
1390 context.client.session.messages({1387 context.client.session.messages({
1391 sessionID: context.openCodeSessionId,1388 sessionID: context.openCodeSessionId,
...@@ -1411,7 +1408,7 @@ export function makeOpenCodeAdapter(...@@ -1411,7 +1408,7 @@ export function makeOpenCodeAdapter(
14111408
1412 const rollbackThread: OpenCodeAdapterShape["rollbackThread"] = Effect.fn("rollbackThread")(1409 const rollbackThread: OpenCodeAdapterShape["rollbackThread"] = Effect.fn("rollbackThread")(
1413 function* (threadId, numTurns) {1410 function* (threadId, numTurns) {
1414 const context = ensureSessionContext(sessions, threadId);1411 const context = yield* ensureSessionContext(sessions, threadId);
1415 const messages = yield* runOpenCodeSdk("session.messages", () =>1412 const messages = yield* runOpenCodeSdk("session.messages", () =>
1416 context.client.session.messages({1413 context.client.session.messages({
1417 sessionID: context.openCodeSessionId,1414 sessionID: context.openCodeSessionId,