import type { OVClient } from "./client.js"; import { appendFileSync, mkdirSync } from "node:fs"; import { dirname } from "node:path"; import type { OVConfig } from "./config.js"; import { deriveHarnessSessionId } from "./shared/session-model.mjs"; import { enqueue, listPending, replayPending } from "./shared/pending-queue.mjs"; import { extractBranchCapturePayloads } from "./lib/capture-adapter.mjs"; import { countUndeliveredForSession, estimatePayloadTokens } from "./lib/takeover-core.mjs"; // --- SyncManager --- export interface AddPayloadResult { accepted: boolean; delivered: boolean; } export interface SyncBranchResult { added: number; tokens: number; allDelivered: boolean; } function debugLog(message: string): void { const file = process.env.OV_DEBUG_LOG; if (!file) return; try { mkdirSync(dirname(file), { recursive: true }); appendFileSync(file, `${new Date().toISOString()} ${message}\n`); } catch { // Best effort; logging must never affect pi. } } export class SyncManager { private client: OVClient; private config: OVConfig; private ovSessionId: string | null = null; private syncedEntryCount = 0; constructor(client: OVClient, config: OVConfig) { this.client = client; this.config = config; } get sessionId(): string | null { return this.ovSessionId; } get syncedCount(): number { return this.syncedEntryCount; } restoreWatermark(n: number): void { const next = Math.max(0, Math.floor(Number(n) || 0)); this.syncedEntryCount = next; } async ensureSession(piSessionId: string): Promise { if (this.ovSessionId) return true; const id = deriveHarnessSessionId("pi-", piSessionId); this.ovSessionId = id; return true; } async replayPending(): Promise { if (!this.client.connected) return; await replayPending( (path: string, init?: any) => this.client.fetchJSON(path, init, 10000), (stage: string, data: unknown) => debugLog(`${stage}: ${JSON.stringify(data)}`), ); } async flushForTakeover(): Promise { if (!this.ovSessionId) return false; await this.replayPending(); const pending = await listPending(); return countUndeliveredForSession(pending, this.ovSessionId) === 0; } async syncBranch(branch: any[]): Promise { if (!this.ovSessionId) return { added: 0, tokens: 0, allDelivered: true }; const extracted = extractBranchCapturePayloads(branch, this.syncedEntryCount, this.config); if (extracted.resetWatermark) this.syncedEntryCount = 0; let added = 0; let tokens = 0; let allDelivered = true; for (const payload of extracted.payloads) { const result = await this.addPayload(payload); if (!result.accepted) break; added++; tokens += estimatePayloadTokens(payload); allDelivered = allDelivered && result.delivered; } if (added === extracted.payloads.length) { this.syncedEntryCount = extracted.nextEntryCount; } if (added > 0 && !this.config.takeoverEnabled) { await this.commitIfNeeded(); } return { added, tokens, allDelivered }; } async addPayload(payload: any): Promise { if (!this.ovSessionId) return { accepted: false, delivered: false }; const ok = await this.client.addMessagePayload(this.ovSessionId, payload); if (ok) return { accepted: true, delivered: true }; await enqueue("addMessage", this.ovSessionId, payload); return { accepted: true, delivered: false }; } async commitIfNeeded(): Promise { if (!this.ovSessionId) return; const meta = await this.client.getSession(this.ovSessionId); const pending = Number(meta?.pending_tokens || 0); if (pending >= this.config.commitTokenThreshold) { await this.commit(); } } async commit(opts: { queueOnFailure?: boolean; keepRecentCount?: number } = {}): Promise { if (!this.ovSessionId) return null; const response = await this.client.commitSessionResponse( this.ovSessionId, opts.keepRecentCount, ); const result = response.result; if (!result) { debugLog( `commit: session=${this.ovSessionId} ok=false status=${response.status ?? 0} ` + `trace_id=${response.traceId || "none"} ` + `error=${response.error?.message || response.error?.code || "unknown"}`, ); if (opts.queueOnFailure !== false) { await enqueue("commitSession", this.ovSessionId, { keep_recent_count: opts.keepRecentCount ?? this.config.commitKeepRecentCount, }); } return null; } debugLog( `commit: session=${this.ovSessionId} ok=true trace_id=${result.trace_id || "none"}`, ); return result; } async shutdown(): Promise { return; } }