From 657f7a80c009f771bb446ec6bfb2c2285fc95292 Mon Sep 17 00:00:00 2001 From: Janpeter Visser Date: Fri, 1 May 2026 20:17:07 +0200 Subject: [PATCH] fix(presence): heartbeat self-heals when worker record disappears (#14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, if the ClaudeWorker record vanished (deleted by prisma_workers_cleanup, manual cleanup, or a race during shutdown of a parallel worker), the heartbeat would log a warning and stop itself permanently. From that moment the NavBar showed 'Geen agent' for the rest of the MCP-server process lifetime — even though the agent was still alive and serving tools. Fix: on result.count === 0, call registerWorker again so the record is re-created. Heartbeat keeps ticking. Self-healing instead of self- terminating. startHeartbeat now also accepts userId (needed for re-registration); caller in index.ts updated. Co-authored-by: Claude Opus 4.7 (1M context) --- src/index.ts | 2 +- src/presence/heartbeat.ts | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 58c185a..0d287f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,7 +68,7 @@ async function main() { // is up, regardless of when the MCP client sends its first request. const auth = await getAuth() await registerWorker({ userId: auth.userId, tokenId: auth.tokenId }) - const { stop: stopHeartbeat } = startHeartbeat({ tokenId: auth.tokenId }) + const { stop: stopHeartbeat } = startHeartbeat({ userId: auth.userId, tokenId: auth.tokenId }) registerShutdownHandlers({ userId: auth.userId, tokenId: auth.tokenId, stopHeartbeat }) const transport = new StdioServerTransport() diff --git a/src/presence/heartbeat.ts b/src/presence/heartbeat.ts index f4cb230..abe7f74 100644 --- a/src/presence/heartbeat.ts +++ b/src/presence/heartbeat.ts @@ -1,6 +1,8 @@ import { prisma } from '../prisma.js' +import { registerWorker } from './worker.js' export function startHeartbeat(opts: { + userId: string tokenId: string intervalMs?: number }): { stop: () => void } { @@ -11,11 +13,18 @@ export function startHeartbeat(opts: { data: { last_seen_at: new Date() }, }) if (result.count === 0) { - console.error('[scrum4me-mcp] Heartbeat: worker record not found — token may be revoked. Stopping.') - clearInterval(timer) + // Record disappeared — likely deleted by prisma_workers_cleanup, + // a manual cleanup, or a race during shutdown of a parallel worker. + // Re-register so the UI's 'Agent verbonden'-indicator self-heals + // instead of going dark for the rest of the process lifetime. + try { + await registerWorker({ userId: opts.userId, tokenId: opts.tokenId }) + } catch (err) { + console.error('[scrum4me-mcp] Heartbeat: re-register failed', err) + } } } catch { - // non-fatal + // non-fatal — next tick retries } }, opts.intervalMs ?? 5_000)