← Files ModRetro Chromatic PluginARCHIVED FILE

dist/recording-archive.js

35.1 KB · Oct 2, 2026 · 00:37 UTC

↓ Download file

import { createHash } from "node:crypto";
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync, } from "node:fs";
import { isAbsolute, join, relative, resolve, sep } from "node:path";
import { artifactUsage, boundedInteger, boundedReadFile, canonicalEventPayload, digest, encoded, MAX_FRAMES, MAX_RECORDING_EVENT_BYTES, MAX_RECORDING_EVENTS, MAX_RECORDING_FRAMES, MAX_REVIEW_ACTIONS, MAX_REVIEW_FRAME_RECORDS, MAX_REVIEW_IMAGES, MAX_REVIEW_METADATA_BYTES, MAX_SAFE_INTEGER, normalizedRecordedAction, recordedButtons, recordingBranchId, recordingLimits, RecordingLimitError, recordingPrefixPin, RECORDING_RESERVE_BYTES, sha256String, } from "./recording-common.js";
const MANIFEST_BYTES = 1024 * 1024;
const JOURNAL_CHUNK_BYTES = 64 * 1024;
const RECORDING_STATUSES = new Set(["recording", "stopped", "cancelled", "failed", "interrupted"]);
const PRESENTATION_TYPES = new Set(["unrendered", "normal", "lcd-off", "artificial", "repeat"]);
const utf8 = new TextDecoder("utf-8", { fatal: true });
function object(value, message) {
    if (value === null || typeof value !== "object" || Array.isArray(value))
        throw new Error(message);
    return value;
}
function jsonObject(bytes, message) {
    return object(JSON.parse(utf8.decode(bytes)), message);
}
function sameJson(left, right) {
    return encoded(left ?? null).equals(encoded(right ?? null));
}
function sessionIdentity(value, message) {
    if (typeof value !== "string" || !/^[0-9a-f]{32}$/u.test(value))
        throw new Error(message);
    return value;
}
function presentationFields(value) {
    const hasFrame = Object.hasOwn(value, "hasPresentedFrame");
    const hasType = Object.hasOwn(value, "presentationType");
    if (!hasFrame && !hasType)
        return {};
    if (!hasFrame || !hasType || typeof value.hasPresentedFrame !== "boolean"
        || typeof value.presentationType !== "string" || !PRESENTATION_TYPES.has(value.presentationType)) {
        throw new Error("Recorded native frame presentation metadata is invalid");
    }
    return { hasPresentedFrame: value.hasPresentedFrame, presentationType: value.presentationType };
}
export function reviewOptions(value) {
    object(value, "Recording review options must be an object");
    const allowed = new Set([
        "recordingPath", "sessionId", "branchId", "fromActionIndex", "toActionIndex",
        "fromFrame", "toFrame", "prefixPin", "maxImages",
    ]);
    if (Object.keys(value).some((key) => !allowed.has(key)))
        throw new Error("Unknown recording review options");
    const sessionId = sessionIdentity(value.sessionId, "Recording review requires an exact sessionId");
    const branchId = recordingBranchId(value.branchId);
    const start = boundedInteger(value.fromActionIndex, "fromActionIndex", 0, MAX_SAFE_INTEGER);
    const end = boundedInteger(value.toActionIndex, "toActionIndex", 0, MAX_SAFE_INTEGER);
    if (end < start || end - start > MAX_REVIEW_ACTIONS) {
        throw new Error("Review requires an ordered interval of at most " + MAX_REVIEW_ACTIONS + " normalized actions; narrow the interval");
    }
    const result = {
        sessionId, branchId, fromActionIndex: start, toActionIndex: end,
        maxImages: boundedInteger(Object.hasOwn(value, "maxImages") ? value.maxImages : 4, "review image limit", 1, MAX_REVIEW_IMAGES),
    };
    if (Object.hasOwn(value, "fromFrame") !== Object.hasOwn(value, "toFrame")) {
        throw new Error("Review frame bounds must be supplied together");
    }
    if (Object.hasOwn(value, "fromFrame")) {
        const fromFrame = boundedInteger(value.fromFrame, "fromFrame", 0, MAX_RECORDING_FRAMES);
        const toFrame = boundedInteger(value.toFrame, "toFrame", 0, MAX_RECORDING_FRAMES);
        if (toFrame < fromFrame || toFrame - fromFrame > MAX_FRAMES) {
            throw new Error("Review requires an ordered interval of at most " + MAX_FRAMES + " native frames; narrow the interval");
        }
        Object.assign(result, { fromFrame, toFrame });
    }
    if (Object.hasOwn(value, "prefixPin"))
        result.prefixPin = recordingPrefixPin(value.prefixPin);
    return result;
}
/** Bounded metadata selected only from the reader's authenticated event stream.
 *
 * Copied normalized actions do not establish physical controller delivery.
 * This class never opens a journal, reads a framebuffer, or invokes an emulator.
 */
