@episoda/cli 0.2.176 → 0.2.178

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.
@@ -3046,7 +3046,7 @@ var require_package = __commonJS({
3046
3046
  "package.json"(exports2, module2) {
3047
3047
  module2.exports = {
3048
3048
  name: "@episoda/cli",
3049
- version: "0.2.176",
3049
+ version: "0.2.178",
3050
3050
  description: "CLI tool for Episoda local development workflow orchestration",
3051
3051
  main: "dist/index.js",
3052
3052
  types: "dist/index.d.ts",
@@ -15067,6 +15067,8 @@ var Daemon = class _Daemon {
15067
15067
  // 2 minutes
15068
15068
  // EP1360: Per-session monotonic event seq for daemon→platform stream gap detection.
15069
15069
  this.agentEventSeq = /* @__PURE__ */ new Map();
15070
+ // sessionId -> last seq
15071
+ this.agentHeartbeatLoops = /* @__PURE__ */ new Map();
15070
15072
  this.cliRuntimeVersion = resolveEffectiveCliVersion(packageJson.version);
15071
15073
  this.ipcServer = new IPCServer();
15072
15074
  this.connectionManager = new ConnectionManager();
@@ -15078,6 +15080,12 @@ var Daemon = class _Daemon {
15078
15080
  handleWorktreeSetup: (command, projectPath, worktreeManager) => this.handleWorktreeSetup(command, projectPath, worktreeManager)
15079
15081
  });
15080
15082
  }
15083
+ static {
15084
+ this.AGENT_HEARTBEAT_INTERVAL_MS = 15e3;
15085
+ }
15086
+ static {
15087
+ this.AGENT_HEARTBEAT_REQUEST_TIMEOUT_MS = 12e3;
15088
+ }
15081
15089
  static {
15082
15090
  this.HEALTH_PORT = 9999;
15083
15091
  }
@@ -15097,6 +15105,81 @@ var Daemon = class _Daemon {
15097
15105
  this.agentEventSeq.set(sessionId, next);
15098
15106
  return next;
15099
15107
  }
15108
+ stopAgentHeartbeatLoop(sessionId) {
15109
+ const loop = this.agentHeartbeatLoops.get(sessionId);
15110
+ if (!loop) return;
15111
+ loop.stop();
15112
+ this.agentHeartbeatLoops.delete(sessionId);
15113
+ }
15114
+ async handleServerRequestedAgentStop(sessionId) {
15115
+ try {
15116
+ const agentManager = getAgentControlPlane();
15117
+ await agentManager.stopSession(sessionId);
15118
+ console.log(`[Daemon] EP1430: Stopped session ${sessionId} due to server continue=false`);
15119
+ } catch (error) {
15120
+ console.warn(
15121
+ `[Daemon] EP1430: Failed to stop session ${sessionId} after server continue=false:`,
15122
+ error
15123
+ );
15124
+ }
15125
+ }
15126
+ startAgentHeartbeatLoop(sessionId) {
15127
+ if (this.agentHeartbeatLoops.has(sessionId)) return;
15128
+ let stopped = false;
15129
+ let inFlight = false;
15130
+ let timer = null;
15131
+ const runHeartbeat = async () => {
15132
+ if (stopped || inFlight) return;
15133
+ inFlight = true;
15134
+ const abortController = new AbortController();
15135
+ const requestTimeout = setTimeout(() => {
15136
+ abortController.abort();
15137
+ }, _Daemon.AGENT_HEARTBEAT_REQUEST_TIMEOUT_MS);
15138
+ try {
15139
+ const config = await (0, import_core22.loadConfig)();
15140
+ if (!config?.access_token) return;
15141
+ const apiUrl = config.api_url || "https://episoda.dev";
15142
+ const response = await fetchWithAuth(
15143
+ `${apiUrl}/api/agent-sessions/${sessionId}/heartbeat`,
15144
+ {
15145
+ method: "POST",
15146
+ signal: abortController.signal
15147
+ }
15148
+ );
15149
+ if (!response.ok && response.status !== 409) {
15150
+ return;
15151
+ }
15152
+ const payload = await response.json().catch(() => null);
15153
+ const shouldContinue = response.status === 409 ? false : Boolean(payload?.data?.continue ?? payload?.continue ?? true);
15154
+ if (!shouldContinue) {
15155
+ await this.handleServerRequestedAgentStop(sessionId);
15156
+ this.stopAgentHeartbeatLoop(sessionId);
15157
+ }
15158
+ } catch (error) {
15159
+ if (error instanceof Error && error.name === "AbortError") {
15160
+ console.warn(
15161
+ `[Daemon] EP1430: Heartbeat request timed out for session ${sessionId} after ${_Daemon.AGENT_HEARTBEAT_REQUEST_TIMEOUT_MS}ms`
15162
+ );
15163
+ }
15164
+ } finally {
15165
+ clearTimeout(requestTimeout);
15166
+ inFlight = false;
15167
+ }
15168
+ };
15169
+ timer = setInterval(() => {
15170
+ void runHeartbeat();
15171
+ }, _Daemon.AGENT_HEARTBEAT_INTERVAL_MS);
15172
+ this.agentHeartbeatLoops.set(sessionId, {
15173
+ stop: () => {
15174
+ stopped = true;
15175
+ if (timer) {
15176
+ clearInterval(timer);
15177
+ timer = null;
15178
+ }
15179
+ }
15180
+ });
15181
+ void runHeartbeat();
15182
+ }
15100
15183
  logReliabilityMetric(metric, fields) {
15101
15184
  console.log(`[Daemon][Metric] ${JSON.stringify({
15102
15185
  metric,
@@ -15500,6 +15583,7 @@ var Daemon = class _Daemon {
15500
15583
  }
15501
15584
  },
15502
15585
  onComplete: async (claudeSessionId, resultMetadata) => {
15586
+ this.stopAgentHeartbeatLoop(sessionId);
15503
15587
  const duration = Date.now() - daemonStreamStart;
15504
15588
  console.log(`[Daemon] EP1191: Stream complete - ${daemonChunkCount} chunks, ${daemonToolUseCount} tool uses forwarded in ${duration}ms`);
15505
15589
  commandQueue.complete(sessionId, commandId);
@@ -15514,6 +15598,7 @@ var Daemon = class _Daemon {
15514
15598
  }
15515
15599
  },
15516
15600
  onError: async (error) => {
15601
+ this.stopAgentHeartbeatLoop(sessionId);
15517
15602
  const duration = Date.now() - daemonStreamStart;
15518
15603
  console.log(`[Daemon] EP1191: Stream error after ${daemonChunkCount} chunks in ${duration}ms - ${error}`);
15519
15604
  commandQueue.complete(sessionId, commandId);
@@ -15533,6 +15618,7 @@ var Daemon = class _Daemon {
15533
15618
  await agentManager.initialize();
15534
15619
  let result;
15535
15620
  if (cmd.action === "start") {
15621
+ this.startAgentHeartbeatLoop(cmd.sessionId);
15536
15622
  const callbacks = createStreamingCallbacks(cmd.sessionId, message.id);
15537
15623
  const agentWorkingDir = await resolveAgentWorkingDirectory({
15538
15624
  command: cmd,
@@ -15573,6 +15659,7 @@ var Daemon = class _Daemon {
15573
15659
  error: startResult.error
15574
15660
  };
15575
15661
  } else if (cmd.action === "message") {
15662
+ this.startAgentHeartbeatLoop(cmd.sessionId);
15576
15663
  const callbacks = createStreamingCallbacks(cmd.sessionId, message.id);
15577
15664
  const sendResult = await agentManager.sendMessage({
15578
15665
  sessionId: cmd.sessionId,
@@ -15597,9 +15684,11 @@ var Daemon = class _Daemon {
15597
15684
  error: sendResult.error
15598
15685
  };
15599
15686
  } else if (cmd.action === "abort") {
15687
+ this.stopAgentHeartbeatLoop(cmd.sessionId);
15600
15688
  await agentManager.abortSession(cmd.sessionId);
15601
15689
  result = { success: true, status: "aborted", sessionId: cmd.sessionId, seq: this.nextAgentSeq(cmd.sessionId) };
15602
15690
  } else if (cmd.action === "stop") {
15691
+ this.stopAgentHeartbeatLoop(cmd.sessionId);
15603
15692
  await agentManager.stopSession(cmd.sessionId);
15604
15693
  result = { success: true, status: "complete", sessionId: cmd.sessionId, seq: this.nextAgentSeq(cmd.sessionId) };
15605
15694
  this.agentEventSeq.delete(cmd.sessionId);
@@ -15611,6 +15700,9 @@ var Daemon = class _Daemon {
15611
15700
  error: `Unknown agent action: ${cmd.action}`
15612
15701
  };
15613
15702
  }
15703
+ if (!result.success && result.sessionId) {
15704
+ this.stopAgentHeartbeatLoop(result.sessionId);
15705
+ }
15614
15706
  await client.send({
15615
15707
  type: "agent_result",
15616
15708
  commandId: message.id,
@@ -15619,6 +15711,7 @@ var Daemon = class _Daemon {
15619
15711
  console.log(`[Daemon] EP912: Agent command ${cmd.action} completed for session ${getAgentCommandSessionId(cmd)}`);
15620
15712
  } catch (error) {
15621
15713
  const sessionId = getAgentCommandSessionId(cmd);
15714
+ this.stopAgentHeartbeatLoop(sessionId);
15622
15715
  const errorMsg = error instanceof Error ? error.message : String(error);
15623
15716
  try {
15624
15717
  await client.send({