Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 225 additions & 1 deletion apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ describe("ProviderCommandReactor", () => {
turnId: asTurnId("turn-1"),
}),
);
const interruptTurn = vi.fn((_: unknown) => Effect.void);
const interruptTurn = vi.fn<ProviderServiceShape["interruptTurn"]>(() => Effect.void);
const respondToRequest = vi.fn<ProviderServiceShape["respondToRequest"]>(() => Effect.void);
const respondToUserInput = vi.fn<ProviderServiceShape["respondToUserInput"]>(() => Effect.void);
const stopSession = vi.fn((input: unknown) =>
Expand Down Expand Up @@ -2937,4 +2937,228 @@ describe("ProviderCommandReactor", () => {
expect(thread?.session?.providerInstanceId).toBe(ProviderInstanceId.make("codex_work"));
expect(thread?.session?.activeTurnId).toBeNull();
});

it("forwards approval declines without interrupting the provider turn", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

await harness.runEffect(
harness.engine.dispatch({
type: "thread.session.set",
commandId: CommandId.make("cmd-session-set-for-approval-decline"),
threadId: ThreadId.make("thread-1"),
session: {
threadId: ThreadId.make("thread-1"),
status: "running",
providerName: "codex",
runtimeMode: "approval-required",
activeTurnId: null,
lastError: null,
updatedAt: now,
},
createdAt: now,
}),
);

await harness.runEffect(
harness.engine.dispatch({
type: "thread.approval.respond",
commandId: CommandId.make("cmd-approval-decline"),
threadId: ThreadId.make("thread-1"),
requestId: asApprovalRequestId("approval-request-1"),
decision: "decline",
createdAt: now,
}),
);

await waitFor(() => harness.respondToRequest.mock.calls.length === 1);
await harness.drain();
expect(harness.respondToRequest.mock.calls[0]?.[0]).toEqual({
threadId: "thread-1",
requestId: "approval-request-1",
decision: "decline",
});
expect(harness.interruptTurn).not.toHaveBeenCalled();
});

it("responds to approval cancellation before interrupting the provider turn", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";
const callOrder: Array<"respond" | "interrupt"> = [];
harness.respondToRequest.mockImplementation(() =>
Effect.sync(() => {
callOrder.push("respond");
}),
);
harness.interruptTurn.mockImplementation(() =>
Effect.sync(() => {
callOrder.push("interrupt");
}),
);

await harness.runEffect(
harness.engine.dispatch({
type: "thread.session.set",
commandId: CommandId.make("cmd-session-set-for-approval-cancel"),
threadId: ThreadId.make("thread-1"),
session: {
threadId: ThreadId.make("thread-1"),
status: "running",
providerName: "codex",
runtimeMode: "approval-required",
activeTurnId: asTurnId("turn-1"),
lastError: null,
updatedAt: now,
},
createdAt: now,
}),
);

await harness.runEffect(
harness.engine.dispatch({
type: "thread.approval.respond",
commandId: CommandId.make("cmd-approval-cancel"),
threadId: ThreadId.make("thread-1"),
requestId: asApprovalRequestId("approval-request-1"),
decision: "cancel",
createdAt: now,
}),
);

await waitFor(() => harness.interruptTurn.mock.calls.length === 1);
await harness.drain();
expect(harness.respondToRequest.mock.calls[0]?.[0]).toEqual({
threadId: "thread-1",
requestId: "approval-request-1",
decision: "cancel",
});
expect(harness.interruptTurn.mock.calls[0]?.[0]).toEqual({
threadId: "thread-1",
});
expect(callOrder).toEqual(["respond", "interrupt"]);
});

