Skip to content

Commit a00a8af

Browse files
committed
fix(providers): bound failed admission and serialize reply cleanup
1 parent 1984876 commit a00a8af

3 files changed

Lines changed: 56 additions & 9 deletions

File tree

‎apps/server/src/provider/Layers/OpenCodeAdapter.test.ts‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1560,6 +1560,54 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
15601560
}),
15611561
);
15621562

1563+
it.effect("bounds native admission recovery when timeout cleanup cannot abort the session", () =>
1564+
Effect.gen(function* () {
1565+
const adapter = yield* OpenCodeAdapter;
1566+
const threadId = asThreadId("thread-native-command-timeout-abort-failure");
1567+
const started = promiseWithResolvers<void>();
1568+
runtimeMock.state.commandImplementation = async () => {
1569+
started.resolve(undefined);
1570+
await new Promise<void>(() => {});
1571+
};
1572+
runtimeMock.state.abortImplementation = async () => {
1573+
throw new Error("abort failed");
1574+
};
1575+
runtimeMock.state.sessionStatus = "idle";
1576+
yield* adapter.startSession({
1577+
provider: ProviderDriverKind.make("opencode"),
1578+
threadId,
1579+
runtimeMode: "full-access",
1580+
});
1581+
const exitedFiber = yield* adapter.streamEvents.pipe(
1582+
Stream.filter((event) => event.threadId === threadId && event.type === "session.exited"),
1583+
Stream.runHead,
1584+
Effect.forkChild,
1585+
);
1586+
const sendFiber = yield* adapter
1587+
.sendTurn({
1588+
threadId,
1589+
input: "/review",
1590+
modelSelection: createModelSelection(ProviderInstanceId.make("opencode"), "openai/gpt-5"),
1591+
})
1592+
.pipe(Effect.exit, Effect.forkChild);
1593+
yield* Effect.promise(() => started.promise);
1594+
yield* advanceTestClock(10_000);
1595+
NodeAssert.equal(Exit.isFailure(yield* Fiber.join(sendFiber)), true);
1596+
yield* advanceTestClock(6_000);
1597+
const exited = Option.getOrThrow(yield* Fiber.join(exitedFiber));
1598+
NodeAssert.ok(exited.type === "session.exited");
1599+
NodeAssert.equal(exited.payload.exitKind, "error");
1600+
NodeAssert.ok(runtimeMock.state.sessionStatusCalls > 0);
1601+
NodeAssert.equal(
1602+
(yield* adapter.listSessions()).some((session) => session.threadId === threadId),
1603+
false,
1604+
);
1605+
const messageCalls = runtimeMock.state.messageCalls.length;
1606+
yield* advanceTestClock(5_000);
1607+
NodeAssert.equal(runtimeMock.state.messageCalls.length, messageCalls);
1608+
}),
1609+
);
1610+
15631611
for (const nativeStartsTurn of [true, false]) {
15641612
it.effect(
15651613
`reports a late native command failure after another steer (starts turn: ${nativeStartsTurn})`,

‎apps/server/src/provider/Layers/OpenCodeAdapter.ts‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ interface OpenCodePromptAdmission {
216216
readonly generation: number;
217217
readonly turnId: TurnId;
218218
readonly messageId: string;
219-
readonly requiresMessageReceipt: boolean;
219+
requiresMessageReceipt: boolean;
220220
readonly priorAwaitingBusy: boolean;
221221
readonly priorIdle: { readonly turnId: TurnId; readonly raw: unknown } | undefined;
222222
idleDuringAdmission: { readonly turnId: TurnId; readonly raw: unknown } | undefined;
@@ -1490,9 +1490,7 @@ export function makeOpenCodeAdapter(
14901490
const delayMs = Math.min(250 * 2 ** retryCount, 2_000);
14911491
yield* Effect.sleep(`${delayMs} millis`);
14921492
}
1493-
if (promptAdmission.accepted) {
1494-
yield* failPromptAdmissionRecovery(context, promptAdmission);
1495-
}
1493+
yield* failPromptAdmissionRecovery(context, promptAdmission);
14961494
}).pipe(
14971495
Effect.catchCause(() => Effect.void),
14981496
Effect.ensuring(
@@ -3306,11 +3304,12 @@ export function makeOpenCodeAdapter(
33063304
);
33073305
},
33083306
}),
3309-
Effect.tapError(() =>
3310-
nativeCommand && promptAdmission.recoveryFiber
3307+
Effect.tapError(() => {
3308+
promptAdmission.requiresMessageReceipt = false;
3309+
return nativeCommand && promptAdmission.recoveryFiber
33113310
? Fiber.interrupt(promptAdmission.recoveryFiber)
3312-
: Effect.void,
3313-
),
3311+
: Effect.void;
3312+
}),
33143313
Effect.tapError((requestError) => {
33153314
if (
33163315
nativeCommand &&

‎apps/server/src/provider/acp/AcpSessionRuntime.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1018,7 +1018,7 @@ export const make = (
10181018
yield* Deferred.succeed(promptOptions.dispatched, undefined);
10191019
}
10201020
return active;
1021-
}),
1021+
}).pipe(notificationSemaphore.withPermit),
10221022
),
10231023
(activePrompt) =>
10241024
Fiber.join(activePrompt.fiber).pipe(

0 commit comments

Comments
 (0)