-
Notifications
You must be signed in to change notification settings - Fork 120
Expand file tree
/
Copy pathstartup.ts
More file actions
334 lines (296 loc) Β· 12.1 KB
/
Copy pathstartup.ts
File metadata and controls
334 lines (296 loc) Β· 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
/**
* Startup & Shutdown
*
* Orchestrates post-boot steps: orphan cleanup, health monitor, master secret,
* server listen, scheduler, gateway init, and graceful shutdown.
*/
import type { AgorConfig } from '@agor/core/config';
import type { Database } from '@agor/core/db';
import type { Id, Paginated, Session, Task } from '@agor/core/types';
import { SessionStatus, TaskStatus } from '@agor/core/types';
import type { Application, SessionsServiceImpl, TasksServiceImpl } from './declarations.js';
import type { GatewayService } from './services/gateway.js';
import { createHealthMonitor } from './services/health-monitor.js';
import type { TerminalsService } from './services/terminals.js';
// ---------------------------------------------------------------------------
// Context
// ---------------------------------------------------------------------------
export interface StartupContext {
app: Application;
db: Database;
config: AgorConfig;
DAEMON_PORT: number;
/** Bind address (default: 'localhost', use '0.0.0.0' for containers) */
DAEMON_HOST: string;
svcEnabled: (group: string) => boolean;
/** Safe service getter β returns undefined if service is not registered */
// biome-ignore lint/suspicious/noExplicitAny: FeathersJS service return type varies by path
safeService: (path: string) => any;
/** Socket.io getSocketServer accessor for graceful shutdown */
getSocketServer: () => import('socket.io').Server | null;
/** Services returned from registerServices() */
sessionsService: SessionsServiceImpl;
terminalsService: TerminalsService | null;
/** Boot start timestamp from startDaemon() for total boot timing */
tBoot: number;
}
// ---------------------------------------------------------------------------
// Orphan cleanup
// ---------------------------------------------------------------------------
async function cleanupOrphans(ctx: StartupContext): Promise<void> {
const { app, sessionsService } = ctx;
// Get tasks service from the app (registered during services phase)
const tasksService = app.service('tasks') as unknown as TasksServiceImpl;
console.log('π§Ή Cleaning up orphaned tasks and sessions...');
// Find all orphaned tasks (running, stopping, awaiting_permission)
const orphanedTasks = await tasksService.getOrphaned();
if (orphanedTasks.length > 0) {
console.log(` Found ${orphanedTasks.length} orphaned task(s)`);
for (const task of orphanedTasks) {
await tasksService.patch(task.task_id, {
status: TaskStatus.STOPPED,
});
console.log(` β Marked task ${task.task_id} as stopped (was: ${task.status})`);
}
}
// Find all orphaned sessions (RUNNING, STOPPING, AWAITING_PERMISSION, AWAITING_INPUT, TIMED_OUT)
const orphanedSessions: Session[] = [];
for (const status of [
SessionStatus.RUNNING,
SessionStatus.STOPPING,
SessionStatus.AWAITING_PERMISSION,
SessionStatus.AWAITING_INPUT,
SessionStatus.TIMED_OUT,
]) {
const result = (await sessionsService.find({
query: { status, $limit: 1000 },
})) as unknown as Paginated<Session>;
orphanedSessions.push(...result.data);
}
if (orphanedSessions.length > 0) {
console.log(` Found ${orphanedSessions.length} orphaned session(s)`);
for (const session of orphanedSessions) {
// IMPORTANT: Use app.service() instead of sessionsService to go through
// FeathersJS service layer and trigger app.publish() for WebSocket events
await app.service('sessions').patch(
session.session_id,
{
status: SessionStatus.IDLE,
ready_for_prompt: true,
},
{}
);
console.log(
` β Marked session ${session.session_id.substring(0, 8)} as idle (was: ${session.status})`
);
}
}
// Also check for sessions that had orphaned tasks (even if session wasn't in RUNNING/STOPPING)
const sessionIdsWithOrphanedTasks = new Set(
orphanedTasks.map((t: Task) => t.session_id as string)
);
if (sessionIdsWithOrphanedTasks.size > 0) {
console.log(
` Checking ${sessionIdsWithOrphanedTasks.size} session(s) with orphaned tasks...`
);
for (const sessionId of sessionIdsWithOrphanedTasks) {
const session = await sessionsService.get(sessionId as Id);
// If session is still in an active state after orphaned task cleanup, set to IDLE
if (
session.status === SessionStatus.RUNNING ||
session.status === SessionStatus.STOPPING ||
session.status === SessionStatus.AWAITING_PERMISSION ||
session.status === SessionStatus.TIMED_OUT
) {
await app.service('sessions').patch(
sessionId as Id,
{
status: SessionStatus.IDLE,
ready_for_prompt: true,
},
{}
);
console.log(
` β Marked session ${sessionId.substring(0, 8)} as idle (had orphaned tasks, was: ${session.status})`
);
}
}
}
if (orphanedTasks.length === 0 && orphanedSessions.length === 0) {
console.log(' No orphaned tasks or sessions found');
}
}
// ---------------------------------------------------------------------------
// Master secret
// ---------------------------------------------------------------------------
async function ensureMasterSecret(config: AgorConfig): Promise<void> {
if (!process.env.AGOR_MASTER_SECRET) {
// Check if we have a saved secret in config
const savedSecret = config.daemon?.masterSecret;
if (savedSecret) {
process.env.AGOR_MASTER_SECRET = savedSecret;
console.log('π Using saved AGOR_MASTER_SECRET from config');
} else {
// Auto-generate a random master secret and persist it in config
const { randomBytes } = await import('node:crypto');
const { setConfigValue } = await import('@agor/core/config');
const generatedSecret = randomBytes(32).toString('hex');
await setConfigValue('daemon.masterSecret', generatedSecret);
process.env.AGOR_MASTER_SECRET = generatedSecret;
console.log('π Generated and saved AGOR_MASTER_SECRET for API key encryption');
console.log(' Secret stored in ~/.agor/config.yaml');
}
} else {
console.log('π API key encryption enabled (AGOR_MASTER_SECRET set)');
}
}
// ---------------------------------------------------------------------------
// Main startup
// ---------------------------------------------------------------------------
export async function startup(ctx: StartupContext): Promise<void> {
const {
app,
db,
config,
DAEMON_PORT,
DAEMON_HOST,
svcEnabled,
safeService,
getSocketServer,
terminalsService,
tBoot,
} = ctx;
const tStartup = performance.now();
// 1. Cleanup orphaned tasks/sessions from previous daemon instance
let t0 = performance.now();
await cleanupOrphans(ctx);
console.log(`β±οΈ [boot] cleanupOrphans: ${(performance.now() - t0).toFixed(0)}ms`);
// 2. Initialize Health Monitor for periodic environment health checks
t0 = performance.now();
const healthMonitor = await createHealthMonitor(app);
console.log(`β±οΈ [boot] createHealthMonitor: ${(performance.now() - t0).toFixed(0)}ms`);
// 3. Validate/generate master secret for API key encryption
t0 = performance.now();
await ensureMasterSecret(config);
console.log(`β±οΈ [boot] ensureMasterSecret: ${(performance.now() - t0).toFixed(0)}ms`);
// 4. Start server
t0 = performance.now();
const server = await app.listen(DAEMON_PORT, DAEMON_HOST);
console.log(`β±οΈ [boot] app.listen: ${(performance.now() - t0).toFixed(0)}ms`);
const displayHost = DAEMON_HOST === '0.0.0.0' ? 'localhost' : DAEMON_HOST;
console.log(
`π Agor daemon running at http://${displayHost}:${DAEMON_PORT} (bound to ${DAEMON_HOST})`
);
console.log(` Health: http://${displayHost}:${DAEMON_PORT}/health`);
console.log(
` Authentication: ${config.daemon?.allowAnonymous !== false ? 'π Anonymous (default)' : 'π Required'}`
);
console.log(` Login: POST http://${displayHost}:${DAEMON_PORT}/authentication`);
console.log(` Services:`);
console.log(` - /sessions`);
console.log(` - /tasks`);
console.log(` - /messages`);
console.log(` - /boards`);
console.log(` - /repos`);
console.log(` - /mcp-servers`);
console.log(` - /config`);
console.log(` - /context`);
console.log(` - /users`);
// 5. Start scheduler service (background worker) β dynamically imported to avoid
// loading the module at all when scheduler is disabled (lean mode optimization)
let schedulerService: import('./services/scheduler.js').SchedulerService | null = null;
if (svcEnabled('scheduler')) {
t0 = performance.now();
const { SchedulerService } = await import('./services/scheduler.js');
schedulerService = new SchedulerService(db, app, {
tickInterval: 30000, // 30 seconds
gracePeriod: 120000, // 2 minutes
debug: process.env.NODE_ENV !== 'production',
unixUserMode: config.execution?.unix_user_mode ?? 'simple',
});
schedulerService.start();
console.log(`β±οΈ [boot] scheduler init: ${(performance.now() - t0).toFixed(0)}ms`);
console.log(`π Scheduler started (tick interval: 30s)`);
}
// 6. Initialize gateway: refresh channel state cache, then start Socket Mode listeners
const gatewayService = safeService('gateway') as unknown as GatewayService | undefined;
if (gatewayService) {
const tGw = performance.now();
gatewayService
.refreshChannelState()
.then(() => {
return gatewayService.startListeners();
})
.then(() => {
console.log(`β±οΈ [boot] gateway init (async): ${(performance.now() - tGw).toFixed(0)}ms`);
})
.catch((error: unknown) => {
console.error('[gateway] Failed to start listeners:', error);
});
}
// 7. Graceful shutdown handler
const shutdown = async (signal: string) => {
console.log(`\nβ³ Received ${signal}, shutting down gracefully...`);
try {
// Clean up health monitor
healthMonitor.cleanup();
// Clean up terminal sessions
if (terminalsService) {
console.log('π₯οΈ Cleaning up terminal sessions...');
terminalsService.cleanup();
}
// Stop gateway listeners
if (gatewayService) {
console.log('π Stopping gateway listeners...');
await gatewayService.stopListeners();
}
// Stop scheduler
if (schedulerService) {
console.log('π Stopping scheduler...');
schedulerService.stop();
}
// Close Socket.io connections (this also closes the HTTP server)
const socketServer = getSocketServer();
if (socketServer) {
console.log('π Closing Socket.io and HTTP server...');
// Disconnect all active clients first
socketServer.disconnectSockets();
// Give sockets a moment to disconnect
await new Promise<void>((resolve) => setTimeout(resolve, 100));
// Now close the server with a timeout
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
console.warn('β οΈ Server close timeout, forcing exit');
resolve();
}, 2000);
socketServer?.close(() => {
clearTimeout(timeout);
console.log('β
Server closed');
resolve();
});
});
} else {
// Fallback: close HTTP server directly if Socket.io wasn't initialized
await new Promise<void>((resolve, reject) => {
server.close((err: Error | undefined) => {
if (err) {
console.error('β Error closing server:', err);
reject(err);
} else {
console.log('β
HTTP server closed');
resolve();
}
});
});
}
process.exit(0);
} catch (error) {
console.error('β Error during shutdown:', error);
process.exit(1);
}
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
console.log(`β±οΈ [boot] Phase 4 β startup: ${(performance.now() - tStartup).toFixed(0)}ms`);
console.log(`β±οΈ [boot] Total boot time: ${(performance.now() - tBoot).toFixed(0)}ms`);
}