Files
PaperClipAI/server/src/services/execution-recovery-attempt.ts
T

95 lines
5.3 KiB
TypeScript

type RetryRun = {
scheduledRetryAttempt?: number | null;
scheduledRetryReason?: string | null;
contextSnapshot?: Record<string, unknown> | null;
};
/** Server-owned counts for one automatic continuation chain. Repair slots live
* in legacyDispositionEpisode and are never charged to either counter here. */
export interface ExecutionRetryAccounting {
version: 1;
failureRetries: number;
maxTurnContinuations: number;
}
function count(value: unknown): number | null {
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
}
function savedAccounting(run: RetryRun): ExecutionRetryAccounting | null {
const value = run.contextSnapshot?.executionRetryAccounting;
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const saved = value as Record<string, unknown>;
const failureRetries = count(saved.failureRetries);
const maxTurnContinuations = count(saved.maxTurnContinuations);
if (saved.version !== 1 || failureRetries === null || maxTurnContinuations === null) return null;
return { version: 1, failureRetries, maxTurnContinuations };
}
function historicalFailureCount(run: RetryRun): number {
if (run.scheduledRetryReason === "max_turns_continuation" || run.scheduledRetryReason === "issue_disposition_repair") return 0;
if (run.scheduledRetryReason === "ai_connection_busy" || run.scheduledRetryReason === "ai_connection_pool_wait") {
const saved = count(run.contextSnapshot?.failureRetriesBeforeAiConnectionWait);
if (saved !== null) return saved;
}
if (run.scheduledRetryReason === "computer_admission_wait") {
const saved = count(run.contextSnapshot?.failureRetriesBeforeComputerWait);
if (saved !== null) return saved;
}
if (run.scheduledRetryReason === "workspace_busy") {
const saved = count(run.contextSnapshot?.failureRetriesBeforeWorkspaceWait);
if (saved !== null) return saved;
}
// Historical ambiguous counters remain conservative rather than resetting.
return count(run.scheduledRetryAttempt) ?? 0;
}
export function executionRetryAccounting(run: RetryRun): ExecutionRetryAccounting {
const saved = savedAccounting(run);
const nonFailureLane = ["computer_admission_wait", "max_turns_continuation", "issue_disposition_repair", "workspace_busy", "ai_connection_busy", "ai_connection_pool_wait"].includes(run.scheduledRetryReason ?? "");
return {
version: 1,
failureRetries: Math.max(saved?.failureRetries ?? 0, saved && nonFailureLane ? 0 : historicalFailureCount(run)),
maxTurnContinuations: Math.max(saved?.maxTurnContinuations ?? 0,
run.scheduledRetryReason === "max_turns_continuation" ? count(run.scheduledRetryAttempt) ?? 0 : 0),
};
}
/** Resource waits, repairs and productive continuations do not spend failures. */
export function executionFailureRetryCount(run: RetryRun): number {
return executionRetryAccounting(run).failureRetries;
}
export function executionRetryAttemptCount(run: RetryRun, reason: string): number {
if (reason === "computer_admission_wait" || reason === "workspace_busy" || reason === "ai_connection_busy" || reason === "ai_connection_pool_wait") {
return run.scheduledRetryReason === reason ? count(run.scheduledRetryAttempt) ?? 0 : 0;
}
const accounting = executionRetryAccounting(run);
return reason === "max_turns_continuation" ? accounting.maxTurnContinuations : accounting.failureRetries;
}
export function accountingForScheduledRetry(run: RetryRun, reason: string, attempt: number): ExecutionRetryAccounting {
const accounting = executionRetryAccounting(run);
if (reason === "max_turns_continuation") accounting.maxTurnContinuations = attempt;
else if (reason !== "computer_admission_wait" && reason !== "workspace_busy" && reason !== "ai_connection_busy" && reason !== "ai_connection_pool_wait") accounting.failureRetries = attempt;
return accounting;
}
/** Only the executor can attest that a pending-stop wait never dispatched and finished cleanup. */
export function isComputerAdmissionWait(run: {
id: string; companyId: string; status: string; runtimeMode: string; errorCode?: string | null;
resultJson?: Record<string, unknown> | null; runnerProfileJson?: Record<string, unknown> | null;
}): boolean {
const object = (value: unknown): Record<string, unknown> => value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
const result = object(run.resultJson), recovery = object(result.executionRecovery), cancellation = object(result.cancellation), admission = object(result.computerAdmission);
return run.runtimeMode === "legacy" && run.status === "cancelled" && run.errorCode === "computer_admission_wait" &&
recovery.kind === "computer_admission_wait" && recovery.providerWorkStarted === false &&
result.computerAdmissionRetryOutcome == null &&
cancellation.expected === true && cancellation.source === "control_plane" &&
object(cancellation.initiator).type === "system" &&
admission.runId === run.id && admission.companyId === run.companyId &&
[admission.computerId, admission.environmentId].every(value => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) &&
typeof admission.stopId === "string" && admission.stopId.length > 0 &&
!object(run.runnerProfileJson).nativeExecutionInput;
}