@songsid/agend 2.1.2-beta.28 → 2.1.2-beta.29

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.
@@ -4,6 +4,7 @@ import { createServer } from "node:http";
4
4
  import { join, dirname, basename } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { getAgendHome, ensureWorkspaceGit } from "./paths.js";
7
+ import { clearUpdateMarker, isUpdateInProgress } from "./update-marker.js";
7
8
  import { sdNotify, sdNotifyBlocking } from "./sd-notify.js";
8
9
  import { readFleetMemory } from "./process-memory.js";
9
10
  import { ReplyDeduper } from "./reply-dedup.js";
@@ -237,6 +238,8 @@ export class FleetManager {
237
238
  failoverActive = new Map(); // instance → current failover model
238
239
  // IPC reconnect: tracks instances being intentionally stopped (skip reconnect)
239
240
  ipcStoppingInstances = new Set();
241
+ /** Set the moment a graceful stop begins — see isPlannedRestart(). */
242
+ shuttingDown = false;
240
243
  /** Coalesce concurrent connection attempts for the same daemon socket. */
241
244
  ipcConnectInFlight = new Map();
242
245
  /** At most one reconnect/backoff loop may exist per instance. */
@@ -306,8 +309,24 @@ export class FleetManager {
306
309
  }
307
310
  });
308
311
  }
312
+ /**
313
+ * Is the fleet going down (or coming back up) on purpose?
314
+ *
315
+ * Instances dying during a planned restart is the restart working, not an
316
+ * incident — but the code that notices a dead pane or a dead MCP server
317
+ * cannot tell the difference on its own. Two sources, because the noise
318
+ * starts before this process is even told to stop: `agend update` replaces
319
+ * the package on disk while this daemon is still running and still watching.
320
+ */
321
+ isPlannedRestart() {
322
+ return this.shuttingDown || isUpdateInProgress(this.dataDir);
323
+ }
309
324
  finishStartup() {
310
325
  this.startupComplete = true;
326
+ // We are the post-update fleet: the update is over by definition. Clearing
327
+ // it here (rather than in the update command, which exits before the new
328
+ // fleet is up) is what keeps the quiet window from outliving the restart.
329
+ clearUpdateMarker(this.dataDir);
311
330
  if (this.reloadPending)
312
331
  this.scheduleReconcile();
313
332
  void this.sweepOrphanedCancelButtons();
@@ -1034,6 +1053,9 @@ export class FleetManager {
1034
1053
  async startAll(configPath) {
1035
1054
  FleetManager.signalTarget = this;
1036
1055
  this.startupComplete = false;
1056
+ // Cleared here, not at the end of doStopAll: a stop has an async tail, and
1057
+ // anything arriving during it is still part of the stop.
1058
+ this.shuttingDown = false;
1037
1059
  this.configPath = configPath;
1038
1060
  this.loadEnvFile();
1039
1061
  // Rotate fleet.log if oversized (before any logging)
@@ -5995,6 +6017,10 @@ When users create specialized instances, suggest these configurations:
5995
6017
  async doStopAll() {
5996
6018
  this.startupComplete = false;
5997
6019
  this.reloadPending = false;
6020
+ // Before anything is stopped: everything that dies from here on dies
6021
+ // because we asked it to. Set synchronously — doStopAll runs to its first
6022
+ // await in the same tick as the signal handler, so no event can slip in.
6023
+ this.shuttingDown = true;
5998
6024
  this.ipcStoppingInstances.add("__fleet_stopping__");
5999
6025
  sdNotifyBlocking("STOPPING=1");
6000
6026
  if (this.watchdogTimer) {