@episoda/cli 0.2.175 → 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.
@@ -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((resolve6, reject) => {
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
- resolve6();
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((resolve6) => {
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
- resolve6(false);
2418
+ resolve7(false);
2405
2419
  } else {
2406
- resolve6(true);
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.reconnectAttempts++;
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.175",
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((resolve6, reject) => {
3364
+ return new Promise((resolve7, reject) => {
3305
3365
  this.server.listen(socketPath, () => {
3306
3366
  fs3.chmodSync(socketPath, 384);
3307
- resolve6();
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((resolve6) => {
3378
+ return new Promise((resolve7) => {
3319
3379
  this.server.close(() => {
3320
3380
  if (fs3.existsSync(socketPath)) {
3321
3381
  fs3.unlinkSync(socketPath);
3322
3382
  }
3323
- resolve6();
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
- console.warn("[EP950] Could not fetch origin:", fetchError);
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((resolve6, reject) => {
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
- resolve6();
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((resolve6) => setTimeout(resolve6, 500));
4072
+ await new Promise((resolve7) => setTimeout(resolve7, 500));
3994
4073
  if (this.isProcessRunning(pid)) {
3995
4074
  this.killByPid(pid, "SIGKILL");
3996
- await new Promise((resolve6) => setTimeout(resolve6, 200));
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((resolve6) => setTimeout(resolve6, 1e3));
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((resolve6) => setTimeout(resolve6, 500));
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((resolve6) => {
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
- resolve6({ success: true, url: tunnelInfo.url });
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
- resolve6({ success: false, error: errorMsg });
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
- resolve6({ success: false, error: error.message });
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
- resolve6({ success: false, error: errorMsg });
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((resolve6) => setTimeout(resolve6, 500));
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((resolve6) => setTimeout(resolve6, 1e3));
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((resolve6) => setTimeout(resolve6, 1e3));
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((resolve6) => {
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
- resolve6();
4492
+ resolve7();
4414
4493
  }, 3e3);
4415
4494
  tunnel.process.once("exit", () => {
4416
4495
  clearTimeout(timeout);
4417
- resolve6();
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((resolve6, reject) => {
4783
+ await new Promise((resolve7, reject) => {
4705
4784
  const ok = stdin.write(data);
4706
4785
  if (ok) {
4707
- resolve6();
4786
+ resolve7();
4708
4787
  return;
4709
4788
  }
4710
4789
  const onDrain = () => {
4711
4790
  cleanup();
4712
- resolve6();
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((resolve6) => {
4812
+ return new Promise((resolve7) => {
4734
4813
  if (!process2 || !alive) {
4735
- resolve6(true);
4814
+ resolve7(true);
4736
4815
  return;
4737
4816
  }
4738
4817
  const onExit = () => {
4739
4818
  clearTimeout(timer);
4740
- resolve6(true);
4819
+ resolve7(true);
4741
4820
  };
4742
4821
  const timer = setTimeout(() => {
4743
4822
  process2.removeListener("exit", onExit);
4744
- resolve6(false);
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((resolve6, reject) => {
5819
+ return new Promise((resolve7, reject) => {
5741
5820
  const onId = (id) => {
5742
5821
  clearTimeout(timer);
5743
- resolve6(id);
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 (resolve6, reject) => {
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: resolve6, reject, timeout });
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((resolve6, reject) => {
6008
+ return new Promise((resolve7, reject) => {
5930
6009
  this.queuedTurns.push({
5931
6010
  message,
5932
6011
  callbacks,
5933
- resolve: resolve6,
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((resolve6, reject) => {
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
- resolve6(String(out));
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((resolve6) => {
7556
- releaseLock = resolve6;
7634
+ this.configWriteLock = new Promise((resolve7) => {
7635
+ releaseLock = resolve7;
7557
7636
  });
7558
7637
  try {
7559
7638
  await previousLock;
@@ -8782,7 +8861,7 @@ var WorktreeManager = class _WorktreeManager {
8782
8861
  const lockContent = fs14.readFileSync(lockPath, "utf-8").trim();
8783
8862
  const lockPid = parseInt(lockContent, 10);
8784
8863
  if (!isNaN(lockPid) && this.isProcessRunning(lockPid)) {
8785
- await new Promise((resolve6) => setTimeout(resolve6, retryInterval));
8864
+ await new Promise((resolve7) => setTimeout(resolve7, retryInterval));
8786
8865
  continue;
8787
8866
  }
8788
8867
  } catch {
@@ -8796,7 +8875,7 @@ var WorktreeManager = class _WorktreeManager {
8796
8875
  } catch {
8797
8876
  continue;
8798
8877
  }
8799
- await new Promise((resolve6) => setTimeout(resolve6, retryInterval));
8878
+ await new Promise((resolve7) => setTimeout(resolve7, retryInterval));
8800
8879
  continue;
8801
8880
  }
8802
8881
  throw err;
@@ -9586,6 +9665,7 @@ function getInstallCommand(cwd) {
9586
9665
  var fs34 = __toESM(require("fs"));
9587
9666
  var http2 = __toESM(require("http"));
9588
9667
  var os15 = __toESM(require("os"));
9668
+ var import_child_process19 = require("child_process");
9589
9669
 
9590
9670
  // src/daemon/ipc-router.ts
9591
9671
  var os14 = __toESM(require("os"));
@@ -10319,7 +10399,7 @@ async function handleExec(command, projectPath) {
10319
10399
  env = {}
10320
10400
  } = command;
10321
10401
  const effectiveTimeout = Math.min(Math.max(timeout, 1e3), MAX_TIMEOUT);
10322
- return new Promise((resolve6) => {
10402
+ return new Promise((resolve7) => {
10323
10403
  let stdout = "";
10324
10404
  let stderr = "";
10325
10405
  let timedOut = false;
@@ -10327,7 +10407,7 @@ async function handleExec(command, projectPath) {
10327
10407
  const done = (result) => {
10328
10408
  if (resolved) return;
10329
10409
  resolved = true;
10330
- resolve6(result);
10410
+ resolve7(result);
10331
10411
  };
10332
10412
  try {
10333
10413
  const proc = (0, import_child_process11.spawn)(cmd, {
@@ -10431,18 +10511,18 @@ var import_core12 = __toESM(require_dist());
10431
10511
  // src/utils/port-check.ts
10432
10512
  var net2 = __toESM(require("net"));
10433
10513
  async function isPortInUse(port) {
10434
- return new Promise((resolve6) => {
10514
+ return new Promise((resolve7) => {
10435
10515
  const server = net2.createServer();
10436
10516
  server.once("error", (err) => {
10437
10517
  if (err.code === "EADDRINUSE") {
10438
- resolve6(true);
10518
+ resolve7(true);
10439
10519
  } else {
10440
- resolve6(false);
10520
+ resolve7(false);
10441
10521
  }
10442
10522
  });
10443
10523
  server.once("listening", () => {
10444
10524
  server.close();
10445
- resolve6(false);
10525
+ resolve7(false);
10446
10526
  });
10447
10527
  server.listen(port);
10448
10528
  });
@@ -10845,7 +10925,7 @@ var DevServerRegistry = class {
10845
10925
  return killed;
10846
10926
  }
10847
10927
  wait(ms) {
10848
- return new Promise((resolve6) => setTimeout(resolve6, ms));
10928
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
10849
10929
  }
10850
10930
  killWsServerOnPort(wsPort, worktreePath) {
10851
10931
  const killed = [];
@@ -11419,7 +11499,7 @@ var DevServerRunner = class extends import_events2.EventEmitter {
11419
11499
  return Math.min(delay, DEV_SERVER_CONSTANTS.MAX_RESTART_DELAY_MS);
11420
11500
  }
11421
11501
  async checkHealth(port) {
11422
- return new Promise((resolve6) => {
11502
+ return new Promise((resolve7) => {
11423
11503
  const req = http.request(
11424
11504
  {
11425
11505
  hostname: "localhost",
@@ -11428,18 +11508,18 @@ var DevServerRunner = class extends import_events2.EventEmitter {
11428
11508
  method: "HEAD",
11429
11509
  timeout: DEV_SERVER_CONSTANTS.HEALTH_CHECK_TIMEOUT_MS
11430
11510
  },
11431
- () => resolve6(true)
11511
+ () => resolve7(true)
11432
11512
  );
11433
- req.on("error", () => resolve6(false));
11513
+ req.on("error", () => resolve7(false));
11434
11514
  req.on("timeout", () => {
11435
11515
  req.destroy();
11436
- resolve6(false);
11516
+ resolve7(false);
11437
11517
  });
11438
11518
  req.end();
11439
11519
  });
11440
11520
  }
11441
11521
  async checkWsHealth(wsPort) {
11442
- return new Promise((resolve6) => {
11522
+ return new Promise((resolve7) => {
11443
11523
  const req = http.request(
11444
11524
  {
11445
11525
  hostname: "127.0.0.1",
@@ -11449,14 +11529,14 @@ var DevServerRunner = class extends import_events2.EventEmitter {
11449
11529
  timeout: DEV_SERVER_CONSTANTS.HEALTH_CHECK_TIMEOUT_MS
11450
11530
  },
11451
11531
  (res) => {
11452
- resolve6(res.statusCode === 200);
11532
+ resolve7(res.statusCode === 200);
11453
11533
  res.resume();
11454
11534
  }
11455
11535
  );
11456
- req.on("error", () => resolve6(false));
11536
+ req.on("error", () => resolve7(false));
11457
11537
  req.on("timeout", () => {
11458
11538
  req.destroy();
11459
- resolve6(false);
11539
+ resolve7(false);
11460
11540
  });
11461
11541
  req.end();
11462
11542
  });
@@ -11484,7 +11564,7 @@ var DevServerRunner = class extends import_events2.EventEmitter {
11484
11564
  return false;
11485
11565
  }
11486
11566
  wait(ms) {
11487
- return new Promise((resolve6) => setTimeout(resolve6, ms));
11567
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
11488
11568
  }
11489
11569
  getLogsDir() {
11490
11570
  const logsDir = path25.join((0, import_core12.getConfigDir)(), "logs");
@@ -11683,7 +11763,7 @@ var PreviewManager = class extends import_events3.EventEmitter {
11683
11763
  for (let attempt = 1; attempt <= MAX_TUNNEL_RETRIES; attempt++) {
11684
11764
  if (attempt > 1) {
11685
11765
  console.log(`[PreviewManager] Retrying tunnel for ${moduleUid} (attempt ${attempt}/${MAX_TUNNEL_RETRIES})...`);
11686
- await new Promise((resolve6) => setTimeout(resolve6, 2e3));
11766
+ await new Promise((resolve7) => setTimeout(resolve7, 2e3));
11687
11767
  }
11688
11768
  tunnelResult = await this.tunnel.startTunnel({
11689
11769
  moduleUid,
@@ -11815,7 +11895,7 @@ var PreviewManager = class extends import_events3.EventEmitter {
11815
11895
  }
11816
11896
  console.log(`[PreviewManager] Restarting preview for ${moduleUid}`);
11817
11897
  await this.stopPreview(moduleUid);
11818
- await new Promise((resolve6) => setTimeout(resolve6, 1e3));
11898
+ await new Promise((resolve7) => setTimeout(resolve7, 1e3));
11819
11899
  return this.startPreview({
11820
11900
  moduleUid,
11821
11901
  worktreePath: state.worktreePath,
@@ -12756,7 +12836,7 @@ async function killProcessOnPort(port) {
12756
12836
  } catch {
12757
12837
  }
12758
12838
  }
12759
- await new Promise((resolve6) => setTimeout(resolve6, 1e3));
12839
+ await new Promise((resolve7) => setTimeout(resolve7, 1e3));
12760
12840
  for (const pid of pids) {
12761
12841
  try {
12762
12842
  (0, import_child_process15.execSync)(`kill -0 ${pid} 2>/dev/null`, { encoding: "utf8" });
@@ -12765,7 +12845,7 @@ async function killProcessOnPort(port) {
12765
12845
  } catch {
12766
12846
  }
12767
12847
  }
12768
- await new Promise((resolve6) => setTimeout(resolve6, 500));
12848
+ await new Promise((resolve7) => setTimeout(resolve7, 500));
12769
12849
  const stillInUse = await isPortInUse(port);
12770
12850
  if (stillInUse) {
12771
12851
  console.error(`[DevServer] EP929: Port ${port} still in use after kill attempts`);
@@ -12785,7 +12865,7 @@ async function waitForPort(port, timeoutMs = 3e4) {
12785
12865
  if (await isPortInUse(port)) {
12786
12866
  return true;
12787
12867
  }
12788
- await new Promise((resolve6) => setTimeout(resolve6, checkInterval));
12868
+ await new Promise((resolve7) => setTimeout(resolve7, checkInterval));
12789
12869
  }
12790
12870
  return false;
12791
12871
  }
@@ -12857,7 +12937,7 @@ async function handleProcessExit(moduleUid, code, signal) {
12857
12937
  const delay = calculateRestartDelay(serverInfo.restartCount);
12858
12938
  console.log(`[DevServer] EP932: Restarting ${moduleUid} in ${delay}ms (attempt ${serverInfo.restartCount + 1}/${MAX_RESTART_ATTEMPTS})`);
12859
12939
  writeToLog(serverInfo.logFile || "", `Scheduling restart in ${delay}ms (attempt ${serverInfo.restartCount + 1})`, false);
12860
- await new Promise((resolve6) => setTimeout(resolve6, delay));
12940
+ await new Promise((resolve7) => setTimeout(resolve7, delay));
12861
12941
  if (!activeServers.has(moduleUid)) {
12862
12942
  console.log(`[DevServer] EP932: Server ${moduleUid} was removed during restart delay, aborting restart`);
12863
12943
  return;
@@ -12982,7 +13062,7 @@ async function stopDevServer(moduleUid) {
12982
13062
  writeToLog(serverInfo.logFile, `Stopping server (manual stop)`, false);
12983
13063
  }
12984
13064
  serverInfo.process.kill("SIGTERM");
12985
- await new Promise((resolve6) => setTimeout(resolve6, 2e3));
13065
+ await new Promise((resolve7) => setTimeout(resolve7, 2e3));
12986
13066
  if (!serverInfo.process.killed) {
12987
13067
  serverInfo.process.kill("SIGKILL");
12988
13068
  }
@@ -13000,7 +13080,7 @@ async function restartDevServer(moduleUid) {
13000
13080
  writeToLog(logFile, `Manual restart requested`, false);
13001
13081
  }
13002
13082
  await stopDevServer(moduleUid);
13003
- await new Promise((resolve6) => setTimeout(resolve6, 1e3));
13083
+ await new Promise((resolve7) => setTimeout(resolve7, 1e3));
13004
13084
  if (await isPortInUse(port)) {
13005
13085
  await killProcessOnPort(port);
13006
13086
  }
@@ -13062,7 +13142,6 @@ var IPCRouter = class {
13062
13142
  });
13063
13143
  this.ipcServer.on("add-project", async (params) => {
13064
13144
  const { projectId, projectPath } = params;
13065
- addProject(projectId, projectPath);
13066
13145
  const MAX_RETRIES = 3;
13067
13146
  const INITIAL_DELAY = 1e3;
13068
13147
  let lastError = "";
@@ -13075,6 +13154,7 @@ var IPCRouter = class {
13075
13154
  lastError = "Connection established but not healthy";
13076
13155
  return { success: false, connected: false, error: lastError };
13077
13156
  }
13157
+ addProject(projectId, projectPath);
13078
13158
  return { success: true, connected: true };
13079
13159
  } catch (error) {
13080
13160
  lastError = error instanceof Error ? error.message : String(error);
@@ -13082,7 +13162,7 @@ var IPCRouter = class {
13082
13162
  if (attempt < MAX_RETRIES) {
13083
13163
  const delay = INITIAL_DELAY * Math.pow(2, attempt - 1);
13084
13164
  console.log(`[Daemon] Retrying in ${delay / 1e3}s...`);
13085
- await new Promise((resolve6) => setTimeout(resolve6, delay));
13165
+ await new Promise((resolve7) => setTimeout(resolve7, delay));
13086
13166
  await this.host.disconnectProject(projectPath);
13087
13167
  }
13088
13168
  }
@@ -13522,6 +13602,8 @@ var UpdateManager = class _UpdateManager {
13522
13602
  const child = (0, import_child_process17.spawn)("node", [this.daemonEntryFile], {
13523
13603
  detached: true,
13524
13604
  stdio: ["ignore", logFd, logFd],
13605
+ cwd: configDir,
13606
+ // Keep daemon process context stable across restarts.
13525
13607
  env: { ...process.env, EPISODA_DAEMON_MODE: "1" }
13526
13608
  });
13527
13609
  if (!child.pid) {
@@ -14234,45 +14316,54 @@ var DaemonCore = class {
14234
14316
  // src/daemon/connection-manager.ts
14235
14317
  var ConnectionManager = class {
14236
14318
  constructor() {
14319
+ // Single source-of-truth for project connectivity.
14237
14320
  this.connections = /* @__PURE__ */ new Map();
14238
- // projectPath -> connection
14239
- this.liveConnections = /* @__PURE__ */ new Set();
14240
- // projectPath
14241
- this.pendingConnections = /* @__PURE__ */ new Set();
14242
14321
  }
14243
- // projectPath
14322
+ // projectPath -> connection
14244
14323
  hasConnection(projectPath) {
14245
14324
  return this.connections.has(projectPath);
14246
14325
  }
14247
14326
  hasLiveConnection(projectPath) {
14248
- return this.liveConnections.has(projectPath);
14327
+ return this.getState(projectPath) === "connected";
14249
14328
  }
14250
14329
  hasPendingConnection(projectPath) {
14251
- return this.pendingConnections.has(projectPath);
14330
+ const state = this.getState(projectPath);
14331
+ return state === "connecting" || state === "authenticating";
14252
14332
  }
14253
14333
  isConnectionHealthy(projectPath) {
14254
- return this.connections.has(projectPath) && this.liveConnections.has(projectPath);
14334
+ return this.getState(projectPath) === "connected" && this.isWebSocketOpen(projectPath);
14255
14335
  }
14256
14336
  getConnection(projectPath) {
14257
14337
  return this.connections.get(projectPath);
14258
14338
  }
14259
14339
  setConnection(projectPath, connection) {
14260
- this.connections.set(projectPath, connection);
14340
+ const stateful = {
14341
+ ...connection,
14342
+ state: connection.state || "connecting",
14343
+ lastTransitionAt: connection.lastTransitionAt || Date.now()
14344
+ };
14345
+ this.connections.set(projectPath, stateful);
14261
14346
  }
14262
14347
  deleteConnection(projectPath) {
14263
14348
  this.connections.delete(projectPath);
14264
14349
  }
14265
14350
  addLiveConnection(projectPath) {
14266
- this.liveConnections.add(projectPath);
14351
+ this.setState(projectPath, "connected");
14267
14352
  }
14268
14353
  removeLiveConnection(projectPath) {
14269
- this.liveConnections.delete(projectPath);
14354
+ const state = this.getState(projectPath);
14355
+ if (state === "connected") {
14356
+ this.setState(projectPath, "disconnected");
14357
+ }
14270
14358
  }
14271
14359
  addPendingConnection(projectPath) {
14272
- this.pendingConnections.add(projectPath);
14360
+ this.setState(projectPath, "connecting");
14273
14361
  }
14274
14362
  removePendingConnection(projectPath) {
14275
- this.pendingConnections.delete(projectPath);
14363
+ const state = this.getState(projectPath);
14364
+ if (state === "connecting" || state === "authenticating") {
14365
+ this.setState(projectPath, "disconnected");
14366
+ }
14276
14367
  }
14277
14368
  isWebSocketOpen(projectPath) {
14278
14369
  const connection = this.connections.get(projectPath);
@@ -14283,10 +14374,18 @@ var ConnectionManager = class {
14283
14374
  return this.connections.size;
14284
14375
  }
14285
14376
  liveConnectionCount() {
14286
- return this.liveConnections.size;
14377
+ let count = 0;
14378
+ for (const connection of this.connections.values()) {
14379
+ if (connection.state === "connected") count++;
14380
+ }
14381
+ return count;
14287
14382
  }
14288
14383
  pendingConnectionCount() {
14289
- return this.pendingConnections.size;
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;
14290
14389
  }
14291
14390
  entries() {
14292
14391
  return this.connections.entries();
@@ -14295,12 +14394,24 @@ var ConnectionManager = class {
14295
14394
  this.connections.clear();
14296
14395
  }
14297
14396
  clearLiveConnections() {
14298
- this.liveConnections.clear();
14397
+ for (const [projectPath, connection] of this.connections.entries()) {
14398
+ if (connection.state === "connected") {
14399
+ this.setState(projectPath, "disconnected");
14400
+ }
14401
+ }
14299
14402
  }
14300
14403
  clearPendingConnections() {
14301
- this.pendingConnections.clear();
14404
+ for (const [projectPath, connection] of this.connections.entries()) {
14405
+ if (connection.state === "connecting" || connection.state === "authenticating") {
14406
+ this.setState(projectPath, "disconnected");
14407
+ }
14408
+ }
14302
14409
  }
14303
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
+ }
14304
14415
  const projects = getAllProjects();
14305
14416
  for (const project of projects) {
14306
14417
  try {
@@ -14310,19 +14421,31 @@ var ConnectionManager = class {
14310
14421
  }
14311
14422
  }
14312
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
+ }
14313
14434
  /**
14314
- * Wait for auth_success or auth_error after a successful WebSocket connect().
14435
+ * Wait for auth_success or fail-fast auth/transport errors after connect().
14315
14436
  *
14316
14437
  * This keeps auth timeout lifecycle in the connection subsystem and ensures
14317
14438
  * handlers are removed deterministically on every exit path.
14318
14439
  */
14319
14440
  async waitForAuthentication(client, timeoutMs = 3e4) {
14320
- await new Promise((resolve6, reject) => {
14441
+ await new Promise((resolve7, reject) => {
14321
14442
  let settled = false;
14322
14443
  const cleanup = () => {
14323
14444
  clearTimeout(timeout);
14324
14445
  client.off("auth_success", authHandler);
14325
- client.off("auth_error", errorHandler);
14446
+ client.off("auth_error", authErrorHandler);
14447
+ client.off("error", serverErrorHandler);
14448
+ client.off("disconnected", disconnectedHandler);
14326
14449
  };
14327
14450
  const timeout = setTimeout(() => {
14328
14451
  if (settled) return;
@@ -14334,17 +14457,41 @@ var ConnectionManager = class {
14334
14457
  if (settled) return;
14335
14458
  settled = true;
14336
14459
  cleanup();
14337
- resolve6();
14460
+ resolve7();
14338
14461
  };
14339
- const errorHandler = (message) => {
14462
+ const authErrorHandler = (message) => {
14340
14463
  if (settled) return;
14341
14464
  settled = true;
14342
14465
  cleanup();
14343
14466
  const errorMsg = message;
14344
14467
  reject(new Error(errorMsg.message || "Authentication failed"));
14345
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
+ };
14346
14491
  client.on("auth_success", authHandler);
14347
- client.on("auth_error", errorHandler);
14492
+ client.on("auth_error", authErrorHandler);
14493
+ client.on("error", serverErrorHandler);
14494
+ client.on("disconnected", disconnectedHandler);
14348
14495
  });
14349
14496
  }
14350
14497
  };
@@ -14911,6 +15058,8 @@ var Daemon = class _Daemon {
14911
15058
  // EP1003: Prevents race conditions between server-orchestrated tunnel commands
14912
15059
  this.tunnelOperationLocks = /* @__PURE__ */ new Map();
14913
15060
  // moduleUid -> operation promise
15061
+ // EP1426: Single in-flight connect attempt per project path.
15062
+ this.connectProjectInFlight = /* @__PURE__ */ new Map();
14914
15063
  // EP1210-7: Health HTTP endpoint for external monitoring
14915
15064
  this.healthServer = null;
14916
15065
  // EP1267: Cloud disconnect watchdog
@@ -14948,6 +15097,14 @@ var Daemon = class _Daemon {
14948
15097
  this.agentEventSeq.set(sessionId, next);
14949
15098
  return next;
14950
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
+ }
14951
15108
  /**
14952
15109
  * Start the daemon
14953
15110
  */
@@ -15144,8 +15301,8 @@ var Daemon = class _Daemon {
15144
15301
  }
15145
15302
  }
15146
15303
  let releaseLock;
15147
- const lockPromise = new Promise((resolve6) => {
15148
- releaseLock = resolve6;
15304
+ const lockPromise = new Promise((resolve7) => {
15305
+ releaseLock = resolve7;
15149
15306
  });
15150
15307
  this.tunnelOperationLocks.set(moduleUid, lockPromise);
15151
15308
  try {
@@ -15162,6 +15319,25 @@ var Daemon = class _Daemon {
15162
15319
  * EP1366: Made public for IPCRouterHost interface
15163
15320
  */
15164
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();
15165
15341
  if (this.connectionManager.hasConnection(projectPath)) {
15166
15342
  if (this.connectionManager.hasLiveConnection(projectPath)) {
15167
15343
  console.log(`[Daemon] Already connected to ${projectPath}`);
@@ -15172,7 +15348,7 @@ var Daemon = class _Daemon {
15172
15348
  const maxWait = 35e3;
15173
15349
  const startTime = Date.now();
15174
15350
  while (this.connectionManager.hasPendingConnection(projectPath) && Date.now() - startTime < maxWait) {
15175
- await new Promise((resolve6) => setTimeout(resolve6, 500));
15351
+ await new Promise((resolve7) => setTimeout(resolve7, 500));
15176
15352
  }
15177
15353
  if (this.connectionManager.hasLiveConnection(projectPath)) {
15178
15354
  console.log(`[Daemon] Pending connection succeeded for ${projectPath}`);
@@ -15183,6 +15359,12 @@ var Daemon = class _Daemon {
15183
15359
  console.warn(`[Daemon] Stale connection detected for ${projectPath}, forcing reconnection`);
15184
15360
  await this.disconnectProject(projectPath);
15185
15361
  }
15362
+ this.logReliabilityMetric("ws_connect_attempt", {
15363
+ projectId,
15364
+ projectPath,
15365
+ reconnect: isReconnectAttempt,
15366
+ previousState: initialState
15367
+ });
15186
15368
  const config = await (0, import_core22.loadConfig)();
15187
15369
  if (!config || !config.access_token) {
15188
15370
  throw new Error("No access token found. Please run: episoda auth");
@@ -15197,12 +15379,15 @@ var Daemon = class _Daemon {
15197
15379
  }
15198
15380
  console.log(`[Daemon] Connecting to ${wsEndpoint.wsUrl} for project ${projectId} (source: ${wsEndpoint.source})...`);
15199
15381
  const client = new import_core22.EpisodaClient();
15382
+ client.setAutoReconnect(false);
15200
15383
  const gitExecutor = new import_core22.GitExecutor();
15201
15384
  const connection = {
15202
15385
  projectId,
15203
15386
  projectPath,
15204
15387
  client,
15205
- gitExecutor
15388
+ gitExecutor,
15389
+ state: "connecting",
15390
+ lastTransitionAt: Date.now()
15206
15391
  };
15207
15392
  this.connectionManager.setConnection(projectPath, connection);
15208
15393
  this.connectionManager.addPendingConnection(projectPath);
@@ -15540,6 +15725,7 @@ var Daemon = class _Daemon {
15540
15725
  }
15541
15726
  });
15542
15727
  client.on("auth_success", async (message) => {
15728
+ client.setAutoReconnect(true);
15543
15729
  console.log(`[Daemon] Authenticated for project ${projectId}`);
15544
15730
  touchProject(projectPath);
15545
15731
  this.connectionManager.addLiveConnection(projectPath);
@@ -15653,9 +15839,59 @@ var Daemon = class _Daemon {
15653
15839
  console.error("[Daemon] EP1237: Error handling agent_reconciliation_commands:", error instanceof Error ? error.message : error);
15654
15840
  }
15655
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
+ });
15656
15885
  client.on("disconnected", (event) => {
15657
15886
  const disconnectEvent = event;
15658
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
+ });
15659
15895
  this.connectionManager.removeLiveConnection(projectPath);
15660
15896
  if (this.connectionManager.liveConnectionCount() === 0) {
15661
15897
  this.lastDisconnectAt = this.lastDisconnectAt || Date.now();
@@ -15693,10 +15929,36 @@ var Daemon = class _Daemon {
15693
15929
  containerId
15694
15930
  });
15695
15931
  console.log(`[Daemon] Successfully connected to project ${projectId}`);
15932
+ this.connectionManager.setState(projectPath, "authenticating");
15696
15933
  await this.connectionManager.waitForAuthentication(client, 3e4);
15697
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
+ });
15698
15942
  } catch (error) {
15699
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
+ );
15700
15962
  this.connectionManager.deleteConnection(projectPath);
15701
15963
  this.connectionManager.removePendingConnection(projectPath);
15702
15964
  throw error;
@@ -16128,14 +16390,13 @@ var Daemon = class _Daemon {
16128
16390
  console.log(`[Daemon] EP1002: ${installCmd.description} (detected from ${installCmd.detectedFrom})`);
16129
16391
  console.log(`[Daemon] EP1002: Running: ${installCmd.command.join(" ")}`);
16130
16392
  try {
16131
- const { execSync: execSync11 } = await import("child_process");
16132
- execSync11(installCmd.command.join(" "), {
16133
- cwd: worktreePath,
16134
- stdio: "inherit",
16135
- timeout: 10 * 60 * 1e3,
16136
- // 10 minute timeout
16137
- env: { ...process.env, CI: "true" }
16138
- });
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
+ );
16139
16400
  installSucceeded = true;
16140
16401
  console.log(`[Daemon] EP1002: Dependencies installed successfully`);
16141
16402
  } catch (installError) {
@@ -16151,13 +16412,12 @@ var Daemon = class _Daemon {
16151
16412
  const bootstrapCmd = buildCmd;
16152
16413
  console.log(`[Daemon] EP1386: Bootstrapping packages after dependency install (${bootstrapCmd})`);
16153
16414
  try {
16154
- const { execSync: execSync11 } = await import("child_process");
16155
- execSync11(bootstrapCmd, {
16156
- cwd: worktreePath,
16157
- stdio: "inherit",
16158
- timeout: 10 * 60 * 1e3,
16159
- env: { ...process.env, CI: "true" }
16160
- });
16415
+ await this.runShellForegroundCommand(
16416
+ bootstrapCmd,
16417
+ worktreePath,
16418
+ { ...process.env, CI: "true" },
16419
+ 10 * 60 * 1e3
16420
+ );
16161
16421
  console.log("[Daemon] EP1386: Package bootstrap completed");
16162
16422
  } catch (buildError) {
16163
16423
  const errorMsg = buildError instanceof Error ? buildError.message : String(buildError);
@@ -16177,6 +16437,74 @@ var Daemon = class _Daemon {
16177
16437
  await this.updateModuleWorktreeStatus(moduleUid, "ready", worktreePath);
16178
16438
  console.log(`[Daemon] EP1002: Worktree setup complete for ${moduleUid}`);
16179
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
+ }
16180
16508
  // EP1003: startTunnelForModule removed - server now orchestrates via tunnel_start commands
16181
16509
  // EP1003: autoStartTunnelsForProject removed - server now orchestrates via reconciliation
16182
16510
  // Recovery flow: daemon sends reconciliation_report → server processes and sends commands