{"record":{"id":"6c24b6551b7afb91","repo":"paperclipai/paperclip","slug":"message-6c24b6","errorCode":null,"errorMessage":"${message}","messagePattern":"\\$\\{message\\}","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"server/src/routes/plugins.ts","lineNumber":2008,"sourceCode":"\n    const plugin = await resolvePlugin(registry, pluginId);\n    if (!plugin) {\n      res.status(404).json({ error: \"Plugin not found\" });\n      return;\n    }\n\n    try {\n      const result = await lifecycle.unload(plugin.id, purge);\n      await logPluginMutationActivity(req, \"plugin.uninstalled\", plugin.id, {\n        pluginId: plugin.id,\n        pluginKey: plugin.pluginKey,\n        purge,\n      });\n      publishGlobalLiveEvent({ type: \"plugin.ui.updated\", payload: { pluginId: plugin.id, action: \"uninstalled\" } });\n      res.json(result);\n    } catch (err) {\n      const message = err instanceof Error ? err.message : String(err);\n      res.status(400).json({ error: message });\n    }\n  });\n\n  /**\n   * POST /api/plugins/:pluginId/enable\n   *\n   * Enable a plugin that is currently disabled or in error state.\n   *\n   * Transitions the plugin to 'ready' state after loading and validation.\n   *\n   * Response: PluginRecord\n   * Errors: 404 if plugin not found, 400 for lifecycle errors\n   */\n  router.post(\"/plugins/:pluginId/enable\", async (req, res) => {\n    assertInstanceAdmin(req);\n    assertPluginManagementVisible();\n    const { pluginId } = req.params;\n","sourceCodeStart":1990,"sourceCodeEnd":2026,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/server/src/routes/plugins.ts#L1990-L2026","documentation":"The catch-all around lifecycle.unload on DELETE /api/plugins/:pluginId, returned as HTTP 400 with the raw lifecycle error text. The dominant message is \"Plugin <key> is already uninstalled. Use removeData=true to permanently delete it.\" — raised when the row already has status \"uninstalled\" and you DELETE again without purge. Other sources are cleanupInstallArtifacts or registry.uninstall failures (filesystem or DB errors); worker-stop failures are best-effort and logged, not thrown.","triggerScenarios":"DELETE /api/plugins/:id on a plugin whose status is already \"uninstalled\" without ?purge=true (double uninstall); install-artifact directory missing or locked on disk when cleanup runs; DB error during soft/hard delete.","commonSituations":"UI double-fire or retry of an uninstall that already succeeded; CI script with delete-then-delete steps; artifacts directory manually removed so cleanup fails.","solutions":["If the message says \"already uninstalled\", append ?purge=true to hard-delete the row (without purge, soft-deleted data is retained ~30 days).","Treat 400 + \"already uninstalled\" as an idempotent success in automation instead of a failure.","For filesystem/DB messages, check server logs for the cleanup stack trace and fix the underlying disk/permission/DB issue before retrying."],"exampleFix":"// before\nawait fetch(`/api/plugins/${id}`, { method: \"DELETE\" });\n\n// after — hard-delete when a previous uninstall already soft-deleted it\nawait fetch(`/api/plugins/${id}?purge=true`, { method: \"DELETE\" });","handlingStrategy":"try-catch","validationCode":"async function uninstallPlugin(apiBase: string, id: string): Promise<void> {\n  const res = await fetch(`${apiBase}/api/plugins/${id}`, { method: \"DELETE\" });\n  if (res.status === 400) {\n    const { error } = await res.json();\n    if (/already uninstalled/i.test(error)) {\n      await fetch(`${apiBase}/api/plugins/${id}?purge=true`, { method: \"DELETE\" });\n      return;\n    }\n    throw new Error(error);\n  }\n  if (!res.ok) throw new Error(`uninstall failed: ${res.status}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await api.uninstall(id, { purge: false });\n} catch (err) {\n  if (err instanceof HttpError && err.status === 400 && /already uninstalled/i.test(err.message)) {\n    await api.uninstall(id, { purge: true }); // desired end state is full removal\n  } else {\n    throw err; // filesystem/db failure — needs operator attention, not retry\n  }\n}","preventionTips":["Read the 400 message before retrying — 'already uninstalled' and cleanup failures need opposite responses.","Decide purge semantics up front and pass ?purge=true consistently when hard delete is intended.","Guard uninstall buttons against double-submission to avoid the second-call error entirely."],"tags":["plugin","lifecycle","uninstall","http-400","invalid-state"],"backgroundTag":"invalid-state-transition","analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}