@songsid/agend 2.1.1-beta.15 → 2.1.1-beta.17

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.js CHANGED
@@ -10,6 +10,7 @@ import { TmuxManager, resolveTmuxLogicalSize } from "./tmux-manager.js";
10
10
  import { TranscriptMonitor } from "./transcript-monitor.js";
11
11
  import { ContextGuardian } from "./context-guardian.js";
12
12
  import { IpcServer } from "./channel/ipc-bridge.js";
13
+ import { daemonBudgetMs } from "./channel/ipc-timeouts.js";
13
14
  import { MessageBus } from "./channel/message-bus.js";
14
15
  import { shellQuote } from "./backend/types.js";
15
16
  import { getTmuxSession } from "./config.js";
@@ -848,6 +849,7 @@ export class Daemon extends EventEmitter {
848
849
  this.logger.warn({ instanceDir: this.instanceDir }, "Instance directory missing — stopping health check");
849
850
  this.healthCheckPaused = true;
850
851
  this.healthCheckTimer = null;
852
+ this.emitSupervisionEnded("instance directory was removed from disk", "Recreate the instance, or delete it from fleet.yaml.");
851
853
  return;
852
854
  }
853
855
  if (!this.tmux || this.spawning || this.healthCheckPaused || Daemon.tmuxServerPaused) {
@@ -898,6 +900,7 @@ export class Daemon extends EventEmitter {
898
900
  this.logger.info("CLI exited normally (code 0) — pausing health check");
899
901
  await this.tmux.killWindow();
900
902
  this.healthCheckPaused = true;
903
+ this.emitSupervisionEnded("the CLI exited normally (code 0)", "Nothing crashed — start it again when you need it.");
901
904
  return;
902
905
  }
903
906
  this.setProcessStatus("crashed");
@@ -1007,6 +1010,7 @@ export class Daemon extends EventEmitter {
1007
1010
  if (max_retries <= 0) {
1008
1011
  this.healthCheckPaused = true;
1009
1012
  this.logger.warn(`${cliLabel} window died — automatic restart is disabled`);
1013
+ this.emitSupervisionEnded("the CLI died and automatic restart is disabled (restart_policy.max_retries is 0)", "Start it manually, or raise max_retries in fleet.yaml.");
1010
1014
  return;
1011
1015
  }
1012
1016
  // Detect rapid crash: sliding window — 3+ crashes in 5 minutes
@@ -1036,6 +1040,8 @@ export class Daemon extends EventEmitter {
1036
1040
  this.lastCrashAt = Date.now();
1037
1041
  if (this.crashCount > max_retries) {
1038
1042
  this.logger.error({ crashCount: this.crashCount, maxRetries: max_retries }, "Max crash retries exceeded — not respawning");
1043
+ this.healthCheckPaused = true;
1044
+ this.emitSupervisionEnded(`it crashed ${this.crashCount} times, exceeding restart_policy.max_retries (${max_retries})`, "Check the logs for the cause, then restart it.");
1039
1045
  return; // don't schedule next — given up
1040
1046
  }
1041
1047
  // Calculate backoff delay
@@ -1119,6 +1125,21 @@ export class Daemon extends EventEmitter {
1119
1125
  * A dead remain-on-exit pane still contains the old ready prompt, so allowing
1120
1126
  * the pane monitor to capture it would re-create a false idle state.
1121
1127
  */
1128
+ /**
1129
+ * Announce that this instance is no longer being supervised.
1130
+ *
1131
+ * Four health-check exits set `healthCheckPaused = true` and returned without
1132
+ * telling anyone: a clean CLI exit, `max_retries <= 0`, crash retries exhausted,
1133
+ * and the instance directory being deleted. Only the crash-LOOP case emitted an
1134
+ * event, so a fleet could quietly contain a dead instance that still looked fine
1135
+ * on the dashboard — while messages routed to it queued or failed with a bare ❌.
1136
+ *
1137
+ * `crash_loop` already had this treatment; this is the same bridge for the other
1138
+ * four, carrying a human-readable cause and the operator's next step.
1139
+ */
1140
+ emitSupervisionEnded(reason, remedy) {
1141
+ this.emit("supervision_ended", { name: this.name, reason, remedy });
1142
+ }
1122
1143
  setProcessStatus(status) {
1123
1144
  if (this.processStatus === status)
1124
1145
  return;
@@ -2236,8 +2257,8 @@ export class Daemon extends EventEmitter {
2236
2257
  });
2237
2258
  const timeout = setTimeout(() => {
2238
2259
  this.pendingIpcRequests.delete(fleetReqId);
2239
- respond(null, "Task operation timed out after 30s");
2240
- }, 30_000);
2260
+ respond(null, `Task operation timed out after ${daemonBudgetMs(tool) / 1000}s`);
2261
+ }, daemonBudgetMs(tool));
2241
2262
  this.pendingIpcRequests.set(fleetReqId, (respMsg) => {
2242
2263
  clearTimeout(timeout);
2243
2264
  respond(respMsg.result, respMsg.error);
@@ -2259,8 +2280,8 @@ export class Daemon extends EventEmitter {
2259
2280
  });
2260
2281
  const timeout = setTimeout(() => {
2261
2282
  this.pendingIpcRequests.delete(fleetReqId);
2262
- respond(null, "Decision operation timed out after 30s");
2263
- }, 30_000);
2283
+ respond(null, `Decision operation timed out after ${daemonBudgetMs(tool) / 1000}s`);
2284
+ }, daemonBudgetMs(tool));
2264
2285
  this.pendingIpcRequests.set(fleetReqId, (respMsg) => {
2265
2286
  clearTimeout(timeout);
2266
2287
  respond(respMsg.result, respMsg.error);
@@ -2286,8 +2307,8 @@ export class Daemon extends EventEmitter {
2286
2307
  // Wait for fleet_schedule_response via pending request map
2287
2308
  const timeout = setTimeout(() => {
2288
2309
  this.pendingIpcRequests.delete(fleetReqId);
2289
- respond(null, "Schedule operation timed out after 30s");
2290
- }, 30_000);
2310
+ respond(null, `Schedule operation timed out after ${daemonBudgetMs(tool) / 1000}s`);
2311
+ }, daemonBudgetMs(tool));
2291
2312
  this.pendingIpcRequests.set(fleetReqId, (respMsg) => {
2292
2313
  clearTimeout(timeout);
2293
2314
  respond(respMsg.result, respMsg.error);
@@ -2308,7 +2329,9 @@ export class Daemon extends EventEmitter {
2308
2329
  fleetRequestId: fleetReqId,
2309
2330
  senderSessionName,
2310
2331
  });
2311
- const crossTimeoutMs = (tool === "start_instance" || tool === "create_instance" || tool === "replace_instance") ? 60_000 : 30_000;
2332
+ // Shared table, so delete_instance and the deployment tools get the same
2333
+ // budget here as the MCP client expects (the ad-hoc list omitted them).
2334
+ const crossTimeoutMs = daemonBudgetMs(tool);
2312
2335
  const timeout = setTimeout(() => {
2313
2336
  this.pendingIpcRequests.delete(fleetReqId);
2314
2337
  respond(null, `Cross-instance operation timed out after ${crossTimeoutMs / 1000}s`);
@@ -2351,8 +2374,8 @@ export class Daemon extends EventEmitter {
2351
2374
  this.ipcServer?.broadcast({ type: "fleet_outbound", tool, args, fleetRequestId: fleetReqId });
2352
2375
  const timeout = setTimeout(() => {
2353
2376
  this.pendingIpcRequests.delete(outboundKey);
2354
- respond(null, "Fleet outbound timed out after 30s");
2355
- }, 30_000);
2377
+ respond(null, `Fleet outbound timed out after ${daemonBudgetMs(tool) / 1000}s`);
2378
+ }, daemonBudgetMs(tool));
2356
2379
  this.pendingIpcRequests.set(outboundKey, (respMsg) => {
2357
2380
  clearTimeout(timeout);
2358
2381
  respond(respMsg.result, respMsg.error);