{"record":{"id":"2b3cdfdb94fe4bd0","repo":"paperclipai/paperclip","slug":"paperclip-service-did-not-become-healthy-expected","errorCode":null,"errorMessage":"Paperclip service did not become healthy${expectedVersion ? ` at version ${expectedVersion}` : \"\"}: ${last.error ?? `reported ${last.serverVersion ?? \"no version\"}`}","messagePattern":"Paperclip service did not become healthy(.+?)` : \"\"\\}: (.+?)`\\}","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cli/src/commands/service.ts","lineNumber":50,"sourceCode":"async function probeHealth(instanceId: string): Promise<HealthResult> {\n  try {\n    const response = await fetch(healthUrl(instanceId), { signal: AbortSignal.timeout(2_000) });\n    const body = await response.json() as { status?: unknown; serverVersion?: unknown; version?: unknown };\n    return { ok: response.ok && body.status === \"ok\", serverVersion: typeof body.serverVersion === \"string\" ? body.serverVersion : typeof body.version === \"string\" ? body.version : null };\n  } catch (error) {\n    return { ok: false, serverVersion: null, error: error instanceof Error ? error.message : String(error) };\n  }\n}\n\nasync function waitForHealth(instanceId: string, expectedVersion: string | null, timeoutMs = 60_000): Promise<HealthResult> {\n  const deadline = Date.now() + timeoutMs;\n  let last: HealthResult = { ok: false, serverVersion: null };\n  while (Date.now() < deadline) {\n    last = await probeHealth(instanceId);\n    if (last.ok && (!expectedVersion || last.serverVersion === expectedVersion)) return last;\n    await new Promise((resolve) => setTimeout(resolve, 500));\n  }\n  throw new Error(`Paperclip service did not become healthy${expectedVersion ? ` at version ${expectedVersion}` : \"\"}: ${last.error ?? `reported ${last.serverVersion ?? \"no version\"}`}`);\n}\n\nexport function resolveRestartExpectedVersion(expectedVersion: string | null | undefined): string | null {\n  return expectedVersion ?? null;\n}\n\nexport async function withHotRestartLock<T>(\n  instanceId: string,\n  callback: () => Promise<T>,\n  options: { timeoutMs?: number; pollMs?: number; isProcessAlive?: (pid: number) => boolean } = {},\n): Promise<T> {\n  const instanceRoot = resolvePaperclipInstanceRoot(instanceId);\n  const lockPath = path.join(instanceRoot, \"hot-restart.lock\");\n  const token = `${process.pid}:${Date.now()}:${Math.random().toString(16).slice(2)}`;\n  const deadline = Date.now() + (options.timeoutMs ?? 120_000);\n  const pollMs = options.pollMs ?? 100;\n  const isProcessAlive = options.isProcessAlive ?? ((pid: number) => {\n    try {","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/cli/src/commands/service.ts#L32-L68","documentation":"Thrown by waitForHealth() when the service did not report healthy (status ok and, if expectedVersion is set, a matching version) within the 60s deadline. The message includes the last probe's error or reported version to aid diagnosis.","triggerScenarios":"After `service restart` (or any caller of waitForHealth), the loop probes health every 500ms for 60s; if the server never returns `{status:\"ok\"}` at `/api/health`, or returns a version that does not match `--expected-version`, this throws.","commonSituations":"Server booting slowly (long migrations, heavy plugin init), server crashed on startup but the supervisor still shows active, wrong expected version after a deploy, health endpoint bound to a different port/host than probed.","solutions":["Check service status and logs: `paperclipai service status`, `paperclipai service logs -n 200`.","If migrations/init are slow, increase patience or pre-warm the DB; consider a longer timeout in the caller.","If using `--expected-version`, confirm it matches the actually deployed server version, or omit it.","Verify the health endpoint is reachable: `curl http://127.0.0.1:<port>/api/health`."],"exampleFix":"# before\npaperclipai service restart --expected-version 1.2.3\n# after (verify version, or drop the requirement)\npaperclipai service status\ncurl http://127.0.0.1:3100/api/health\npaperclipai service restart","handlingStrategy":"retry","validationCode":"async function healthy(url: string, timeoutMs = 60_000): Promise<boolean> {\n  const deadline = Date.now() + timeoutMs;\n  while (Date.now() < deadline) {\n    try { const r = await fetch(url); if (r.ok) return true; } catch {}\n    await new Promise(r => setTimeout(r, 500));\n  }\n  return false;\n}","typeGuard":null,"tryCatchPattern":"try {\n  await waitForHealth(instanceId, expectedVersion);\n} catch (err) {\n  console.error('Health timeout. Check service logs:', err);\n  // inspect logs, then optionally retry once with longer timeout\n}","preventionTips":["Pre-warm the database (run migrations) before restart to cut boot time.","Omit --expected-version unless you truly need version pinning."],"tags":["service","health","timeout","restart","startup"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}