@@ -48,6 +48,7 @@ export type RunDiagnosisCategory =
4848 | "source_pull_request_moved"
4949 | "validation_failed"
5050 | "budget_exhausted"
51+ | "engine_stalled"
5152 | "engine_error"
5253 | "step_failed"
5354 | "unknown" ;
@@ -142,6 +143,10 @@ const NEXT_ACTIONS: Record<RunDiagnosisCategory, string[]> = {
142143 "The run stopped after exhausting its configured budget, not from a failure." ,
143144 "Raise the workflow's budget limit or narrow the ticket's scope before retrying." ,
144145 ] ,
146+ engine_stalled : [
147+ "The watchdog marked this run failed after a workflow step stopped making progress." ,
148+ "Inspect the named step and worker or sandbox health before retrying the run." ,
149+ ] ,
145150 engine_error : [
146151 "Check the workflow definition graph for an unresolvable trigger, node, or edge." ,
147152 ] ,
@@ -237,6 +242,10 @@ const SOURCE_PULL_REQUEST_MOVED_KEYWORDS = [
237242// stopped by a budget check (workflows/agent.ts:2537-2543).
238243const BUDGET_EXHAUSTED_PREFIX = "Run stopped on budget:" ;
239244
245+ // WATCHDOG_FAILURE_REASON_PREFIX (lib/telemetry/run-telemetry.ts:116), written
246+ // only by the engine-stall watchdog as a durable failed-run reason.
247+ const ENGINE_STALLED_PREFIX = "Run engine stalled:" ;
248+
240249// fallbackTerminalError's "blocked" lead (lib/overview/sanitize-run-detail.ts:
241250// 104-113): the observed face of three silent stop paths that record no
242251// statusReason: markRunBlockedOnCancel and sweepOrphanedAwaitingRuns
@@ -265,6 +274,19 @@ const VALIDATION_FAILED_PREFIXES = [
265274const DEPENDENCY_AUTH_PREFIX =
266275 "The AI provider rejected the credentials (authentication failed)." ;
267276
277+ // Curated provider sentence for an account/project spend-limit rejection
278+ // (workflow-definition/failure-message.ts:148-151). This is deliberately a
279+ // whole trusted lead rather than a raw `spend limit` search: runs.diagnose
280+ // receives the already-sanitized run reason, and only this code-owned sentence
281+ // is safe to route to billing remediation. It stays distinct from
282+ // `budget_exhausted`, which describes the workflow's own configured budget.
283+ const PROVIDER_SPEND_LIMIT_PREFIX =
284+ "The AI provider rejected the request: the account has reached its configured spend limit." ;
285+ const PROVIDER_SPEND_LIMIT_ACTIONS = [
286+ "Raise or remove the provider project's configured spend limit in billing settings before retrying." ,
287+ "Confirm the intended provider project/account is selected and that the new limit has propagated before rerunning." ,
288+ ] as const ;
289+
268290// The other PROVIDER_CAUSES sentences (workflow-definition/failure-message.ts:
269291// 90-113): billing/credit, rate limit, model unavailable, and overloaded. Plus
270292// SAFE_EXECUTION_ERROR_MESSAGES.provider (interpreter.ts:89), the uncurated
@@ -390,6 +412,15 @@ const RULES: readonly Rule[] = [
390412 return { confidence : "low" , evidenceRefs : evidenceFrom ( input ) } ;
391413 } ,
392414 } ,
415+ {
416+ category : "engine_stalled" ,
417+ match : ( input ) => {
418+ if ( input . status !== "failed" ) return null ;
419+ const message = input . error ?. message ;
420+ if ( ! message || ! message . startsWith ( ENGINE_STALLED_PREFIX ) ) return null ;
421+ return { confidence : "low" , evidenceRefs : evidenceFrom ( input ) } ;
422+ } ,
423+ } ,
393424 {
394425 category : "cancelled" ,
395426 match : ( input ) => {
@@ -476,6 +507,22 @@ const RULES: readonly Rule[] = [
476507 return { confidence : "low" , evidenceRefs : evidenceFrom ( input ) } ;
477508 } ,
478509 } ,
510+ {
511+ // This is a provider dependency failure, but the operator action is
512+ // billing remediation rather than a blind retry or a status-page check.
513+ // Keep it ahead of the generic dependency_unavailable rule below so the
514+ // stable curated spend-limit sentence gets its specific guidance.
515+ category : "dependency_unavailable" ,
516+ match : ( input ) => {
517+ const message = input . error ?. message ;
518+ if ( ! message || ! message . startsWith ( PROVIDER_SPEND_LIMIT_PREFIX ) ) return null ;
519+ return {
520+ confidence : "low" ,
521+ evidenceRefs : evidenceFrom ( input ) ,
522+ nextActions : PROVIDER_SPEND_LIMIT_ACTIONS ,
523+ } ;
524+ } ,
525+ } ,
479526 {
480527 category : "dependency_unavailable" ,
481528 match : ( input ) => {
0 commit comments