export class RecordingReviewSelection {
    options;
    sessionId;
    branchId;
    start;
    end;
    inputs = [];
    frames = [];
    physicalEvents = [];
    uncertainties = [];
    priorFailure = null;
    metadataBytes = 0;
    foundBranch = false;
    bootComplete = false;
    actionCount = 0;
    actionFrame = 0;
    boundaries = new Map();
    actionFrames = new Map();
    normalizedButtons = [];
    initialButtons = [];
    matchedControllers = new Set();
    lastAction = null;
    prefixState = null;
    constructor(options) {
        this.options = reviewOptions(options);
        this.sessionId = this.options.sessionId;
        this.branchId = this.options.branchId;
        this.start = this.options.fromActionIndex;
        this.end = this.options.toActionIndex;
    }
    retain(destination, value) {
        this.metadataBytes += encoded(value).length + 1;
        if (this.metadataBytes > MAX_REVIEW_METADATA_BYTES) {
            throw new Error("Review metadata exceeds 2 MiB; narrow the interval");
        }
        destination.push(value);
    }
    eventIdentity(row, eventDigest) {
        return { sessionId: this.sessionId, branchId: this.branchId, sequence: row.sequence, digest: eventDigest };
    }
    beginBranch(condition, options) {
        if (object(condition, "Review requires a supported recorded starting condition").type !== "clean-boot") {
            throw new Error("Review requires a supported recorded starting condition");
        }
        this.foundBranch = true;
        this.bootComplete = options.imported;
        this.actionCount = 0;
        this.actionFrame = boundedInteger(condition.initialFrames, "recorded boot frames", 0, MAX_FRAMES);
        this.boundaries.set(0, this.actionFrame);
    }
    action(value, index, options) {
        const action = normalizedRecordedAction(value);
        if (index !== this.actionCount + 1)
            throw new Error("Review controller action indices are inconsistent");
        const start = this.actionFrame;
        const end = start + (action.type === "step" ? action.frames : 0);
        if (end > MAX_RECORDING_FRAMES || (options.frame !== undefined && options.frame !== end)
            || (options.startFrame !== undefined && options.startFrame !== start)) {
            throw new Error("Review controller action frames are inconsistent");
        }
        this.actionCount = index;
        this.actionFrame = end;
        if (action.type === "set_buttons")
            this.normalizedButtons = [...action.buttons];
        if (index === this.start)
            this.initialButtons = [...this.normalizedButtons];
        if (index === this.start || index === this.end)
            this.boundaries.set(index, end);
        if (this.start <= index && index <= this.end)
            this.actionFrames.set(index, end);
        if (this.start < index && index <= this.end) {
            this.retain(this.inputs, {
                actionIndex: index, startFrame: start, frame: end, action,
                source: options.source, origin: options.origin,
            });
        }
    }
    frame(item, options) {
        const index = boundedInteger(item.actionIndex, "recorded frame action index", 0, this.actionCount);
        const frame = boundedInteger(item.frame, "recorded evidence frame", 0, MAX_RECORDING_FRAMES);
        const presentation = presentationFields(item);
        if (index < this.start || index > this.end)
            return;
        const bootBoundary = this.boundaries.get(0);
        if ((index > 0 && this.actionFrames.get(index) !== frame)
            || (index === 0 && (bootBoundary === undefined || frame > bootBoundary))) {
            throw new Error("Recorded review image differs from its normalized action boundary");
        }
        if (Object.hasOwn(this.options, "fromFrame")
            && (frame < this.options.fromFrame || frame > this.options.toFrame))
            return;
        if (item.width !== 160 || item.height !== 144) {
            throw new Error("Recorded review images must have native 160x144 dimensions");
        }
        const { origin } = options;
        const locator = origin.kind === "direct" ? "event-" + origin.event.sequence
            : "import-" + origin.importEvent.sequence + "-prefix-" + origin.prefixOrdinal;
        if (this.frames.length >= MAX_REVIEW_FRAME_RECORDS) {
            throw new Error("Review retained-image metadata exceeds its limit; narrow the interval");
        }
        // Candidate metadata is not a returned PNG. A fragmented 3,600-tick step
        // may have 3,600 records, while only maxImages records enter the response.
        this.frames.push({
            sessionId: this.sessionId, branchId: this.branchId,
            evidenceId: this.sessionId + "/" + this.branchId + "/" + locator,
            sequence: boundedInteger(item.sequence, "recorded frame sequence", 1, MAX_RECORDING_EVENTS),
            frame, actionIndex: index, path: item.path,
            sha256: sha256String(item.sha256, "recorded PNG SHA-256"),
            rgbaSha256: sha256String(item.rgbaSha256, "recorded RGBA SHA-256"),
            width: 160, height: 144, origin, ...presentation,
        });
    }
    accept(row, eventDigest, branches) {
        const kind = row.kind;
        if (kind === "header" && row.sessionId !== this.sessionId) {
            throw new Error("The requested recording sessionId differs from its authenticated header");
        }
        if (row.branchId !== this.branchId)
            return;
        const identity = this.eventIdentity(row, eventDigest);
        if (kind === "header") {
            this.beginBranch(row.startingCondition, { imported: false });
        }
        else if (kind === "boot_complete") {
            this.bootComplete = true;
            this.actionFrame = boundedInteger(row.frame, "recorded boot frame", 0, MAX_FRAMES);
            this.boundaries.set(0, this.actionFrame);
        }
        else if (kind === "branch") {
            this.beginBranch(row.startingCondition, { imported: true });
            if (!Array.isArray(row.prefixActions))
                throw new Error("The imported action prefix must be an array");
            const sourceSession = sessionIdentity(row.sourceSessionId, "The imported recording prefix identity is invalid");
            const sourceBranch = recordingBranchId(row.parentBranchId);
            row.prefixActions.forEach((action, index) => {
                const ordinal = index + 1;
                this.action(action, ordinal, {
                    source: "imported-prefix",
                    origin: {
                        kind: "imported-prefix", importEvent: identity, prefixOrdinal: ordinal,
                        sourceSessionId: sourceSession, sourceBranchId: sourceBranch,
                    },
                });
            });
            if (this.actionFrame !== row.frame) {
                throw new Error("The imported action prefix does not reach its recorded branch frame");
            }
            const branch = requireBranch(branches, this.branchId);
            branch.evidence.forEach((item, index) => {
                this.frame(item, {
                    origin: {
                        kind: "imported-prefix", importEvent: identity, prefixOrdinal: index + 1,
                        sourceSessionId: sourceSession,
                        sourceBranchId: recordingBranchId(Object.hasOwn(item, "sourceBranchId") ? item.sourceBranchId : sourceBranch),
                        copiedSequence: item.sequence,
                    },
                });
            });
        }
        else if (kind === "action") {
            const source = Object.hasOwn(row, "source") ? row.source : "manual";
            if (source !== "manual" && source !== "clock") {
                throw new Error("The recorded action has an unsupported input source");
            }
            this.action(row.action, row.actionIndex, {
                source, origin: { kind: "direct", event: identity }, frame: row.frame,
                ...(Object.hasOwn(row, "startFrame") ? { startFrame: row.startFrame } : {}),
            });
            this.lastAction = { sequence: row.sequence, actionIndex: row.actionIndex, frame: row.frame, action: row.action };
        }
        else if (kind === "frame") {
            this.frame(requireBranch(branches, this.branchId).evidence.at(-1), { origin: { kind: "direct", event: identity } });
        }
        else if (kind === "controller") {
            const releases = recordedButtons(row.releases);
            const presses = recordedButtons(row.presses);
            const active = recordedButtons(row.activeButtons);
            boundedInteger(row.appliesBeforeFrame, "controller delivery frame", 1, MAX_RECORDING_FRAMES + 1);
            if (releases.some((button) => presses.includes(button)) || row.appliesBeforeFrame !== row.frame + 1) {
                throw new Error("Recorded controller-delivery metadata is inconsistent");
            }
            const previous = this.lastAction;
            const associated = previous !== null && previous.sequence === row.sequence - 1
                && previous.frame === row.frame && sameJson(previous.action, { type: "set_buttons", buttons: active });
            const actionIndex = associated ? previous.actionIndex : null;
            if ((actionIndex !== null && this.start < actionIndex && actionIndex <= this.end)
                || (actionIndex === null && this.start <= this.actionCount && this.actionCount <= this.end)) {
                this.retain(this.physicalEvents, {
                    event: identity, actionIndex, atActionIndex: this.actionCount, frame: row.frame,
                    appliesBeforeFrame: row.appliesBeforeFrame, releases, presses, activeButtons: active,
                });
                if (actionIndex !== null)
                    this.matchedControllers.add(actionIndex);
                else
                    this.retain(this.uncertainties, {
                        kind: "unassociated-controller-event", event: identity, actionIndex: this.actionCount, frame: row.frame,
                    });
            }
        }
        else if (kind === "failure") {
            // Failed physical delivery cannot be reconstructed from a later action.
            // Expose only the uncertainty boundary, not an arbitrary raw error log.
            const boundary = { kind: "failure-boundary", event: identity, actionIndex: this.actionCount, frame: row.frame };
            if (this.actionCount < this.start)
                this.priorFailure = boundary;
            else if (this.actionCount <= this.end)
                this.retain(this.uncertainties, boundary);
        }
    }
    freeze(branches) {
        const branch = branches[this.branchId];
        if (branch === undefined || !this.foundBranch) {
            throw new Error("The requested recording branch does not exist in the committed prefix");
        }
        if (this.end > this.actionCount)
            throw new Error("The requested action interval exceeds the committed recording prefix");
        if (!this.bootComplete && this.actionCount === 0)
            this.boundaries.set(0, branch.frame);
        const startFrame = this.boundaries.get(this.start);
        const endFrame = this.boundaries.get(this.end);
        if (startFrame === undefined || endFrame === undefined || endFrame < startFrame || endFrame - startFrame > MAX_FRAMES) {
            throw new Error("Review spans more than " + MAX_FRAMES + " native frames; narrow the interval");
        }
        if (Object.hasOwn(this.options, "fromFrame")
            && (this.options.fromFrame !== startFrame || this.options.toFrame !== endFrame)) {
            throw new Error("Review frame bounds differ from the normalized action boundaries");
        }
        this.prefixState = {
            fromFrame: startFrame, toFrame: endFrame, branchActionCount: this.actionCount, branchFrame: branch.frame,
        };
    }
}
function requireBranch(branches, branchId) {
    const branch = branches[branchId];
    if (branch === undefined)
        throw new Error("Recording event names an unavailable branch");
    return branch;
}
function sameFile(left, right) {
    return left.dev === right.dev && left.ino === right.ino;
}
function guardDirectory(root, identity, canonical) {
    const current = lstatSync(root);
    if (current.isSymbolicLink() || !current.isDirectory() || !sameFile(identity, current)
        || realpathSync(root) !== canonical) {
        throw new Error("The recording directory changed while it was being read");
    }
}
function evidencePath(root, value, resolvePath, imported) {
    if (typeof value !== "string" || value.length === 0)
        throw new Error("Recorded frames require a file path");
    const candidate = resolve(root, value);
    const filename = resolvePath(candidate, { existing: true });
    const tail = relative(root, filename);
    if (tail === ".." || tail.startsWith(".." + sep) || isAbsolute(tail)) {
        throw new Error(imported ? "Imported recorded frames must remain inside their recording"
            : "Recorded frames must remain inside their recording");
    }
    return filename;
}
/** Read at most one bounded event plus a small chunk, never the whole journal. */
function* journalLines(descriptor, checkCancel) {
    let fragments = [];
    let length = 0;
    for (;;) {
        checkCancel?.();
        const chunk = Buffer.allocUnsafe(JOURNAL_CHUNK_BYTES);
        const count = readSync(descriptor, chunk, 0, chunk.length, null);
        if (count === 0) {
            if (length > 0)
                yield Buffer.concat(fragments, length);
            return;
        }
        let start = 0;
        while (start < count) {
            checkCancel?.();
            const newline = chunk.subarray(0, count).indexOf(10, start);
            const end = newline === -1 ? count : newline + 1;
            const part = chunk.subarray(start, end);
            length += part.length;
            if (length > MAX_RECORDING_EVENT_BYTES)
                throw new Error("The recording has an incomplete or oversized event");
            fragments.push(part);
            if (newline !== -1) {
                yield fragments.length === 1 ? part : Buffer.concat(fragments, length);
                fragments = [];
                length = 0;
            }
            start = end;
        }
    }
}
function verifyReadPrefix(descriptor, byteLength, expected, checkCancel) {
    const hash = createHash("sha256");
    const chunk = Buffer.allocUnsafe(JOURNAL_CHUNK_BYTES);
    let position = 0;
    while (position < byteLength) {
        checkCancel?.();
        const count = readSync(descriptor, chunk, 0, Math.min(chunk.length, byteLength - position), position);
        if (count === 0)
            throw new Error("The recording event source changed while it was being read");
        hash.update(chunk.subarray(0, count));
        position += count;
    }
    if (hash.digest("hex") !== expected)
        throw new Error("The recording event source changed while it was being read");
}
export function readRecording(root, resolvePath, options = {}) {
    const { review, checkCancel } = options;
    const prefixPin = options.prefixPin === undefined ? undefined : recordingPrefixPin(options.prefixPin);
    checkCancel?.();
    const rootIdentity = lstatSync(root);
    const canonicalRoot = realpathSync(root);
    if (rootIdentity.isSymbolicLink() || !rootIdentity.isDirectory()) {
        throw new Error("The recording directory has no valid recording.json");
    }
    // Authorize the directory itself, even if it has no frame records that would
    // otherwise call the resolver. Do not grant access from a valid journal hash.
    root = resolvePath(root, { existing: true });
    if (root !== canonicalRoot)
        throw new Error("The recording directory changed while it was being read");
    const manifestPath = join(root, "recording.json");
    guardDirectory(root, rootIdentity, canonicalRoot);
    const manifestBytes = boundedReadFile(manifestPath, MANIFEST_BYTES, "Recording manifest");
    const manifest = jsonObject(manifestBytes, "Recording manifest must be an object");
    if (manifest.schemaVersion !== 1 || manifest.kind !== "gb-studio-playtest") {
        throw new Error("Unsupported playtest recording format");
    }
    if (!RECORDING_STATUSES.has(manifest.status))
        throw new Error("Unsupported playtest recording status");
    const limits = recordingLimits(manifest.limits);
    const maxBytes = limits.maxBytes;
    const eventPath = join(root, "events.jsonl");
    const eventIdentity = lstatSync(eventPath);
    if (eventIdentity.isSymbolicLink() || !eventIdentity.isFile() || eventIdentity.size > maxBytes) {
        throw new Error("The recording event log is missing or exceeds its limit");
    }
    const observedEventBytes = eventIdentity.size;
    if (prefixPin !== undefined && prefixPin.byteLength > observedEventBytes) {
        throw new Error("The requested recording prefix is not present");
    }
    const branches = Object.create(null);
    const checkpoints = Object.create(null);
    let current = "branch-0001";
    let previous = "0".repeat(64);
    let sequence = 0;
    let status = "interrupted";
    let truncatedTail = false;
    let bootFinished = false;
    let final;
    let totalFrames = 0;
    const eventHasher = createHash("sha256");
    let eventBytes = 0;
    let selectedPrefix;
    const flags = constants.O_RDONLY | (process.platform === "win32" ? 0 : (constants.O_NOFOLLOW ?? 0))
        | (constants.O_NONBLOCK ?? 0);
    const descriptor = openSync(eventPath, flags);
    try {
        const opened = fstatSync(descriptor);
        guardDirectory(root, rootIdentity, canonicalRoot);
        if (!opened.isFile() || !sameFile(opened, eventIdentity) || opened.size > maxBytes) {
            throw new Error("The recording event source changed while it was being read");
        }
        for (const line of journalLines(descriptor, checkCancel)) {
            if (line.at(-1) !== 10) {
                if (manifest.status === "recording") {
                    truncatedTail = true;
                    break;
                }
                throw new Error("The recording has an incomplete or oversized event");
            }
            const row = jsonObject(line, "A recording event must be an object");
            const claimed = row.digest;
            delete row.digest;
            boundedInteger(row.sequence, "recording event sequence", 1, MAX_RECORDING_EVENTS);
            boundedInteger(row.frame, "recording event frame", 0, MAX_RECORDING_FRAMES);
            recordingBranchId(row.branchId);
            // Parsing JSON into JS numbers loses Python's integral-float spelling.
            // Canonicalize the source tokens, keeping that distinction, for old logs.
            if (row.sequence !== sequence + 1 || row.previousDigest !== previous
                || digest(canonicalEventPayload(line)) !== claimed) {
                throw new Error("Recording event integrity check failed");
            }
            sequence += 1;
            previous = claimed;
            eventHasher.update(line);
            eventBytes += line.length;
            if (eventBytes > maxBytes)
                throw new RecordingLimitError("The recording event log exceeds its byte limit");
            if (final !== undefined)
                throw new Error("The recording contains events after its final footer");
            const kind = row.kind;
            if (kind === "header") {
                if (sequence !== 1 || row.sessionId !== manifest.sessionId || row.branchId !== current) {
                    throw new Error("Recording header identity differs");
                }
                if (["rom", "runtime", "limits", "startingCondition"].some((key) => !sameJson(row[key], manifest[key]))) {
                    throw new Error("Recording manifest differs from its authenticated header");
                }
                branches[current] = {
                    actions: [], evidence: [], frame: 0,
                    startingCondition: object(row.startingCondition, "Recording starting condition must be an object"),
                };
            }
            else if (sequence === 1) {
                throw new Error("Recording header is missing");
            }
            else if (kind === "boot_complete") {
                const branch = requireBranch(branches, current);
                branch.startingCondition = object(row.startingCondition, "Recording starting condition must be an object");
                branch.frame = row.frame;
                totalFrames = Math.max(totalFrames, row.frame);
                bootFinished = true;
            }
            else if (kind === "branch") {
                current = row.branchId;
                if (Object.hasOwn(branches, current))
                    throw new Error("Recording branch identity is duplicated");
                if (!Array.isArray(row.prefixActions) || !Array.isArray(row.prefixEvidence)
                    || row.prefixActions.length > MAX_RECORDING_EVENTS || row.prefixEvidence.length > MAX_RECORDING_EVENTS) {
                    throw new Error("The imported recording prefix must contain bounded action and evidence arrays");
                }
                const evidence = row.prefixEvidence.map((value) => {
                    const item = { ...object(value, "Imported recorded frame metadata must be an object") };
                    const filename = evidencePath(root, item.file, resolvePath, true);
                    delete item.file;
                    return { ...item, path: filename, ...presentationFields(item) };
                });
                branches[current] = {
                    actions: row.prefixActions.map(normalizedRecordedAction), evidence, frame: row.frame,
                    parentBranchId: recordingBranchId(row.parentBranchId), sourceSessionId: row.sourceSessionId,
                    checkpointId: row.checkpointId,
                    startingCondition: object(row.startingCondition, "Recording starting condition must be an object"),
                };
            }
            else if (kind === "action") {
                const branch = requireBranch(branches, row.branchId);
                boundedInteger(row.actionIndex, "recording action index", 1, MAX_SAFE_INTEGER);
                if (Object.hasOwn(row, "startFrame"))
                    boundedInteger(row.startFrame, "recording action start frame", 0, MAX_RECORDING_FRAMES);
                if (row.actionIndex !== branch.actions.length + 1)
                    throw new Error("Recording controller action index differs");
                const action = normalizedRecordedAction(row.action);
                if (action.type === "step")
                    totalFrames += action.frames;
                branch.actions.push(action);
                branch.frame = row.frame;
            }
            else if (kind === "frame") {
                const branch = requireBranch(branches, row.branchId);
                boundedInteger(row.actionIndex, "recorded frame action index", 0, MAX_SAFE_INTEGER);
                if (row.actionIndex !== branch.actions.length) {
                    throw new Error("Recorded frame has an inconsistent controller action index");
                }
                const filename = evidencePath(root, row.file, resolvePath, false);
                const evidence = {};
                for (const key of ["sequence", "branchId", "frame", "sha256", "rgbaSha256", "width", "height", "actionIndex"]) {
                    evidence[key] = row[key];
                }
                evidence.path = filename;
                Object.assign(evidence, presentationFields(row));
                branch.evidence.push(evidence);
                branch.frame = row.frame;
                if (!bootFinished) {
                    // Retain a shorter valid boot prefix after a hard interruption.
                    totalFrames = Math.max(totalFrames, row.frame);
                    branch.startingCondition = { ...branch.startingCondition, initialFrames: row.frame };
                }
            }
            else if (kind === "finish") {
                if (!RECORDING_STATUSES.has(row.status) || row.status === "recording") {
                    throw new Error("Unsupported playtest recording status");
                }
                status = row.status;
                totalFrames = boundedInteger(row.totalFrames, "recording total frames", 0, MAX_RECORDING_FRAMES);
                final = row;
            }
            else if (kind === "checkpoint_saved") {
                if (typeof row.checkpointId !== "string" || row.checkpointId.length === 0) {
                    throw new Error("Recording checkpoint identity is invalid");
                }
                if (Object.hasOwn(checkpoints, row.checkpointId))
                    throw new Error("Checkpoint identity is duplicated in the recording");
                checkpoints[row.checkpointId] = row;
            }
            else if (!["controller", "pause", "resume", "failure", "capture", "outcome"].includes(kind)) {
                throw new Error("Unsupported recording event kind");
            }
            if (review !== undefined && (prefixPin === undefined || sequence <= prefixPin.eventCount)) {
                review.accept(row, claimed, branches);
            }
            if (prefixPin !== undefined && sequence === prefixPin.eventCount) {
                selectedPrefix = {
                    eventCount: sequence, eventDigest: previous, byteLength: eventBytes,
                    sha256: eventHasher.copy().digest("hex"),
                };
                if (!sameJson(selectedPrefix, prefixPin)) {
                    throw new Error("The requested recording prefix failed its byte and event-chain integrity check");
                }
                review?.freeze(branches);
                // Live pins name committed bytes. Finalized archives must still have
                // their complete tail checked, even when only an earlier span is shown.
                if (manifest.status === "recording")
                    break;
            }
        }
        const guardJournal = () => {
            const completed = fstatSync(descriptor);
            const named = lstatSync(eventPath);
            guardDirectory(root, rootIdentity, canonicalRoot);
            if (!completed.isFile() || named.isSymbolicLink() || !named.isFile()
                || !sameFile(completed, named) || !sameFile(completed, eventIdentity)
                || completed.size < eventBytes || completed.size > maxBytes
                || (manifest.status !== "recording"
                    && (completed.size !== observedEventBytes || completed.size !== eventBytes
                        || completed.mtimeMs !== eventIdentity.mtimeMs || completed.ctimeMs !== eventIdentity.ctimeMs))) {
                throw new Error("The recording event source changed while it was being read");
            }
        };
        guardJournal();
        // Appends to a live journal are allowed; rewrites of authenticated bytes
        // are not. Re-read only the committed prefix from the guarded descriptor.
        verifyReadPrefix(descriptor, eventBytes, eventHasher.copy().digest("hex"), checkCancel);
        guardJournal();
    }
    finally {
        closeSync(descriptor);
    }
    if (Object.keys(branches).length === 0)
        throw new Error("Recording header is missing");
    if (truncatedTail)
        status = "interrupted";
    if (manifest.status !== "recording"
        && (manifest.eventDigest !== previous || manifest.eventCount !== sequence || manifest.status !== status)) {
        throw new Error("Finalized recording integrity check failed");
    }
    if (prefixPin !== undefined && selectedPrefix === undefined)
        throw new Error("The requested recording prefix is not present");
    const completePrefix = {
        eventCount: sequence, eventDigest: previous, byteLength: eventBytes, sha256: eventHasher.digest("hex"),
    };
    if (prefixPin === undefined)
        review?.freeze(branches);
    checkCancel?.();
    if (!boundedReadFile(manifestPath, MANIFEST_BYTES, "Recording manifest").equals(manifestBytes)) {
        throw new Error("The recording manifest changed while it was being read");
    }
    guardDirectory(root, rootIdentity, canonicalRoot);
    const result = {
        ...manifest, recordingPath: root, status, branchId: current,
        frame: requireBranch(branches, current).frame, totalFrames, eventCount: sequence, eventDigest: previous,
        branchesById: branches, checkpointsById: checkpoints, truncatedTail,
        prefixPin: selectedPrefix ?? completePrefix,
        sourcePins: {
            manifest: { byteLength: manifestBytes.length, sha256: digest(manifestBytes) },
            events: completePrefix, observedEventLogBytes: observedEventBytes,
            finalized: manifest.status !== "recording",
            tailValidation: manifest.status !== "recording" ? "finalized-complete" : "committed-prefix",
            manifestStatus: manifest.status,
        },
        bytesUsed: artifactUsage(root, resolvePath),
    };
    if (result.bytesUsed > maxBytes)
        throw new RecordingLimitError("The retained recording exceeds its byte limit");
    for (const key of ["failure", "outcome", "reason"]) {
        if (final !== undefined && Object.hasOwn(final, key))
            result[key] = final[key];
    }
    const failuresPath = join(root, "worker-failures.jsonl");
    if (status === "interrupted") {
        let failureIdentity;
        try {
            failureIdentity = lstatSync(failuresPath);
        }
        catch (error) {
            if (error.code !== "ENOENT")
                throw error;
        }
        if (failureIdentity !== undefined) {
            if (failureIdentity.isSymbolicLink() || !failureIdentity.isFile() || failureIdentity.size > RECORDING_RESERVE_BYTES) {
                throw new Error("Worker failure record exceeds its limit");
            }
            const failureBytes = boundedReadFile(failuresPath, RECORDING_RESERVE_BYTES, "Worker failure record");
            const rows = utf8.decode(failureBytes).split(/\r?\n/u);
            if (rows.at(-1) === "")
                rows.pop();
            const last = rows.at(-1);
            if (last !== undefined) {
                const failure = object(JSON.parse(last), "Worker failure record must be an object");
                result.failure = Object.hasOwn(failure, "message") ? failure.message : "The emulator worker was interrupted";
            }
        }
    }
    result.branches = Object.entries(branches).map(([branchId, branch]) => ({
        branchId, frame: branch.frame,
        ...(Object.hasOwn(branch, "parentBranchId") ? { parentBranchId: branch.parentBranchId } : {}),
        ...(Object.hasOwn(branch, "sourceSessionId") ? { sourceSessionId: branch.sourceSessionId } : {}),
        ...(Object.hasOwn(branch, "checkpointId") ? { checkpointId: branch.checkpointId } : {}),
    }));
    checkCancel?.();
    if (!boundedReadFile(manifestPath, MANIFEST_BYTES, "Recording manifest").equals(manifestBytes)) {
        throw new Error("The recording manifest changed while it was being read");
    }
    guardDirectory(root, rootIdentity, canonicalRoot);
    return result;
}
//# sourceMappingURL=recording-archive.js.map

SHA-256: 7137d98aeda4b2a8d17325489d6c0d650620d6a812fbd7b83bfa187a93d7cdff