@telora/daemon 0.14.2 → 0.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/build-info.json +2 -2
  2. package/dist/queries/schemas.d.ts +4 -4
  3. package/dist/queries/schemas.js +1 -1
  4. package/dist/queries/schemas.js.map +1 -1
  5. package/dist/spawner-lifecycle.d.ts +43 -0
  6. package/dist/spawner-lifecycle.d.ts.map +1 -0
  7. package/dist/spawner-lifecycle.js +152 -0
  8. package/dist/spawner-lifecycle.js.map +1 -0
  9. package/dist/spawner-stream-handlers.d.ts +67 -0
  10. package/dist/spawner-stream-handlers.d.ts.map +1 -0
  11. package/dist/spawner-stream-handlers.js +193 -0
  12. package/dist/spawner-stream-handlers.js.map +1 -0
  13. package/dist/spawner.d.ts +15 -36
  14. package/dist/spawner.d.ts.map +1 -1
  15. package/dist/spawner.js +29 -327
  16. package/dist/spawner.js.map +1 -1
  17. package/dist/strategy-completion-event.d.ts +29 -0
  18. package/dist/strategy-completion-event.d.ts.map +1 -0
  19. package/dist/strategy-completion-event.js +69 -0
  20. package/dist/strategy-completion-event.js.map +1 -0
  21. package/dist/strategy-completion.d.ts.map +1 -1
  22. package/dist/strategy-completion.js +27 -11
  23. package/dist/strategy-completion.js.map +1 -1
  24. package/dist/strategy-executor.d.ts +8 -45
  25. package/dist/strategy-executor.d.ts.map +1 -1
  26. package/dist/strategy-executor.js +22 -423
  27. package/dist/strategy-executor.js.map +1 -1
  28. package/dist/strategy-spawn-helpers.d.ts +67 -0
  29. package/dist/strategy-spawn-helpers.d.ts.map +1 -0
  30. package/dist/strategy-spawn-helpers.js +160 -0
  31. package/dist/strategy-spawn-helpers.js.map +1 -0
  32. package/dist/strategy-team-lifecycle.d.ts +50 -0
  33. package/dist/strategy-team-lifecycle.d.ts.map +1 -0
  34. package/dist/strategy-team-lifecycle.js +218 -0
  35. package/dist/strategy-team-lifecycle.js.map +1 -0
  36. package/dist/unified-engine-lifecycle.d.ts +36 -0
  37. package/dist/unified-engine-lifecycle.d.ts.map +1 -0
  38. package/dist/unified-engine-lifecycle.js +250 -0
  39. package/dist/unified-engine-lifecycle.js.map +1 -0
  40. package/dist/unified-shell-config.d.ts +25 -0
  41. package/dist/unified-shell-config.d.ts.map +1 -1
  42. package/dist/unified-shell-config.js +45 -0
  43. package/dist/unified-shell-config.js.map +1 -1
  44. package/dist/unified-shell-status.d.ts.map +1 -1
  45. package/dist/unified-shell-status.js +16 -2
  46. package/dist/unified-shell-status.js.map +1 -1
  47. package/dist/unified-shell.d.ts +2 -15
  48. package/dist/unified-shell.d.ts.map +1 -1
  49. package/dist/unified-shell.js +18 -619
  50. package/dist/unified-shell.js.map +1 -1
  51. package/package.json +1 -1
@@ -25,391 +25,16 @@
25
25
  import { readFileSync, openSync, closeSync } from 'node:fs';
26
26
  import { dirname, join, resolve } from 'node:path';
27
27
  import { fileURLToPath } from 'node:url';
28
- import { hostname as osHostname, networkInterfaces } from 'node:os';
29
28
  import { spawn, execFileSync } from 'node:child_process';
30
- import { loadUnifiedConfig, buildEngineConfig, acquirePidLock, releasePidLock, setupSignalHandlers, PidLockError, forceCloseAllCircuitBreakers, ResourceGovernor, } from '@telora/daemon-core';
31
- import { StrategyEngine } from './strategy-engine.js';
32
- import { PMEngine } from './pm-engine.js';
33
- import { LoopEngine } from './loop-engine.js';
29
+ import { loadUnifiedConfig, acquirePidLock, releasePidLock, setupSignalHandlers, PidLockError, } from '@telora/daemon-core';
34
30
  import { startAutoUpdateLoop } from './auto-update.js';
35
31
  import { setDaemonShuttingDown } from './shutdown-state.js';
36
32
  import { rotateDaemonLog, setupCanonicalLogTee, writeDaemonMeta, removeDaemonMeta, resolveDaemonLogPath, resolvePidFilePath, ensureGlobalStateDir } from './daemon-process.js';
33
+ import { printUnifiedConfigSummary } from './unified-shell-status.js';
34
+ import { resolveAutoUpdateEnabled, resolveAutoUpdateIntervalMs } from './unified-shell-config.js';
35
+ import { createEnginesAndGovernor, initializeAndStartEngines, claimProducts, registerDiagnosticSignals, } from './unified-engine-lifecycle.js';
37
36
  // ---------------------------------------------------------------------------
