UNPKG

@phren/cli

Version:

Knowledge layer for AI agents - CLI, MCP server, and data layer

75 lines (74 loc) • 3.51 kB
import { open } from "node:fs/promises"; import path from "node:path"; // How far back from the end of a rollout to look for the session's settings. // A long Codex session writes tens of megabytes; one turn rarely spans more. const SCAN_LIMIT = 64 * 1_048_576; const CHUNK = 1_048_576; /** Whether Codex's automatic reviewer, not the owner, decides this session's * approval requests. Codex runs the PermissionRequest hook before its reviewer * and the payload does not say which one will decide (permission_mode is * "default" either way), so read the settings the session itself recorded: the * latest turn_context or thread_settings_applied line in its rollout, which * follows /approvals changes mid-session. Codex routes a request to the * reviewer only with approvals_reviewer "auto_review" and an on-request or * granular approval policy; anything else, or a rollout that can't be read, * keeps the owner as the approver. */ export async function codexAutoReview(transcriptPath) { if (typeof transcriptPath !== "string" || !path.isAbsolute(transcriptPath) || !/^rollout-.*\.jsonl$/.test(path.basename(transcriptPath))) return false; const file = await open(transcriptPath, "r").catch(() => undefined); if (!file) return false; try { const size = (await file.stat()).size; let end = size, carry = Buffer.alloc(0); while (end > 0 && size - end < SCAN_LIMIT) { const start = Math.max(0, end - CHUNK), buffer = Buffer.alloc(end - start); await file.read(buffer, 0, buffer.length, start); const bytes = Buffer.concat([buffer, carry]); // Lines newest first; the bytes before the first newline may continue // a line that began in an earlier chunk, so they carry over. let lineEnd = bytes.length; for (let at = bytes.lastIndexOf(10, lineEnd - 1); at >= 0; at = bytes.lastIndexOf(10, lineEnd - 1)) { const settings = reviewSettings(bytes.subarray(at + 1, lineEnd).toString("utf8")); if (settings) return autoReviewed(settings); lineEnd = at; if (at === 0) break; } carry = bytes.subarray(0, lineEnd); end = start; } const settings = end === 0 ? reviewSettings(carry.toString("utf8")) : undefined; return !!settings && autoReviewed(settings); } catch { return false; } finally { await file.close().catch(() => { }); } } function reviewSettings(line) { if (!line.includes("\"approvals_reviewer\"") || !(line.includes("\"type\":\"turn_context\"") || line.includes("\"type\":\"thread_settings_applied\""))) return undefined; try { const entry = JSON.parse(line); const payload = entry?.payload; const settings = entry?.type === "turn_context" ? payload : entry?.type === "event_msg" && payload?.type === "thread_settings_applied" ? payload.thread_settings : undefined; if (!settings || !("approvals_reviewer" in settings)) return undefined; return { reviewer: settings.approvals_reviewer, policy: settings.approval_policy }; } catch { return undefined; } } function autoReviewed({ reviewer, policy }) { return reviewer === "auto_review" && (policy === "on-request" || (!!policy && typeof policy === "object" && "granular" in policy)); }