@episoda/cli 0.2.174 → 0.2.176
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/dist/daemon/daemon-process.js +496 -141
- package/dist/daemon/daemon-process.js.map +1 -1
- package/dist/index.js +375 -138
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -2187,6 +2187,7 @@ var require_websocket_client = __commonJS({
|
|
|
2187
2187
|
constructor() {
|
|
2188
2188
|
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
2189
2189
|
this.reconnectAttempts = 0;
|
|
2190
|
+
this.autoReconnectEnabled = true;
|
|
2190
2191
|
this.url = "";
|
|
2191
2192
|
this.token = "";
|
|
2192
2193
|
this.isConnected = false;
|
|
@@ -2235,7 +2236,7 @@ var require_websocket_client = __commonJS({
|
|
|
2235
2236
|
clearTimeout(this.reconnectTimeout);
|
|
2236
2237
|
this.reconnectTimeout = void 0;
|
|
2237
2238
|
}
|
|
2238
|
-
return new Promise((
|
|
2239
|
+
return new Promise((resolve7, reject) => {
|
|
2239
2240
|
const connectionTimeout = setTimeout(() => {
|
|
2240
2241
|
if (this.ws) {
|
|
2241
2242
|
this.ws.terminate();
|
|
@@ -2266,7 +2267,7 @@ var require_websocket_client = __commonJS({
|
|
|
2266
2267
|
daemonPid: this.daemonPid
|
|
2267
2268
|
});
|
|
2268
2269
|
this.startHeartbeat();
|
|
2269
|
-
|
|
2270
|
+
resolve7();
|
|
2270
2271
|
});
|
|
2271
2272
|
this.ws.on("pong", () => {
|
|
2272
2273
|
if (this.heartbeatTimeoutTimer) {
|
|
@@ -2288,7 +2289,7 @@ var require_websocket_client = __commonJS({
|
|
|
2288
2289
|
this.ws.on("close", (code, reason) => {
|
|
2289
2290
|
console.log(`[EpisodaClient] WebSocket closed: ${code} ${reason.toString()}`);
|
|
2290
2291
|
this.isConnected = false;
|
|
2291
|
-
const willReconnect = !this.isDisconnecting;
|
|
2292
|
+
const willReconnect = !this.isDisconnecting && this.autoReconnectEnabled;
|
|
2292
2293
|
this.emit({
|
|
2293
2294
|
type: "disconnected",
|
|
2294
2295
|
code,
|
|
@@ -2312,6 +2313,19 @@ var require_websocket_client = __commonJS({
|
|
|
2312
2313
|
}
|
|
2313
2314
|
});
|
|
2314
2315
|
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Enable/disable automatic reconnect scheduling.
|
|
2318
|
+
*
|
|
2319
|
+
* This lets daemon startup/handshake own retry policy without competing
|
|
2320
|
+
* with client-side reconnect loops.
|
|
2321
|
+
*/
|
|
2322
|
+
setAutoReconnect(enabled) {
|
|
2323
|
+
this.autoReconnectEnabled = enabled;
|
|
2324
|
+
if (!enabled && this.reconnectTimeout) {
|
|
2325
|
+
clearTimeout(this.reconnectTimeout);
|
|
2326
|
+
this.reconnectTimeout = void 0;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2315
2329
|
/**
|
|
2316
2330
|
* Disconnect from the server
|
|
2317
2331
|
* @param intentional - If true, prevents automatic reconnection (user-initiated disconnect)
|
|
@@ -2397,13 +2411,13 @@ var require_websocket_client = __commonJS({
|
|
|
2397
2411
|
console.warn("[EpisodaClient] Cannot send - WebSocket not connected");
|
|
2398
2412
|
return false;
|
|
2399
2413
|
}
|
|
2400
|
-
return new Promise((
|
|
2414
|
+
return new Promise((resolve7) => {
|
|
2401
2415
|
this.ws.send(JSON.stringify(message), (error) => {
|
|
2402
2416
|
if (error) {
|
|
2403
2417
|
console.error("[EpisodaClient] Failed to send message:", error);
|
|
2404
|
-
|
|
2418
|
+
resolve7(false);
|
|
2405
2419
|
} else {
|
|
2406
|
-
|
|
2420
|
+
resolve7(true);
|
|
2407
2421
|
}
|
|
2408
2422
|
});
|
|
2409
2423
|
});
|
|
@@ -2526,7 +2540,12 @@ var require_websocket_client = __commonJS({
|
|
|
2526
2540
|
if (this.rateLimitBackoffUntil && Date.now() < this.rateLimitBackoffUntil) {
|
|
2527
2541
|
const waitTime = this.rateLimitBackoffUntil - Date.now();
|
|
2528
2542
|
console.log(`[EpisodaClient] Rate limited, waiting ${Math.round(waitTime / 1e3)}s before retry`);
|
|
2529
|
-
this.
|
|
2543
|
+
this.emit({
|
|
2544
|
+
type: "reconnect_scheduled",
|
|
2545
|
+
attempt: this.reconnectAttempts + 1,
|
|
2546
|
+
delayMs: waitTime,
|
|
2547
|
+
strategy: "rate_limited"
|
|
2548
|
+
});
|
|
2530
2549
|
this.reconnectTimeout = setTimeout(() => {
|
|
2531
2550
|
this.rateLimitBackoffUntil = void 0;
|
|
2532
2551
|
this.scheduleReconnect();
|
|
@@ -2534,6 +2553,7 @@ var require_websocket_client = __commonJS({
|
|
|
2534
2553
|
return;
|
|
2535
2554
|
}
|
|
2536
2555
|
let delay;
|
|
2556
|
+
let strategy = "connection_lost";
|
|
2537
2557
|
let shouldRetry = true;
|
|
2538
2558
|
const isCloudMode = this.environment === "cloud";
|
|
2539
2559
|
const MAX_CLOUD_AUTH_FAILURES = 3;
|
|
@@ -2545,20 +2565,28 @@ var require_websocket_client = __commonJS({
|
|
|
2545
2565
|
} else {
|
|
2546
2566
|
delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), MAX_CLOUD_RECONNECT_DELAY);
|
|
2547
2567
|
delay = applyJitter(delay);
|
|
2568
|
+
strategy = "cloud";
|
|
2548
2569
|
const delayStr = delay >= 6e4 ? `${Math.round(delay / 6e4)}m` : `${Math.round(delay / 1e3)}s`;
|
|
2549
2570
|
console.log(`[EpisodaClient] Cloud mode: reconnecting in ${delayStr}... (attempt ${this.reconnectAttempts + 1}, never giving up)`);
|
|
2550
2571
|
}
|
|
2551
2572
|
} else if (this.isGracefulShutdown && this.reconnectAttempts < 7) {
|
|
2552
2573
|
delay = Math.min(500 * Math.pow(2, this.reconnectAttempts), 5e3);
|
|
2553
2574
|
delay = applyJitter(delay);
|
|
2575
|
+
strategy = "graceful_shutdown";
|
|
2554
2576
|
console.log(`[EpisodaClient] Server restarting, reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/7)`);
|
|
2555
2577
|
} else {
|
|
2556
2578
|
delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), MAX_LOCAL_RECONNECT_DELAY);
|
|
2557
2579
|
delay = applyJitter(delay);
|
|
2580
|
+
strategy = "connection_lost";
|
|
2558
2581
|
const delayStr = delay >= 6e4 ? `${Math.round(delay / 6e4)}m` : `${Math.round(delay / 1e3)}s`;
|
|
2559
2582
|
console.log(`[EpisodaClient] Connection lost, retrying in ${delayStr}... (attempt ${this.reconnectAttempts + 1}, retrying until connected)`);
|
|
2560
2583
|
}
|
|
2561
2584
|
if (!shouldRetry) {
|
|
2585
|
+
this.emit({
|
|
2586
|
+
type: "reconnect_exhausted",
|
|
2587
|
+
attempts: this.reconnectAttempts,
|
|
2588
|
+
reason: `retry_exhausted_after_${this.consecutiveAuthFailures}_auth_failures`
|
|
2589
|
+
});
|
|
2562
2590
|
this.emit({
|
|
2563
2591
|
type: "disconnected",
|
|
2564
2592
|
code: 1006,
|
|
@@ -2567,9 +2595,20 @@ var require_websocket_client = __commonJS({
|
|
|
2567
2595
|
});
|
|
2568
2596
|
return;
|
|
2569
2597
|
}
|
|
2570
|
-
this.reconnectAttempts
|
|
2598
|
+
const reconnectAttempt = this.reconnectAttempts + 1;
|
|
2599
|
+
this.reconnectAttempts = reconnectAttempt;
|
|
2600
|
+
this.emit({
|
|
2601
|
+
type: "reconnect_scheduled",
|
|
2602
|
+
attempt: reconnectAttempt,
|
|
2603
|
+
delayMs: delay,
|
|
2604
|
+
strategy
|
|
2605
|
+
});
|
|
2571
2606
|
this.reconnectTimeout = setTimeout(() => {
|
|
2572
2607
|
console.log("[EpisodaClient] Attempting reconnection...");
|
|
2608
|
+
this.emit({
|
|
2609
|
+
type: "reconnect_attempt",
|
|
2610
|
+
attempt: reconnectAttempt
|
|
2611
|
+
});
|
|
2573
2612
|
this.connect(this.url, this.token, this.machineId, {
|
|
2574
2613
|
hostname: this.hostname,
|
|
2575
2614
|
osPlatform: this.osPlatform,
|
|
@@ -2582,6 +2621,11 @@ var require_websocket_client = __commonJS({
|
|
|
2582
2621
|
containerId: this.containerId
|
|
2583
2622
|
}).then(() => {
|
|
2584
2623
|
console.log("[EpisodaClient] Reconnection successful");
|
|
2624
|
+
this.emit({
|
|
2625
|
+
type: "reconnect_result",
|
|
2626
|
+
attempt: reconnectAttempt,
|
|
2627
|
+
success: true
|
|
2628
|
+
});
|
|
2585
2629
|
this.reconnectAttempts = 0;
|
|
2586
2630
|
this.isGracefulShutdown = false;
|
|
2587
2631
|
this.firstDisconnectTime = void 0;
|
|
@@ -2589,6 +2633,12 @@ var require_websocket_client = __commonJS({
|
|
|
2589
2633
|
this.consecutiveAuthFailures = 0;
|
|
2590
2634
|
}).catch((error) => {
|
|
2591
2635
|
console.error("[EpisodaClient] Reconnection failed:", error.message);
|
|
2636
|
+
this.emit({
|
|
2637
|
+
type: "reconnect_result",
|
|
2638
|
+
attempt: reconnectAttempt,
|
|
2639
|
+
success: false,
|
|
2640
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2641
|
+
});
|
|
2592
2642
|
});
|
|
2593
2643
|
}, delay);
|
|
2594
2644
|
}
|
|
@@ -2996,7 +3046,7 @@ var require_package = __commonJS({
|
|
|
2996
3046
|
"package.json"(exports2, module2) {
|
|
2997
3047
|
module2.exports = {
|
|
2998
3048
|
name: "@episoda/cli",
|
|
2999
|
-
version: "0.2.
|
|
3049
|
+
version: "0.2.176",
|
|
3000
3050
|
description: "CLI tool for Episoda local development workflow orchestration",
|
|
3001
3051
|
main: "dist/index.js",
|
|
3002
3052
|
types: "dist/index.d.ts",
|
|
@@ -3255,6 +3305,16 @@ function touchProject(projectPath) {
|
|
|
3255
3305
|
writeProjects(data);
|
|
3256
3306
|
}
|
|
3257
3307
|
}
|
|
3308
|
+
function pruneMissingProjectPaths() {
|
|
3309
|
+
const data = readProjects();
|
|
3310
|
+
const initialLength = data.projects.length;
|
|
3311
|
+
data.projects = data.projects.filter((project) => fs2.existsSync(project.path));
|
|
3312
|
+
const removedCount = initialLength - data.projects.length;
|
|
3313
|
+
if (removedCount > 0) {
|
|
3314
|
+
writeProjects(data);
|
|
3315
|
+
}
|
|
3316
|
+
return removedCount;
|
|
3317
|
+
}
|
|
3258
3318
|
|
|
3259
3319
|
// src/daemon/daemon-manager.ts
|
|
3260
3320
|
var path3 = __toESM(require("path"));
|
|
@@ -3301,10 +3361,10 @@ var IPCServer = class {
|
|
|
3301
3361
|
this.server = net.createServer((socket) => {
|
|
3302
3362
|
this.handleConnection(socket);
|
|
3303
3363
|
});
|
|
3304
|
-
return new Promise((
|
|
3364
|
+
return new Promise((resolve7, reject) => {
|
|
3305
3365
|
this.server.listen(socketPath, () => {
|
|
3306
3366
|
fs3.chmodSync(socketPath, 384);
|
|
3307
|
-
|
|
3367
|
+
resolve7();
|
|
3308
3368
|
});
|
|
3309
3369
|
this.server.on("error", reject);
|
|
3310
3370
|
});
|
|
@@ -3315,12 +3375,12 @@ var IPCServer = class {
|
|
|
3315
3375
|
async stop() {
|
|
3316
3376
|
if (!this.server) return;
|
|
3317
3377
|
const socketPath = getSocketPath();
|
|
3318
|
-
return new Promise((
|
|
3378
|
+
return new Promise((resolve7) => {
|
|
3319
3379
|
this.server.close(() => {
|
|
3320
3380
|
if (fs3.existsSync(socketPath)) {
|
|
3321
3381
|
fs3.unlinkSync(socketPath);
|
|
3322
3382
|
}
|
|
3323
|
-
|
|
3383
|
+
resolve7();
|
|
3324
3384
|
});
|
|
3325
3385
|
});
|
|
3326
3386
|
}
|
|
@@ -3389,8 +3449,24 @@ var import_child_process2 = require("child_process");
|
|
|
3389
3449
|
var import_util = require("util");
|
|
3390
3450
|
var import_core5 = __toESM(require_dist());
|
|
3391
3451
|
var execAsync = (0, import_util.promisify)(import_child_process2.exec);
|
|
3452
|
+
async function isGitRepository(projectPath) {
|
|
3453
|
+
try {
|
|
3454
|
+
await execAsync("git rev-parse --git-dir", { cwd: projectPath, timeout: 5e3 });
|
|
3455
|
+
return true;
|
|
3456
|
+
} catch {
|
|
3457
|
+
return false;
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3392
3460
|
async function cleanupStaleCommits(projectPath) {
|
|
3393
3461
|
try {
|
|
3462
|
+
if (!await isGitRepository(projectPath)) {
|
|
3463
|
+
return {
|
|
3464
|
+
success: true,
|
|
3465
|
+
deleted_count: 0,
|
|
3466
|
+
kept_count: 0,
|
|
3467
|
+
message: "Skipping cleanup: project path is not a git repository"
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3394
3470
|
const machineId = await getMachineId();
|
|
3395
3471
|
const config = await (0, import_core5.loadConfig)();
|
|
3396
3472
|
if (!config?.access_token) {
|
|
@@ -3404,7 +3480,10 @@ async function cleanupStaleCommits(projectPath) {
|
|
|
3404
3480
|
try {
|
|
3405
3481
|
await execAsync("git fetch origin", { cwd: projectPath, timeout: 3e4 });
|
|
3406
3482
|
} catch (fetchError) {
|
|
3407
|
-
|
|
3483
|
+
const fetchMessage = fetchError instanceof Error ? fetchError.message : String(fetchError);
|
|
3484
|
+
if (!fetchMessage.includes("not a git repository")) {
|
|
3485
|
+
console.warn("[EP950] Could not fetch origin:", fetchError);
|
|
3486
|
+
}
|
|
3408
3487
|
}
|
|
3409
3488
|
let currentBranch = "";
|
|
3410
3489
|
try {
|
|
@@ -3546,7 +3625,7 @@ function getDownloadUrl() {
|
|
|
3546
3625
|
return platformUrls[arch4] || null;
|
|
3547
3626
|
}
|
|
3548
3627
|
async function downloadFile(url, destPath) {
|
|
3549
|
-
return new Promise((
|
|
3628
|
+
return new Promise((resolve7, reject) => {
|
|
3550
3629
|
const followRedirect = (currentUrl, redirectCount = 0) => {
|
|
3551
3630
|
if (redirectCount > 5) {
|
|
3552
3631
|
reject(new Error("Too many redirects"));
|
|
@@ -3576,7 +3655,7 @@ async function downloadFile(url, destPath) {
|
|
|
3576
3655
|
response.pipe(file);
|
|
3577
3656
|
file.on("finish", () => {
|
|
3578
3657
|
file.close();
|
|
3579
|
-
|
|
3658
|
+
resolve7();
|
|
3580
3659
|
});
|
|
3581
3660
|
file.on("error", (err) => {
|
|
3582
3661
|
fs4.unlinkSync(destPath);
|
|
@@ -3990,10 +4069,10 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
3990
4069
|
const isTracked = Array.from(this.tunnelStates.values()).some((s) => s.info.pid === pid);
|
|
3991
4070
|
console.log(`[Tunnel] EP904: Found cloudflared PID ${pid} on port ${port} (tracked: ${isTracked})`);
|
|
3992
4071
|
this.killByPid(pid, "SIGTERM");
|
|
3993
|
-
await new Promise((
|
|
4072
|
+
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
3994
4073
|
if (this.isProcessRunning(pid)) {
|
|
3995
4074
|
this.killByPid(pid, "SIGKILL");
|
|
3996
|
-
await new Promise((
|
|
4075
|
+
await new Promise((resolve7) => setTimeout(resolve7, 200));
|
|
3997
4076
|
}
|
|
3998
4077
|
killed.push(pid);
|
|
3999
4078
|
}
|
|
@@ -4027,7 +4106,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4027
4106
|
if (!this.tunnelStates.has(moduleUid)) {
|
|
4028
4107
|
console.log(`[Tunnel] EP877: Found orphaned process PID ${pid} for ${moduleUid}, killing...`);
|
|
4029
4108
|
this.killByPid(pid, "SIGTERM");
|
|
4030
|
-
await new Promise((
|
|
4109
|
+
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
4031
4110
|
if (this.isProcessRunning(pid)) {
|
|
4032
4111
|
this.killByPid(pid, "SIGKILL");
|
|
4033
4112
|
}
|
|
@@ -4042,7 +4121,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4042
4121
|
if (!trackedPids.includes(pid) && !cleaned.includes(pid)) {
|
|
4043
4122
|
console.log(`[Tunnel] EP877: Found untracked cloudflared process PID ${pid}, killing...`);
|
|
4044
4123
|
this.killByPid(pid, "SIGTERM");
|
|
4045
|
-
await new Promise((
|
|
4124
|
+
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
4046
4125
|
if (this.isProcessRunning(pid)) {
|
|
4047
4126
|
this.killByPid(pid, "SIGKILL");
|
|
4048
4127
|
}
|
|
@@ -4132,7 +4211,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4132
4211
|
return { success: false, error: `Failed to get cloudflared: ${errorMessage}` };
|
|
4133
4212
|
}
|
|
4134
4213
|
}
|
|
4135
|
-
return new Promise((
|
|
4214
|
+
return new Promise((resolve7) => {
|
|
4136
4215
|
const tunnelInfo = {
|
|
4137
4216
|
moduleUid,
|
|
4138
4217
|
url: previewUrl || "",
|
|
@@ -4198,7 +4277,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4198
4277
|
moduleUid,
|
|
4199
4278
|
url: tunnelInfo.url
|
|
4200
4279
|
});
|
|
4201
|
-
|
|
4280
|
+
resolve7({ success: true, url: tunnelInfo.url });
|
|
4202
4281
|
}
|
|
4203
4282
|
};
|
|
4204
4283
|
process2.stderr?.on("data", (data) => {
|
|
@@ -4225,7 +4304,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4225
4304
|
onStatusChange?.("error", errorMsg);
|
|
4226
4305
|
this.emitEvent({ type: "error", moduleUid, error: errorMsg });
|
|
4227
4306
|
}
|
|
4228
|
-
|
|
4307
|
+
resolve7({ success: false, error: errorMsg });
|
|
4229
4308
|
} else if (wasConnected) {
|
|
4230
4309
|
if (currentState && !currentState.intentionallyStopped) {
|
|
4231
4310
|
console.log(`[Tunnel] EP948: Named tunnel ${moduleUid} crashed unexpectedly, attempting reconnect...`);
|
|
@@ -4256,7 +4335,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4256
4335
|
this.emitEvent({ type: "error", moduleUid, error: error.message });
|
|
4257
4336
|
}
|
|
4258
4337
|
if (!connected) {
|
|
4259
|
-
|
|
4338
|
+
resolve7({ success: false, error: error.message });
|
|
4260
4339
|
}
|
|
4261
4340
|
});
|
|
4262
4341
|
setTimeout(() => {
|
|
@@ -4279,7 +4358,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4279
4358
|
onStatusChange?.("error", errorMsg);
|
|
4280
4359
|
this.emitEvent({ type: "error", moduleUid, error: errorMsg });
|
|
4281
4360
|
}
|
|
4282
|
-
|
|
4361
|
+
resolve7({ success: false, error: errorMsg });
|
|
4283
4362
|
}
|
|
4284
4363
|
}, TUNNEL_TIMEOUTS.NAMED_TUNNEL_CONNECT);
|
|
4285
4364
|
});
|
|
@@ -4340,7 +4419,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4340
4419
|
if (orphanPid && this.isProcessRunning(orphanPid)) {
|
|
4341
4420
|
console.log(`[Tunnel] EP877: Killing orphaned process ${orphanPid} for ${moduleUid} before starting new tunnel`);
|
|
4342
4421
|
this.killByPid(orphanPid, "SIGTERM");
|
|
4343
|
-
await new Promise((
|
|
4422
|
+
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
4344
4423
|
if (this.isProcessRunning(orphanPid)) {
|
|
4345
4424
|
this.killByPid(orphanPid, "SIGKILL");
|
|
4346
4425
|
}
|
|
@@ -4349,7 +4428,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4349
4428
|
const killedOnPort = await this.killCloudflaredOnPort(port);
|
|
4350
4429
|
if (killedOnPort.length > 0) {
|
|
4351
4430
|
console.log(`[Tunnel] EP904: Pre-start port cleanup killed ${killedOnPort.length} process(es) on port ${port}`);
|
|
4352
|
-
await new Promise((
|
|
4431
|
+
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
4353
4432
|
}
|
|
4354
4433
|
const cleanup = await this.cleanupOrphanedProcesses();
|
|
4355
4434
|
if (cleanup.cleaned > 0) {
|
|
@@ -4389,7 +4468,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4389
4468
|
if (orphanPid && this.isProcessRunning(orphanPid)) {
|
|
4390
4469
|
console.log(`[Tunnel] EP877: Stopping orphaned process ${orphanPid} for ${moduleUid} via PID file`);
|
|
4391
4470
|
this.killByPid(orphanPid, "SIGTERM");
|
|
4392
|
-
await new Promise((
|
|
4471
|
+
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
4393
4472
|
if (this.isProcessRunning(orphanPid)) {
|
|
4394
4473
|
this.killByPid(orphanPid, "SIGKILL");
|
|
4395
4474
|
}
|
|
@@ -4405,16 +4484,16 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4405
4484
|
const tunnel = state.info;
|
|
4406
4485
|
if (tunnel.process && !tunnel.process.killed) {
|
|
4407
4486
|
tunnel.process.kill("SIGTERM");
|
|
4408
|
-
await new Promise((
|
|
4487
|
+
await new Promise((resolve7) => {
|
|
4409
4488
|
const timeout = setTimeout(() => {
|
|
4410
4489
|
if (tunnel.process && !tunnel.process.killed) {
|
|
4411
4490
|
tunnel.process.kill("SIGKILL");
|
|
4412
4491
|
}
|
|
4413
|
-
|
|
4492
|
+
resolve7();
|
|
4414
4493
|
}, 3e3);
|
|
4415
4494
|
tunnel.process.once("exit", () => {
|
|
4416
4495
|
clearTimeout(timeout);
|
|
4417
|
-
|
|
4496
|
+
resolve7();
|
|
4418
4497
|
});
|
|
4419
4498
|
});
|
|
4420
4499
|
}
|
|
@@ -4701,15 +4780,15 @@ async function writeToStdinWithBackpressure(process2, data, drainTimeoutMs, labe
|
|
|
4701
4780
|
if (!stdin || stdin.destroyed) {
|
|
4702
4781
|
throw new Error(`[${label}] stdin not available. session=${sessionId}`);
|
|
4703
4782
|
}
|
|
4704
|
-
await new Promise((
|
|
4783
|
+
await new Promise((resolve7, reject) => {
|
|
4705
4784
|
const ok = stdin.write(data);
|
|
4706
4785
|
if (ok) {
|
|
4707
|
-
|
|
4786
|
+
resolve7();
|
|
4708
4787
|
return;
|
|
4709
4788
|
}
|
|
4710
4789
|
const onDrain = () => {
|
|
4711
4790
|
cleanup();
|
|
4712
|
-
|
|
4791
|
+
resolve7();
|
|
4713
4792
|
};
|
|
4714
4793
|
const onError = (err) => {
|
|
4715
4794
|
cleanup();
|
|
@@ -4730,18 +4809,18 @@ async function writeToStdinWithBackpressure(process2, data, drainTimeoutMs, labe
|
|
|
4730
4809
|
});
|
|
4731
4810
|
}
|
|
4732
4811
|
function waitForProcessExit(process2, alive, timeoutMs) {
|
|
4733
|
-
return new Promise((
|
|
4812
|
+
return new Promise((resolve7) => {
|
|
4734
4813
|
if (!process2 || !alive) {
|
|
4735
|
-
|
|
4814
|
+
resolve7(true);
|
|
4736
4815
|
return;
|
|
4737
4816
|
}
|
|
4738
4817
|
const onExit = () => {
|
|
4739
4818
|
clearTimeout(timer);
|
|
4740
|
-
|
|
4819
|
+
resolve7(true);
|
|
4741
4820
|
};
|
|
4742
4821
|
const timer = setTimeout(() => {
|
|
4743
4822
|
process2.removeListener("exit", onExit);
|
|
4744
|
-
|
|
4823
|
+
resolve7(false);
|
|
4745
4824
|
}, timeoutMs);
|
|
4746
4825
|
process2.once("exit", onExit);
|
|
4747
4826
|
});
|
|
@@ -5737,10 +5816,10 @@ var CodexPersistentRuntime = class {
|
|
|
5737
5816
|
}
|
|
5738
5817
|
waitForThreadId(timeoutMs) {
|
|
5739
5818
|
if (this._agentSessionId) return Promise.resolve(this._agentSessionId);
|
|
5740
|
-
return new Promise((
|
|
5819
|
+
return new Promise((resolve7, reject) => {
|
|
5741
5820
|
const onId = (id) => {
|
|
5742
5821
|
clearTimeout(timer);
|
|
5743
|
-
|
|
5822
|
+
resolve7(id);
|
|
5744
5823
|
};
|
|
5745
5824
|
const timer = setTimeout(() => {
|
|
5746
5825
|
this.threadIdWaiters = this.threadIdWaiters.filter((w) => w !== onId);
|
|
@@ -5771,12 +5850,12 @@ var CodexPersistentRuntime = class {
|
|
|
5771
5850
|
sendRequest(method, params, timeoutMs = INIT_TIMEOUT_MS) {
|
|
5772
5851
|
const id = this.nextId++;
|
|
5773
5852
|
const msg = { jsonrpc: "2.0", id, method, params };
|
|
5774
|
-
return new Promise(async (
|
|
5853
|
+
return new Promise(async (resolve7, reject) => {
|
|
5775
5854
|
const timeout = setTimeout(() => {
|
|
5776
5855
|
this.pending.delete(id);
|
|
5777
5856
|
reject(new Error(`JSON-RPC timeout: ${method}`));
|
|
5778
5857
|
}, timeoutMs);
|
|
5779
|
-
this.pending.set(id, { resolve:
|
|
5858
|
+
this.pending.set(id, { resolve: resolve7, reject, timeout });
|
|
5780
5859
|
try {
|
|
5781
5860
|
await this.writeToStdin(JSON.stringify(msg) + "\n");
|
|
5782
5861
|
} catch (err) {
|
|
@@ -5926,11 +6005,11 @@ var UnifiedAgentRuntime = class {
|
|
|
5926
6005
|
if (!this.currentTurn && this.impl.turnState === "idle") {
|
|
5927
6006
|
return this.dispatchTurn(message, callbacks);
|
|
5928
6007
|
}
|
|
5929
|
-
return new Promise((
|
|
6008
|
+
return new Promise((resolve7, reject) => {
|
|
5930
6009
|
this.queuedTurns.push({
|
|
5931
6010
|
message,
|
|
5932
6011
|
callbacks,
|
|
5933
|
-
resolve:
|
|
6012
|
+
resolve: resolve7,
|
|
5934
6013
|
reject
|
|
5935
6014
|
});
|
|
5936
6015
|
});
|
|
@@ -7443,13 +7522,13 @@ async function getChildProcessRssMb(pid) {
|
|
|
7443
7522
|
if (!pid || pid <= 0) return null;
|
|
7444
7523
|
try {
|
|
7445
7524
|
if (typeof import_child_process9.execFile !== "function") return null;
|
|
7446
|
-
const stdout = await new Promise((
|
|
7525
|
+
const stdout = await new Promise((resolve7, reject) => {
|
|
7447
7526
|
(0, import_child_process9.execFile)("ps", ["-o", "rss=", "-p", String(pid)], { timeout: 1e3 }, (err, out) => {
|
|
7448
7527
|
if (err) {
|
|
7449
7528
|
reject(err);
|
|
7450
7529
|
return;
|
|
7451
7530
|
}
|
|
7452
|
-
|
|
7531
|
+
resolve7(String(out));
|
|
7453
7532
|
});
|
|
7454
7533
|
});
|
|
7455
7534
|
const kb = Number(String(stdout).trim());
|
|
@@ -7552,8 +7631,8 @@ var AgentControlPlane = class {
|
|
|
7552
7631
|
async withConfigLock(fn) {
|
|
7553
7632
|
const previousLock = this.configWriteLock;
|
|
7554
7633
|
let releaseLock;
|
|
7555
|
-
this.configWriteLock = new Promise((
|
|
7556
|
-
releaseLock =
|
|
7634
|
+
this.configWriteLock = new Promise((resolve7) => {
|
|
7635
|
+
releaseLock = resolve7;
|
|
7557
7636
|
});
|
|
7558
7637
|
try {
|
|
7559
7638
|
await previousLock;
|
|
@@ -7630,26 +7709,12 @@ var AgentControlPlane = class {
|
|
|
7630
7709
|
const existingSession = this.sessions.get(sessionId);
|
|
7631
7710
|
if (existingSession) {
|
|
7632
7711
|
if (existingSession.provider === provider && existingSession.moduleId === moduleId) {
|
|
7633
|
-
console.log(`[AgentManager]
|
|
7712
|
+
console.log(`[AgentManager] EP1417: Session ${sessionId} already exists, acknowledging duplicate start`);
|
|
7634
7713
|
if (requestedWorkspaceId && !normalizeWorkspaceId(existingSession.workspaceId)) {
|
|
7635
7714
|
existingSession.workspaceId = requestedWorkspaceId;
|
|
7636
7715
|
console.log(`[AgentManager] EP1357: Updated session ${sessionId} workspaceId from start options`);
|
|
7637
7716
|
}
|
|
7638
|
-
return
|
|
7639
|
-
sessionId,
|
|
7640
|
-
message,
|
|
7641
|
-
isFirstMessage: false,
|
|
7642
|
-
// Not first since session exists
|
|
7643
|
-
canWrite,
|
|
7644
|
-
readOnlyReason,
|
|
7645
|
-
onChunk,
|
|
7646
|
-
onToolUse,
|
|
7647
|
-
// EP1236: Pass tool use callback
|
|
7648
|
-
onToolResult,
|
|
7649
|
-
// EP1311: Pass tool result callback
|
|
7650
|
-
onComplete,
|
|
7651
|
-
onError
|
|
7652
|
-
});
|
|
7717
|
+
return { success: true };
|
|
7653
7718
|
}
|
|
7654
7719
|
console.log(`[AgentManager] EP1232: Session ${sessionId} exists with incompatible config - provider: ${existingSession.provider} vs ${provider}, module: ${existingSession.moduleId} vs ${moduleId}`);
|
|
7655
7720
|
return { success: false, error: "Session already exists with incompatible configuration" };
|
|
@@ -8796,7 +8861,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
8796
8861
|
const lockContent = fs14.readFileSync(lockPath, "utf-8").trim();
|
|
8797
8862
|
const lockPid = parseInt(lockContent, 10);
|
|
8798
8863
|
if (!isNaN(lockPid) && this.isProcessRunning(lockPid)) {
|
|
8799
|
-
await new Promise((
|
|
8864
|
+
await new Promise((resolve7) => setTimeout(resolve7, retryInterval));
|
|
8800
8865
|
continue;
|
|
8801
8866
|
}
|
|
8802
8867
|
} catch {
|
|
@@ -8810,7 +8875,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
8810
8875
|
} catch {
|
|
8811
8876
|
continue;
|
|
8812
8877
|
}
|
|
8813
|
-
await new Promise((
|
|
8878
|
+
await new Promise((resolve7) => setTimeout(resolve7, retryInterval));
|
|
8814
8879
|
continue;
|
|
8815
8880
|
}
|
|
8816
8881
|
throw err;
|
|
@@ -9600,6 +9665,7 @@ function getInstallCommand(cwd) {
|
|
|
9600
9665
|
var fs34 = __toESM(require("fs"));
|
|
9601
9666
|
var http2 = __toESM(require("http"));
|
|
9602
9667
|
var os15 = __toESM(require("os"));
|
|
9668
|
+
var import_child_process19 = require("child_process");
|
|
9603
9669
|
|
|
9604
9670
|
// src/daemon/ipc-router.ts
|
|
9605
9671
|
var os14 = __toESM(require("os"));
|
|
@@ -10333,7 +10399,7 @@ async function handleExec(command, projectPath) {
|
|
|
10333
10399
|
env = {}
|
|
10334
10400
|
} = command;
|
|
10335
10401
|
const effectiveTimeout = Math.min(Math.max(timeout, 1e3), MAX_TIMEOUT);
|
|
10336
|
-
return new Promise((
|
|
10402
|
+
return new Promise((resolve7) => {
|
|
10337
10403
|
let stdout = "";
|
|
10338
10404
|
let stderr = "";
|
|
10339
10405
|
let timedOut = false;
|
|
@@ -10341,7 +10407,7 @@ async function handleExec(command, projectPath) {
|
|
|
10341
10407
|
const done = (result) => {
|
|
10342
10408
|
if (resolved) return;
|
|
10343
10409
|
resolved = true;
|
|
10344
|
-
|
|
10410
|
+
resolve7(result);
|
|
10345
10411
|
};
|
|
10346
10412
|
try {
|
|
10347
10413
|
const proc = (0, import_child_process11.spawn)(cmd, {
|
|
@@ -10445,18 +10511,18 @@ var import_core12 = __toESM(require_dist());
|
|
|
10445
10511
|
// src/utils/port-check.ts
|
|
10446
10512
|
var net2 = __toESM(require("net"));
|
|
10447
10513
|
async function isPortInUse(port) {
|
|
10448
|
-
return new Promise((
|
|
10514
|
+
return new Promise((resolve7) => {
|
|
10449
10515
|
const server = net2.createServer();
|
|
10450
10516
|
server.once("error", (err) => {
|
|
10451
10517
|
if (err.code === "EADDRINUSE") {
|
|
10452
|
-
|
|
10518
|
+
resolve7(true);
|
|
10453
10519
|
} else {
|
|
10454
|
-
|
|
10520
|
+
resolve7(false);
|
|
10455
10521
|
}
|
|
10456
10522
|
});
|
|
10457
10523
|
server.once("listening", () => {
|
|
10458
10524
|
server.close();
|
|
10459
|
-
|
|
10525
|
+
resolve7(false);
|
|
10460
10526
|
});
|
|
10461
10527
|
server.listen(port);
|
|
10462
10528
|
});
|
|
@@ -10859,7 +10925,7 @@ var DevServerRegistry = class {
|
|
|
10859
10925
|
return killed;
|
|
10860
10926
|
}
|
|
10861
10927
|
wait(ms) {
|
|
10862
|
-
return new Promise((
|
|
10928
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
10863
10929
|
}
|
|
10864
10930
|
killWsServerOnPort(wsPort, worktreePath) {
|
|
10865
10931
|
const killed = [];
|
|
@@ -11433,7 +11499,7 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
11433
11499
|
return Math.min(delay, DEV_SERVER_CONSTANTS.MAX_RESTART_DELAY_MS);
|
|
11434
11500
|
}
|
|
11435
11501
|
async checkHealth(port) {
|
|
11436
|
-
return new Promise((
|
|
11502
|
+
return new Promise((resolve7) => {
|
|
11437
11503
|
const req = http.request(
|
|
11438
11504
|
{
|
|
11439
11505
|
hostname: "localhost",
|
|
@@ -11442,18 +11508,18 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
11442
11508
|
method: "HEAD",
|
|
11443
11509
|
timeout: DEV_SERVER_CONSTANTS.HEALTH_CHECK_TIMEOUT_MS
|
|
11444
11510
|
},
|
|
11445
|
-
() =>
|
|
11511
|
+
() => resolve7(true)
|
|
11446
11512
|
);
|
|
11447
|
-
req.on("error", () =>
|
|
11513
|
+
req.on("error", () => resolve7(false));
|
|
11448
11514
|
req.on("timeout", () => {
|
|
11449
11515
|
req.destroy();
|
|
11450
|
-
|
|
11516
|
+
resolve7(false);
|
|
11451
11517
|
});
|
|
11452
11518
|
req.end();
|
|
11453
11519
|
});
|
|
11454
11520
|
}
|
|
11455
11521
|
async checkWsHealth(wsPort) {
|
|
11456
|
-
return new Promise((
|
|
11522
|
+
return new Promise((resolve7) => {
|
|
11457
11523
|
const req = http.request(
|
|
11458
11524
|
{
|
|
11459
11525
|
hostname: "127.0.0.1",
|
|
@@ -11463,14 +11529,14 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
11463
11529
|
timeout: DEV_SERVER_CONSTANTS.HEALTH_CHECK_TIMEOUT_MS
|
|
11464
11530
|
},
|
|
11465
11531
|
(res) => {
|
|
11466
|
-
|
|
11532
|
+
resolve7(res.statusCode === 200);
|
|
11467
11533
|
res.resume();
|
|
11468
11534
|
}
|
|
11469
11535
|
);
|
|
11470
|
-
req.on("error", () =>
|
|
11536
|
+
req.on("error", () => resolve7(false));
|
|
11471
11537
|
req.on("timeout", () => {
|
|
11472
11538
|
req.destroy();
|
|
11473
|
-
|
|
11539
|
+
resolve7(false);
|
|
11474
11540
|
});
|
|
11475
11541
|
req.end();
|
|
11476
11542
|
});
|
|
@@ -11498,7 +11564,7 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
11498
11564
|
return false;
|
|
11499
11565
|
}
|
|
11500
11566
|
wait(ms) {
|
|
11501
|
-
return new Promise((
|
|
11567
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
11502
11568
|
}
|
|
11503
11569
|
getLogsDir() {
|
|
11504
11570
|
const logsDir = path25.join((0, import_core12.getConfigDir)(), "logs");
|
|
@@ -11697,7 +11763,7 @@ var PreviewManager = class extends import_events3.EventEmitter {
|
|
|
11697
11763
|
for (let attempt = 1; attempt <= MAX_TUNNEL_RETRIES; attempt++) {
|
|
11698
11764
|
if (attempt > 1) {
|
|
11699
11765
|
console.log(`[PreviewManager] Retrying tunnel for ${moduleUid} (attempt ${attempt}/${MAX_TUNNEL_RETRIES})...`);
|
|
11700
|
-
await new Promise((
|
|
11766
|
+
await new Promise((resolve7) => setTimeout(resolve7, 2e3));
|
|
11701
11767
|
}
|
|
11702
11768
|
tunnelResult = await this.tunnel.startTunnel({
|
|
11703
11769
|
moduleUid,
|
|
@@ -11829,7 +11895,7 @@ var PreviewManager = class extends import_events3.EventEmitter {
|
|
|
11829
11895
|
}
|
|
11830
11896
|
console.log(`[PreviewManager] Restarting preview for ${moduleUid}`);
|
|
11831
11897
|
await this.stopPreview(moduleUid);
|
|
11832
|
-
await new Promise((
|
|
11898
|
+
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
11833
11899
|
return this.startPreview({
|
|
11834
11900
|
moduleUid,
|
|
11835
11901
|
worktreePath: state.worktreePath,
|
|
@@ -12770,7 +12836,7 @@ async function killProcessOnPort(port) {
|
|
|
12770
12836
|
} catch {
|
|
12771
12837
|
}
|
|
12772
12838
|
}
|
|
12773
|
-
await new Promise((
|
|
12839
|
+
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
12774
12840
|
for (const pid of pids) {
|
|
12775
12841
|
try {
|
|
12776
12842
|
(0, import_child_process15.execSync)(`kill -0 ${pid} 2>/dev/null`, { encoding: "utf8" });
|
|
@@ -12779,7 +12845,7 @@ async function killProcessOnPort(port) {
|
|
|
12779
12845
|
} catch {
|
|
12780
12846
|
}
|
|
12781
12847
|
}
|
|
12782
|
-
await new Promise((
|
|
12848
|
+
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
12783
12849
|
const stillInUse = await isPortInUse(port);
|
|
12784
12850
|
if (stillInUse) {
|
|
12785
12851
|
console.error(`[DevServer] EP929: Port ${port} still in use after kill attempts`);
|
|
@@ -12799,7 +12865,7 @@ async function waitForPort(port, timeoutMs = 3e4) {
|
|
|
12799
12865
|
if (await isPortInUse(port)) {
|
|
12800
12866
|
return true;
|
|
12801
12867
|
}
|
|
12802
|
-
await new Promise((
|
|
12868
|
+
await new Promise((resolve7) => setTimeout(resolve7, checkInterval));
|
|
12803
12869
|
}
|
|
12804
12870
|
return false;
|
|
12805
12871
|
}
|
|
@@ -12871,7 +12937,7 @@ async function handleProcessExit(moduleUid, code, signal) {
|
|
|
12871
12937
|
const delay = calculateRestartDelay(serverInfo.restartCount);
|
|
12872
12938
|
console.log(`[DevServer] EP932: Restarting ${moduleUid} in ${delay}ms (attempt ${serverInfo.restartCount + 1}/${MAX_RESTART_ATTEMPTS})`);
|
|
12873
12939
|
writeToLog(serverInfo.logFile || "", `Scheduling restart in ${delay}ms (attempt ${serverInfo.restartCount + 1})`, false);
|
|
12874
|
-
await new Promise((
|
|
12940
|
+
await new Promise((resolve7) => setTimeout(resolve7, delay));
|
|
12875
12941
|
if (!activeServers.has(moduleUid)) {
|
|
12876
12942
|
console.log(`[DevServer] EP932: Server ${moduleUid} was removed during restart delay, aborting restart`);
|
|
12877
12943
|
return;
|
|
@@ -12996,7 +13062,7 @@ async function stopDevServer(moduleUid) {
|
|
|
12996
13062
|
writeToLog(serverInfo.logFile, `Stopping server (manual stop)`, false);
|
|
12997
13063
|
}
|
|
12998
13064
|
serverInfo.process.kill("SIGTERM");
|
|
12999
|
-
await new Promise((
|
|
13065
|
+
await new Promise((resolve7) => setTimeout(resolve7, 2e3));
|
|
13000
13066
|
if (!serverInfo.process.killed) {
|
|
13001
13067
|
serverInfo.process.kill("SIGKILL");
|
|
13002
13068
|
}
|
|
@@ -13014,7 +13080,7 @@ async function restartDevServer(moduleUid) {
|
|
|
13014
13080
|
writeToLog(logFile, `Manual restart requested`, false);
|
|
13015
13081
|
}
|
|
13016
13082
|
await stopDevServer(moduleUid);
|
|
13017
|
-
await new Promise((
|
|
13083
|
+
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
13018
13084
|
if (await isPortInUse(port)) {
|
|
13019
13085
|
await killProcessOnPort(port);
|
|
13020
13086
|
}
|
|
@@ -13076,7 +13142,6 @@ var IPCRouter = class {
|
|
|
13076
13142
|
});
|
|
13077
13143
|
this.ipcServer.on("add-project", async (params) => {
|
|
13078
13144
|
const { projectId, projectPath } = params;
|
|
13079
|
-
addProject(projectId, projectPath);
|
|
13080
13145
|
const MAX_RETRIES = 3;
|
|
13081
13146
|
const INITIAL_DELAY = 1e3;
|
|
13082
13147
|
let lastError = "";
|
|
@@ -13089,6 +13154,7 @@ var IPCRouter = class {
|
|
|
13089
13154
|
lastError = "Connection established but not healthy";
|
|
13090
13155
|
return { success: false, connected: false, error: lastError };
|
|
13091
13156
|
}
|
|
13157
|
+
addProject(projectId, projectPath);
|
|
13092
13158
|
return { success: true, connected: true };
|
|
13093
13159
|
} catch (error) {
|
|
13094
13160
|
lastError = error instanceof Error ? error.message : String(error);
|
|
@@ -13096,7 +13162,7 @@ var IPCRouter = class {
|
|
|
13096
13162
|
if (attempt < MAX_RETRIES) {
|
|
13097
13163
|
const delay = INITIAL_DELAY * Math.pow(2, attempt - 1);
|
|
13098
13164
|
console.log(`[Daemon] Retrying in ${delay / 1e3}s...`);
|
|
13099
|
-
await new Promise((
|
|
13165
|
+
await new Promise((resolve7) => setTimeout(resolve7, delay));
|
|
13100
13166
|
await this.host.disconnectProject(projectPath);
|
|
13101
13167
|
}
|
|
13102
13168
|
}
|
|
@@ -13536,6 +13602,8 @@ var UpdateManager = class _UpdateManager {
|
|
|
13536
13602
|
const child = (0, import_child_process17.spawn)("node", [this.daemonEntryFile], {
|
|
13537
13603
|
detached: true,
|
|
13538
13604
|
stdio: ["ignore", logFd, logFd],
|
|
13605
|
+
cwd: configDir,
|
|
13606
|
+
// Keep daemon process context stable across restarts.
|
|
13539
13607
|
env: { ...process.env, EPISODA_DAEMON_MODE: "1" }
|
|
13540
13608
|
});
|
|
13541
13609
|
if (!child.pid) {
|
|
@@ -13609,6 +13677,8 @@ var HealthOrchestrator = class _HealthOrchestrator {
|
|
|
13609
13677
|
this.tunnelPollInterval = null;
|
|
13610
13678
|
// EP833: Track consecutive health check failures per tunnel
|
|
13611
13679
|
this.tunnelHealthFailures = /* @__PURE__ */ new Map();
|
|
13680
|
+
// Restart after 2 consecutive failures
|
|
13681
|
+
this.tunnelRestartCooldownUntil = /* @__PURE__ */ new Map();
|
|
13612
13682
|
// 3 second timeout for health checks
|
|
13613
13683
|
// EP929: Health check polling interval (restored from EP843 removal)
|
|
13614
13684
|
// Health checks are orthogonal to push-based state sync - they detect dead tunnels
|
|
@@ -13623,7 +13693,11 @@ var HealthOrchestrator = class _HealthOrchestrator {
|
|
|
13623
13693
|
this.HEALTH_CHECK_FAILURE_THRESHOLD = 2;
|
|
13624
13694
|
}
|
|
13625
13695
|
static {
|
|
13626
|
-
//
|
|
13696
|
+
// moduleUid -> unix ms
|
|
13697
|
+
this.HEALTH_RESTART_COOLDOWN_MS = 3 * 60 * 1e3;
|
|
13698
|
+
}
|
|
13699
|
+
static {
|
|
13700
|
+
// 3 minutes
|
|
13627
13701
|
this.HEALTH_CHECK_TIMEOUT_MS = 3e3;
|
|
13628
13702
|
}
|
|
13629
13703
|
static {
|
|
@@ -14043,20 +14117,55 @@ var HealthOrchestrator = class _HealthOrchestrator {
|
|
|
14043
14117
|
const isHealthy = await this.checkTunnelHealth(tunnel);
|
|
14044
14118
|
if (isHealthy) {
|
|
14045
14119
|
this.tunnelHealthFailures.delete(tunnel.moduleUid);
|
|
14120
|
+
this.tunnelRestartCooldownUntil.delete(tunnel.moduleUid);
|
|
14046
14121
|
} else {
|
|
14047
14122
|
const failures = (this.tunnelHealthFailures.get(tunnel.moduleUid) || 0) + 1;
|
|
14048
14123
|
this.tunnelHealthFailures.set(tunnel.moduleUid, failures);
|
|
14049
14124
|
console.log(`[Daemon] EP833: Health check failed for ${tunnel.moduleUid} (${failures}/${_HealthOrchestrator.HEALTH_CHECK_FAILURE_THRESHOLD})`);
|
|
14050
|
-
if (failures
|
|
14051
|
-
|
|
14052
|
-
await this.host.withTunnelLock(tunnel.moduleUid, async () => {
|
|
14053
|
-
await this.restartTunnel(tunnel.moduleUid, tunnel.port);
|
|
14054
|
-
});
|
|
14055
|
-
this.tunnelHealthFailures.delete(tunnel.moduleUid);
|
|
14125
|
+
if (failures < _HealthOrchestrator.HEALTH_CHECK_FAILURE_THRESHOLD) {
|
|
14126
|
+
continue;
|
|
14056
14127
|
}
|
|
14128
|
+
const now = Date.now();
|
|
14129
|
+
const cooldownUntil = this.tunnelRestartCooldownUntil.get(tunnel.moduleUid) || 0;
|
|
14130
|
+
if (cooldownUntil > now) {
|
|
14131
|
+
continue;
|
|
14132
|
+
}
|
|
14133
|
+
const healthProjectId = config.project_id || config.projectId;
|
|
14134
|
+
if (healthProjectId) {
|
|
14135
|
+
const activeModuleUids = await this.fetchActiveModuleUids(healthProjectId);
|
|
14136
|
+
if (activeModuleUids && !activeModuleUids.includes(tunnel.moduleUid)) {
|
|
14137
|
+
console.log(`[Daemon] EP1417: Skipping health auto-restart for ${tunnel.moduleUid}; module is no longer active`);
|
|
14138
|
+
await this.retireInactiveTunnel(tunnel.moduleUid);
|
|
14139
|
+
this.tunnelHealthFailures.delete(tunnel.moduleUid);
|
|
14140
|
+
this.tunnelRestartCooldownUntil.delete(tunnel.moduleUid);
|
|
14141
|
+
continue;
|
|
14142
|
+
}
|
|
14143
|
+
}
|
|
14144
|
+
console.log(`[Daemon] EP833: Tunnel unhealthy for ${tunnel.moduleUid}, restarting...`);
|
|
14145
|
+
await this.host.withTunnelLock(tunnel.moduleUid, async () => {
|
|
14146
|
+
await this.restartTunnel(tunnel.moduleUid, tunnel.port);
|
|
14147
|
+
});
|
|
14148
|
+
this.tunnelHealthFailures.delete(tunnel.moduleUid);
|
|
14149
|
+
this.tunnelRestartCooldownUntil.set(
|
|
14150
|
+
tunnel.moduleUid,
|
|
14151
|
+
now + _HealthOrchestrator.HEALTH_RESTART_COOLDOWN_MS
|
|
14152
|
+
);
|
|
14057
14153
|
}
|
|
14058
14154
|
}
|
|
14059
14155
|
}
|
|
14156
|
+
async retireInactiveTunnel(moduleUid) {
|
|
14157
|
+
try {
|
|
14158
|
+
const tunnelManager = getTunnelManager();
|
|
14159
|
+
await tunnelManager.stopTunnel(moduleUid);
|
|
14160
|
+
} catch (error) {
|
|
14161
|
+
console.warn(`[Daemon] EP1417: Failed to stop inactive tunnel for ${moduleUid}:`, error);
|
|
14162
|
+
}
|
|
14163
|
+
try {
|
|
14164
|
+
await stopDevServer(moduleUid);
|
|
14165
|
+
} catch (error) {
|
|
14166
|
+
console.warn(`[Daemon] EP1417: Failed to stop inactive dev server for ${moduleUid}:`, error);
|
|
14167
|
+
}
|
|
14168
|
+
}
|
|
14060
14169
|
/**
|
|
14061
14170
|
* EP833: Check if a tunnel is healthy
|
|
14062
14171
|
* EP1042: Now also verifies dev server ownership (correct worktree)
|
|
@@ -14207,45 +14316,54 @@ var DaemonCore = class {
|
|
|
14207
14316
|
// src/daemon/connection-manager.ts
|
|
14208
14317
|
var ConnectionManager = class {
|
|
14209
14318
|
constructor() {
|
|
14319
|
+
// Single source-of-truth for project connectivity.
|
|
14210
14320
|
this.connections = /* @__PURE__ */ new Map();
|
|
14211
|
-
// projectPath -> connection
|
|
14212
|
-
this.liveConnections = /* @__PURE__ */ new Set();
|
|
14213
|
-
// projectPath
|
|
14214
|
-
this.pendingConnections = /* @__PURE__ */ new Set();
|
|
14215
14321
|
}
|
|
14216
|
-
// projectPath
|
|
14322
|
+
// projectPath -> connection
|
|
14217
14323
|
hasConnection(projectPath) {
|
|
14218
14324
|
return this.connections.has(projectPath);
|
|
14219
14325
|
}
|
|
14220
14326
|
hasLiveConnection(projectPath) {
|
|
14221
|
-
return this.
|
|
14327
|
+
return this.getState(projectPath) === "connected";
|
|
14222
14328
|
}
|
|
14223
14329
|
hasPendingConnection(projectPath) {
|
|
14224
|
-
|
|
14330
|
+
const state = this.getState(projectPath);
|
|
14331
|
+
return state === "connecting" || state === "authenticating";
|
|
14225
14332
|
}
|
|
14226
14333
|
isConnectionHealthy(projectPath) {
|
|
14227
|
-
return this.
|
|
14334
|
+
return this.getState(projectPath) === "connected" && this.isWebSocketOpen(projectPath);
|
|
14228
14335
|
}
|
|
14229
14336
|
getConnection(projectPath) {
|
|
14230
14337
|
return this.connections.get(projectPath);
|
|
14231
14338
|
}
|
|
14232
14339
|
setConnection(projectPath, connection) {
|
|
14233
|
-
|
|
14340
|
+
const stateful = {
|
|
14341
|
+
...connection,
|
|
14342
|
+
state: connection.state || "connecting",
|
|
14343
|
+
lastTransitionAt: connection.lastTransitionAt || Date.now()
|
|
14344
|
+
};
|
|
14345
|
+
this.connections.set(projectPath, stateful);
|
|
14234
14346
|
}
|
|
14235
14347
|
deleteConnection(projectPath) {
|
|
14236
14348
|
this.connections.delete(projectPath);
|
|
14237
14349
|
}
|
|
14238
14350
|
addLiveConnection(projectPath) {
|
|
14239
|
-
this.
|
|
14351
|
+
this.setState(projectPath, "connected");
|
|
14240
14352
|
}
|
|
14241
14353
|
removeLiveConnection(projectPath) {
|
|
14242
|
-
this.
|
|
14354
|
+
const state = this.getState(projectPath);
|
|
14355
|
+
if (state === "connected") {
|
|
14356
|
+
this.setState(projectPath, "disconnected");
|
|
14357
|
+
}
|
|
14243
14358
|
}
|
|
14244
14359
|
addPendingConnection(projectPath) {
|
|
14245
|
-
this.
|
|
14360
|
+
this.setState(projectPath, "connecting");
|
|
14246
14361
|
}
|
|
14247
14362
|
removePendingConnection(projectPath) {
|
|
14248
|
-
this.
|
|
14363
|
+
const state = this.getState(projectPath);
|
|
14364
|
+
if (state === "connecting" || state === "authenticating") {
|
|
14365
|
+
this.setState(projectPath, "disconnected");
|
|
14366
|
+
}
|
|
14249
14367
|
}
|
|
14250
14368
|
isWebSocketOpen(projectPath) {
|
|
14251
14369
|
const connection = this.connections.get(projectPath);
|
|
@@ -14256,10 +14374,18 @@ var ConnectionManager = class {
|
|
|
14256
14374
|
return this.connections.size;
|
|
14257
14375
|
}
|
|
14258
14376
|
liveConnectionCount() {
|
|
14259
|
-
|
|
14377
|
+
let count = 0;
|
|
14378
|
+
for (const connection of this.connections.values()) {
|
|
14379
|
+
if (connection.state === "connected") count++;
|
|
14380
|
+
}
|
|
14381
|
+
return count;
|
|
14260
14382
|
}
|
|
14261
14383
|
pendingConnectionCount() {
|
|
14262
|
-
|
|
14384
|
+
let count = 0;
|
|
14385
|
+
for (const connection of this.connections.values()) {
|
|
14386
|
+
if (connection.state === "connecting" || connection.state === "authenticating") count++;
|
|
14387
|
+
}
|
|
14388
|
+
return count;
|
|
14263
14389
|
}
|
|
14264
14390
|
entries() {
|
|
14265
14391
|
return this.connections.entries();
|
|
@@ -14268,12 +14394,24 @@ var ConnectionManager = class {
|
|
|
14268
14394
|
this.connections.clear();
|
|
14269
14395
|
}
|
|
14270
14396
|
clearLiveConnections() {
|
|
14271
|
-
this.
|
|
14397
|
+
for (const [projectPath, connection] of this.connections.entries()) {
|
|
14398
|
+
if (connection.state === "connected") {
|
|
14399
|
+
this.setState(projectPath, "disconnected");
|
|
14400
|
+
}
|
|
14401
|
+
}
|
|
14272
14402
|
}
|
|
14273
14403
|
clearPendingConnections() {
|
|
14274
|
-
this.
|
|
14404
|
+
for (const [projectPath, connection] of this.connections.entries()) {
|
|
14405
|
+
if (connection.state === "connecting" || connection.state === "authenticating") {
|
|
14406
|
+
this.setState(projectPath, "disconnected");
|
|
14407
|
+
}
|
|
14408
|
+
}
|
|
14275
14409
|
}
|
|
14276
14410
|
async restoreConnections(connectProject) {
|
|
14411
|
+
const pruned = pruneMissingProjectPaths();
|
|
14412
|
+
if (pruned > 0) {
|
|
14413
|
+
console.log(`[Daemon] Pruned ${pruned} stale project tracking entr${pruned === 1 ? "y" : "ies"} before restore`);
|
|
14414
|
+
}
|
|
14277
14415
|
const projects = getAllProjects();
|
|
14278
14416
|
for (const project of projects) {
|
|
14279
14417
|
try {
|
|
@@ -14283,19 +14421,31 @@ var ConnectionManager = class {
|
|
|
14283
14421
|
}
|
|
14284
14422
|
}
|
|
14285
14423
|
}
|
|
14424
|
+
getState(projectPath) {
|
|
14425
|
+
return this.connections.get(projectPath)?.state || null;
|
|
14426
|
+
}
|
|
14427
|
+
setState(projectPath, state, error) {
|
|
14428
|
+
const connection = this.connections.get(projectPath);
|
|
14429
|
+
if (!connection) return;
|
|
14430
|
+
connection.state = state;
|
|
14431
|
+
connection.lastTransitionAt = Date.now();
|
|
14432
|
+
connection.lastError = error;
|
|
14433
|
+
}
|
|
14286
14434
|
/**
|
|
14287
|
-
* Wait for auth_success or
|
|
14435
|
+
* Wait for auth_success or fail-fast auth/transport errors after connect().
|
|
14288
14436
|
*
|
|
14289
14437
|
* This keeps auth timeout lifecycle in the connection subsystem and ensures
|
|
14290
14438
|
* handlers are removed deterministically on every exit path.
|
|
14291
14439
|
*/
|
|
14292
14440
|
async waitForAuthentication(client, timeoutMs = 3e4) {
|
|
14293
|
-
await new Promise((
|
|
14441
|
+
await new Promise((resolve7, reject) => {
|
|
14294
14442
|
let settled = false;
|
|
14295
14443
|
const cleanup = () => {
|
|
14296
14444
|
clearTimeout(timeout);
|
|
14297
14445
|
client.off("auth_success", authHandler);
|
|
14298
|
-
client.off("auth_error",
|
|
14446
|
+
client.off("auth_error", authErrorHandler);
|
|
14447
|
+
client.off("error", serverErrorHandler);
|
|
14448
|
+
client.off("disconnected", disconnectedHandler);
|
|
14299
14449
|
};
|
|
14300
14450
|
const timeout = setTimeout(() => {
|
|
14301
14451
|
if (settled) return;
|
|
@@ -14307,17 +14457,41 @@ var ConnectionManager = class {
|
|
|
14307
14457
|
if (settled) return;
|
|
14308
14458
|
settled = true;
|
|
14309
14459
|
cleanup();
|
|
14310
|
-
|
|
14460
|
+
resolve7();
|
|
14311
14461
|
};
|
|
14312
|
-
const
|
|
14462
|
+
const authErrorHandler = (message) => {
|
|
14313
14463
|
if (settled) return;
|
|
14314
14464
|
settled = true;
|
|
14315
14465
|
cleanup();
|
|
14316
14466
|
const errorMsg = message;
|
|
14317
14467
|
reject(new Error(errorMsg.message || "Authentication failed"));
|
|
14318
14468
|
};
|
|
14469
|
+
const serverErrorHandler = (message) => {
|
|
14470
|
+
if (settled) return;
|
|
14471
|
+
settled = true;
|
|
14472
|
+
cleanup();
|
|
14473
|
+
const errorMsg = message;
|
|
14474
|
+
const code = errorMsg.code || "SERVER_ERROR";
|
|
14475
|
+
if (code === "TOO_SOON" && typeof errorMsg.retryAfter === "number") {
|
|
14476
|
+
reject(new Error(`Authentication deferred by server (${code}, retryAfter=${errorMsg.retryAfter}s)`));
|
|
14477
|
+
return;
|
|
14478
|
+
}
|
|
14479
|
+
reject(new Error(errorMsg.message || `Authentication failed (${code})`));
|
|
14480
|
+
};
|
|
14481
|
+
const disconnectedHandler = (message) => {
|
|
14482
|
+
if (settled) return;
|
|
14483
|
+
settled = true;
|
|
14484
|
+
cleanup();
|
|
14485
|
+
const event = message;
|
|
14486
|
+
const code = typeof event.code === "number" ? event.code : -1;
|
|
14487
|
+
const reason = event.reason || "connection closed";
|
|
14488
|
+
const suffix = event.willReconnect ? ", reconnect scheduled" : "";
|
|
14489
|
+
reject(new Error(`Connection closed before authentication (code=${code}, reason=${reason}${suffix})`));
|
|
14490
|
+
};
|
|
14319
14491
|
client.on("auth_success", authHandler);
|
|
14320
|
-
client.on("auth_error",
|
|
14492
|
+
client.on("auth_error", authErrorHandler);
|
|
14493
|
+
client.on("error", serverErrorHandler);
|
|
14494
|
+
client.on("disconnected", disconnectedHandler);
|
|
14321
14495
|
});
|
|
14322
14496
|
}
|
|
14323
14497
|
};
|
|
@@ -14884,6 +15058,8 @@ var Daemon = class _Daemon {
|
|
|
14884
15058
|
// EP1003: Prevents race conditions between server-orchestrated tunnel commands
|
|
14885
15059
|
this.tunnelOperationLocks = /* @__PURE__ */ new Map();
|
|
14886
15060
|
// moduleUid -> operation promise
|
|
15061
|
+
// EP1426: Single in-flight connect attempt per project path.
|
|
15062
|
+
this.connectProjectInFlight = /* @__PURE__ */ new Map();
|
|
14887
15063
|
// EP1210-7: Health HTTP endpoint for external monitoring
|
|
14888
15064
|
this.healthServer = null;
|
|
14889
15065
|
// EP1267: Cloud disconnect watchdog
|
|
@@ -14921,6 +15097,14 @@ var Daemon = class _Daemon {
|
|
|
14921
15097
|
this.agentEventSeq.set(sessionId, next);
|
|
14922
15098
|
return next;
|
|
14923
15099
|
}
|
|
15100
|
+
logReliabilityMetric(metric, fields) {
|
|
15101
|
+
console.log(`[Daemon][Metric] ${JSON.stringify({
|
|
15102
|
+
metric,
|
|
15103
|
+
machineId: this.machineId || "unknown",
|
|
15104
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15105
|
+
...fields
|
|
15106
|
+
})}`);
|
|
15107
|
+
}
|
|
14924
15108
|
/**
|
|
14925
15109
|
* Start the daemon
|
|
14926
15110
|
*/
|
|
@@ -15117,8 +15301,8 @@ var Daemon = class _Daemon {
|
|
|
15117
15301
|
}
|
|
15118
15302
|
}
|
|
15119
15303
|
let releaseLock;
|
|
15120
|
-
const lockPromise = new Promise((
|
|
15121
|
-
releaseLock =
|
|
15304
|
+
const lockPromise = new Promise((resolve7) => {
|
|
15305
|
+
releaseLock = resolve7;
|
|
15122
15306
|
});
|
|
15123
15307
|
this.tunnelOperationLocks.set(moduleUid, lockPromise);
|
|
15124
15308
|
try {
|
|
@@ -15135,6 +15319,25 @@ var Daemon = class _Daemon {
|
|
|
15135
15319
|
* EP1366: Made public for IPCRouterHost interface
|
|
15136
15320
|
*/
|
|
15137
15321
|
async connectProject(projectId, projectPath) {
|
|
15322
|
+
const existing = this.connectProjectInFlight.get(projectPath);
|
|
15323
|
+
if (existing) {
|
|
15324
|
+
console.log(`[Daemon] Connection attempt already in progress for ${projectPath}, joining existing attempt`);
|
|
15325
|
+
return existing;
|
|
15326
|
+
}
|
|
15327
|
+
const attempt = this.connectProjectInternal(projectId, projectPath);
|
|
15328
|
+
this.connectProjectInFlight.set(projectPath, attempt);
|
|
15329
|
+
try {
|
|
15330
|
+
await attempt;
|
|
15331
|
+
} finally {
|
|
15332
|
+
if (this.connectProjectInFlight.get(projectPath) === attempt) {
|
|
15333
|
+
this.connectProjectInFlight.delete(projectPath);
|
|
15334
|
+
}
|
|
15335
|
+
}
|
|
15336
|
+
}
|
|
15337
|
+
async connectProjectInternal(projectId, projectPath) {
|
|
15338
|
+
const initialState = this.connectionManager.getState(projectPath);
|
|
15339
|
+
const isReconnectAttempt = initialState !== null;
|
|
15340
|
+
const connectAttemptStartedAt = Date.now();
|
|
15138
15341
|
if (this.connectionManager.hasConnection(projectPath)) {
|
|
15139
15342
|
if (this.connectionManager.hasLiveConnection(projectPath)) {
|
|
15140
15343
|
console.log(`[Daemon] Already connected to ${projectPath}`);
|
|
@@ -15145,7 +15348,7 @@ var Daemon = class _Daemon {
|
|
|
15145
15348
|
const maxWait = 35e3;
|
|
15146
15349
|
const startTime = Date.now();
|
|
15147
15350
|
while (this.connectionManager.hasPendingConnection(projectPath) && Date.now() - startTime < maxWait) {
|
|
15148
|
-
await new Promise((
|
|
15351
|
+
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
15149
15352
|
}
|
|
15150
15353
|
if (this.connectionManager.hasLiveConnection(projectPath)) {
|
|
15151
15354
|
console.log(`[Daemon] Pending connection succeeded for ${projectPath}`);
|
|
@@ -15156,6 +15359,12 @@ var Daemon = class _Daemon {
|
|
|
15156
15359
|
console.warn(`[Daemon] Stale connection detected for ${projectPath}, forcing reconnection`);
|
|
15157
15360
|
await this.disconnectProject(projectPath);
|
|
15158
15361
|
}
|
|
15362
|
+
this.logReliabilityMetric("ws_connect_attempt", {
|
|
15363
|
+
projectId,
|
|
15364
|
+
projectPath,
|
|
15365
|
+
reconnect: isReconnectAttempt,
|
|
15366
|
+
previousState: initialState
|
|
15367
|
+
});
|
|
15159
15368
|
const config = await (0, import_core22.loadConfig)();
|
|
15160
15369
|
if (!config || !config.access_token) {
|
|
15161
15370
|
throw new Error("No access token found. Please run: episoda auth");
|
|
@@ -15170,12 +15379,15 @@ var Daemon = class _Daemon {
|
|
|
15170
15379
|
}
|
|
15171
15380
|
console.log(`[Daemon] Connecting to ${wsEndpoint.wsUrl} for project ${projectId} (source: ${wsEndpoint.source})...`);
|
|
15172
15381
|
const client = new import_core22.EpisodaClient();
|
|
15382
|
+
client.setAutoReconnect(false);
|
|
15173
15383
|
const gitExecutor = new import_core22.GitExecutor();
|
|
15174
15384
|
const connection = {
|
|
15175
15385
|
projectId,
|
|
15176
15386
|
projectPath,
|
|
15177
15387
|
client,
|
|
15178
|
-
gitExecutor
|
|
15388
|
+
gitExecutor,
|
|
15389
|
+
state: "connecting",
|
|
15390
|
+
lastTransitionAt: Date.now()
|
|
15179
15391
|
};
|
|
15180
15392
|
this.connectionManager.setConnection(projectPath, connection);
|
|
15181
15393
|
this.connectionManager.addPendingConnection(projectPath);
|
|
@@ -15513,6 +15725,7 @@ var Daemon = class _Daemon {
|
|
|
15513
15725
|
}
|
|
15514
15726
|
});
|
|
15515
15727
|
client.on("auth_success", async (message) => {
|
|
15728
|
+
client.setAutoReconnect(true);
|
|
15516
15729
|
console.log(`[Daemon] Authenticated for project ${projectId}`);
|
|
15517
15730
|
touchProject(projectPath);
|
|
15518
15731
|
this.connectionManager.addLiveConnection(projectPath);
|
|
@@ -15626,9 +15839,59 @@ var Daemon = class _Daemon {
|
|
|
15626
15839
|
console.error("[Daemon] EP1237: Error handling agent_reconciliation_commands:", error instanceof Error ? error.message : error);
|
|
15627
15840
|
}
|
|
15628
15841
|
});
|
|
15842
|
+
client.on("reconnect_scheduled", (event) => {
|
|
15843
|
+
const reconnectEvent = event;
|
|
15844
|
+
this.logReliabilityMetric("ws_transport_reconnect_scheduled", {
|
|
15845
|
+
projectId,
|
|
15846
|
+
projectPath,
|
|
15847
|
+
attempt: reconnectEvent.attempt,
|
|
15848
|
+
delayMs: reconnectEvent.delayMs,
|
|
15849
|
+
strategy: reconnectEvent.strategy
|
|
15850
|
+
});
|
|
15851
|
+
});
|
|
15852
|
+
client.on("reconnect_attempt", (event) => {
|
|
15853
|
+
const reconnectEvent = event;
|
|
15854
|
+
this.logReliabilityMetric("ws_transport_reconnect_attempt", {
|
|
15855
|
+
projectId,
|
|
15856
|
+
projectPath,
|
|
15857
|
+
attempt: reconnectEvent.attempt
|
|
15858
|
+
});
|
|
15859
|
+
});
|
|
15860
|
+
client.on("reconnect_result", (event) => {
|
|
15861
|
+
const reconnectEvent = event;
|
|
15862
|
+
this.logReliabilityMetric("ws_transport_reconnect_result", {
|
|
15863
|
+
projectId,
|
|
15864
|
+
projectPath,
|
|
15865
|
+
attempt: reconnectEvent.attempt,
|
|
15866
|
+
success: reconnectEvent.success,
|
|
15867
|
+
error: reconnectEvent.error
|
|
15868
|
+
});
|
|
15869
|
+
});
|
|
15870
|
+
client.on("reconnect_exhausted", (event) => {
|
|
15871
|
+
const reconnectEvent = event;
|
|
15872
|
+
this.logReliabilityMetric("ws_transport_reconnect_exhausted", {
|
|
15873
|
+
projectId,
|
|
15874
|
+
projectPath,
|
|
15875
|
+
attempts: reconnectEvent.attempts,
|
|
15876
|
+
reason: reconnectEvent.reason
|
|
15877
|
+
});
|
|
15878
|
+
this.logReliabilityMetric("manual_reconnect_needed", {
|
|
15879
|
+
source: "daemon",
|
|
15880
|
+
projectId,
|
|
15881
|
+
projectPath,
|
|
15882
|
+
reason: reconnectEvent.reason
|
|
15883
|
+
});
|
|
15884
|
+
});
|
|
15629
15885
|
client.on("disconnected", (event) => {
|
|
15630
15886
|
const disconnectEvent = event;
|
|
15631
15887
|
console.log(`[Daemon] Connection closed for ${projectId}: code=${disconnectEvent.code}, willReconnect=${disconnectEvent.willReconnect}`);
|
|
15888
|
+
this.logReliabilityMetric("ws_transport_disconnect", {
|
|
15889
|
+
projectId,
|
|
15890
|
+
projectPath,
|
|
15891
|
+
code: disconnectEvent.code,
|
|
15892
|
+
reason: disconnectEvent.reason,
|
|
15893
|
+
willReconnect: disconnectEvent.willReconnect
|
|
15894
|
+
});
|
|
15632
15895
|
this.connectionManager.removeLiveConnection(projectPath);
|
|
15633
15896
|
if (this.connectionManager.liveConnectionCount() === 0) {
|
|
15634
15897
|
this.lastDisconnectAt = this.lastDisconnectAt || Date.now();
|
|
@@ -15666,10 +15929,36 @@ var Daemon = class _Daemon {
|
|
|
15666
15929
|
containerId
|
|
15667
15930
|
});
|
|
15668
15931
|
console.log(`[Daemon] Successfully connected to project ${projectId}`);
|
|
15932
|
+
this.connectionManager.setState(projectPath, "authenticating");
|
|
15669
15933
|
await this.connectionManager.waitForAuthentication(client, 3e4);
|
|
15670
15934
|
console.log(`[Daemon] Authentication complete for project ${projectId}`);
|
|
15935
|
+
this.logReliabilityMetric("ws_connect_result", {
|
|
15936
|
+
projectId,
|
|
15937
|
+
projectPath,
|
|
15938
|
+
reconnect: isReconnectAttempt,
|
|
15939
|
+
success: true,
|
|
15940
|
+
durationMs: Date.now() - connectAttemptStartedAt
|
|
15941
|
+
});
|
|
15671
15942
|
} catch (error) {
|
|
15672
15943
|
console.error(`[Daemon] Failed to connect to ${projectId}:`, error);
|
|
15944
|
+
this.logReliabilityMetric("ws_connect_result", {
|
|
15945
|
+
projectId,
|
|
15946
|
+
projectPath,
|
|
15947
|
+
reconnect: isReconnectAttempt,
|
|
15948
|
+
success: false,
|
|
15949
|
+
durationMs: Date.now() - connectAttemptStartedAt,
|
|
15950
|
+
error: error instanceof Error ? error.message : String(error)
|
|
15951
|
+
});
|
|
15952
|
+
try {
|
|
15953
|
+
await client.disconnect(true);
|
|
15954
|
+
} catch {
|
|
15955
|
+
}
|
|
15956
|
+
client.clearAllHandlers();
|
|
15957
|
+
this.connectionManager.setState(
|
|
15958
|
+
projectPath,
|
|
15959
|
+
"disconnected",
|
|
15960
|
+
error instanceof Error ? error.message : String(error)
|
|
15961
|
+
);
|
|
15673
15962
|
this.connectionManager.deleteConnection(projectPath);
|
|
15674
15963
|
this.connectionManager.removePendingConnection(projectPath);
|
|
15675
15964
|
throw error;
|
|
@@ -16101,14 +16390,13 @@ var Daemon = class _Daemon {
|
|
|
16101
16390
|
console.log(`[Daemon] EP1002: ${installCmd.description} (detected from ${installCmd.detectedFrom})`);
|
|
16102
16391
|
console.log(`[Daemon] EP1002: Running: ${installCmd.command.join(" ")}`);
|
|
16103
16392
|
try {
|
|
16104
|
-
|
|
16105
|
-
|
|
16106
|
-
|
|
16107
|
-
|
|
16108
|
-
|
|
16109
|
-
|
|
16110
|
-
|
|
16111
|
-
});
|
|
16393
|
+
await this.runForegroundCommand(
|
|
16394
|
+
installCmd.command[0],
|
|
16395
|
+
installCmd.command.slice(1),
|
|
16396
|
+
worktreePath,
|
|
16397
|
+
{ ...process.env, CI: "true" },
|
|
16398
|
+
10 * 60 * 1e3
|
|
16399
|
+
);
|
|
16112
16400
|
installSucceeded = true;
|
|
16113
16401
|
console.log(`[Daemon] EP1002: Dependencies installed successfully`);
|
|
16114
16402
|
} catch (installError) {
|
|
@@ -16124,13 +16412,12 @@ var Daemon = class _Daemon {
|
|
|
16124
16412
|
const bootstrapCmd = buildCmd;
|
|
16125
16413
|
console.log(`[Daemon] EP1386: Bootstrapping packages after dependency install (${bootstrapCmd})`);
|
|
16126
16414
|
try {
|
|
16127
|
-
|
|
16128
|
-
|
|
16129
|
-
|
|
16130
|
-
|
|
16131
|
-
|
|
16132
|
-
|
|
16133
|
-
});
|
|
16415
|
+
await this.runShellForegroundCommand(
|
|
16416
|
+
bootstrapCmd,
|
|
16417
|
+
worktreePath,
|
|
16418
|
+
{ ...process.env, CI: "true" },
|
|
16419
|
+
10 * 60 * 1e3
|
|
16420
|
+
);
|
|
16134
16421
|
console.log("[Daemon] EP1386: Package bootstrap completed");
|
|
16135
16422
|
} catch (buildError) {
|
|
16136
16423
|
const errorMsg = buildError instanceof Error ? buildError.message : String(buildError);
|
|
@@ -16150,6 +16437,74 @@ var Daemon = class _Daemon {
|
|
|
16150
16437
|
await this.updateModuleWorktreeStatus(moduleUid, "ready", worktreePath);
|
|
16151
16438
|
console.log(`[Daemon] EP1002: Worktree setup complete for ${moduleUid}`);
|
|
16152
16439
|
}
|
|
16440
|
+
async runForegroundCommand(command, args, cwd, env, timeoutMs) {
|
|
16441
|
+
await new Promise((resolve7, reject) => {
|
|
16442
|
+
const commandLabel = `${command} ${args.join(" ")}`.trim();
|
|
16443
|
+
const child = (0, import_child_process19.spawn)(command, args, {
|
|
16444
|
+
cwd,
|
|
16445
|
+
env,
|
|
16446
|
+
stdio: "inherit",
|
|
16447
|
+
shell: false
|
|
16448
|
+
});
|
|
16449
|
+
let settled = false;
|
|
16450
|
+
let timedOut = false;
|
|
16451
|
+
const timeoutSeconds = Math.round(timeoutMs / 1e3);
|
|
16452
|
+
const termGraceMs = 5e3;
|
|
16453
|
+
let termTimer = null;
|
|
16454
|
+
let killTimer = null;
|
|
16455
|
+
const finish = (error) => {
|
|
16456
|
+
if (settled) return;
|
|
16457
|
+
settled = true;
|
|
16458
|
+
if (termTimer) clearTimeout(termTimer);
|
|
16459
|
+
if (killTimer) clearTimeout(killTimer);
|
|
16460
|
+
if (error) {
|
|
16461
|
+
reject(error);
|
|
16462
|
+
return;
|
|
16463
|
+
}
|
|
16464
|
+
resolve7();
|
|
16465
|
+
};
|
|
16466
|
+
termTimer = setTimeout(() => {
|
|
16467
|
+
timedOut = true;
|
|
16468
|
+
try {
|
|
16469
|
+
child.kill("SIGTERM");
|
|
16470
|
+
} catch {
|
|
16471
|
+
}
|
|
16472
|
+
killTimer = setTimeout(() => {
|
|
16473
|
+
if (settled) return;
|
|
16474
|
+
try {
|
|
16475
|
+
child.kill("SIGKILL");
|
|
16476
|
+
} catch {
|
|
16477
|
+
}
|
|
16478
|
+
finish(
|
|
16479
|
+
new Error(`Command timed out after ${timeoutSeconds}s (terminated with SIGKILL fallback): ${commandLabel}`)
|
|
16480
|
+
);
|
|
16481
|
+
}, termGraceMs);
|
|
16482
|
+
}, timeoutMs);
|
|
16483
|
+
child.on("error", (error) => {
|
|
16484
|
+
finish(error);
|
|
16485
|
+
});
|
|
16486
|
+
child.on("exit", (code, signal) => {
|
|
16487
|
+
if (timedOut) {
|
|
16488
|
+
finish(
|
|
16489
|
+
new Error(`Command timed out after ${timeoutSeconds}s (exit code=${code ?? "null"}, signal=${signal ?? "none"}): ${commandLabel}`)
|
|
16490
|
+
);
|
|
16491
|
+
return;
|
|
16492
|
+
}
|
|
16493
|
+
if (code === 0) {
|
|
16494
|
+
finish();
|
|
16495
|
+
return;
|
|
16496
|
+
}
|
|
16497
|
+
finish(new Error(`Command failed (code=${code ?? "null"}, signal=${signal ?? "none"}): ${commandLabel}`));
|
|
16498
|
+
});
|
|
16499
|
+
});
|
|
16500
|
+
}
|
|
16501
|
+
async runShellForegroundCommand(command, cwd, env, timeoutMs) {
|
|
16502
|
+
if (process.platform === "win32") {
|
|
16503
|
+
await this.runForegroundCommand("cmd", ["/d", "/s", "/c", command], cwd, env, timeoutMs);
|
|
16504
|
+
return;
|
|
16505
|
+
}
|
|
16506
|
+
await this.runForegroundCommand("sh", ["-lc", command], cwd, env, timeoutMs);
|
|
16507
|
+
}
|
|
16153
16508
|
// EP1003: startTunnelForModule removed - server now orchestrates via tunnel_start commands
|
|
16154
16509
|
// EP1003: autoStartTunnelsForProject removed - server now orchestrates via reconciliation
|
|
16155
16510
|
// Recovery flow: daemon sends reconciliation_report → server processes and sends commands
|