38
- // LAN host detection (mirrored from daemon's config.ts to avoid circular deps)
39
- // ---------------------------------------------------------------------------
40
- /**
41
- * Auto-detect the first non-internal IPv4 address for LAN-accessible URLs.
42
- * Falls back to 'localhost' if no suitable address is found.
43
- */
44
- function detectLanHost() {
45
- const interfaces = networkInterfaces();
46
- for (const ifaceEntries of Object.values(interfaces)) {
47
- if (!ifaceEntries)
48
- continue;
49
- for (const iface of ifaceEntries) {
50
- if (iface.family === 'IPv4' && !iface.internal) {
51
- return iface.address;
52
- }
53
- }
54
- }
55
- return 'localhost';
56
- }
57
- // ---------------------------------------------------------------------------
58
- // Numeric env-var helpers (mirrored from daemon/config.ts)
59
- // ---------------------------------------------------------------------------
60
- function resolveInt(envVal, fileVal, defaultVal) {
61
- if (envVal !== undefined && envVal !== '') {
62
- return parseInt(envVal, 10);
63
- }
64
- if (typeof fileVal === 'number') {
65
- return fileVal;
66
- }
67
- return defaultVal;
68
- }
69
- function resolveFloat(envVal, fileVal, defaultVal) {
70
- if (envVal !== undefined && envVal !== '') {
71
- return parseFloat(envVal);
72
- }
73
- if (typeof fileVal === 'number') {
74
- return fileVal;
75
- }
76
- return defaultVal;
77
- }
78
- // ---------------------------------------------------------------------------
79
- // Engine config builders
80
- // ---------------------------------------------------------------------------
81
- /**
82
- * Build a complete strategy engine config (DaemonConfig) from the unified
83
- * base config and the strategy engine section. Applies the same defaults
84
- * and env-var overrides as daemon/src/config.ts loadConfig().
85
- */
86
- function buildStrategyConfig(base, section, env = process.env) {
87
- // Start with the flat merge from daemon-core
88
- const flat = buildEngineConfig(base, section);
89
- const fields = section.fields;
90
- const repoPath = flat.repoPath || process.cwd();
91
- const claudeCodePath = flat.claudeCodePath || 'claude';
92
- const logDir = flat.logDir || resolve(repoPath, '.telora', 'logs');
93
- const sessionTimeoutMs = flat.sessionTimeoutMs ?? 3600000;
94
- // Strategy-specific fields with env overrides
95
- const worktreeDir = env.TELORA_WORKTREE_DIR
96
- || (typeof fields.worktreeDir === 'string' ? fields.worktreeDir : '')
97
- || resolve(repoPath, '.telora', 'worktrees');
98
- const integrationBranch = env.TELORA_INTEGRATION_BRANCH
99
- || (typeof fields.integrationBranch === 'string' ? fields.integrationBranch : '')
100
- || 'integration';
101
- const maxTotalSessions = resolveInt(env.MAX_TOTAL_SESSIONS, fields.maxTotalSessions, 5);
102
- const tokenLimit = resolveInt(env.TOKEN_LIMIT, fields.tokenLimit, 200000);
103
- const costLimit = resolveFloat(env.COST_LIMIT, fields.costLimit, 10.0);
104
- const mergeLockTimeoutMs = resolveInt(env.MERGE_LOCK_TIMEOUT_MS, fields.mergeLockTimeoutMs, 300000);
105
- const mergeLockContentionWarningMs = resolveInt(env.MERGE_LOCK_CONTENTION_WARNING_MS, fields.mergeLockContentionWarningMs, 30000);
106
- // Guard failure mode
107
- const rawPolicyFailureMode = env.TELORA_POLICY_FAILURE_MODE
108
- || (typeof fields.policyFailureMode === 'string' ? fields.policyFailureMode : '')
109
- || 'fail-open';
110
- if (rawPolicyFailureMode !== 'fail-open' && rawPolicyFailureMode !== 'fail-closed') {
111
- throw new Error(`Invalid TELORA_POLICY_FAILURE_MODE: "${rawPolicyFailureMode}". Must be "fail-open" or "fail-closed".`);
112
- }
113
- // Log retention
114
- const logMaxAgeDays = resolveInt(env.LOG_MAX_AGE_DAYS, fields.logMaxAgeDays, 7);
115
- const logMaxTotalBytes = resolveInt(env.LOG_MAX_TOTAL_BYTES, fields.logMaxTotalBytes, 1073741824);
116
- const logMaxFiles = resolveInt(env.LOG_MAX_FILES, fields.logMaxFiles, 500);
117
- // Telemetry
118
- const telemetryFile = (typeof fields.telemetry === 'object' && fields.telemetry !== null)
119
- ? fields.telemetry
120
- : {};
121
- const telemetry = {
122
- enabled: env.TELORA_TELEMETRY_ENABLED !== undefined
123
- ? env.TELORA_TELEMETRY_ENABLED !== '0' && env.TELORA_TELEMETRY_ENABLED !== 'false'
124
- : (typeof telemetryFile.enabled === 'boolean' ? telemetryFile.enabled : true),
125
- port: resolveInt(env.TELORA_TELEMETRY_PORT, telemetryFile.port, 4318),
126
- flushIntervalMs: resolveInt(env.TELORA_TELEMETRY_FLUSH_INTERVAL_MS, telemetryFile.flushIntervalMs, 5000),
127
- retentionDays: resolveInt(env.TELORA_TELEMETRY_RETENTION_DAYS, telemetryFile.retentionDays, 30),
128
- };
129
- // QA environment
130
- const qaPortRangeStart = resolveInt(env.TELORA_QA_PORT_RANGE_START, fields.qaPortRangeStart, 9100);
131
- const qaPortRangeEnd = resolveInt(env.TELORA_QA_PORT_RANGE_END, fields.qaPortRangeEnd, 9199);
132
- const qaHost = env.TELORA_QA_HOST
133
- || (typeof fields.qaHost === 'string' ? fields.qaHost : '')
134
- || detectLanHost();
135
- // Local Supabase
136
- const localSupabaseUrl = env.TELORA_LOCAL_SUPABASE_URL
137
- || (typeof fields.localSupabaseUrl === 'string' ? fields.localSupabaseUrl : '')
138
- || 'http://127.0.0.1:54321';
139
- const localSupabaseAnonKey = env.TELORA_LOCAL_SUPABASE_ANON_KEY
140
- || (typeof fields.localSupabaseAnonKey === 'string' ? fields.localSupabaseAnonKey : '')
141
- || null;
142
- // Products array from base config (already resolved by loadUnifiedConfig)
143
- const products = flat.products ?? [];
144
- return {
145
- // Base fields
146
- teloraUrl: flat.teloraUrl,
147
- trackerId: flat.trackerId,
148
- organizationId: flat.organizationId,
149
- productId: flat.productId,
150
- repoPath,
151
- products,
152
- claudeCodePath,
153
- logDir,
154
- sessionTimeoutMs,
155
- // Strategy-specific fields
156
- worktreeDir,
157
- integrationBranch,
158
- maxTotalSessions,
159
- tokenLimit,
160
- costLimit,
161
- mergeLockTimeoutMs,
162
- mergeLockContentionWarningMs,
163
- policyFailureMode: rawPolicyFailureMode,
164
- logMaxAgeDays,
165
- logMaxTotalBytes,
166
- logMaxFiles,
167
- telemetry,
168
- qaPortRangeStart,
169
- qaPortRangeEnd,
170
- qaHost,
171
- localSupabaseUrl,
172
- localSupabaseAnonKey,
173
- };
174
- }
175
- /**
176
- * Build a complete factory engine config (FactoryConfig) from the unified
177
- * base config and the factory engine section. Applies the same defaults
178
- * and env-var overrides as factory/src/config.ts loadConfig().
179
- */
180
- function buildFactoryConfig(base, section, env = process.env) {
181
- const flat = buildEngineConfig(base, section);
182
- const fields = section.fields;
183
- const repoPath = flat.repoPath || process.cwd();
184
- const claudeCodePath = flat.claudeCodePath || 'claude';
185
- // Factory uses a separate log directory by default
186
- const logDir = flat.logDir || resolve(repoPath, '.telora', 'factory-logs');
187
- const sessionTimeoutMs = flat.sessionTimeoutMs ?? 3600000;
188
- // Factory-specific fields with env overrides
189
- const factoryWorktreeDir = env.TELORA_FACTORY_WORKTREE_DIR
190
- || (typeof fields.factoryWorktreeDir === 'string' ? fields.factoryWorktreeDir : '')
191
- || resolve(repoPath, '.telora', 'factory-worktrees');
192
- const maxConcurrentInstances = env.FACTORY_MAX_CONCURRENT_INSTANCES
193
- ? parseInt(env.FACTORY_MAX_CONCURRENT_INSTANCES, 10)
194
- : (typeof fields.maxConcurrentInstances === 'number' ? fields.maxConcurrentInstances : 3);
195
- // Telemetry: inherit from flat config (resolved by buildEngineConfig) or default
196
- const telemetry = (flat.telemetry && typeof flat.telemetry === 'object')
197
- ? flat.telemetry
198
- : { enabled: true, port: 4318 };
199
- // Products array from base config (already resolved by loadUnifiedConfig)
200
- const products = flat.products ?? [];
201
- return {
202
- // Base fields
203
- teloraUrl: flat.teloraUrl,
204
- trackerId: flat.trackerId,
205
- organizationId: flat.organizationId,
206
- productId: flat.productId,
207
- repoPath,
208
- products,
209
- claudeCodePath,
210
- logDir,
211
- sessionTimeoutMs,
212
- // Factory-specific fields
213
- factoryWorktreeDir,
214
- maxConcurrentInstances,
215
- telemetry,
216
- };
217
- }
218
- /**
219
- * Extract the PM engine section from the raw file config.
220
- * Returns an EngineSection with PM-specific fields (pollCadence, etc.).
221
- */
222
- function pmSectionFromRawConfig(rawFileConfig) {
223
- const engines = rawFileConfig.engines;
224
- if (typeof engines === 'object' && engines !== null && !Array.isArray(engines)) {
225
- const pm = engines.pm;
226
- if (typeof pm === 'object' && pm !== null && !Array.isArray(pm)) {
227
- const pmObj = pm;
228
- const fields = {};
229
- for (const [key, value] of Object.entries(pmObj)) {
230
- if (key !== 'enabled') {
231
- fields[key] = value;
232
- }
233
- }
234
- return { enabled: pmObj.enabled !== false, fields };
235
- }
236
- }
237
- return { enabled: true, fields: {} };
238
- }
239
- /**
240
- * Extract the Loop engine section from the raw file config.
241
- * Returns an EngineSection with loop-specific fields (intervals, thresholds, etc.).
242
- */
243
- function loopSectionFromRawConfig(rawFileConfig) {
244
- const engines = rawFileConfig.engines;
245
- if (typeof engines === 'object' && engines !== null && !Array.isArray(engines)) {
246
- const loop = engines.loop;
247
- if (typeof loop === 'object' && loop !== null && !Array.isArray(loop)) {
248
- const loopObj = loop;
249
- const fields = {};
250
- for (const [key, value] of Object.entries(loopObj)) {
251
- if (key !== 'enabled') {
252
- fields[key] = value;
253
- }
254
- }
255
- return { enabled: loopObj.enabled !== false, fields };
256
- }
257
- }
258
- return { enabled: true, fields: {} };
259
- }
260
- // ---------------------------------------------------------------------------
261
- // Status reporting
262
- // ---------------------------------------------------------------------------
263
- /**
264
- * Print aggregated status from all active engines.
265
- */
266
- function printAggregatedStatus(engines, governor) {
267
- console.log('\n=== Unified Daemon Status ===');
268
- console.log(`Engines: ${engines.map(e => e.name).join(', ')}`);
269
- console.log(`PID: ${process.pid}`);
270
- console.log(`Uptime: ${Math.round(process.uptime())}s`);
271
- // Resource governor utilization
272
- if (governor) {
273
- const util = governor.getUtilization();
274
- console.log(`\n--- Resource Governor ---`);
275
- console.log(` Global slots: ${util.total}/${util.max} in use`);
276
- for (const [engine, count] of Object.entries(util.perEngine)) {
277
- console.log(` ${engine}: ${count} slots`);
278
- }
279
- if (util.queueDepth > 0) {
280
- console.log(` Queue depth: ${util.queueDepth}`);
281
- for (const [engine, depth] of Object.entries(util.queueDepthPerEngine)) {
282
- console.log(` ${engine}: ${depth} waiting`);
283
- }
284
- }
285
- }
286
- for (const engine of engines) {
287
- const health = engine.healthCheck();
288
- const resources = engine.getResourceUsage();
289
- console.log(`\n--- ${engine.name} engine ---`);
290
- console.log(` Status: ${health.status}`);
291
- console.log(` Active work items: ${health.activeWorkItems}`);
292
- console.log(` Claude processes: ${resources.activeClaudeProcesses}`);
293
- console.log(` Active worktrees: ${resources.activeWorktrees}`);
294
- // Print engine-specific details
295
- const details = health.details;
296
- for (const [key, value] of Object.entries(details)) {
297
- if (Array.isArray(value)) {
298
- if (value.length > 0) {
299
- console.log(` ${key}:`);
300
- for (const item of value) {
301
- if (typeof item === 'object' && item !== null) {
302
- const parts = Object.entries(item)
303
- .map(([k, v]) => `${k}: ${String(v)}`)
304
- .join(' | ');
305
- console.log(` - ${parts}`);
306
- }
307
- else {
308
- console.log(` - ${String(item)}`);
309
- }
310
- }
311
- }
312
- }
313
- else if (typeof value === 'object' && value !== null) {
314
- const entries = Object.entries(value);
315
- if (entries.length > 0) {
316
- console.log(` ${key}:`);
317
- for (const [k, v] of entries) {
318
- console.log(` ${k}: ${String(v)}`);
319
- }
320
- }
321
- }
322
- else {
323
- console.log(` ${key}: ${String(value)}`);
324
- }
325
- }
326
- }
327
- console.log('');
328
- }
329
- /**
330
- * Print a dry-run summary of the unified config.
331
- */
332
- function printUnifiedConfigSummary(config) {
333
- console.log('Unified Daemon Configuration:');
334
- console.log(` Telora URL: ${config.base.teloraUrl ?? '[not set]'}`);
335
- console.log(` Tracker ID: ${config.base.trackerId ? '[configured]' : '[not set]'}`);
336
- console.log(` Organization ID: ${config.base.organizationId ?? '[not set]'}`);
337
- const products = config.base.products ?? [];
338
- if (products.length > 0) {
339
- console.log(` Products: ${products.length}`);
340
- for (const p of products) {
341
- console.log(` - ${p.id} -> ${p.repoPath}`);
342
- }
343
- }
344
- else {
345
- console.log(` Product ID: ${config.base.productId ?? '[not set]'}`);
346
- console.log(` Repository Path: ${config.base.repoPath ?? '[cwd]'}`);
347
- }
348
- console.log(` Claude Code Path: ${config.base.claudeCodePath ?? 'claude'}`);
349
- console.log(` Log Directory: ${config.base.logDir ?? '[default]'}`);
350
- console.log(` Session Timeout: ${config.base.sessionTimeoutMs ?? 3600000}ms`);
351
- console.log(`\n Strategy engine: ${config.strategy.enabled ? 'enabled' : 'disabled'}`);
352
- if (config.strategy.enabled) {
353
- const sf = config.strategy.fields;
354
- console.log(` worktreeDir: ${String(sf.worktreeDir ?? '[default]')}`);
355
- console.log(` integrationBranch: ${String(sf.integrationBranch ?? 'integration')}`);
356
- console.log(` maxTotalSessions: ${String(sf.maxTotalSessions ?? 5)}`);
357
- }
358
- console.log(`\n Factory engine: ${config.factory.enabled ? 'enabled' : 'disabled'}`);
359
- if (config.factory.enabled) {
360
- const ff = config.factory.fields;
361
- console.log(` factoryWorktreeDir: ${String(ff.factoryWorktreeDir ?? '[default]')}`);
362
- console.log(` maxConcurrentInstances: ${String(ff.maxConcurrentInstances ?? 3)}`);
363
- }
364
- const pmSection = pmSectionFromRawConfig(config.rawFileConfig);
365
- console.log(`\n PM engine: ${pmSection.enabled ? 'enabled' : 'disabled'}`);
366
- const loopSec = loopSectionFromRawConfig(config.rawFileConfig);
367
- console.log(` Loop engine: ${loopSec.enabled ? 'enabled' : 'disabled'}`);
368
- }
369
- // ---------------------------------------------------------------------------
370
- // Auto-update config resolution
371
- // ---------------------------------------------------------------------------
372
- const DEFAULT_AUTO_UPDATE_INTERVAL_MS = 3_600_000; // 1 hour
373
- /**
374
- * Resolve whether auto-update is enabled from env vars and config file.
375
- * Env var TELORA_AUTO_UPDATE takes precedence over config file autoUpdate.enabled.
376
- * Default: true.
377
- */
378
- function resolveAutoUpdateEnabled(config) {
379
- const envVal = process.env.TELORA_AUTO_UPDATE;
380
- if (envVal !== undefined && envVal !== '') {
381
- return envVal !== '0' && envVal !== 'false';
382
- }
383
- const fileConfig = config.rawFileConfig;
384
- if (typeof fileConfig.autoUpdate === 'object' && fileConfig.autoUpdate !== null) {
385
- const au = fileConfig.autoUpdate;
386
- if (typeof au.enabled === 'boolean')
387
- return au.enabled;
388
- }
389
- return true;
390
- }
391
- /**
392
- * Resolve auto-update check interval from env vars and config file.
393
- * Env var TELORA_AUTO_UPDATE_INTERVAL_MS takes precedence.
394
- * Default: 3600000 (1 hour).
395
- */
396
- function resolveAutoUpdateIntervalMs(config) {
397
- const envVal = process.env.TELORA_AUTO_UPDATE_INTERVAL_MS;
398
- if (envVal !== undefined && envVal !== '') {
399
- const parsed = parseInt(envVal, 10);
400
- if (!isNaN(parsed) && parsed > 0)
401
- return parsed;
402
- }
403
- const fileConfig = config.rawFileConfig;
404
- if (typeof fileConfig.autoUpdate === 'object' && fileConfig.autoUpdate !== null) {
405
- const au = fileConfig.autoUpdate;
406
- if (typeof au.intervalMs === 'number' && au.intervalMs > 0)
407
- return au.intervalMs;
408
- }
409
- return DEFAULT_AUTO_UPDATE_INTERVAL_MS;
410
- }
411
- // ---------------------------------------------------------------------------
412
- // Extracted helpers for runUnifiedDaemon()
37
+ // Config loading helper
413
38
  // ---------------------------------------------------------------------------
