{"record":{"id":"bc70bdd2291dc46b","repo":"paperclipai/paperclip","slug":"plugin-worker-is-not-running","errorCode":null,"errorMessage":"Plugin worker is not running","messagePattern":"Plugin worker is not running","errorType":"http","errorClass":null,"httpStatus":503,"severity":"error","filePath":"server/src/routes/plugins.ts","lineNumber":1853,"sourceCode":"      res.status(501).json({ error: \"Plugin scoped API routes are not enabled\" });\n      return;\n    }\n\n    const { pluginId } = req.params;\n    const plugin = await resolvePlugin(registry, pluginId);\n    if (!plugin) {\n      res.status(404).json({ error: \"Plugin not found\" });\n      return;\n    }\n    if (plugin.status !== \"ready\") {\n      res.status(503).json({ error: `Plugin is not ready (current status: ${plugin.status})` });\n      return;\n    }\n    const isWorkerRunning = typeof bridgeDeps.workerManager.isRunning === \"function\"\n      ? bridgeDeps.workerManager.isRunning(plugin.id)\n      : true;\n    if (!isWorkerRunning) {\n      res.status(503).json({ error: \"Plugin worker is not running\" });\n      return;\n    }\n    if (!plugin.manifestJson.capabilities.includes(\"api.routes.register\")) {\n      res.status(404).json({ error: \"Plugin does not expose scoped API routes\" });\n      return;\n    }\n\n    const requestPath = req.path || \"/\";\n    const routes = plugin.manifestJson.apiRoutes ?? [];\n    const match = routes\n      .map((route) => ({ route, params: matchScopedApiRoute(route, req.method, requestPath) }))\n      .find((candidate) => candidate.params !== null);\n    if (!match || !match.params) {\n      res.status(404).json({ error: \"Plugin API route not found\" });\n      return;\n    }\n\n    try {","sourceCodeStart":1835,"sourceCodeEnd":1871,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/server/src/routes/plugins.ts#L1835-L1871","documentation":"Returned as HTTP 503 when the plugin's database status is \"ready\" but workerManager.isRunning(plugin.id) reports the worker process is not running. The DB record and runtime have diverged: the worker crashed, was stopped out-of-band, or was never respawned after a host restart. If the workerManager has no isRunning function the check is skipped (assumed running), so this error specifically means a real isRunning() probe returned false.","triggerScenarios":"Calling /api/plugins/:pluginId/api/* when the worker child process died (OOM kill, unhandled worker exception) without the status flipping to \"error\", or during the window after a server restart where the DB row still says ready but the loader has not re-spawned workers yet.","commonSituations":"Worker crash-loop from a bad plugin entrypoint; container restart where PG data (status=ready) survives but processes do not; worker manually killed by an operator; memory limits reaping the child process.","solutions":["Retry after a short delay — if the loader supports runtime activation it may respawn the worker on its next pass.","Force a worker restart via a disable/enable cycle: POST /api/plugins/:id/disable then POST /api/plugins/:id/enable (only valid from ready -> disabled -> ready).","Run GET /api/plugins/:pluginId/health and check the worker check plus server logs for the crash cause (missing config, bad entrypoint, missing dependency).","Fix the underlying worker crash (see the plugin's logs via GET /api/plugins/:pluginId/logs?level=error)."],"exampleFix":"// before\nconst res = await fetch(`/api/plugins/${id}/api/issues`);\nif (res.status === 503) throw new Error(\"plugin API failed\");\n\n// after\nconst res = await fetch(`/api/plugins/${id}/api/issues`);\nif (res.status === 503) {\n  await fetch(`/api/plugins/${id}/disable`, { method: \"POST\", headers, body: JSON.stringify({ reason: \"worker not running\" }) });\n  await fetch(`/api/plugins/${id}/enable`, { method: \"POST\", headers });\n}","handlingStrategy":"retry","validationCode":"async function pluginWorkerRunning(apiBase: string, pluginId: string): Promise<boolean> {\n  const res = await fetch(`${apiBase}/api/plugins/${encodeURIComponent(pluginId)}/health`);\n  if (!res.ok) return false;\n  const health = await res.json();\n  return health.checks?.every((c: { passed: boolean }) => c.passed) ?? false;\n}","typeGuard":"interface HealthCheck { name: string; passed: boolean; message: string }\nfunction allChecksPassed(checks: HealthCheck[]): boolean {\n  return Array.isArray(checks) && checks.length > 0 && checks.every((c) => c.passed === true);\n}","tryCatchPattern":"let lastErr: unknown;\nfor (let attempt = 0; attempt < 3; attempt++) {\n  try { return await callScopedApi(pluginId, path); }\n  catch (err) {\n    lastErr = err;\n    if (!(err instanceof HttpError) || err.status !== 503 || !/worker is not running/i.test(err.message)) throw err;\n    await delay(2 ** attempt * 500); // worker may respawn\n  }\n}\nthrow lastErr;","preventionTips":["Health-check plugins (GET /plugins/:id/health) before long-running integration flows that depend on their workers.","Monitor worker crash logs (GET /plugins/:id/logs?level=error) and fix root causes instead of absorbing 503s.","After server restarts, wait for the loader's activation pass (or probe health) before dispatching plugin API traffic."],"tags":["plugin","worker","http-503","runtime-drift","scoped-api"],"backgroundTag":"worker-not-running","analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}