@songsid/agend 2.1.2-beta.1 → 2.1.2-beta.3

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.
@@ -163,6 +163,10 @@ export class FleetManager {
163
163
  failoverActive = new Map(); // instance → current failover model
164
164
  // IPC reconnect: tracks instances being intentionally stopped (skip reconnect)
165
165
  ipcStoppingInstances = new Set();
166
+ /** Coalesce concurrent connection attempts for the same daemon socket. */
167
+ ipcConnectInFlight = new Map();
168
+ /** At most one reconnect/backoff loop may exist per instance. */
169
+ ipcReconnectInFlight = new Map();
166
170
  // Adapter restart: prevents re-entrant restart attempts
167
171
  adapterRestarting = new Set();
168
172
  // Adapter isolation: track state per adapter for retry + visibility
@@ -1931,19 +1935,38 @@ export class FleetManager {
1931
1935
  this.logger.info({ adapterId, type: channelConfig.type }, "Additional adapter started");
1932
1936
  }
1933
1937
  /** Connect IPC to a single instance with all handlers */
1934
- async connectIpcToInstance(name) {
1938
+ connectIpcToInstance(name) {
1939
+ const inFlight = this.ipcConnectInFlight.get(name);
1940
+ if (inFlight)
1941
+ return inFlight;
1942
+ const connection = this.connectIpcToInstanceInternal(name)
1943
+ .finally(() => {
1944
+ if (this.ipcConnectInFlight.get(name) === connection) {
1945
+ this.ipcConnectInFlight.delete(name);
1946
+ }
1947
+ });
1948
+ this.ipcConnectInFlight.set(name, connection);
1949
+ return connection;
1950
+ }
1951
+ async connectIpcToInstanceInternal(name) {
1935
1952
  // Close existing client to prevent socket leak on reconnect
1936
1953
  const existing = this.instanceIpcClients.get(name);
1937
1954
  if (existing) {
1938
- this.ipcStoppingInstances.add(name);
1955
+ // Remove application listeners before destroying the socket. Even if a
1956
+ // future regression creates two clients, the replaced one cannot keep
1957
+ // handling fleet_outbound messages as an orphan.
1958
+ existing.removeAllListeners();
1939
1959
  try {
1940
- existing.close();
1960
+ await existing.close();
1941
1961
  }
1942
1962
  catch (err) {
1943
1963
  this.logger.debug({ err, name }, "IPC client close failed (likely already closed)");
1944
1964
  }
1945
- this.instanceIpcClients.delete(name);
1946
- this.ipcStoppingInstances.delete(name);
1965
+ finally {
1966
+ if (this.instanceIpcClients.get(name) === existing) {
1967
+ this.instanceIpcClients.delete(name);
1968
+ }
1969
+ }
1947
1970
  }
1948
1971
  const sockPath = join(this.getInstanceDir(name), "channel.sock");
1949
1972
  if (!existsSync(sockPath))
@@ -2021,6 +2044,10 @@ export class FleetManager {
2021
2044
  }
2022
2045
  // Auto-reconnect on disconnect (unless intentionally stopping)
2023
2046
  ipc.on("disconnect", () => {
2047
+ // A delayed event from a replaced/stale client must never delete the
2048
+ // current connection or start another reconnect loop.
2049
+ if (this.instanceIpcClients.get(name) !== ipc)
2050
+ return;
2024
2051
  this.instanceIpcClients.delete(name);
2025
2052
  if (this.ipcStoppingInstances.has(name))
2026
2053
  return;
@@ -2032,7 +2059,20 @@ export class FleetManager {
2032
2059
  }
2033
2060
  }
2034
2061
  /** Attempt IPC reconnection with exponential backoff */
2035
- async ipcReconnect(name) {
2062
+ ipcReconnect(name) {
2063
+ const inFlight = this.ipcReconnectInFlight.get(name);
2064
+ if (inFlight)
2065
+ return inFlight;
2066
+ const reconnect = this.runIpcReconnect(name)
2067
+ .finally(() => {
2068
+ if (this.ipcReconnectInFlight.get(name) === reconnect) {
2069
+ this.ipcReconnectInFlight.delete(name);
2070
+ }
2071
+ });
2072
+ this.ipcReconnectInFlight.set(name, reconnect);
2073
+ return reconnect;
2074
+ }
2075
+ async runIpcReconnect(name) {
2036
2076
  for (let attempt = 1;; attempt++) {
2037
2077
  if (this.ipcStoppingInstances.has(name) || !this.daemons.has(name))
2038
2078
  return;