414
39
  /**
415
40
  * Load unified config, apply engine filter, handle verbose/dry-run output.
@@ -448,232 +73,6 @@ function buildUnifiedConfig(opts) {
448
73
  }
449
74
  return config;
450
75
  }
451
- /**
452
- * Create engine instances from config, set up the resource governor, and
453
- * inject it into each engine. Exits the process if no engines are available.
454
- */
455
- async function createEnginesAndGovernor(config, pidFilePath) {
456
- const engines = [];
457
- if (config.strategy.enabled) {
458
- engines.push(new StrategyEngine());
459
- }
460
- if (config.factory.enabled) {
461
- try {
462
- // Dynamic import: @telora/factory may not be installed in all deployments.
463
- // The specifier is constructed via a variable so TypeScript does not attempt
464
- // static module resolution (the factory dist/ may not exist at typecheck time).
465
- const factoryModulePath = '@telora/factory/factory-engine';
466
- const factoryModule = await import(/* webpackIgnore: true */ factoryModulePath);
467
- engines.push(new factoryModule.FactoryEngine());
468
- }
469
- catch (err) {
470
- console.warn(`Factory engine is enabled but @telora/factory could not be loaded: ` +
471
- `${err instanceof Error ? err.message : String(err)}`);
472
- console.warn('The factory engine will be skipped. Install @telora/factory to enable it.');
473
- }
474
- }
475
- // PM engine: enabled by default, no dynamic import needed
476
- const pmSection = config.rawFileConfig.engines
477
- ? config.rawFileConfig.engines.pm
478
- : undefined;
479
- const pmEnabled = pmSection !== undefined
480
- ? pmSection.enabled !== false
481
- : true;
482
- // Allow env-var override for PM engine
483
- const pmEnvEnabled = process.env.TELORA_ENGINE_PM_ENABLED;
484
- const pmFinalEnabled = pmEnvEnabled !== undefined
485
- ? pmEnvEnabled !== '0' && pmEnvEnabled !== 'false'
486
- : pmEnabled;
487
- if (pmFinalEnabled) {
488
- engines.push(new PMEngine());
489
- }
490
- // Loop engine: enabled by default, no dynamic import needed
491
- const loopSection = config.rawFileConfig.engines
492
- ? config.rawFileConfig.engines.loop
493
- : undefined;
494
- const loopEnabled = loopSection !== undefined
495
- ? loopSection.enabled !== false
496
- : true;
497
- const loopEnvEnabled = process.env.TELORA_ENGINE_LOOP_ENABLED;
498
- const loopFinalEnabled = loopEnvEnabled !== undefined
499
- ? loopEnvEnabled !== '0' && loopEnvEnabled !== 'false'
500
- : loopEnabled;
501
- if (loopFinalEnabled) {
502
- engines.push(new LoopEngine());
503
- }
504
- if (engines.length === 0) {
505
- console.error('No engines are enabled. Enable at least one engine in the config or remove --engine filter.');
506
- releasePidLock(pidFilePath);
507
- process.exit(1);
508
- }
509
- // Compute per-engine limits from config (using resolved defaults)
510
- const engineLimits = {};
511
- if (config.strategy.enabled && engines.some(e => e.name === 'strategy')) {
512
- const strategyMax = typeof config.strategy.fields.maxTotalSessions === 'number'
513
- ? config.strategy.fields.maxTotalSessions : 5;
514
- engineLimits.strategy = strategyMax;
515
- }
516
- if (config.factory.enabled && engines.some(e => e.name === 'factory')) {
517
- const factoryMax = typeof config.factory.fields.maxConcurrentInstances === 'number'
518
- ? config.factory.fields.maxConcurrentInstances : 3;
519
- engineLimits.factory = factoryMax;
520
- }
521
- // Global max: sum of per-engine limits (or env override)
522
- const env = process.env;
523
- const defaultGlobalMax = Object.values(engineLimits).reduce((sum, v) => sum + v, 0) || 8;
524
- const maxGlobalSessions = env.TELORA_MAX_GLOBAL_SESSIONS
525
- ? parseInt(env.TELORA_MAX_GLOBAL_SESSIONS, 10)
526
- : (typeof config.rawFileConfig.maxGlobalSessions === 'number'
527
- ? config.rawFileConfig.maxGlobalSessions : defaultGlobalMax);
528
- const governor = new ResourceGovernor({
529
- maxGlobalSessions,
530
- engineLimits,
531
- });
532
- // Inject governor into engines via the ExecutionEngine interface
533
- for (const engine of engines) {
534
- engine.setGovernor?.(governor);
535
- }
536
- console.log(`Resource governor: max ${maxGlobalSessions} global sessions ` +
537
- `(${Object.entries(engineLimits).map(([k, v]) => `${k}: ${v}`).join(', ')})`);
538
- return { engines, governor };
539
- }
540
- /**
541
- * Initialize, run crash recovery, and start each engine. On failure, stops
542
- * any engines that already started, releases the PID lock, and exits.
543
- */
544
- async function initializeAndStartEngines(engines, config, opts, pidFilePath) {
545
- try {
546
- for (const engine of engines) {
547
- console.log(`\n--- Initializing ${engine.name} engine ---`);
548
- // Build the full config for this engine with all defaults applied
549
- let engineConfig;
550
- if (engine.name === 'strategy') {
551
- engineConfig = buildStrategyConfig(config.base, config.strategy);
552
- }
553
- else if (engine.name === 'factory') {
554
- engineConfig = buildFactoryConfig(config.base, config.factory);
555
- }
556
- else if (engine.name === 'pm') {
557
- // PM engine uses base config + PM-specific fields from engines.pm section
558
- const pmSection = pmSectionFromRawConfig(config.rawFileConfig);
559
- engineConfig = buildEngineConfig(config.base, pmSection);
560
- }
561
- else if (engine.name === 'loop') {
562
- // Loop engine uses base config + loop-specific fields from engines.loop section
563
- const loopSec = loopSectionFromRawConfig(config.rawFileConfig);
564
- engineConfig = buildEngineConfig(config.base, loopSec);
565
- }
566
- else {
567
- // Unknown engine type -- pass a basic merged config
568
- const section = config.strategy.enabled ? config.strategy : config.factory;
569
- engineConfig = buildEngineConfig(config.base, section);
570
- }
571
- // Init
572
- await engine.init(engineConfig);
573
- console.log(`[${engine.name}] Initialized`);
574
- // Crash recovery
575
- if (opts.skipRecovery) {
576
- console.log(`[${engine.name}] Skipping crash recovery (--skip-recovery flag)`);
577
- }
578
- else {
579
- await engine.recoverFromCrash();
580
- console.log(`[${engine.name}] Crash recovery complete`);
581
- }
582
- // Start
583
- await engine.start();
584
- console.log(`[${engine.name}] Started`);
585
- }
586
- }
587
- catch (err) {
588
- console.error('Failed to initialize engines:', err instanceof Error ? err.message : String(err));
589
- // Clean up: stop any engines that already started
590
- for (const engine of engines) {
591
- try {
592
- engine.stop();
593
- }
594
- catch {
595
- // Best effort
596
- }
597
- }
598
- releasePidLock(pidFilePath);
599
- process.exit(1);
600
- }
601
- }
602
- /**
603
- * Register SIGUSR1 (status dump) and SIGUSR2 (circuit breaker reset) handlers.
604
- */
605
- function registerDiagnosticSignals(engines, governor) {
606
- process.on('SIGUSR1', () => {
607
- printAggregatedStatus(engines, governor);
608
- });
609
- process.on('SIGUSR2', () => {
610
- forceCloseAllCircuitBreakers();
611
- });
612
- }
613
- // ---------------------------------------------------------------------------
614
- // Product claiming
615
- // ---------------------------------------------------------------------------
616
- /**
617
- * Claim products for this daemon's hostname. Unclaimed products are
618
- * auto-claimed; products already claimed by another host are skipped.
619
- *
620
- * Mutates `config.base.products` in place to contain only claimed products.
621
- * Uses a direct fetch call to avoid needing the full API client infrastructure
622
- * (which is initialized per-engine later).
623
- */
624
- async function claimProducts(config) {
625
- const base = config.base;
626
- const products = base.products ?? [];
627
- if (products.length === 0)
628
- return;
629
- const teloraUrl = base.teloraUrl;
630
- const trackerId = base.trackerId;
631
- if (!teloraUrl || !trackerId)
632
- return;
633
- const myHostname = osHostname();
634
- const apiUrl = `${teloraUrl}/functions/v1/product-api`;
635
- const claimed = [];
636
- for (const product of products) {
637
- try {
638
- const response = await fetch(apiUrl, {
639
- method: 'POST',
640
- headers: {
641
- Authorization: `Bearer ${trackerId}`,
642
- 'Content-Type': 'application/json',
643
- },
644
- body: JSON.stringify({
645
- action: 'daemon_claim_product',
646
- productId: product.id,
647
- fields: { daemonHostname: myHostname },
648
- }),
649
- });
650
- if (!response.ok) {
651
- console.warn(`[claim] Failed to claim product ${product.id}: HTTP ${response.status}`);
652
- continue;
653
- }
654
- const result = await response.json();
655
- if (result.claimed) {
656
- claimed.push(product);
657
- console.log(`[claim] Product ${product.id} claimed by ${myHostname}`);
658
- }
659
- else {
660
- console.warn(`[claim] Product ${product.id} is claimed by ${result.claimedBy} -- skipping`);
661
- }
662
- }
663
- catch (err) {
664
- console.warn(`[claim] Failed to claim product ${product.id}: ${err.message}`);
665
- }
666
- }
667
- if (claimed.length === 0) {
668
- throw new Error(`No products claimed by this host (${myHostname}). ` +
669
- 'All configured products are claimed by other daemons. ' +
670
- 'Use the Telora UI to reassign a product to this host, or stop the other daemon.');
671
- }
672
- if (claimed.length < products.length) {
673
- console.log(`[claim] Processing ${claimed.length} of ${products.length} configured products`);
674
- }
675
- base.products = claimed;
676
- }
677
76
  // ---------------------------------------------------------------------------
