import { OperationLedger, LEDGER_SCHEMA, LEDGER_PHASES } from "./operation-ledger.mjs"; import { fnv1a64 } from "./replay.mjs"; export const XYZAC_COMMAND_LEDGER_SCHEMA = "xyzac-command-ledger.v1"; const TERMINAL_PHASES = new Set(["committed", "aborted", "failed"]); export class XyzacCommandLedger extends OperationLedger { constructor(entries = []) { super(entries); this.scope = "selected_xyzac_simulation"; } prepare(input) { if (["HELLO", "CAPABILITIES", "SNAPSHOT_REQUEST", "CANCEL"].includes(input?.kind)) return { status: "untracked", replay: false, entry: null }; const result = super.prepare(input); if (result.entry) { result.entry.scope = this.scope; result.entry.intent_hash = result.entry.intent_hash ?? fnv1a64({ kind: input.kind, payload: input.payload ?? {}, operation_id: input.operation_id }); } return result; } acknowledge(operationId, response) { return this.verify(operationId, response); } result(operationId, response, seq) { this.acknowledge(operationId, response); return this.commit(operationId, seq); } pending() { return [...this.entries.values()] .filter((entry) => !TERMINAL_PHASES.has(entry.phase)) .sort((left, right) => left.seq - right.seq) .map((entry) => structuredClone(entry)); } reconcileAll() { return this.pending().map((entry) => ({ operation_id: entry.operation_id, status: entry.phase, entry })); } timeoutReconcile(operationId) { const reconciliation = this.reconcile(operationId); return { status: "reconcile_required", operation_id: operationId, retry_allowed: false, reconciliation }; } audit({ includeResponses = true, maxEntries = Number.POSITIVE_INFINITY } = {}) { // High-frequency SIM_STEP responses request only a small audit window. // OperationLedger.export() clones every historical response before the // window can be selected, which turns a long run into quadratic browser // serialization work. The map is insertion ordered by command sequence, // so select the tail first and clone only the returned entries. const values = [...this.entries.values()]; const pending = this.pending(); const totalEntries = values.length; const windowed = Number.isFinite(maxEntries) && maxEntries >= 0 ? values.slice(-Math.floor(maxEntries)) : values; const entries = includeResponses ? windowed.map((entry) => structuredClone(entry)) : windowed.map((entry) => { const { response, ...summary } = entry; return structuredClone(summary); }); const unsigned = { schema: XYZAC_COMMAND_LEDGER_SCHEMA, operation_schema: LEDGER_SCHEMA, phases: [...LEDGER_PHASES], scope: this.scope, entries, entry_count: totalEntries, truncated: entries.length < totalEntries, pending_count: pending.length }; return { ...unsigned, state_hash: fnv1a64(unsigned) }; } }