1import type * as CodexSchema from "effect-codex-app-server/schema";
2import type * as CodexClient from "effect-codex-app-server/client";
3import * as CodexErrors from "effect-codex-app-server/errors";
4import * as Effect from "effect/Effect";
5
6/** Codex 0.156 replaces count-based rollback with a boundary in paginated history. */
7export const revertCodexThread = Effect.fn("revertCodexThread")(function* (
8 client: Pick<CodexClient.CodexAppServerClient["Service"], "request">,
9 threadId: string,
10 numTurns: number,
11) {
12 let remaining = numTurns;
13 let beforeTurnId: string | undefined;
14 let cursor: string | null = null;
15 const visited = new Set<string | null>();
16 while (remaining > 0) {
17 if (visited.has(cursor)) {
18 return yield* CodexErrors.CodexAppServerRequestError.internalError(
19 "Thread history pagination repeated a cursor.",
20 undefined,
21 { method: "thread/turns/list", operation: "decode-payload" },
22 );
23 }
24 visited.add(cursor);
25 const page: CodexSchema.V2ThreadTurnsListResponse = yield* client.request("thread/turns/list", {
26 threadId,
27 cursor,
28 limit: Math.min(remaining, 100),
29 sortDirection: "desc",
30 itemsView: "summary",
31 });
32 for (const turn of page.data) {
33 beforeTurnId = turn.id;
34 if (--remaining === 0) break;
35 }
36 cursor = page.nextCursor ?? null;
37 if (cursor === null) break;
38 }
39 return beforeTurnId === undefined
40 ? yield* client.request("thread/read", { threadId, includeTurns: false })
41 : yield* client.request("thread/revert", { threadId, beforeTurnId });
42});