// Cancellation control sidecar: durable acknowledgement that a child has // crossed its Cancellation cutover and drained every direct descendant. It is // deliberately separate from Completion evidence; cancellation never becomes // a failed child Completion outcome. import { validateOperation, type CompletionOperation } from "./completion-sidecar.ts"; import { isTransientFileError, publishAtomicFile, readJsonFileWithRetry, } from "./file-retry.ts"; const CANCELLATION_SIDECAR_VERSION = 1 as const; export interface CancellationSidecar { version: typeof CANCELLATION_SIDECAR_VERSION; operationId: string; type: "cancelled"; } interface CancellationRequest { version: typeof CANCELLATION_SIDECAR_VERSION; operationId: string; type: "request"; } export type CancellationSidecarReadResult = | { kind: "missing" } | { kind: "valid"; sidecar: CancellationSidecar } | { kind: "invalid"; error: string } /** A transient Windows handle/AV lock is not malformed control data. */ | { kind: "transient"; error: string }; export function getCancellationSidecarPath(ref: CompletionOperation): string { validateOperation(ref); return ref.artifacts.path("cancellation"); } function getCancellationRequestPath(ref: CompletionOperation): string { validateOperation(ref); return ref.artifacts.path("cancellationRequest"); } function isCancellationRecord( value: unknown, operationId: string, type: "request" | "cancelled", ): boolean { if (value == null || typeof value !== "object" || Array.isArray(value)) return false; const record = value as Record; return Object.keys(record).length === 3 && record.version === CANCELLATION_SIDECAR_VERSION && record.operationId === operationId && record.type === type; } function validate(ref: CompletionOperation, value: unknown): CancellationSidecar { validateOperation(ref); if (!isCancellationRecord(value, ref.operationId, "cancelled")) { throw new Error("Invalid cancellation sidecar."); } return { version: CANCELLATION_SIDECAR_VERSION, operationId: ref.operationId, type: "cancelled", }; } export function readCancellationSidecar( ref: CompletionOperation, ): CancellationSidecarReadResult { try { const value = readJsonFileWithRetry(getCancellationSidecarPath(ref)); if (value === undefined) return { kind: "missing" }; return { kind: "valid", sidecar: validate(ref, value) }; } catch (error) { if (isTransientFileError(error)) { return { kind: "transient", error: error instanceof Error ? error.message : String(error), }; } return { kind: "invalid", error: error instanceof Error ? error.message : String(error), }; } } function existingCancellationArtifactMatches( path: string, payload: CancellationSidecar | CancellationRequest, ): boolean { try { const value = readJsonFileWithRetry(path); if (value == null || typeof value !== "object" || Array.isArray(value)) return false; return isCancellationRecord(value, payload.operationId, payload.type); } catch { return false; } } function publishCancellationArtifact( path: string, payload: CancellationSidecar | CancellationRequest, ): string { return publishAtomicFile( path, JSON.stringify(payload) + "\n", { idempotent: true, createParentDirectory: false, isExistingDestinationAcceptable: (existingPath) => existingCancellationArtifactMatches(existingPath, payload), }, ); } /** Request cancellation of one Child operation without using Completion evidence. */ export function publishCancellationRequest(ref: CompletionOperation): string { const payload: CancellationRequest = { version: CANCELLATION_SIDECAR_VERSION, operationId: ref.operationId, type: "request", }; return publishCancellationArtifact(getCancellationRequestPath(ref), payload); } /** Whether the parent has requested lifecycle cancellation for this operation. */ export function hasCancellationRequest(ref: CompletionOperation): boolean { try { const value = readJsonFileWithRetry(getCancellationRequestPath(ref)); if (value === undefined || value == null || typeof value !== "object" || Array.isArray(value)) { return false; } return isCancellationRecord(value, ref.operationId, "request"); } catch { return false; } } /** Publish one cancellation acknowledgement atomically and idempotently. */ export function publishCancellationSidecar(ref: CompletionOperation): string { const payload = validate(ref, { version: CANCELLATION_SIDECAR_VERSION, operationId: ref.operationId, type: "cancelled", }); return publishCancellationArtifact(getCancellationSidecarPath(ref), payload); }