episoda 0.2.104 → 0.2.105
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 +80 -2
- package/dist/daemon/daemon-process.js.map +1 -1
- package/dist/index.js +19 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -2117,6 +2117,7 @@ var require_websocket_client = __commonJS({
|
|
|
2117
2117
|
this.lastCommandTime = Date.now();
|
|
2118
2118
|
this.isIntentionalDisconnect = false;
|
|
2119
2119
|
this.lastConnectAttemptTime = 0;
|
|
2120
|
+
this.consecutiveAuthFailures = 0;
|
|
2120
2121
|
}
|
|
2121
2122
|
/**
|
|
2122
2123
|
* Connect to episoda.dev WebSocket gateway
|
|
@@ -2384,6 +2385,10 @@ var require_websocket_client = __commonJS({
|
|
|
2384
2385
|
this.rateLimitBackoffUntil = Date.now() + retryAfterMs;
|
|
2385
2386
|
console.log(`[EpisodaClient] ${errorMessage.code}: will retry after ${retryAfterMs / 1e3}s`);
|
|
2386
2387
|
}
|
|
2388
|
+
if (errorMessage.code === "AUTH_FAILED" || errorMessage.code === "UNAUTHORIZED" || errorMessage.code === "INVALID_TOKEN") {
|
|
2389
|
+
this.consecutiveAuthFailures++;
|
|
2390
|
+
console.warn(`[EpisodaClient] Auth failure (${this.consecutiveAuthFailures}): ${errorMessage.code}`);
|
|
2391
|
+
}
|
|
2387
2392
|
}
|
|
2388
2393
|
const handlers = this.eventHandlers.get(message.type) || [];
|
|
2389
2394
|
handlers.forEach((handler) => {
|
|
@@ -2442,7 +2447,19 @@ var require_websocket_client = __commonJS({
|
|
|
2442
2447
|
}
|
|
2443
2448
|
let delay;
|
|
2444
2449
|
let shouldRetry = true;
|
|
2445
|
-
|
|
2450
|
+
const isCloudMode = this.environment === "cloud";
|
|
2451
|
+
const MAX_CLOUD_AUTH_FAILURES = 3;
|
|
2452
|
+
const MAX_CLOUD_RECONNECT_DELAY = 3e5;
|
|
2453
|
+
if (isCloudMode) {
|
|
2454
|
+
if (this.consecutiveAuthFailures >= MAX_CLOUD_AUTH_FAILURES) {
|
|
2455
|
+
console.error(`[EpisodaClient] Cloud mode: ${MAX_CLOUD_AUTH_FAILURES} consecutive auth failures - token may be invalid. Giving up.`);
|
|
2456
|
+
shouldRetry = false;
|
|
2457
|
+
} else {
|
|
2458
|
+
delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), MAX_CLOUD_RECONNECT_DELAY);
|
|
2459
|
+
const delayStr = delay >= 6e4 ? `${Math.round(delay / 6e4)}m` : `${Math.round(delay / 1e3)}s`;
|
|
2460
|
+
console.log(`[EpisodaClient] Cloud mode: reconnecting in ${delayStr}... (attempt ${this.reconnectAttempts + 1}, never giving up)`);
|
|
2461
|
+
}
|
|
2462
|
+
} else if (this.isGracefulShutdown) {
|
|
2446
2463
|
if (this.reconnectAttempts >= 7) {
|
|
2447
2464
|
console.error('[EpisodaClient] Server restart reconnection failed after 7 attempts. Run "episoda dev" to reconnect.');
|
|
2448
2465
|
shouldRetry = false;
|
|
@@ -2485,6 +2502,7 @@ var require_websocket_client = __commonJS({
|
|
|
2485
2502
|
this.isGracefulShutdown = false;
|
|
2486
2503
|
this.firstDisconnectTime = void 0;
|
|
2487
2504
|
this.rateLimitBackoffUntil = void 0;
|
|
2505
|
+
this.consecutiveAuthFailures = 0;
|
|
2488
2506
|
}).catch((error) => {
|
|
2489
2507
|
console.error("[EpisodaClient] Reconnection failed:", error.message);
|
|
2490
2508
|
});
|
|
@@ -2786,7 +2804,7 @@ var require_package = __commonJS({
|
|
|
2786
2804
|
"package.json"(exports2, module2) {
|
|
2787
2805
|
module2.exports = {
|
|
2788
2806
|
name: "episoda",
|
|
2789
|
-
version: "0.2.
|
|
2807
|
+
version: "0.2.105",
|
|
2790
2808
|
description: "CLI tool for Episoda local development workflow orchestration",
|
|
2791
2809
|
main: "dist/index.js",
|
|
2792
2810
|
types: "dist/index.d.ts",
|
|
@@ -9466,6 +9484,7 @@ function getInstallCommand(cwd) {
|
|
|
9466
9484
|
|
|
9467
9485
|
// src/daemon/daemon-process.ts
|
|
9468
9486
|
var fs21 = __toESM(require("fs"));
|
|
9487
|
+
var http2 = __toESM(require("http"));
|
|
9469
9488
|
var os8 = __toESM(require("os"));
|
|
9470
9489
|
var path22 = __toESM(require("path"));
|
|
9471
9490
|
var packageJson = require_package();
|
|
@@ -9600,6 +9619,8 @@ var Daemon = class _Daemon {
|
|
|
9600
9619
|
// 60 seconds
|
|
9601
9620
|
// EP1190: Worktree cleanup runs every N health checks (5 * 60s = 5 minutes)
|
|
9602
9621
|
this.healthCheckCounter = 0;
|
|
9622
|
+
// EP1210-7: Health HTTP endpoint for external monitoring
|
|
9623
|
+
this.healthServer = null;
|
|
9603
9624
|
this.ipcServer = new IPCServer();
|
|
9604
9625
|
}
|
|
9605
9626
|
static {
|
|
@@ -9619,6 +9640,9 @@ var Daemon = class _Daemon {
|
|
|
9619
9640
|
static {
|
|
9620
9641
|
this.WORKTREE_CLEANUP_EVERY_N_CHECKS = 5;
|
|
9621
9642
|
}
|
|
9643
|
+
static {
|
|
9644
|
+
this.HEALTH_PORT = 9999;
|
|
9645
|
+
}
|
|
9622
9646
|
/**
|
|
9623
9647
|
* Start the daemon
|
|
9624
9648
|
*/
|
|
@@ -9640,6 +9664,7 @@ var Daemon = class _Daemon {
|
|
|
9640
9664
|
await this.auditWorktreesOnStartup();
|
|
9641
9665
|
this.startHealthCheckPolling();
|
|
9642
9666
|
this.setupShutdownHandlers();
|
|
9667
|
+
this.startHealthEndpoint();
|
|
9643
9668
|
console.log("[Daemon] Daemon started successfully");
|
|
9644
9669
|
const modeConfig = getDaemonModeConfig();
|
|
9645
9670
|
console.log("[Daemon] EP1115: Mode config:", {
|
|
@@ -9669,6 +9694,55 @@ var Daemon = class _Daemon {
|
|
|
9669
9694
|
}
|
|
9670
9695
|
}
|
|
9671
9696
|
// EP738: Removed startHttpServer - device info now flows through WebSocket broadcast + database
|
|
9697
|
+
/**
|
|
9698
|
+
* EP1210-7: Start health HTTP endpoint for external monitoring
|
|
9699
|
+
*
|
|
9700
|
+
* Provides a simple HTTP endpoint that external systems can use to check daemon status.
|
|
9701
|
+
* Returns 200 when daemon has at least one live connection, 503 when disconnected.
|
|
9702
|
+
*
|
|
9703
|
+
* Only binds to localhost (127.0.0.1) for security.
|
|
9704
|
+
*/
|
|
9705
|
+
startHealthEndpoint() {
|
|
9706
|
+
try {
|
|
9707
|
+
this.healthServer = http2.createServer((req, res) => {
|
|
9708
|
+
if (req.url === "/health" || req.url === "/") {
|
|
9709
|
+
const isConnected = this.liveConnections.size > 0;
|
|
9710
|
+
const projects = Array.from(this.connections.entries()).map(([path23, conn]) => ({
|
|
9711
|
+
path: path23,
|
|
9712
|
+
connected: this.liveConnections.has(path23)
|
|
9713
|
+
}));
|
|
9714
|
+
const status = {
|
|
9715
|
+
status: isConnected ? "healthy" : "degraded",
|
|
9716
|
+
connected: isConnected,
|
|
9717
|
+
machineId: this.machineId,
|
|
9718
|
+
uptime: process.uptime(),
|
|
9719
|
+
liveConnections: this.liveConnections.size,
|
|
9720
|
+
totalConnections: this.connections.size,
|
|
9721
|
+
projects
|
|
9722
|
+
};
|
|
9723
|
+
res.writeHead(isConnected ? 200 : 503, { "Content-Type": "application/json" });
|
|
9724
|
+
res.end(JSON.stringify(status));
|
|
9725
|
+
} else {
|
|
9726
|
+
res.writeHead(404);
|
|
9727
|
+
res.end("Not found");
|
|
9728
|
+
}
|
|
9729
|
+
});
|
|
9730
|
+
this.healthServer.listen(_Daemon.HEALTH_PORT, "127.0.0.1", () => {
|
|
9731
|
+
console.log(`[Daemon] EP1210-7: Health endpoint listening on http://127.0.0.1:${_Daemon.HEALTH_PORT}/health`);
|
|
9732
|
+
});
|
|
9733
|
+
this.healthServer.on("error", (err) => {
|
|
9734
|
+
if (err.code === "EADDRINUSE") {
|
|
9735
|
+
console.warn(`[Daemon] EP1210-7: Health port ${_Daemon.HEALTH_PORT} already in use, skipping health endpoint`);
|
|
9736
|
+
} else {
|
|
9737
|
+
console.warn("[Daemon] EP1210-7: Health endpoint failed to start:", err.message);
|
|
9738
|
+
}
|
|
9739
|
+
this.healthServer = null;
|
|
9740
|
+
});
|
|
9741
|
+
} catch (err) {
|
|
9742
|
+
console.warn("[Daemon] EP1210-7: Failed to create health server:", err.message);
|
|
9743
|
+
this.healthServer = null;
|
|
9744
|
+
}
|
|
9745
|
+
}
|
|
9672
9746
|
/**
|
|
9673
9747
|
* Register IPC command handlers
|
|
9674
9748
|
*/
|
|
@@ -11750,6 +11824,10 @@ var Daemon = class _Daemon {
|
|
|
11750
11824
|
if (this.shuttingDown) return;
|
|
11751
11825
|
this.shuttingDown = true;
|
|
11752
11826
|
console.log("[Daemon] Shutting down...");
|
|
11827
|
+
if (this.healthServer) {
|
|
11828
|
+
this.healthServer.close();
|
|
11829
|
+
this.healthServer = null;
|
|
11830
|
+
}
|
|
11753
11831
|
this.stopTunnelPolling();
|
|
11754
11832
|
this.stopHealthCheckPolling();
|
|
11755
11833
|
for (const [projectPath, connection] of this.connections) {
|