| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- import { promises as fs } from 'fs';
- import path from 'path';
- import { sendCronDaemonRequest } from './services/cron-daemon-owner.js';
- import { getAlwaysOnHeartbeatPath } from './services/always-on-paths.js';
- function normalizeString(value) {
- return typeof value === 'string' ? value.trim() : '';
- }
- async function resolveProjectRoot(project) {
- const direct = normalizeString(project?.projectRoot) || normalizeString(project?.fullPath) || normalizeString(project?.path);
- if (direct) return path.resolve(direct);
- const projectName = normalizeString(project?.projectName) || normalizeString(project?.name);
- if (!projectName) return '';
- const { extractProjectDirectory } = await import('./projects.js');
- return path.resolve(await extractProjectDirectory(projectName));
- }
- async function resolveProjectRoots(projects) {
- if (!Array.isArray(projects)) return [];
- const roots = [];
- for (const project of projects) {
- const root = await resolveProjectRoot(project);
- if (root) roots.push(root);
- }
- return [...new Set(roots)];
- }
- async function registerProject(projectRoot) {
- try {
- await sendCronDaemonRequest({ type: 'register_project', projectRoot });
- } catch {
- // The daemon may not be up yet; the next heartbeat will retry.
- }
- }
- export function createAlwaysOnHeartbeatManager({
- getActivePilotDeckSessions = () => [],
- registerProjectFn = registerProject
- } = {}) {
- const wsFiles = new WeakMap();
- const wsIds = new WeakMap();
- const wsProjectRoots = new WeakMap();
- function getWsId(ws) {
- let id = wsIds.get(ws);
- if (!id) {
- id = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
- wsIds.set(ws, id);
- }
- return id;
- }
- async function writeBeat(ws, projectRoot, payload) {
- const wsId = getWsId(ws);
- const fileName = `webui-${wsId}.beat`;
- const filePath = getAlwaysOnHeartbeatPath(projectRoot, fileName);
- const processingSessionIds = Array.isArray(payload.processingSessionIds)
- ? payload.processingSessionIds.filter((id) => typeof id === 'string')
- : [];
- const beat = {
- schemaVersion: 1,
- writerKind: 'webui',
- writerId: wsId,
- writtenAt: new Date().toISOString(),
- agentBusy: Boolean(payload.agentBusy) || processingSessionIds.length > 0,
- processingSessionIds,
- lastUserMsgAt: normalizeString(payload.lastUserMsgAt) || null,
- };
- await fs.mkdir(path.dirname(filePath), { recursive: true });
- await fs.writeFile(filePath, JSON.stringify(beat, null, 2), 'utf8');
- const files = wsFiles.get(ws) || new Set();
- files.add(filePath);
- wsFiles.set(ws, files);
- await registerProjectFn(projectRoot);
- }
- async function handlePresence(ws, payload = {}) {
- const roots = new Map();
- const selectedRoot = await resolveProjectRoot(payload.selectedProject);
- const alwaysOnRoots = await resolveProjectRoots(payload.alwaysOnProjects);
- for (const projectRoot of alwaysOnRoots) {
- roots.set(projectRoot, {
- agentBusy: false,
- processingSessionIds: [],
- lastUserMsgAt: selectedRoot === projectRoot ? payload.lastUserMsgAt : null,
- });
- }
- const activeSessions = getActivePilotDeckSessions();
- for (const session of activeSessions) {
- const cwd = normalizeString(session?.cwd);
- if (!cwd) continue;
- const projectRoot = path.resolve(cwd);
- const existing = roots.get(projectRoot);
- if (!existing) continue;
- existing.agentBusy = true;
- existing.processingSessionIds.push(session.sessionId);
- roots.set(projectRoot, existing);
- }
- for (const [projectRoot, beatPayload] of roots) {
- await writeBeat(ws, projectRoot, beatPayload);
- }
- wsProjectRoots.set(ws, new Set(roots.keys()));
- return [...roots.keys()];
- }
- async function clearPresence(ws) {
- const files = wsFiles.get(ws);
- if (!files) {
- wsProjectRoots.delete(ws);
- return;
- }
- await Promise.all([...files].map((filePath) => fs.rm(filePath, { force: true }).catch(() => {})));
- wsFiles.delete(ws);
- wsProjectRoots.delete(ws);
- }
- return {
- getWriterId: getWsId,
- getProjectRoots(ws) {
- return [...(wsProjectRoots.get(ws) || [])];
- },
- handlePresence,
- clearPresence,
- };
- }
|