{"record":{"id":"c6f4e61d422ddd28","repo":"tinyhumansai/openhuman","slug":"download-failed","errorCode":"DOWNLOAD_FAILED","errorMessage":"${reason}","messagePattern":"\\$\\{reason\\}","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/services/artifactDownloadService.ts","lineNumber":256,"sourceCode":"      code: 'NOT_DESKTOP',\n      error: 'Downloads are only available in the desktop app',\n    };\n  }\n\n  const resolved = await resolveArtifactForExport(artifactId, fallbackTitle, extension);\n  if (!resolved.ok) {\n    return { ok: false, code: resolved.code, error: resolved.error };\n  }\n\n  try {\n    const dest = await invoke<string>('download_artifact_to_downloads', {\n      sourcePath: resolved.sourcePath,\n      filename: resolved.filename,\n    });\n    return { ok: true, path: dest };\n  } catch (err) {\n    const reason = err instanceof Error ? err.message : String(err);\n    return { ok: false, code: 'DOWNLOAD_FAILED', error: reason };\n  }\n}\n\n/**\n * Export an artifact via the native Save-As dialog (#3162), pre-filled\n * with the artifact's filename. On the user dismissing the dialog,\n * returns `{ ok: false, code: 'CANCELLED' }` (the caller should treat\n * this as a no-op, not an error). If the dialog itself is unavailable —\n * e.g. headless Linux with no xdg-desktop portal — falls back to the\n * Downloads copy so the artifact is still recoverable.\n */\nexport async function saveArtifactViaDialog(\n  artifactId: string,\n  fallbackTitle: string,\n  extension: string\n): Promise<DownloadArtifactOutcome> {\n  if (!isTauri()) {\n    return { ok: false, code: 'NOT_DESKTOP', error: 'Saving is only available in the desktop app' };","sourceCodeStart":238,"sourceCodeEnd":274,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/services/artifactDownloadService.ts#L238-L274","documentation":"Returned by downloadArtifact when the Tauri IPC command download_artifact_to_downloads throws after the artifact had already resolved successfully (resolveArtifactForExport returned ok). The copy happens inside the Rust shell, so the failure is native: the source blob vanished, the Downloads destination is missing or unwritable, the disk is full, or the invoke ran outside a real Tauri context. The JS side wraps the raw invoke error message as DOWNLOAD_FAILED.","triggerScenarios":"Calling downloadArtifact(artifactId) where resolveArtifactForExport succeeds but invoke('download_artifact_to_downloads', { sourcePath, filename }) rejects: the on-disk artifact file was deleted or moved after its ArtifactMeta row was registered, ~/Downloads is absent or read-only, the volume is full, or the frontend runs in a plain browser where the command cannot resolve.","commonSituations":"A stale artifact row still listed in the UI while another flow or the user already removed its file; macOS privacy permissions denying Downloads writes; running the UI via `pnpm dev` (browser only) instead of `pnpm dev:app`; full disks on long-lived machines.","solutions":["Read the outcome's error field - it is the raw Tauri command error and distinguishes 'source file missing' from permission or full-disk failures.","If the source blob is gone, call deleteArtifact(artifactId) to drop the stale ArtifactMeta row, then regenerate the artifact.","Confirm you are inside the Tauri desktop shell (`pnpm dev:app` or a built app) and that ~/Downloads exists and is writable.","Fall back to revealArtifactInFileManager, or re-run generation when the copy keeps failing."],"exampleFix":"// before\nconst out = await downloadArtifact(id, title, ext);\nif (!out.ok) showError('Download failed'); // collapses every code into one message\n\n// after\nconst out = await downloadArtifact(id, title, ext);\nif (!out.ok) {\n  if (out.code === 'DOWNLOAD_FAILED' && /no such file|not found/i.test(out.error ?? '')) {\n    await deleteArtifact(id); // stale row: blob already gone\n    showError('Artifact file is missing. Regenerating.');\n  } else {\n    showError(`Download failed: ${out.error}`);\n  }\n}","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"downloadArtifact never throws - it returns a DownloadArtifactOutcome. Branch on code: DOWNLOAD_FAILED with a file-missing reason -> offer delete-and-regenerate or revealArtifactInFileManager as fallback; other codes -> surface the error text verbatim.","preventionTips":["Reconcile UI artifact rows against core state on mount so stale blobs surface before the user clicks download.","Always switch on outcome.code rather than collapsing all failures into one message - the reason field is the raw native error.","Only offer download UI inside isTauri() contexts."],"tags":["tauri","artifact","download","filesystem","ipc"],"backgroundTag":"file-copy-failed","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}