CherryHQ/cherry-studio · error · PpioTaskFailedError
result.task.reason || 'Task failed'
Error message
result.task.reason || 'Task failed'
What it means
PpioTaskFailedError is the terminal failure for the PPIO async task lifecycle: getTaskResult returned task.status === 'TASK_STATUS_FAILED'. It is a distinct Error subclass so the poll loop can propagate it without retrying (previously it was misclassified as transient and silently retried 10×, burning credits). The message is the vendor's task.reason or 'Task failed'.
Source
Thrown at src/main/ai/provider/custom/ppio/ppioTransport.ts:436
while (attempts < maxAttempts) {
if (signal?.aborted) {
throw createAbortError('Task polling aborted')
}
try {
const result = await this.getTaskResult(taskId, 10000, signal)
transientRetries = 0
if (result.task.progress_percent !== undefined && onProgress) {
onProgress(result.task.progress_percent)
}
if (result.task.status === 'TASK_STATUS_SUCCEED') {
return result
}
if (result.task.status === 'TASK_STATUS_FAILED') {
throw new PpioTaskFailedError(result.task.reason || 'Task failed')
}
} catch (error) {
if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {
throw createAbortError('Task polling aborted')
}
// Terminal classifications — propagate without retrying. A 4xx (bar
// 429) poll response won't recover; 5xx / 429 fall through to the
// transient handling below (network blips, server hiccups, rate limits).
if (error instanceof PpioApiError && isTerminalHttpStatus(error.statusCode)) {
throw error
}
if (error instanceof PpioTaskFailedError) {
throw error
}
transientRetries++View on GitHub (pinned to 726446b54c)
Solutions
- Read task.reason — it names the specific vendor cause (NSFW, insufficient credit, policy, etc.).
- For NSFW/policy: adjust the prompt; for credits: top up and retry; for invalid params: correct size/seed.
- Do NOT retry automatically — TASK_STATUS_FAILED is terminal and retrying burns credits. Surface to the user.
- Catch PpioTaskFailedError specifically upstream and report as a user-facing task failure (not a transient retry).
Example fix
// before
try { await pollUntilDone(transport, taskId, ctx) } catch (e) { throw e }
// after — distinguish terminal task failure from transient
try {
await pollUntilDone(transport, taskId, ctx)
} catch (e) {
if (e instanceof PpioTaskFailedError) return { failed: true, reason: e.message }
throw e
} Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-validate prompt against obvious policy trip before submit (best-effort)
if (/(<explicit terms>)/i.test(input.prompt ?? '')) {
throw new Error('Prompt may trip PPIO content policy')
} Type guard
export function isPpioTaskFailedError(e: unknown): e is Error {
return e instanceof Error && e.name === 'PpioTaskFailedError'
} Try / catch
try {
await pollUntilDone(transport, taskId, ctx)
} catch (e) {
if (isPpioTaskFailedError(e)) {
return { failed: true, reason: e.message } // terminal, do NOT retry
}
throw e
} Prevention
- Catch PpioTaskFailedError and treat as terminal — never auto-retry (it burns credits).
- Read task.reason for the specific cause (NSFW, credit, policy).
- Surface the reason to the user with a prompt-edit affordance.
When it happens
Trigger: PPIO's task-result endpoint returns TASK_STATUS_FAILED. Concrete causes: NSFW content detected, insufficient credits for the operation, content-policy violation, model-side generation error, or invalid input rejected at execution time.
Common situations: Prompt trips NSFW/policy filters, account out of credits mid-task, unsupported size/seed combination, or vendor model error.
Related errors
- PPIO API error: ${response.status} - ${errorText}
- PPIO API request timeout after ${timeout / 1000}s
- Unknown model: ${input.modelId}
- Task polling timeout
- OpenAI-compatible reranking model only supports text documen
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/e8daf9e6973c4e1c.
Report an issue: GitHub.