File size: 3,595 Bytes
5cb63c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/** Formats stable cron timeout and execution error messages. */
import { formatEmbeddedAgentExecutionPhase } from "../../agents/embedded-agent-runner/execution-phase.js";
import { extractErrorCode, formatErrorMessageWithCode } from "../../infra/errors.js";
import {
  CRON_JOB_EXECUTION_TIMEOUT_ERROR,
  CRON_PRE_EXECUTION_TIMEOUT_ERROR,
  CRON_SETUP_TIMEOUT_ERROR,
  isCronTimeoutErrorText,
} from "../execution-error-constants.js";
import type { CronAgentExecutionStarted } from "../types.js";

function formatCronAgentExecutionPhase(execution?: CronAgentExecutionStarted): string | undefined {
  return formatEmbeddedAgentExecutionPhase(execution?.phase);
}

function hasCronTimeoutPrefix(error: string, prefix: string): boolean {
  return error === prefix || error.startsWith(prefix + " ");
}

export function isCronTerminalAbortReasonText(error: string): boolean {
  return isCronTimeoutErrorText(error);
}

/** Formats the generic cron execution timeout message with last-known phase context when available. */
export function timeoutErrorMessage(execution?: CronAgentExecutionStarted): string {
  const phase = formatCronAgentExecutionPhase(execution);
  if (!phase) {
    return CRON_JOB_EXECUTION_TIMEOUT_ERROR;
  }
  return `${CRON_JOB_EXECUTION_TIMEOUT_ERROR} (last phase: ${phase})`;
}

/** Formats timeout text for runs that stalled before the isolated runner started. */
export function setupTimeoutErrorMessage(execution?: CronAgentExecutionStarted): string {
  const phase = formatCronAgentExecutionPhase(execution);
  if (!phase) {
    return CRON_SETUP_TIMEOUT_ERROR;
  }
  return `${CRON_SETUP_TIMEOUT_ERROR} (last phase: ${phase})`;
}

/** Returns true for the setup-timeout class that fires before the isolated runner starts. */
export function isSetupTimeoutErrorText(error: string): boolean {
  return hasCronTimeoutPrefix(error, CRON_SETUP_TIMEOUT_ERROR);
}

/** Formats timeout text for runs that stalled after setup but before execution start. */
export function preExecutionTimeoutErrorMessage(execution?: CronAgentExecutionStarted): string {
  const phase = formatCronAgentExecutionPhase(execution);
  if (!phase) {
    return CRON_PRE_EXECUTION_TIMEOUT_ERROR;
  }
  return `${CRON_PRE_EXECUTION_TIMEOUT_ERROR} (last phase: ${phase})`;
}

/** Extracts a human timeout/abort reason, falling back to the canonical cron timeout text. */
export function resolveCronAbortReasonText(reason: unknown): string | undefined {
  if (typeof reason === "string" && reason.trim()) {
    return reason.trim();
  }
  if (reason instanceof Error) {
    const message = reason.message.trim();
    // Only an empty abort or one already carrying cron's canonical timeout text
    // is unspecified. Coded aborts and other messages retain their exact reason.
    if (extractErrorCode(reason) === undefined && (!message || message === timeoutErrorMessage())) {
      return undefined;
    }
    return formatErrorMessageWithCode(reason);
  }
  return undefined;
}

/** Extracts a human timeout/abort reason, falling back to the canonical cron timeout text. */
export function abortErrorMessage(signal?: AbortSignal): string {
  return resolveCronAbortReasonText(signal?.reason) ?? timeoutErrorMessage();
}

/** Normalizes thrown cron run failures into stable log/run-history text. */
export function normalizeCronRunErrorText(err: unknown): string {
  if (
    err instanceof Error &&
    (err.name === "AbortError" || err.message.trim() === timeoutErrorMessage())
  ) {
    return resolveCronAbortReasonText(err) ?? timeoutErrorMessage();
  }
  return formatErrorMessageWithCode(err);
}