678
77
  // Main entry point
679
78
  // ---------------------------------------------------------------------------
@@ -687,13 +86,13 @@ async function claimProducts(config) {
687
86
  * @param opts CLI options (verbose, config path, dry-run, skip-recovery, engine filter).
688
87
  */
689
88
  export async function runUnifiedDaemon(opts) {
690
- // ── 1. Load unified config, apply filters, handle dry-run ─────────────
89
+ // -- 1. Load unified config, apply filters, handle dry-run ----------------
691
90
  const config = buildUnifiedConfig(opts);
692
91
  if (!config)
693
92
  return; // dry-run mode
694
- // ── 2. Resolve repo path ──────────────────────────────────────────────
93
+ // -- 2. Resolve repo path -------------------------------------------------
695
94
  const repoPath = config.base.repoPath || process.cwd();
696
- // ── 3. Acquire PID lock ───────────────────────────────────────────────
95
+ // -- 3. Acquire PID lock --------------------------------------------------
697
96
  ensureGlobalStateDir();
698
97
  const pidFilePath = resolvePidFilePath();
699
98
  try {
@@ -706,7 +105,7 @@ export async function runUnifiedDaemon(opts) {
706
105
  }
707
106
  throw err;
708
107
  }
709
- // ── 4. Canonical log setup ────────────────────────────────────────────
108
+ // -- 4. Canonical log setup -----------------------------------------------
710
109
  // Three startup modes:
711
110
  // 1. TTY foreground (`telora-daemon run` in terminal): rotate log, tee
712
111
  // stdout/stderr to canonical log file.
@@ -724,7 +123,7 @@ export async function runUnifiedDaemon(opts) {
724
123
  cleanupLogTee = setupCanonicalLogTee();
725
124
  }
726
125
  writeDaemonMeta(logPath);
727
- // ── 5. Register top-level error handlers (early, before engine init) ──
126
+ // -- 5. Register top-level error handlers (early, before engine init) -----
728
127
  let isShuttingDown = false;
729
128
  // shutdown is assigned later in this scope once engines and governor exist.
730
129
  // It will be set before the event loop yields, so async handlers will see it.
@@ -750,7 +149,7 @@ export async function runUnifiedDaemon(opts) {
750
149
  void shutdown('unhandledRejection');
751
150
  }
752
151
  });
