import { isReusableWorkReceiptState } from './work-receipt-state-machine'; import type { ToolExecutionFailureV1 } from '../tool-execution-error'; export type WorkReceiptFailureKind = 'terminal' | 'repairable'; export const WORK_RECEIPT_FAILURE_KIND_CODE: Record< WorkReceiptFailureKind, number > = { terminal: 0, repairable: 1, }; export function workReceiptFailureKindFromCode( value: unknown, ): WorkReceiptFailureKind { return Number(value) === WORK_RECEIPT_FAILURE_KIND_CODE.repairable ? 'repairable' : 'terminal'; } export function workReceiptFailureKindCode( value: WorkReceiptFailureKind | null | undefined, ): number { return WORK_RECEIPT_FAILURE_KIND_CODE[value ?? 'terminal']; } /** * Failure writes are always reclaimable by a later explicit Play Run. * * The input remains accepted at compatibility boundaries because older * runners still send `terminal`, but new storage state must never make a * failed semantic call permanent. Claim fencing, not this bit, prevents the * owning run from automatically repeating the failure. */ export function workReceiptFailureKindCodeForWrite( _value?: WorkReceiptFailureKind | null, ): number { return WORK_RECEIPT_FAILURE_KIND_CODE.repairable; } export type WorkReceiptStatus = | 'queued' | 'pending' | 'running' | 'completed' | 'failed' | 'skipped'; export type WorkReceipt = { key: string; status: WorkReceiptStatus; output?: unknown; error?: string | null; errorPayload?: ToolExecutionFailureV1 | null; failureKind?: WorkReceiptFailureKind | null; runId?: string | null; leaseId?: string | null; leaseOwnerRunId?: string | null; leaseOwnerAttempt?: number | null; leaseExpiresAt?: string | null; updatedAt?: string | null; }; export type WorkReceiptCommand = { playName: string; runId: string; key: string; leaseId?: string | null; runAttempt?: number | null; leaseTtlMs?: number | null; leaseAware?: boolean; reclaimRunning?: boolean; forceRefresh?: boolean; forceFailedRefresh?: boolean; }; export type WorkReceiptBatchClaimCommand = { playName: string; runId: string; keys: string[]; leaseIds?: string[]; leaseAware?: boolean; reclaimRunning?: boolean; forceRefresh?: boolean; forceFailedRefresh?: boolean; runAttempt?: number | null; leaseTtlMs?: number | null; }; export type WorkReceiptBatchCompleteCommand = { playName: string; receipts: Array<{ runId: string; key: string; output: unknown; leaseId?: string | null; runAttempt?: number | null; }>; }; export type WorkReceiptBatchFailCommand = { playName: string; receipts: Array<{ runId: string; key: string; error: string; errorPayload?: ToolExecutionFailureV1 | null; failureKind?: WorkReceiptFailureKind | null; leaseId?: string | null; runAttempt?: number | null; }>; }; export type WorkReceiptBatchHeartbeatCommand = { playName: string; runId: string; runAttempt?: number | null; leaseId: string; keys: string[]; leaseTtlMs?: number | null; }; export type WorkReceiptBatchMarkRunningCommand = { playName: string; receipts: Array<{ runId: string; key: string; leaseId?: string | null; runAttempt?: number | null; }>; leaseTtlMs?: number | null; }; export type WorkReceiptGetCommand = { playName: string; key: string; }; export type WorkReceiptClaim = | { disposition: 'claimed'; receipt: WorkReceipt; wasFailed?: boolean; } | { disposition: 'reused'; receipt: WorkReceipt; } | { disposition: 'running'; receipt: WorkReceipt; } | { disposition: 'blocked_active_lease'; receipt: WorkReceipt; } | { disposition: 'failed'; receipt: WorkReceipt; }; export type WorkReceiptStore = { getReceipt?(input: WorkReceiptGetCommand): Promise; claimReceipt(input: WorkReceiptCommand): Promise; claimReceipts?( input: WorkReceiptBatchClaimCommand, ): Promise; markReceiptsRunning?( input: WorkReceiptBatchMarkRunningCommand, ): Promise>; completeReceipt( input: WorkReceiptCommand & { output: unknown }, ): Promise; completeReceipts?( input: WorkReceiptBatchCompleteCommand, ): Promise>; failReceipt( input: WorkReceiptCommand & { error: string; errorPayload?: ToolExecutionFailureV1 | null; failureKind?: WorkReceiptFailureKind | null; }, ): Promise; failReceipts?( input: WorkReceiptBatchFailCommand, ): Promise>; releaseReceipt?(input: WorkReceiptCommand): Promise; heartbeatReceipts?( input: WorkReceiptBatchHeartbeatCommand, ): Promise>; }; const SCOPED_WORK_RECEIPT_KEY_VERSION = 'ctxv2'; function encodeWorkReceiptKeyComponent(value: string): string { return encodeURIComponent(value); } function normalizeWorkReceiptScopeComponent( value: string | null | undefined, fallback: string, ): string { return value?.trim() || fallback; } export function buildScopedWorkReceiptKey(input: { orgId?: string | null; playName: string; key: string; }): string { const orgId = normalizeWorkReceiptScopeComponent(input.orgId, 'org'); const playName = normalizeWorkReceiptScopeComponent(input.playName, 'play'); return [ SCOPED_WORK_RECEIPT_KEY_VERSION, encodeWorkReceiptKeyComponent(orgId), encodeWorkReceiptKeyComponent(playName), encodeWorkReceiptKeyComponent(input.key), ].join(':'); } export function scopedWorkReceiptKeyPrefix(input: { orgId: string; playName: string; }): string { return [ SCOPED_WORK_RECEIPT_KEY_VERSION, encodeWorkReceiptKeyComponent(input.orgId.trim()), encodeWorkReceiptKeyComponent(input.playName.trim()), '', ].join(':'); } export function isReusableWorkReceipt(receipt: WorkReceipt): boolean { return isReusableWorkReceiptState(receipt); }