import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js"; import { ChaosMonkey } from "./chaos-monkey.js"; import { writeTurnCloseoutGitRecord, writeTurnGitTransaction, } from "./gitops.js"; import { acquireWriterToken, nextWriteRecord, releaseWriterToken, } from "./writer.js"; const GITOPS_TIMEOUT_MS = 10_000; function writeGitTransactionWithTimeout(args) { return Promise.race([ writeTurnGitTransaction(args), new Promise((_, reject) => setTimeout( () => reject(new Error("Git transaction timed out")), GITOPS_TIMEOUT_MS, ), ), ]); } export function createTurnObserver(options) { let current = null; let writerToken = null; const phaseResults = []; const chaosMonkey = options.enableChaosMonkey ? new ChaosMonkey() : null; /** * Enrich metadata with write sequence info when a writer token is active. * * Purpose: Provide audit/traceability by attaching sequence numbers to * gitops and audit metadata. When no token is active (e.g., early in * turn setup), returns metadata unchanged. * * @param {string} category — e.g., "gitops", "audit" * @param {string} operation — e.g., "insert", "update" * @param {object} [metadata] — caller-provided metadata * @returns {object} metadata with optional writeSequence and writerTokenId */ function nextSequenceMetadata(category, operation, metadata) { if (!writerToken) return metadata ?? {}; const record = nextWriteRecord({ basePath: options.basePath, token: writerToken, category, operation, metadata, }); return { ...(metadata ?? {}), writeSequence: record.sequence.sequence, writerTokenId: record.writerToken.tokenId, }; } return { onTurnStart(contract) { if (chaosMonkey) chaosMonkey.strike("turn-start"); current = { ...contract, runControl: options.runControl, permissionProfile: options.permissionProfile, }; phaseResults.length = 0; writerToken = acquireWriterToken({ basePath: options.basePath, traceId: current.traceId, turnId: current.turnId, }); if (options.enableGitops) { writeGitTransactionWithTimeout({ basePath: options.basePath, traceId: current.traceId, turnId: current.turnId, unitType: current.unitType, unitId: current.unitId, stage: "turn-start", action: options.gitAction, push: options.gitPush, status: "ok", metadata: nextSequenceMetadata("gitops", "insert", { iteration: current.iteration, sidecarKind: current.sidecarKind, runControl: current.runControl, permissionProfile: current.permissionProfile, }), }).catch((err) => { console.error(`[loop-adapter] Git transaction failed: ${err.message}`); }); } if (options.enableAudit) { emitUokAuditEvent( options.basePath, buildAuditEnvelope({ traceId: current.traceId, turnId: current.turnId, category: "orchestration", type: "turn-start", payload: nextSequenceMetadata("audit", "append", { iteration: current.iteration, unitType: current.unitType, unitId: current.unitId, sidecarKind: current.sidecarKind, runControl: current.runControl, permissionProfile: current.permissionProfile, }), }), ); } }, onPhaseResult(phase, action, data) { if (chaosMonkey) chaosMonkey.strike(`after-${phase}`); phaseResults.push({ phase, action, ts: new Date().toISOString(), data, }); if (!current || !options.enableGitops) return; if (phase === "dispatch") { writeGitTransactionWithTimeout({ basePath: options.basePath, traceId: current.traceId, turnId: current.turnId, unitType: data?.unitType, unitId: data?.unitId, stage: "stage", action: options.gitAction, push: options.gitPush, status: "ok", metadata: nextSequenceMetadata("gitops", "update", { action }), }).catch((err) => { console.error(`[loop-adapter] Git transaction failed: ${err.message}`); }); } if (phase === "unit") { writeGitTransactionWithTimeout({ basePath: options.basePath, traceId: current.traceId, turnId: current.turnId, unitType: data?.unitType, unitId: data?.unitId, stage: "checkpoint", action: options.gitAction, push: options.gitPush, status: "ok", metadata: nextSequenceMetadata("gitops", "update", { action }), }).catch((err) => { console.error(`[loop-adapter] Git transaction failed: ${err.message}`); }); } if (phase === "finalize") { writeGitTransactionWithTimeout({ basePath: options.basePath, traceId: current.traceId, turnId: current.turnId, unitType: data?.unitType, unitId: data?.unitId, stage: "publish", action: options.gitAction, push: options.gitPush, status: "ok", metadata: nextSequenceMetadata("gitops", "update", { action }), }).catch((err) => { console.error(`[loop-adapter] Git transaction failed: ${err.message}`); }); } }, onTurnResult(result) { const merged = { runControl: options.runControl, permissionProfile: options.permissionProfile, ...result, phaseResults: result.phaseResults.length > 0 ? result.phaseResults : [...phaseResults], }; if (options.enableAudit) { emitUokAuditEvent( options.basePath, buildAuditEnvelope({ traceId: merged.traceId, turnId: merged.turnId, category: "orchestration", type: "turn-result", payload: nextSequenceMetadata("audit", "append", { unitType: merged.unitType, unitId: merged.unitId, status: merged.status, failureClass: merged.failureClass, error: merged.error, phaseCount: merged.phaseResults.length, runControl: merged.runControl, permissionProfile: merged.permissionProfile, }), }), ); } if (options.enableGitops) { const closeout = merged.closeout ?? { traceId: merged.traceId, turnId: merged.turnId, unitType: merged.unitType, unitId: merged.unitId, status: merged.status, failureClass: merged.failureClass, gitAction: options.gitAction, gitPushed: options.gitPush, finishedAt: merged.finishedAt, }; Promise.race([ writeTurnCloseoutGitRecord( options.basePath, closeout, nextSequenceMetadata("gitops", "update", { action: "record" }), ), new Promise((_, reject) => setTimeout( () => reject(new Error("Git closeout timed out")), GITOPS_TIMEOUT_MS, ), ), ]).catch((err) => { console.error(`[loop-adapter] Git closeout failed: ${err.message}`); }); } if (writerToken) { releaseWriterToken(options.basePath, writerToken); } writerToken = null; current = null; phaseResults.length = 0; }, }; }