it("does not interrupt when the approval cancellation response fails", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";
harness.respondToRequest.mockImplementation(() =>
Effect.fail(
new ProviderAdapterRequestError({
provider: ProviderDriverKind.make("codex"),
method: "session/request_permission",
detail: "Approval response failed",
}),
),
);

await harness.runEffect(
harness.engine.dispatch({
type: "thread.session.set",
commandId: CommandId.make("cmd-session-set-for-failed-approval-cancel"),
threadId: ThreadId.make("thread-1"),
session: {
threadId: ThreadId.make("thread-1"),
status: "running",
providerName: "codex",
runtimeMode: "approval-required",
activeTurnId: asTurnId("turn-1"),
lastError: null,
updatedAt: now,
},
createdAt: now,
}),
);

await harness.runEffect(
harness.engine.dispatch({
type: "thread.approval.respond",
commandId: CommandId.make("cmd-approval-cancel-response-failed"),
threadId: ThreadId.make("thread-1"),
requestId: asApprovalRequestId("approval-request-1"),
decision: "cancel",
createdAt: now,
}),
);

await waitFor(async () => {
const readModel = await harness.readModel();
const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"));
return (
thread?.activities.some(
(activity) => activity.kind === "provider.approval.respond.failed",
) ?? false
);
});
await harness.drain();

expect(harness.respondToRequest).toHaveBeenCalledTimes(1);
expect(harness.interruptTurn).not.toHaveBeenCalled();
});

it("records a failed interrupt after a successful approval cancellation", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";
harness.interruptTurn.mockImplementation(() =>
Effect.fail(
new ProviderAdapterRequestError({
provider: ProviderDriverKind.make("codex"),
method: "turn/interrupt",
detail: "Turn interrupt failed",
}),
),
);

await harness.runEffect(
harness.engine.dispatch({
type: "thread.session.set",
commandId: CommandId.make("cmd-session-set-for-failed-approval-interrupt"),
threadId: ThreadId.make("thread-1"),
session: {
threadId: ThreadId.make("thread-1"),
status: "running",
providerName: "codex",
runtimeMode: "approval-required",
activeTurnId: asTurnId("turn-1"),
lastError: null,
updatedAt: now,
},
createdAt: now,
}),
);

await harness.runEffect(
harness.engine.dispatch({
type: "thread.approval.respond",
commandId: CommandId.make("cmd-approval-cancel-interrupt-failed"),
threadId: ThreadId.make("thread-1"),
requestId: asApprovalRequestId("approval-request-1"),
decision: "cancel",
createdAt: now,
}),
);

await waitFor(async () => {
const readModel = await harness.readModel();
const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"));
return (
thread?.activities.some((activity) => activity.kind === "provider.turn.interrupt.failed") ??
false
);
});
await harness.drain();

const readModel = await harness.readModel();
const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"));
expect(harness.respondToRequest).toHaveBeenCalledTimes(1);
expect(harness.interruptTurn).toHaveBeenCalledTimes(1);
expect(
thread?.activities.find((activity) => activity.kind === "provider.turn.interrupt.failed"),
).toMatchObject({
summary: "Provider turn interrupt failed",
turnId: "turn-1",
payload: {
detail: expect.stringContaining("Turn interrupt failed"),
},
});
});
});
18 changes: 17 additions & 1 deletion apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1227,13 +1227,14 @@ const make = Effect.gen(function* () {
});
}

yield* providerService
const responseSucceeded = yield* providerService
.respondToRequest({
threadId: event.payload.threadId,
requestId: event.payload.requestId,
decision: event.payload.decision,
})
.pipe(
Effect.as(true),
Effect.catchCause((cause) =>
appendProviderFailureActivity({
threadId: event.payload.threadId,
Expand All @@ -1245,9 +1246,24 @@ const make = Effect.gen(function* () {
turnId: null,
createdAt: event.payload.createdAt,
requestId: event.payload.requestId,
}).pipe(Effect.as(false)),
),
);

if (responseSucceeded && event.payload.decision === "cancel") {
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
yield* providerService.interruptTurn({ threadId: event.payload.threadId }).pipe(
Effect.catchCause((cause) =>
appendProviderFailureActivity({
threadId: event.payload.threadId,
kind: "provider.turn.interrupt.failed",
summary: "Provider turn interrupt failed",
detail: Cause.pretty(cause),
turnId: thread.session?.activeTurnId ?? null,
createdAt: event.payload.createdAt,
}),
),
);
}
});

const processUserInputResponseRequested = Effect.fn("processUserInputResponseRequested")(
Expand Down
Loading