import { createHash } from "node:crypto"; import type { ResolvedComboTarget } from "./types.ts"; type NativeTurnPin = { comboName: string; modelStr: string; provider: string; connectionId: string; createdAt: number; expiresAt: number; }; const TTL_MS = 45 * 60_000; const MAX_PINS = 1_000; const pins = new Map(); function record(value: unknown): Record | undefined { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : undefined; } function turnMetadata(body: Record): Record | undefined { const metadata = record(body.client_metadata); const raw = metadata?.["x-codex-turn-metadata"]; if (typeof raw === "string") { try { return record(JSON.parse(raw)); } catch { return undefined; } } return record(raw); } export function nativeCodexTurnKey( body: Record, comboName: string ): string | null { const metadata = turnMetadata(body); const threadId = typeof metadata?.thread_id === "string" ? metadata.thread_id : ""; const turnId = typeof metadata?.turn_id === "string" ? metadata.turn_id : ""; if (!threadId || !turnId) return null; return createHash("sha256").update(JSON.stringify({ comboName, threadId, turnId })).digest("hex"); } function prune(now = Date.now()): void { for (const [key, pin] of pins) if (pin.expiresAt <= now) pins.delete(key); while (pins.size > MAX_PINS) { const oldest = pins.keys().next().value as string | undefined; if (!oldest) break; pins.delete(oldest); } } export function getNativeCodexTurnPin( body: Record, comboName: string ): NativeTurnPin | null { prune(); const key = nativeCodexTurnKey(body, comboName); return key ? (pins.get(key) ?? null) : null; } export function pinNativeCodexTurn(args: { body: Record; comboName: string; target: ResolvedComboTarget; connectionId: string; }): void { const key = nativeCodexTurnKey(args.body, args.comboName); if (!key || !args.connectionId) return; const existing = pins.get(key); if ( existing && (existing.modelStr !== args.target.modelStr || existing.provider !== args.target.provider) ) { throw new Error("Native Codex turn target changed after output was emitted"); } // ConnectionId changes are allowed (failover to sibling connection) // as long as provider + model stay the same. const now = Date.now(); pins.set(key, { comboName: args.comboName, modelStr: args.target.modelStr, provider: args.target.provider, connectionId: args.connectionId, createdAt: existing?.createdAt ?? now, expiresAt: now + TTL_MS, }); prune(now); } /** * Apply a native Codex turn pin to the target list. * * Returns all compatible targets (same provider + model) with the pinned * connection preferred first. This allows fill-first failover: if the * pinned connection is rejected by a pre-dispatch gate, the combo engine * tries the next compatible connection instead of returning 503. * * Provider + model remain locked for the turn — only the connection * can fall over. */ export function applyNativeCodexTurnPin( targets: ResolvedComboTarget[], pin: NativeTurnPin ): ResolvedComboTarget[] { const compatible = targets.filter( (candidate) => candidate.modelStr === pin.modelStr && candidate.provider === pin.provider ); if (compatible.length === 0) return []; let pinnedIndex = compatible.findIndex((t) => t.connectionId === pin.connectionId); // No candidate already carries the pinned connectionId (e.g. the caller // resolved the target before a connection was assigned) — assign the pin // onto the first compatible candidate so dispatch targets it directly. if (pinnedIndex < 0) pinnedIndex = 0; // Resolve the pinned slot's connectionId in ORIGINAL order first, so // allowedConnectionIds reflects the same set/order regardless of which // candidate ends up first in the returned (pinned-first) array. const resolved = compatible.map((t, i) => i === pinnedIndex ? { ...t, connectionId: pin.connectionId } : t ); const allowedConnectionIds = resolved .map((t) => t.connectionId) .filter((id): id is string => id !== null); // Pinned connection first, then same-provider/model siblings as fallback const pinned = resolved[pinnedIndex]; const siblings = resolved.filter((_, i) => i !== pinnedIndex); const ordered = [pinned, ...siblings]; return ordered.map((target) => ({ ...target, // Allow only connections for the pinned provider+model allowedConnectionIds, })); } export function revokeNativeCodexTurnPinsForConnection(connectionId: string): number { let revoked = 0; for (const [key, pin] of pins) { if (pin.connectionId !== connectionId) continue; pins.delete(key); revoked += 1; } return revoked; } export function clearNativeCodexTurnPinsForTests(): void { pins.clear(); }