| 1 | import type * as CodexSchema from "effect-codex-app-server/schema"; |
| 2 | import type * as CodexClient from "effect-codex-app-server/client"; |
| 3 | import * as CodexErrors from "effect-codex-app-server/errors"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | |
| 6 | /** Codex 0.156 replaces count-based rollback with a boundary in paginated history. */ |
| 7 | export 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 | }); |