@episoda/cli 0.2.169 → 0.2.170

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.
@@ -2978,7 +2978,7 @@ var require_package = __commonJS({
2978
2978
  "package.json"(exports2, module2) {
2979
2979
  module2.exports = {
2980
2980
  name: "@episoda/cli",
2981
- version: "0.2.169",
2981
+ version: "0.2.170",
2982
2982
  description: "CLI tool for Episoda local development workflow orchestration",
2983
2983
  main: "dist/index.js",
2984
2984
  types: "dist/index.d.ts",
@@ -7928,10 +7928,12 @@ function getAgentCommandQueue() {
7928
7928
  }
7929
7929
  var AgentCommandQueue = class {
7930
7930
  constructor() {
7931
- /** Map of sessionId -> PendingCommand */
7931
+ /** Map of sessionId -> FIFO commands */
7932
7932
  this.pendingCommands = /* @__PURE__ */ new Map();
7933
7933
  /** Maximum retry attempts before giving up */
7934
7934
  this.maxRetries = 3;
7935
+ /** Bounded queue depth per session to avoid unbounded backlog */
7936
+ this.maxDepthPerSession = parseInt(process.env.AGENT_COMMAND_QUEUE_MAX_DEPTH || "8", 10);
7935
7937
  /** Command timeout in ms (commands older than this are discarded) */
7936
7938
  this.commandTimeout = 6e4;
7937
7939
  // 1 minute
@@ -7943,25 +7945,36 @@ var AgentCommandQueue = class {
7943
7945
  }
7944
7946
  /**
7945
7947
  * Add a command to the queue before processing.
7946
- * If a command for this session already exists, it's replaced.
7948
+ * Commands are tracked in FIFO order per session.
7947
7949
  *
7948
7950
  * @param sessionId - Agent session ID
7949
7951
  * @param command - The agent command to execute
7950
7952
  * @param commandId - WebSocket message ID for response routing
7951
7953
  */
7952
7954
  enqueue(sessionId, command, commandId) {
7953
- const existing = this.pendingCommands.get(sessionId);
7954
- if (existing) {
7955
- console.log(`[AgentCommandQueue] EP1237: Replacing existing command for session ${sessionId}`);
7955
+ const queue = this.pendingCommands.get(sessionId) || [];
7956
+ if (queue.length >= this.maxDepthPerSession) {
7957
+ console.warn(
7958
+ `[AgentCommandQueue] EP1398: Queue full for session ${sessionId} (${queue.length}/${this.maxDepthPerSession})`
7959
+ );
7960
+ return {
7961
+ enqueued: false,
7962
+ queueDepth: queue.length,
7963
+ reason: "QUEUE_DEPTH_EXCEEDED"
7964
+ };
7956
7965
  }
7957
- this.pendingCommands.set(sessionId, {
7966
+ queue.push({
7958
7967
  command,
7959
7968
  commandId,
7960
7969
  receivedAt: Date.now(),
7961
7970
  retryCount: 0,
7962
7971
  state: "pending"
7963
7972
  });
7964
- console.log(`[AgentCommandQueue] EP1237: Enqueued command for session ${sessionId} (action: ${command.action})`);
7973
+ this.pendingCommands.set(sessionId, queue);
7974
+ console.log(
7975
+ `[AgentCommandQueue] EP1237: Enqueued command for session ${sessionId} (action: ${command.action}, depth: ${queue.length})`
7976
+ );
7977
+ return { enqueued: true, queueDepth: queue.length };
7965
7978
  }
7966
7979
  /**
7967
7980
  * Mark a command as being processed.
@@ -7969,11 +7982,11 @@ var AgentCommandQueue = class {
7969
7982
  *
7970
7983
  * @param sessionId - Agent session ID
7971
7984
  */
7972
- markProcessing(sessionId) {
7973
- const pending = this.pendingCommands.get(sessionId);
7985
+ markProcessing(sessionId, commandId) {
7986
+ const pending = this.getCommand(sessionId, commandId);
7974
7987
  if (pending) {
7975
7988
  pending.state = "processing";
7976
- console.log(`[AgentCommandQueue] EP1237: Marked session ${sessionId} as processing`);
7989
+ console.log(`[AgentCommandQueue] EP1237: Marked session ${sessionId} command ${pending.commandId} as processing`);
7977
7990
  }
7978
7991
  }
7979
7992
  /**
@@ -7981,12 +7994,8 @@ var AgentCommandQueue = class {
7981
7994
  *
7982
7995
  * @param sessionId - Agent session ID
7983
7996
  */
7984
- complete(sessionId) {
7985
- const pending = this.pendingCommands.get(sessionId);
7986
- if (pending) {
7987
- console.log(`[AgentCommandQueue] EP1237: Completed command for session ${sessionId}`);
7988
- this.pendingCommands.delete(sessionId);
7989
- }
7997
+ complete(sessionId, commandId) {
7998
+ this.removeCommand(sessionId, commandId, "completed");
7990
7999
  }
7991
8000
  /**
7992
8001
  * Mark a command as failed (will not be retried).
@@ -7994,13 +8003,13 @@ var AgentCommandQueue = class {
7994
8003
  * @param sessionId - Agent session ID
7995
8004
  * @param error - Error message for logging
7996
8005
  */
7997
- fail(sessionId, error) {
7998
- const pending = this.pendingCommands.get(sessionId);
8006
+ fail(sessionId, error, commandId) {
8007
+ const pending = this.getCommand(sessionId, commandId);
7999
8008
  if (pending) {
8000
8009
  pending.state = "failed";
8001
- console.log(`[AgentCommandQueue] EP1237: Failed command for session ${sessionId}: ${error}`);
8002
- this.pendingCommands.delete(sessionId);
8010
+ console.log(`[AgentCommandQueue] EP1237: Failed command for session ${sessionId} (${pending.commandId}): ${error}`);
8003
8011
  }
8012
+ this.removeCommand(sessionId, commandId, "failed");
8004
8013
  }
8005
8014
  /**
8006
8015
  * Check if a session has a pending command.
@@ -8009,7 +8018,7 @@ var AgentCommandQueue = class {
8009
8018
  * @returns True if there's a pending or processing command
8010
8019
  */
8011
8020
  has(sessionId) {
8012
- return this.pendingCommands.has(sessionId);
8021
+ return (this.pendingCommands.get(sessionId)?.length || 0) > 0;
8013
8022
  }
8014
8023
  /**
8015
8024
  * Get a pending command by session ID.
@@ -8018,7 +8027,7 @@ var AgentCommandQueue = class {
8018
8027
  * @returns The pending command or undefined
8019
8028
  */
8020
8029
  get(sessionId) {
8021
- return this.pendingCommands.get(sessionId);
8030
+ return this.pendingCommands.get(sessionId)?.[0];
8022
8031
  }
8023
8032
  /**
8024
8033
  * Get all commands that need retry after reconnect.
@@ -8032,20 +8041,19 @@ var AgentCommandQueue = class {
8032
8041
  getPendingCommands() {
8033
8042
  const now = Date.now();
8034
8043
  const retryable = [];
8035
- for (const [sessionId, pending] of this.pendingCommands) {
8036
- if (pending.state === "completed" || pending.state === "failed") {
8037
- continue;
8038
- }
8039
- if (now - pending.receivedAt > this.commandTimeout) {
8040
- console.log(`[AgentCommandQueue] EP1237: Command for session ${sessionId} timed out`);
8041
- this.pendingCommands.delete(sessionId);
8042
- continue;
8043
- }
8044
- if (pending.retryCount >= this.maxRetries) {
8045
- console.log(`[AgentCommandQueue] EP1237: Command for session ${sessionId} exceeded max retries`);
8046
- continue;
8044
+ for (const [sessionId, queue] of this.pendingCommands) {
8045
+ for (const pending of queue) {
8046
+ if (pending.state === "completed" || pending.state === "failed") continue;
8047
+ if (now - pending.receivedAt > this.commandTimeout) {
8048
+ console.log(`[AgentCommandQueue] EP1237: Command for session ${sessionId} timed out`);
8049
+ continue;
8050
+ }
8051
+ if (pending.retryCount >= this.maxRetries) {
8052
+ console.log(`[AgentCommandQueue] EP1237: Command for session ${sessionId} exceeded max retries`);
8053
+ continue;
8054
+ }
8055
+ retryable.push(pending);
8047
8056
  }
8048
- retryable.push(pending);
8049
8057
  }
8050
8058
  return retryable;
8051
8059
  }
@@ -8055,13 +8063,20 @@ var AgentCommandQueue = class {
8055
8063
  *
8056
8064
  * @param sessionId - Agent session ID
8057
8065
  */
8058
- incrementRetryCount(sessionId) {
8059
- const pending = this.pendingCommands.get(sessionId);
8066
+ incrementRetryCount(sessionId, commandId) {
8067
+ const pending = this.getCommand(sessionId, commandId);
8060
8068
  if (pending) {
8069
+ if (pending.retryCount >= this.maxRetries) {
8070
+ return false;
8071
+ }
8061
8072
  pending.retryCount++;
8062
8073
  pending.state = "pending";
8063
- console.log(`[AgentCommandQueue] EP1237: Retry ${pending.retryCount}/${this.maxRetries} for session ${sessionId}`);
8074
+ console.log(
8075
+ `[AgentCommandQueue] EP1237: Retry ${pending.retryCount}/${this.maxRetries} for session ${sessionId} (${pending.commandId})`
8076
+ );
8077
+ return true;
8064
8078
  }
8079
+ return false;
8065
8080
  }
8066
8081
  /**
8067
8082
  * Get commands that have exceeded the retry limit.
@@ -8071,9 +8086,11 @@ var AgentCommandQueue = class {
8071
8086
  */
8072
8087
  getFailedCommands() {
8073
8088
  const failed = [];
8074
- for (const pending of this.pendingCommands.values()) {
8075
- if (pending.retryCount >= this.maxRetries && pending.state !== "completed" && pending.state !== "failed") {
8076
- failed.push(pending);
8089
+ for (const queue of this.pendingCommands.values()) {
8090
+ for (const pending of queue) {
8091
+ if (pending.retryCount >= this.maxRetries && pending.state !== "completed" && pending.state !== "failed") {
8092
+ failed.push(pending);
8093
+ }
8077
8094
  }
8078
8095
  }
8079
8096
  return failed;
@@ -8085,10 +8102,13 @@ var AgentCommandQueue = class {
8085
8102
  cleanup() {
8086
8103
  const now = Date.now();
8087
8104
  let cleaned = 0;
8088
- for (const [sessionId, pending] of this.pendingCommands) {
8089
- if (now - pending.receivedAt > this.commandTimeout) {
8105
+ for (const [sessionId, queue] of this.pendingCommands) {
8106
+ const valid3 = queue.filter((pending) => now - pending.receivedAt <= this.commandTimeout);
8107
+ cleaned += queue.length - valid3.length;
8108
+ if (valid3.length === 0) {
8090
8109
  this.pendingCommands.delete(sessionId);
8091
- cleaned++;
8110
+ } else {
8111
+ this.pendingCommands.set(sessionId, valid3);
8092
8112
  }
8093
8113
  }
8094
8114
  if (cleaned > 0) {
@@ -8105,7 +8125,14 @@ var AgentCommandQueue = class {
8105
8125
  * Get the current queue size.
8106
8126
  */
8107
8127
  size() {
8108
- return this.pendingCommands.size;
8128
+ let total = 0;
8129
+ for (const queue of this.pendingCommands.values()) {
8130
+ total += queue.length;
8131
+ }
8132
+ return total;
8133
+ }
8134
+ getMaxDepthPerSession() {
8135
+ return this.maxDepthPerSession;
8109
8136
  }
8110
8137
  /**
8111
8138
  * Shutdown the queue (stop cleanup interval).
@@ -8116,7 +8143,29 @@ var AgentCommandQueue = class {
8116
8143
  clearInterval(this.cleanupInterval);
8117
8144
  this.cleanupInterval = null;
8118
8145
  }
8119
- console.log(`[AgentCommandQueue] EP1237: Shutdown with ${this.pendingCommands.size} pending command(s)`);
8146
+ console.log(`[AgentCommandQueue] EP1237: Shutdown with ${this.size()} pending command(s)`);
8147
+ }
8148
+ getCommand(sessionId, commandId) {
8149
+ const queue = this.pendingCommands.get(sessionId);
8150
+ if (!queue || queue.length === 0) return void 0;
8151
+ if (!commandId) return queue[0];
8152
+ return queue.find((pending) => pending.commandId === commandId);
8153
+ }
8154
+ removeCommand(sessionId, commandId, state) {
8155
+ const queue = this.pendingCommands.get(sessionId);
8156
+ if (!queue || queue.length === 0) return;
8157
+ const index = commandId ? queue.findIndex((pending) => pending.commandId === commandId) : 0;
8158
+ if (index < 0) return;
8159
+ const [removed] = queue.splice(index, 1);
8160
+ removed.state = state;
8161
+ console.log(
8162
+ `[AgentCommandQueue] EP1237: ${state} command for session ${sessionId} (${removed.commandId}), depth=${queue.length}`
8163
+ );
8164
+ if (queue.length === 0) {
8165
+ this.pendingCommands.delete(sessionId);
8166
+ } else {
8167
+ this.pendingCommands.set(sessionId, queue);
8168
+ }
8120
8169
  }
8121
8170
  };
8122
8171
 
@@ -14710,8 +14759,24 @@ var Daemon = class _Daemon {
14710
14759
  client.updateActivity();
14711
14760
  const commandQueue = getAgentCommandQueue();
14712
14761
  if (shouldQueueAgentCommandForRetry(cmd.action)) {
14713
- commandQueue.enqueue(cmd.sessionId, cmd, message.id);
14714
- commandQueue.markProcessing(cmd.sessionId);
14762
+ const queueResult = commandQueue.enqueue(cmd.sessionId, cmd, message.id);
14763
+ if (!queueResult.enqueued) {
14764
+ const errorMessage = `TURN_IN_PROGRESS: Session queue is full (${queueResult.queueDepth}/${commandQueue.getMaxDepthPerSession()}). Please retry.`;
14765
+ await client.send({
14766
+ type: "agent_result",
14767
+ commandId: message.id,
14768
+ result: {
14769
+ success: false,
14770
+ status: "error",
14771
+ sessionId: cmd.sessionId,
14772
+ seq: this.nextAgentSeq(cmd.sessionId),
14773
+ error: errorMessage
14774
+ }
14775
+ });
14776
+ console.warn(`[Daemon] EP1398: Rejected command due to bounded queue depth for session ${cmd.sessionId}`);
14777
+ return;
14778
+ }
14779
+ commandQueue.markProcessing(cmd.sessionId, message.id);
14715
14780
  }
14716
14781
  let daemonChunkCount = 0;
14717
14782
  let daemonToolUseCount = 0;
@@ -14792,7 +14857,7 @@ var Daemon = class _Daemon {
14792
14857
  onComplete: async (claudeSessionId, resultMetadata) => {
14793
14858
  const duration = Date.now() - daemonStreamStart;
14794
14859
  console.log(`[Daemon] EP1191: Stream complete - ${daemonChunkCount} chunks, ${daemonToolUseCount} tool uses forwarded in ${duration}ms`);
14795
- commandQueue.complete(sessionId);
14860
+ commandQueue.complete(sessionId, commandId);
14796
14861
  try {
14797
14862
  await client.send({
14798
14863
  type: "agent_result",
@@ -14806,7 +14871,7 @@ var Daemon = class _Daemon {
14806
14871
  onError: async (error) => {
14807
14872
  const duration = Date.now() - daemonStreamStart;
14808
14873
  console.log(`[Daemon] EP1191: Stream error after ${daemonChunkCount} chunks in ${duration}ms - ${error}`);
14809
- commandQueue.complete(sessionId);
14874
+ commandQueue.complete(sessionId, commandId);
14810
14875
  try {
14811
14876
  await client.send({
14812
14877
  type: "agent_result",
@@ -14922,7 +14987,7 @@ var Daemon = class _Daemon {
14922
14987
  error: errorMsg
14923
14988
  }
14924
14989
  });
14925
- commandQueue.complete(sessionId);
14990
+ commandQueue.complete(sessionId, message.id);
14926
14991
  } catch (sendError) {
14927
14992
  console.error(`[Daemon] EP1237: Failed to send error result (WebSocket disconnected), command will be retried on reconnect`);
14928
14993
  }
@@ -15480,7 +15545,7 @@ var Daemon = class _Daemon {
15480
15545
  error: `Command failed after ${pending.retryCount} retry attempts during WebSocket reconnection`
15481
15546
  }
15482
15547
  });
15483
- commandQueue.fail(sessionId, "Max retries exceeded");
15548
+ commandQueue.fail(sessionId, "Max retries exceeded", pending.commandId);
15484
15549
  } catch (sendError) {
15485
15550
  console.error("[Daemon] EP1237: Failed to report max retry failure:", sendError);
15486
15551
  }
@@ -15490,10 +15555,14 @@ var Daemon = class _Daemon {
15490
15555
  const sessionId = cmd.sessionId || "unknown";
15491
15556
  if (pending.retryCount >= commandQueue.getMaxRetries()) {
15492
15557
  console.log(`[Daemon] EP1237: Command for session ${sessionId} will exceed max retries on next attempt`);
15493
- commandQueue.fail(sessionId, "Max retries exceeded");
15558
+ commandQueue.fail(sessionId, "Max retries exceeded", pending.commandId);
15559
+ continue;
15560
+ }
15561
+ const incremented = commandQueue.incrementRetryCount(sessionId, pending.commandId);
15562
+ if (!incremented) {
15563
+ commandQueue.fail(sessionId, "Max retries exceeded", pending.commandId);
15494
15564
  continue;
15495
15565
  }
15496
- commandQueue.incrementRetryCount(sessionId);
15497
15566
  console.log(`[Daemon] EP1237: Command for session ${sessionId} ready for retry (attempt ${pending.retryCount + 1}/${commandQueue.getMaxRetries()})`);
15498
15567
  }
15499
15568
  }