import type { IncomingMessage, ServerResponse } from 'http'; import type { HostApiContext } from '../context'; import { parseJsonBody, sendJson } from '../route-utils'; interface GatewayCronJob { id: string; name: string; description?: string; enabled: boolean; createdAtMs: number; updatedAtMs: number; schedule: { kind: string; expr?: string; everyMs?: number; at?: string; tz?: string }; payload: { kind: string; message?: string; text?: string }; delivery?: { mode: string; channel?: string; to?: string }; sessionTarget?: string; state: { nextRunAtMs?: number; lastRunAtMs?: number; lastStatus?: string; lastError?: string; lastDurationMs?: number; }; } function transformCronJob(job: GatewayCronJob) { const message = job.payload?.message || job.payload?.text || ''; const channelType = job.delivery?.channel; const target = channelType ? { channelType, channelId: channelType, channelName: channelType } : undefined; const lastRun = job.state?.lastRunAtMs ? { time: new Date(job.state.lastRunAtMs).toISOString(), success: job.state.lastStatus === 'ok', error: job.state.lastError, duration: job.state.lastDurationMs, } : undefined; const nextRun = job.state?.nextRunAtMs ? new Date(job.state.nextRunAtMs).toISOString() : undefined; return { id: job.id, name: job.name, message, schedule: job.schedule, target, enabled: job.enabled, createdAt: new Date(job.createdAtMs).toISOString(), updatedAt: new Date(job.updatedAtMs).toISOString(), lastRun, nextRun, }; } export async function handleCronRoutes( req: IncomingMessage, res: ServerResponse, url: URL, ctx: HostApiContext, ): Promise { if (url.pathname === '/api/cron/jobs' && req.method === 'GET') { try { const result = await ctx.gatewayManager.rpc('cron.list', { includeDisabled: true }); const data = result as { jobs?: GatewayCronJob[] }; const jobs = data?.jobs ?? []; for (const job of jobs) { const isIsolatedAgent = (job.sessionTarget === 'isolated' || !job.sessionTarget) && job.payload?.kind === 'agentTurn'; const needsRepair = isIsolatedAgent && job.delivery?.mode === 'announce' && !job.delivery?.channel; if (needsRepair) { try { await ctx.gatewayManager.rpc('cron.update', { id: job.id, patch: { delivery: { mode: 'none' } }, }); job.delivery = { mode: 'none' }; if (job.state?.lastError?.includes('Channel is required')) { job.state.lastError = undefined; job.state.lastStatus = 'ok'; } } catch { // ignore per-job repair failure } } } sendJson(res, 200, jobs.map(transformCronJob)); } catch (error) { sendJson(res, 500, { success: false, error: String(error) }); } return true; } if (url.pathname === '/api/cron/jobs' && req.method === 'POST') { try { const input = await parseJsonBody<{ name: string; message: string; schedule: string; enabled?: boolean }>(req); const result = await ctx.gatewayManager.rpc('cron.add', { name: input.name, schedule: { kind: 'cron', expr: input.schedule }, payload: { kind: 'agentTurn', message: input.message }, enabled: input.enabled ?? true, wakeMode: 'next-heartbeat', sessionTarget: 'isolated', delivery: { mode: 'none' }, }); sendJson(res, 200, result && typeof result === 'object' ? transformCronJob(result as GatewayCronJob) : result); } catch (error) { sendJson(res, 500, { success: false, error: String(error) }); } return true; } if (url.pathname.startsWith('/api/cron/jobs/') && req.method === 'PUT') { try { const id = decodeURIComponent(url.pathname.slice('/api/cron/jobs/'.length)); const input = await parseJsonBody>(req); const patch = { ...input }; if (typeof patch.schedule === 'string') { patch.schedule = { kind: 'cron', expr: patch.schedule }; } if (typeof patch.message === 'string') { patch.payload = { kind: 'agentTurn', message: patch.message }; delete patch.message; } sendJson(res, 200, await ctx.gatewayManager.rpc('cron.update', { id, patch })); } catch (error) { sendJson(res, 500, { success: false, error: String(error) }); } return true; } if (url.pathname.startsWith('/api/cron/jobs/') && req.method === 'DELETE') { try { const id = decodeURIComponent(url.pathname.slice('/api/cron/jobs/'.length)); sendJson(res, 200, await ctx.gatewayManager.rpc('cron.remove', { id })); } catch (error) { sendJson(res, 500, { success: false, error: String(error) }); } return true; } if (url.pathname === '/api/cron/toggle' && req.method === 'POST') { try { const body = await parseJsonBody<{ id: string; enabled: boolean }>(req); sendJson(res, 200, await ctx.gatewayManager.rpc('cron.update', { id: body.id, patch: { enabled: body.enabled } })); } catch (error) { sendJson(res, 500, { success: false, error: String(error) }); } return true; } if (url.pathname === '/api/cron/trigger' && req.method === 'POST') { try { const body = await parseJsonBody<{ id: string }>(req); sendJson(res, 200, await ctx.gatewayManager.rpc('cron.run', { id: body.id, mode: 'force' })); } catch (error) { sendJson(res, 500, { success: false, error: String(error) }); } return true; } return false; }