episoda 0.2.115 → 0.2.118

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.
@@ -2815,7 +2815,7 @@ var require_package = __commonJS({
2815
2815
  "package.json"(exports2, module2) {
2816
2816
  module2.exports = {
2817
2817
  name: "episoda",
2818
- version: "0.2.114",
2818
+ version: "0.2.117",
2819
2819
  description: "CLI tool for Episoda local development workflow orchestration",
2820
2820
  main: "dist/index.js",
2821
2821
  types: "dist/index.d.ts",
@@ -8939,9 +8939,19 @@ If changes are needed, explain what needs to be done.`;
8939
8939
  session.claudeSessionId = resumeSessionId;
8940
8940
  }
8941
8941
  const mcpServersToRegister = this.getMcpServersForSession(session);
8942
- for (const mcpServer of mcpServersToRegister) {
8943
- args.push("--mcp", ...mcpServer.command.split(" "));
8944
- console.log(`[AgentManager] EP1233: Registering MCP server: ${mcpServer.name}`);
8942
+ if (mcpServersToRegister.length > 0) {
8943
+ const mcpConfig = {};
8944
+ for (const mcpServer of mcpServersToRegister) {
8945
+ const parts = mcpServer.command.split(" ");
8946
+ mcpConfig[mcpServer.name] = {
8947
+ command: parts[0],
8948
+ args: parts.slice(1)
8949
+ };
8950
+ console.log(`[AgentManager] EP1233: Registering MCP server: ${mcpServer.name}`);
8951
+ }
8952
+ const mcpConfigJson = JSON.stringify({ mcpServers: mcpConfig });
8953
+ args.push("--mcp-config", mcpConfigJson);
8954
+ console.log(`[AgentManager] EP1233: MCP config: ${mcpConfigJson}`);
8945
8955
  }
8946
8956
  args.push("--", message);
8947
8957
  }
@@ -9323,6 +9333,48 @@ If changes are needed, explain what needs to be done.`;
9323
9333
  getAllSessions() {
9324
9334
  return Array.from(this.sessions.values());
9325
9335
  }
9336
+ /**
9337
+ * EP1237: Get all active sessions with their current status
9338
+ * Used for reconciliation after WebSocket reconnection.
9339
+ *
9340
+ * Returns session info needed for server to compare with DB state:
9341
+ * - sessionId: Local session identifier
9342
+ * - moduleId: Module UUID for DB lookup
9343
+ * - moduleUid: Module UID for logging
9344
+ * - status: Current local status
9345
+ * - agentSessionId: Provider's session ID for resume
9346
+ * - lastActivityAt: For staleness detection
9347
+ */
9348
+ getActiveSessionsForReconciliation() {
9349
+ const result = [];
9350
+ for (const [sessionId, session] of this.sessions) {
9351
+ let reconStatus;
9352
+ switch (session.status) {
9353
+ case "starting":
9354
+ case "running":
9355
+ reconStatus = "running";
9356
+ break;
9357
+ case "stopping":
9358
+ case "stopped":
9359
+ reconStatus = "idle";
9360
+ break;
9361
+ case "error":
9362
+ reconStatus = "error";
9363
+ break;
9364
+ default:
9365
+ reconStatus = "idle";
9366
+ }
9367
+ result.push({
9368
+ sessionId,
9369
+ moduleId: session.moduleId,
9370
+ moduleUid: session.moduleUid,
9371
+ status: reconStatus,
9372
+ agentSessionId: session.agentSessionId || session.claudeSessionId,
9373
+ lastActivityAt: session.lastActivityAt
9374
+ });
9375
+ }
9376
+ return result;
9377
+ }
9326
9378
  /**
9327
9379
  * Check if a session exists
9328
9380
  */
@@ -9386,6 +9438,208 @@ If changes are needed, explain what needs to be done.`;
9386
9438
  }
9387
9439
  };
9388
9440
 
9441
+ // src/agent/agent-command-queue.ts
9442
+ var instance4 = null;
9443
+ function getAgentCommandQueue() {
9444
+ if (!instance4) {
9445
+ instance4 = new AgentCommandQueue();
9446
+ }
9447
+ return instance4;
9448
+ }
9449
+ var AgentCommandQueue = class {
9450
+ constructor() {
9451
+ /** Map of sessionId -> PendingCommand */
9452
+ this.pendingCommands = /* @__PURE__ */ new Map();
9453
+ /** Maximum retry attempts before giving up */
9454
+ this.maxRetries = 3;
9455
+ /** Command timeout in ms (commands older than this are discarded) */
9456
+ this.commandTimeout = 6e4;
9457
+ // 1 minute
9458
+ /** Cleanup interval handle */
9459
+ this.cleanupInterval = null;
9460
+ this.cleanupInterval = setInterval(() => {
9461
+ this.cleanup();
9462
+ }, 3e4);
9463
+ }
9464
+ /**
9465
+ * Add a command to the queue before processing.
9466
+ * If a command for this session already exists, it's replaced.
9467
+ *
9468
+ * @param sessionId - Agent session ID
9469
+ * @param command - The agent command to execute
9470
+ * @param commandId - WebSocket message ID for response routing
9471
+ */
9472
+ enqueue(sessionId, command, commandId) {
9473
+ const existing = this.pendingCommands.get(sessionId);
9474
+ if (existing) {
9475
+ console.log(`[AgentCommandQueue] EP1237: Replacing existing command for session ${sessionId}`);
9476
+ }
9477
+ this.pendingCommands.set(sessionId, {
9478
+ command,
9479
+ commandId,
9480
+ receivedAt: Date.now(),
9481
+ retryCount: 0,
9482
+ state: "pending"
9483
+ });
9484
+ console.log(`[AgentCommandQueue] EP1237: Enqueued command for session ${sessionId} (action: ${command.action})`);
9485
+ }
9486
+ /**
9487
+ * Mark a command as being processed.
9488
+ * This prevents duplicate processing attempts.
9489
+ *
9490
+ * @param sessionId - Agent session ID
9491
+ */
9492
+ markProcessing(sessionId) {
9493
+ const pending = this.pendingCommands.get(sessionId);
9494
+ if (pending) {
9495
+ pending.state = "processing";
9496
+ console.log(`[AgentCommandQueue] EP1237: Marked session ${sessionId} as processing`);
9497
+ }
9498
+ }
9499
+ /**
9500
+ * Remove a command from the queue after successful completion.
9501
+ *
9502
+ * @param sessionId - Agent session ID
9503
+ */
9504
+ complete(sessionId) {
9505
+ const pending = this.pendingCommands.get(sessionId);
9506
+ if (pending) {
9507
+ console.log(`[AgentCommandQueue] EP1237: Completed command for session ${sessionId}`);
9508
+ this.pendingCommands.delete(sessionId);
9509
+ }
9510
+ }
9511
+ /**
9512
+ * Mark a command as failed (will not be retried).
9513
+ *
9514
+ * @param sessionId - Agent session ID
9515
+ * @param error - Error message for logging
9516
+ */
9517
+ fail(sessionId, error) {
9518
+ const pending = this.pendingCommands.get(sessionId);
9519
+ if (pending) {
9520
+ pending.state = "failed";
9521
+ console.log(`[AgentCommandQueue] EP1237: Failed command for session ${sessionId}: ${error}`);
9522
+ this.pendingCommands.delete(sessionId);
9523
+ }
9524
+ }
9525
+ /**
9526
+ * Check if a session has a pending command.
9527
+ *
9528
+ * @param sessionId - Agent session ID
9529
+ * @returns True if there's a pending or processing command
9530
+ */
9531
+ has(sessionId) {
9532
+ return this.pendingCommands.has(sessionId);
9533
+ }
9534
+ /**
9535
+ * Get a pending command by session ID.
9536
+ *
9537
+ * @param sessionId - Agent session ID
9538
+ * @returns The pending command or undefined
9539
+ */
9540
+ get(sessionId) {
9541
+ return this.pendingCommands.get(sessionId);
9542
+ }
9543
+ /**
9544
+ * Get all commands that need retry after reconnect.
9545
+ * Returns commands that are:
9546
+ * - In 'pending' or 'processing' state (processing may have failed mid-way)
9547
+ * - Within the retry limit
9548
+ * - Not timed out
9549
+ *
9550
+ * @returns Array of pending commands eligible for retry
9551
+ */
9552
+ getPendingCommands() {
9553
+ const now = Date.now();
9554
+ const retryable = [];
9555
+ for (const [sessionId, pending] of this.pendingCommands) {
9556
+ if (pending.state === "completed" || pending.state === "failed") {
9557
+ continue;
9558
+ }
9559
+ if (now - pending.receivedAt > this.commandTimeout) {
9560
+ console.log(`[AgentCommandQueue] EP1237: Command for session ${sessionId} timed out`);
9561
+ this.pendingCommands.delete(sessionId);
9562
+ continue;
9563
+ }
9564
+ if (pending.retryCount >= this.maxRetries) {
9565
+ console.log(`[AgentCommandQueue] EP1237: Command for session ${sessionId} exceeded max retries`);
9566
+ continue;
9567
+ }
9568
+ retryable.push(pending);
9569
+ }
9570
+ return retryable;
9571
+ }
9572
+ /**
9573
+ * Increment the retry count for a command.
9574
+ * Call this before retrying a command.
9575
+ *
9576
+ * @param sessionId - Agent session ID
9577
+ */
9578
+ incrementRetryCount(sessionId) {
9579
+ const pending = this.pendingCommands.get(sessionId);
9580
+ if (pending) {
9581
+ pending.retryCount++;
9582
+ pending.state = "pending";
9583
+ console.log(`[AgentCommandQueue] EP1237: Retry ${pending.retryCount}/${this.maxRetries} for session ${sessionId}`);
9584
+ }
9585
+ }
9586
+ /**
9587
+ * Get commands that have exceeded the retry limit.
9588
+ * These should be reported as permanently failed.
9589
+ *
9590
+ * @returns Array of commands that exceeded retry limit
9591
+ */
9592
+ getFailedCommands() {
9593
+ const failed = [];
9594
+ for (const pending of this.pendingCommands.values()) {
9595
+ if (pending.retryCount >= this.maxRetries && pending.state !== "completed" && pending.state !== "failed") {
9596
+ failed.push(pending);
9597
+ }
9598
+ }
9599
+ return failed;
9600
+ }
9601
+ /**
9602
+ * Clean up stale commands (older than timeout).
9603
+ * Called periodically and on reconnect.
9604
+ */
9605
+ cleanup() {
9606
+ const now = Date.now();
9607
+ let cleaned = 0;
9608
+ for (const [sessionId, pending] of this.pendingCommands) {
9609
+ if (now - pending.receivedAt > this.commandTimeout) {
9610
+ this.pendingCommands.delete(sessionId);
9611
+ cleaned++;
9612
+ }
9613
+ }
9614
+ if (cleaned > 0) {
9615
+ console.log(`[AgentCommandQueue] EP1237: Cleaned up ${cleaned} stale command(s)`);
9616
+ }
9617
+ }
9618
+ /**
9619
+ * Get the maximum number of retries.
9620
+ */
9621
+ getMaxRetries() {
9622
+ return this.maxRetries;
9623
+ }
9624
+ /**
9625
+ * Get the current queue size.
9626
+ */
9627
+ size() {
9628
+ return this.pendingCommands.size;
9629
+ }
9630
+ /**
9631
+ * Shutdown the queue (stop cleanup interval).
9632
+ * Call this when the daemon is shutting down.
9633
+ */
9634
+ shutdown() {
9635
+ if (this.cleanupInterval) {
9636
+ clearInterval(this.cleanupInterval);
9637
+ this.cleanupInterval = null;
9638
+ }
9639
+ console.log(`[AgentCommandQueue] EP1237: Shutdown with ${this.pendingCommands.size} pending command(s)`);
9640
+ }
9641
+ };
9642
+
9389
9643
  // src/utils/dev-server.ts
9390
9644
  var import_child_process13 = require("child_process");
9391
9645
  var import_core11 = __toESM(require_dist());
@@ -10697,6 +10951,11 @@ var Daemon = class _Daemon {
10697
10951
  const cmd = message.command;
10698
10952
  console.log(`[Daemon] EP912: Received agent command for ${projectId}:`, cmd.action);
10699
10953
  client.updateActivity();
10954
+ const commandQueue = getAgentCommandQueue();
10955
+ if (cmd.action === "start" || cmd.action === "message") {
10956
+ commandQueue.enqueue(cmd.sessionId, cmd, message.id);
10957
+ commandQueue.markProcessing(cmd.sessionId);
10958
+ }
10700
10959
  let daemonChunkCount = 0;
10701
10960
  let daemonToolUseCount = 0;
10702
10961
  const daemonStreamStart = Date.now();
@@ -10738,6 +10997,7 @@ var Daemon = class _Daemon {
10738
10997
  onComplete: async (claudeSessionId) => {
10739
10998
  const duration = Date.now() - daemonStreamStart;
10740
10999
  console.log(`[Daemon] EP1191: Stream complete - ${daemonChunkCount} chunks, ${daemonToolUseCount} tool uses forwarded in ${duration}ms`);
11000
+ commandQueue.complete(sessionId);
10741
11001
  try {
10742
11002
  await client.send({
10743
11003
  type: "agent_result",
@@ -10751,6 +11011,7 @@ var Daemon = class _Daemon {
10751
11011
  onError: async (error) => {
10752
11012
  const duration = Date.now() - daemonStreamStart;
10753
11013
  console.log(`[Daemon] EP1191: Stream error after ${daemonChunkCount} chunks in ${duration}ms - ${error}`);
11014
+ commandQueue.complete(sessionId);
10754
11015
  try {
10755
11016
  await client.send({
10756
11017
  type: "agent_result",
@@ -10869,6 +11130,8 @@ var Daemon = class _Daemon {
10869
11130
  });
10870
11131
  console.log(`[Daemon] EP912: Agent command ${cmd.action} completed for session ${cmd.action === "start" || cmd.action === "message" ? cmd.sessionId : cmd.sessionId}`);
10871
11132
  } catch (error) {
11133
+ const sessionId = cmd.sessionId || "unknown";
11134
+ const errorMsg = error instanceof Error ? error.message : String(error);
10872
11135
  try {
10873
11136
  await client.send({
10874
11137
  type: "agent_result",
@@ -10876,12 +11139,13 @@ var Daemon = class _Daemon {
10876
11139
  result: {
10877
11140
  success: false,
10878
11141
  status: "error",
10879
- sessionId: cmd.sessionId || "unknown",
10880
- error: error instanceof Error ? error.message : String(error)
11142
+ sessionId,
11143
+ error: errorMsg
10881
11144
  }
10882
11145
  });
11146
+ commandQueue.complete(sessionId);
10883
11147
  } catch (sendError) {
10884
- console.error(`[Daemon] EP912: Failed to send error result (WebSocket may be disconnected):`, sendError);
11148
+ console.error(`[Daemon] EP1237: Failed to send error result (WebSocket disconnected), command will be retried on reconnect`);
10885
11149
  }
10886
11150
  console.error(`[Daemon] EP912: Agent command execution error:`, error);
10887
11151
  }
@@ -11011,6 +11275,9 @@ var Daemon = class _Daemon {
11011
11275
  this.reconcileWorktrees(projectId, projectPath, client).catch((err) => {
11012
11276
  console.warn("[Daemon] EP1003: Reconciliation report failed:", err.message);
11013
11277
  });
11278
+ this.runAgentReconciliation(projectId, projectPath, client).catch((err) => {
11279
+ console.warn("[Daemon] EP1237: Agent reconciliation failed:", err.message);
11280
+ });
11014
11281
  });
11015
11282
  client.on("module_state_changed", async (message) => {
11016
11283
  if (message.type === "module_state_changed") {
@@ -11064,6 +11331,22 @@ var Daemon = class _Daemon {
11064
11331
  console.error("[Daemon] EP1095: Error handling machine_uuid_update:", error instanceof Error ? error.message : error);
11065
11332
  }
11066
11333
  });
11334
+ client.on("agent_reconciliation_commands", async (message) => {
11335
+ try {
11336
+ const cmdMsg = message;
11337
+ console.log(`[Daemon] EP1237: Received agent reconciliation commands - abort: ${cmdMsg.abort.length}, restart: ${cmdMsg.restart.length}`);
11338
+ const agentManager = getAgentManager();
11339
+ for (const sessionId of cmdMsg.abort) {
11340
+ console.log(`[Daemon] EP1237: Aborting orphaned session ${sessionId}`);
11341
+ await agentManager.abortSession(sessionId);
11342
+ }
11343
+ if (cmdMsg.restart.length > 0) {
11344
+ console.log(`[Daemon] EP1237: Server will send agent_command for ${cmdMsg.restart.length} session(s) that need restart`);
11345
+ }
11346
+ } catch (error) {
11347
+ console.error("[Daemon] EP1237: Error handling agent_reconciliation_commands:", error instanceof Error ? error.message : error);
11348
+ }
11349
+ });
11067
11350
  client.on("disconnected", (event) => {
11068
11351
  const disconnectEvent = event;
11069
11352
  console.log(`[Daemon] Connection closed for ${projectId}: code=${disconnectEvent.code}, willReconnect=${disconnectEvent.willReconnect}`);
@@ -11545,6 +11828,121 @@ var Daemon = class _Daemon {
11545
11828
  // EP1025: Removed cleanupModuleWorktree - was dead code (never called).
11546
11829
  // EP1188: Background ops queue removed - worktree cleanup now handled by Inngest events
11547
11830
  // (worktree/cleanup) which send WebSocket commands to connected daemons via ws-proxy.
11831
+ /**
11832
+ * EP1237: Run agent reconciliation steps sequentially.
11833
+ *
11834
+ * This orchestrates the two reconciliation steps in order:
11835
+ * 1. Report pending commands (updates retry counts, reports failures)
11836
+ * 2. Reconcile sessions (reports active sessions to server)
11837
+ *
11838
+ * Running these sequentially avoids race conditions where retry logic
11839
+ * might modify state while reconciliation is reading it.
11840
+ */
11841
+ async runAgentReconciliation(projectId, projectPath, client) {
11842
+ console.log("[Daemon] EP1237: Running agent reconciliation sequence");
11843
+ try {
11844
+ await this.reportPendingAgentCommands(projectId, projectPath, client);
11845
+ } catch (err) {
11846
+ console.warn("[Daemon] EP1237: Report pending commands failed:", err instanceof Error ? err.message : err);
11847
+ }
11848
+ try {
11849
+ await this.reconcileAgentSessions(projectId, client);
11850
+ } catch (err) {
11851
+ console.warn("[Daemon] EP1237: Reconcile sessions failed:", err instanceof Error ? err.message : err);
11852
+ }
11853
+ console.log("[Daemon] EP1237: Agent reconciliation sequence complete");
11854
+ }
11855
+ /**
11856
+ * EP1237: Report pending agent commands after WebSocket reconnection.
11857
+ *
11858
+ * Commands that were processing when the WebSocket disconnected need attention.
11859
+ * We report their status via agent_reconciliation_report, and the server will
11860
+ * re-send commands for sessions that need to be restarted.
11861
+ *
11862
+ * This is safer than trying to re-process locally because:
11863
+ * 1. Credentials may have expired and server needs to refresh them
11864
+ * 2. Session state in DB needs to be consistent with our local state
11865
+ * 3. Server can make informed decisions about which sessions to restart
11866
+ */
11867
+ async reportPendingAgentCommands(projectId, projectPath, client) {
11868
+ const commandQueue = getAgentCommandQueue();
11869
+ const pendingCommands = commandQueue.getPendingCommands();
11870
+ const failedCommands = commandQueue.getFailedCommands();
11871
+ if (pendingCommands.length === 0 && failedCommands.length === 0) {
11872
+ console.log("[Daemon] EP1237: No pending agent commands");
11873
+ return;
11874
+ }
11875
+ console.log(`[Daemon] EP1237: Found ${pendingCommands.length} pending and ${failedCommands.length} failed command(s)`);
11876
+ for (const pending of failedCommands) {
11877
+ const cmd = pending.command;
11878
+ const sessionId = cmd.sessionId || "unknown";
11879
+ console.log(`[Daemon] EP1237: Reporting failed command for session ${sessionId} (exceeded max retries)`);
11880
+ try {
11881
+ await client.send({
11882
+ type: "agent_result",
11883
+ commandId: pending.commandId,
11884
+ result: {
11885
+ success: false,
11886
+ status: "error",
11887
+ sessionId,
11888
+ error: `Command failed after ${pending.retryCount} retry attempts during WebSocket reconnection`
11889
+ }
11890
+ });
11891
+ commandQueue.fail(sessionId, "Max retries exceeded");
11892
+ } catch (sendError) {
11893
+ console.error("[Daemon] EP1237: Failed to report max retry failure:", sendError);
11894
+ }
11895
+ }
11896
+ for (const pending of pendingCommands) {
11897
+ const cmd = pending.command;
11898
+ const sessionId = cmd.sessionId || "unknown";
11899
+ if (pending.retryCount >= commandQueue.getMaxRetries()) {
11900
+ console.log(`[Daemon] EP1237: Command for session ${sessionId} will exceed max retries on next attempt`);
11901
+ commandQueue.fail(sessionId, "Max retries exceeded");
11902
+ continue;
11903
+ }
11904
+ commandQueue.incrementRetryCount(sessionId);
11905
+ console.log(`[Daemon] EP1237: Command for session ${sessionId} ready for retry (attempt ${pending.retryCount + 1}/${commandQueue.getMaxRetries()})`);
11906
+ }
11907
+ }
11908
+ /**
11909
+ * EP1237: Reconcile agent sessions on connect/reconnect.
11910
+ *
11911
+ * Reports active local agent sessions to the server so it can compare with DB state.
11912
+ * Server responds with abort/restart commands to fix any mismatches.
11913
+ */
11914
+ async reconcileAgentSessions(projectId, client) {
11915
+ console.log(`[Daemon] EP1237: Starting agent session reconciliation for project ${projectId}`);
11916
+ try {
11917
+ if (!this.machineUuid) {
11918
+ console.log("[Daemon] EP1237: Cannot reconcile agent sessions - machineUuid not available yet");
11919
+ return;
11920
+ }
11921
+ const agentManager = getAgentManager();
11922
+ const activeSessions = agentManager.getActiveSessionsForReconciliation();
11923
+ console.log(`[Daemon] EP1237: Reporting ${activeSessions.length} active agent session(s)`);
11924
+ const report = {
11925
+ projectId,
11926
+ machineId: this.machineUuid,
11927
+ sessions: activeSessions.map((s) => ({
11928
+ sessionId: s.sessionId,
11929
+ moduleId: s.moduleId,
11930
+ moduleUid: s.moduleUid,
11931
+ status: s.status,
11932
+ agentSessionId: s.agentSessionId,
11933
+ lastActivityAt: s.lastActivityAt.toISOString()
11934
+ }))
11935
+ };
11936
+ await client.send({
11937
+ type: "agent_reconciliation_report",
11938
+ report
11939
+ });
11940
+ console.log("[Daemon] EP1237: Agent session reconciliation report sent - awaiting server commands");
11941
+ } catch (error) {
11942
+ console.error("[Daemon] EP1237: Agent session reconciliation error:", error instanceof Error ? error.message : error);
11943
+ throw error;
11944
+ }
11945
+ }
11548
11946
  /**
11549
11947
  * EP1002: Handle worktree_setup command from server
11550
11948
  * This provides a unified setup flow for both local and cloud environments.