@songsid/agend 2.1.2-beta.43 → 2.1.2-beta.44

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.
@@ -588,7 +588,25 @@ export class FleetManager {
588
588
  getInstanceStatus(name) {
589
589
  if (this.lifecycle.isPaused(name))
590
590
  return "paused";
591
+ const daemon = this.lifecycle.daemons.get(name);
591
592
  const processStatus = this.instanceProcessStatus.get(name);
593
+ // IPC can be disconnected during a respawn, so the event which announces
594
+ // the new live pane may be missed. The in-process daemon is authoritative
595
+ // in that case; do not leave a stale `crashed` cache masking an instance
596
+ // that has already recovered and can answer messages.
597
+ const daemonStatus = daemon?.getProcessStatus?.();
598
+ if (daemonStatus === "running") {
599
+ if (processStatus)
600
+ this.instanceProcessStatus.delete(name);
601
+ // A recovered in-process daemon is also the authority for clearing a
602
+ // marker left by a crash-loop/reconnect race. This keeps standalone
603
+ // `agend ls` from seeing the old marker after the next API outage.
604
+ try {
605
+ unlinkSync(join(this.getInstanceDir(name), "crash-state.json"));
606
+ }
607
+ catch { /* absent */ }
608
+ return "running";
609
+ }
592
610
  if (processStatus)
593
611
  return processStatus;
594
612
  const pidPath = join(this.getInstanceDir(name), "daemon.pid");
@@ -670,6 +688,13 @@ export class FleetManager {
670
688
  cacheInstanceProcessStatus(name, status) {
671
689
  if (status === "running") {
672
690
  this.instanceProcessStatus.delete(name);
691
+ // A prior crash-loop marker is one-shot. Successful respawn is the
692
+ // authoritative recovery signal even when the marker outlived an IPC
693
+ // disconnect and was not consumed by a new Daemon constructor.
694
+ try {
695
+ unlinkSync(join(this.getInstanceDir(name), "crash-state.json"));
696
+ }
697
+ catch { /* absent */ }
673
698
  return;
674
699
  }
675
700
  if (status !== "crashed" && status !== "stopped")
@@ -901,11 +926,41 @@ export class FleetManager {
901
926
  await new Promise(resolve => setTimeout(resolve, 250));
902
927
  await this.startClassicInstance(instanceName, this.classicChannels.getBackendByInstance(instanceName, this.fleetConfig?.defaults?.backend), this.classicChannels.getPreTaskCommand(channel.channelId, channel.adapterId), this.classicChannels.getModel(channel.channelId, channel.adapterId, this.fleetConfig?.defaults?.model), this.classicChannels.getAutoPauseAfter(channel.channelId, channel.adapterId, this.fleetConfig?.defaults?.auto_pause_after));
903
928
  }
904
- async startInstance(name, config, topicMode, kind = "fleet-topic") {
929
+ async startInstance(name, config, topicMode, kind = "fleet-topic",
930
+ /**
931
+ * Explicit starts (CLI/API) may resume a paused or failed daemon. Startup
932
+ * and reconcile calls leave this false so a persisted pause remains paused
933
+ * across a fleet restart.
934
+ */
935
+ resumePaused = false) {
936
+ if (resumePaused && this.lifecycle.isPaused(name)) {
937
+ await this.lifecycle.wake(name, 30_000);
938
+ // A successful wake clears the persisted pause marker and produces a
939
+ // fresh instance_state snapshot. Drop any stale process error left by a
940
+ // pre-pause crash so /api/fleet and `agend ls` converge on running.
941
+ this.instanceProcessStatus.delete(name);
942
+ return;
943
+ }
905
944
  if (this.lifecycle.isPaused(name)) {
906
945
  this.logger.info({ name }, "Persisted paused instance — skipping startup");
907
946
  return;
908
947
  }
948
+ if (this.lifecycle.daemons.has(name)) {
949
+ // A crash-loop daemon remains in the lifecycle map so its health monitor
950
+ // can expose the failure. The old start path treated that object as
951
+ // already running and merely deleted the process-status cache, leaving a
952
+ // dead pane (and crash marker) behind. An explicit start is a recovery
953
+ // request: tear down the failed daemon and build a fresh one.
954
+ if (resumePaused) {
955
+ const status = this.getInstanceStatus(name);
956
+ if (status === "crashed" || status === "stopped") {
957
+ await this.restartSingleInstance(name);
958
+ return;
959
+ }
960
+ }
961
+ this.logger.info({ name }, "Instance already running, skipping");
962
+ return;
963
+ }
909
964
  if (config.general_topic) {
910
965
  // antigravity (agy) does not read MCP instructions — fleet context and
911
966
  // routing instructions are not injected, so it cannot act as a dispatcher.
@@ -916,12 +971,19 @@ export class FleetManager {
916
971
  }
917
972
  this.ensureGeneralInstructions(config.working_directory, config.backend);
918
973
  }
919
- this.instanceProcessStatus.delete(name);
920
974
  await this.lifecycle.start(name, config, topicMode, {
921
975
  kind,
922
976
  backend: config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code",
923
977
  model: this.resolveInstanceModel(name).display,
924
978
  });
979
+ // Only clear a stale process status after a real start succeeded. Clearing
980
+ // it before lifecycle.start() can turn a crash-loop daemon's dead pane into
981
+ // a falsely running instance when lifecycle.start() returns early.
982
+ this.instanceProcessStatus.delete(name);
983
+ try {
984
+ unlinkSync(join(this.getInstanceDir(name), "crash-state.json"));
985
+ }
986
+ catch { /* consumed or absent */ }
925
987
  // Auto-connect IPC — daemon.start() ensures socket is ready before resolving
926
988
  await this.connectIpcToInstance(name);
927
989
  }
@@ -6862,7 +6924,7 @@ When users create specialized instances, suggest these configurations:
6862
6924
  (async () => {
6863
6925
  try {
6864
6926
  const topicMode = this.fleetConfig?.channel?.mode === "topic";
6865
- await this.startInstance(name, config, topicMode ?? false);
6927
+ await this.startInstance(name, config, topicMode ?? false, "fleet-topic", true);
6866
6928
  this.emitSseEvent("status", this.getUiStatus());
6867
6929
  res.writeHead(200);
6868
6930
  res.end(JSON.stringify({ ok: true }));