From 3df62fa54acd4fdbd607c5650960aac3f2e07633 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Thu, 7 May 2026 23:52:34 +0200 Subject: [PATCH] feat: add session daemon health lifecycle --- .ai-work/07-session-daemon-health-lifecycle.md | 2 +- src/server/sessiond.ts | 14 ++++++++++++++ src/server/sessiond/sessionProxyRoutes.ts | 2 ++ src/server/sessions/piSessionService.ts | 16 ++++++++++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/.ai-work/07-session-daemon-health-lifecycle.md b/.ai-work/07-session-daemon-health-lifecycle.md index 9389eb5..6cc4d00 100644 --- a/.ai-work/07-session-daemon-health-lifecycle.md +++ b/.ai-work/07-session-daemon-health-lifecycle.md @@ -1,5 +1,5 @@ # 07. Improve session daemon health and lifecycle -Status: pending +Status: completed Add health visibility and explicit lifecycle cleanup for the long-lived session daemon. diff --git a/src/server/sessiond.ts b/src/server/sessiond.ts index d36b761..9691733 100644 --- a/src/server/sessiond.ts +++ b/src/server/sessiond.ts @@ -14,6 +14,20 @@ const eventHub = new SessionEventHub(); const sessions = new PiSessionService(eventHub); registerSessionRoutes(app, sessions, eventHub); +app.get("/health", () => ({ ok: true, activeSessions: sessions.activeCount(), checkedAt: new Date().toISOString() })); + +let shuttingDown = false; +async function shutdown(signal: NodeJS.Signals): Promise { + if (shuttingDown) return; + shuttingDown = true; + app.log.info({ signal }, "shutting down session daemon"); + await sessions.dispose(); + await app.close(); +} + +process.once("SIGINT", (signal) => { void shutdown(signal); }); +process.once("SIGTERM", (signal) => { void shutdown(signal); }); + const portValue = process.env["PI_WEB_SESSIOND_PORT"]; const port = portValue !== undefined && portValue !== "" ? Number(portValue) : undefined; const host = process.env["PI_WEB_SESSIOND_HOST"] ?? "127.0.0.1"; diff --git a/src/server/sessiond/sessionProxyRoutes.ts b/src/server/sessiond/sessionProxyRoutes.ts index 23ec538..ab3542c 100644 --- a/src/server/sessiond/sessionProxyRoutes.ts +++ b/src/server/sessiond/sessionProxyRoutes.ts @@ -16,6 +16,8 @@ export function registerSessionProxyRoutes(app: FastifyInstance, daemon = new Se } }; + app.get("/api/sessiond/health", (_request, reply) => proxy({ method: "GET", url: "/api/health" }, reply)); + app.get<{ Querystring: { cwd?: string } }>("/api/sessions", (request, reply) => proxy(request, reply)); app.post<{ Body: { cwd: string } }>("/api/sessions", (request, reply) => proxy(request, reply)); app.get<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/messages", (request, reply) => proxy(request, reply)); diff --git a/src/server/sessions/piSessionService.ts b/src/server/sessions/piSessionService.ts index d2999f9..f6c6318 100644 --- a/src/server/sessions/piSessionService.ts +++ b/src/server/sessions/piSessionService.ts @@ -47,6 +47,22 @@ export class PiSessionService { ); } + activeCount(): number { + return this.active.size; + } + + async dispose(): Promise { + clearInterval(this.heartbeat); + const activeSessions = Array.from(new Set(this.active.values())); + this.active.clear(); + this.activities.clear(); + await Promise.all(activeSessions.map(async (active) => { + active.unsubscribe(); + await active.runtime.session.abort(); + await active.runtime.dispose(); + })); + } + async list(cwd: string): Promise { const [sessions, archivedRecords] = await Promise.all([SessionManager.list(cwd), this.archiveStore.list()]); const archivedById = new Map(archivedRecords.filter((record) => record.cwd === cwd).map((record) => [record.sessionId, record]));