753
- // ── 6. Claim products for this hostname ──────────────────────────────
152
+ // -- 6. Claim products for this hostname ----------------------------------
754
153
  try {
755
154
  await claimProducts(config);
756
155
  }
@@ -759,9 +158,9 @@ export async function runUnifiedDaemon(opts) {
759
158
  releasePidLock(pidFilePath);
760
159
  process.exit(1);
761
160
  }
762
- // ── 7. Create engine instances and resource governor ──────────────────
161
+ // -- 7. Create engine instances and resource governor ---------------------
763
162
  const { engines, governor } = await createEnginesAndGovernor(config, pidFilePath);
764
- // ── 8. Set up shutdown handler ────────────────────────────────────────
163
+ // -- 8. Set up shutdown handler -------------------------------------------
765
164
  shutdown = async (signal) => {
766
165
  if (isShuttingDown)
767
166
  return;
@@ -861,11 +260,11 @@ export async function runUnifiedDaemon(opts) {
861
260
  // Register signal handlers (debounced by setupSignalHandlers)
862
261
  // shutdown is guaranteed non-null here (assigned above)
863
262
  setupSignalHandlers(shutdown);
864
- // ── 9. Initialize and start engines ───────────────────────────────────
263
+ // -- 9. Initialize and start engines --------------------------------------
865
264
  await initializeAndStartEngines(engines, config, opts, pidFilePath);
866
- // ── 10. Register diagnostic signal handlers ───────────────────────────
265
+ // -- 10. Register diagnostic signal handlers ------------------------------
867
266
  registerDiagnosticSignals(engines, governor);
868
- // ── 11. Print startup message ─────────────────────────────────────────
267
+ // -- 11. Print startup message --------------------------------------------
869
268
  const engineNames = engines.map(e => e.name).join(', ');
870
269
  let currentVersion = '0.0.0';
871
270
  try {
@@ -878,7 +277,7 @@ export async function runUnifiedDaemon(opts) {
878
277
  console.log(`\n[unified-daemon] v${currentVersion} Started at ${new Date().toISOString()} (PID ${process.pid})`);
879
278
  console.log(`Engines: ${engineNames}`);
880
279
  console.log('Press Ctrl+C to stop\n');
881
- // ── 12. Start auto-update loop ────────────────────────────────────────
280
+ // -- 12. Start auto-update loop -------------------------------------------
882
281
  const autoUpdateEnabled = !opts.noAutoUpdate && resolveAutoUpdateEnabled(config);
883
282
  if (autoUpdateEnabled) {
884
283
  const autoUpdateIntervalMs = resolveAutoUpdateIntervalMs(config);
@@ -893,7 +292,7 @@ export async function runUnifiedDaemon(opts) {
893
292
  },
894
293
  });
895
294
  }
896
- // ── 13. Keep process alive ────────────────────────────────────────────
295
+ // -- 13. Keep process alive -----------------------------------------------
897
296
  await new Promise(() => { });
898
297
  }
899
298
  //# sourceMappingURL=unified-shell.js.map