@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.
- package/build-info.json +2 -2
- package/dist/queries/schemas.d.ts +4 -4
- package/dist/queries/schemas.js +1 -1
- package/dist/queries/schemas.js.map +1 -1
- package/dist/spawner-lifecycle.d.ts +43 -0
- package/dist/spawner-lifecycle.d.ts.map +1 -0
- package/dist/spawner-lifecycle.js +152 -0
- package/dist/spawner-lifecycle.js.map +1 -0
- package/dist/spawner-stream-handlers.d.ts +67 -0
- package/dist/spawner-stream-handlers.d.ts.map +1 -0
- package/dist/spawner-stream-handlers.js +193 -0
- package/dist/spawner-stream-handlers.js.map +1 -0
- package/dist/spawner.d.ts +15 -36
- package/dist/spawner.d.ts.map +1 -1
- package/dist/spawner.js +29 -327
- package/dist/spawner.js.map +1 -1
- package/dist/strategy-completion-event.d.ts +29 -0
- package/dist/strategy-completion-event.d.ts.map +1 -0
- package/dist/strategy-completion-event.js +69 -0
- package/dist/strategy-completion-event.js.map +1 -0
- package/dist/strategy-completion.d.ts.map +1 -1
- package/dist/strategy-completion.js +27 -11
- package/dist/strategy-completion.js.map +1 -1
- package/dist/strategy-executor.d.ts +8 -45
- package/dist/strategy-executor.d.ts.map +1 -1
- package/dist/strategy-executor.js +22 -423
- package/dist/strategy-executor.js.map +1 -1
- package/dist/strategy-spawn-helpers.d.ts +67 -0
- package/dist/strategy-spawn-helpers.d.ts.map +1 -0
- package/dist/strategy-spawn-helpers.js +160 -0
- package/dist/strategy-spawn-helpers.js.map +1 -0
- package/dist/strategy-team-lifecycle.d.ts +50 -0
- package/dist/strategy-team-lifecycle.d.ts.map +1 -0
- package/dist/strategy-team-lifecycle.js +218 -0
- package/dist/strategy-team-lifecycle.js.map +1 -0
- package/dist/unified-engine-lifecycle.d.ts +36 -0
- package/dist/unified-engine-lifecycle.d.ts.map +1 -0
- package/dist/unified-engine-lifecycle.js +250 -0
- package/dist/unified-engine-lifecycle.js.map +1 -0
- package/dist/unified-shell-config.d.ts +25 -0
- package/dist/unified-shell-config.d.ts.map +1 -1
- package/dist/unified-shell-config.js +45 -0
- package/dist/unified-shell-config.js.map +1 -1
- package/dist/unified-shell-status.d.ts.map +1 -1
- package/dist/unified-shell-status.js +16 -2
- package/dist/unified-shell-status.js.map +1 -1
- package/dist/unified-shell.d.ts +2 -15
- package/dist/unified-shell.d.ts.map +1 -1
- package/dist/unified-shell.js +18 -619
- package/dist/unified-shell.js.map +1 -1
- package/package.json +1 -1
package/dist/unified-shell.js
CHANGED
|
@@ -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,
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
93
|
+
// -- 2. Resolve repo path -------------------------------------------------
|
|
695
94
|
const repoPath = config.base.repoPath || process.cwd();
|
|
696
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
161
|
+
// -- 7. Create engine instances and resource governor ---------------------
|
|
763
162
|
const { engines, governor } = await createEnginesAndGovernor(config, pidFilePath);
|
|
764
|
-
//
|
|
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
|
-
//
|
|
263
|
+
// -- 9. Initialize and start engines --------------------------------------
|
|
865
264
|
await initializeAndStartEngines(engines, config, opts, pidFilePath);
|
|
866
|
-
//
|
|
265
|
+
// -- 10. Register diagnostic signal handlers ------------------------------
|
|
867
266
|
registerDiagnosticSignals(engines, governor);
|
|
868
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
295
|
+
// -- 13. Keep process alive -----------------------------------------------
|
|
897
296
|
await new Promise(() => { });
|
|
898
297
|
}
|
|
899
298
|
//# sourceMappingURL=unified-shell.js.map
|