lifecycleion 0.0.12 → 0.0.14

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.
@@ -1006,6 +1006,9 @@ var ComponentLifecycle = class {
1006
1006
  getSignalStatus() {
1007
1007
  return this.manager.getSignalStatus();
1008
1008
  }
1009
+ getShutdownEscalationStatus() {
1010
+ return this.manager.getShutdownEscalationStatus();
1011
+ }
1009
1012
  triggerReload() {
1010
1013
  return this.manager.triggerReload();
1011
1014
  }
@@ -1184,6 +1187,15 @@ var LifecycleManagerEvents = class {
1184
1187
  lifecycleManagerShutdownCompleted(input) {
1185
1188
  this.emit("lifecycle-manager:shutdown-completed", input);
1186
1189
  }
1190
+ lifecycleManagerShutdownEscalationArmed(input) {
1191
+ this.emit("lifecycle-manager:shutdown-escalation-armed", input);
1192
+ }
1193
+ lifecycleManagerShutdownEscalationExpired(input) {
1194
+ this.emit("lifecycle-manager:shutdown-escalation-expired", input);
1195
+ }
1196
+ lifecycleManagerShutdownEscalationForced(input) {
1197
+ this.emit("lifecycle-manager:shutdown-escalation-forced", input);
1198
+ }
1187
1199
  componentStarting(name) {
1188
1200
  this.emit("component:starting", { name });
1189
1201
  }
@@ -1251,6 +1263,16 @@ var LifecycleManagerEvents = class {
1251
1263
  code: info?.code
1252
1264
  });
1253
1265
  }
1266
+ componentStalledResolved(name, stallInfo, stalledDurationMS) {
1267
+ this.emit("component:stalled-resolved", {
1268
+ name,
1269
+ stallInfo,
1270
+ stalledDurationMS
1271
+ });
1272
+ }
1273
+ componentUnexpectedStop(name, error) {
1274
+ this.emit("component:unexpected-stop", { name, error });
1275
+ }
1254
1276
  componentShutdownForceCompleted(name) {
1255
1277
  this.emit("component:shutdown-force-completed", { name });
1256
1278
  }
@@ -1260,8 +1282,8 @@ var LifecycleManagerEvents = class {
1260
1282
  componentStartupRollback(name) {
1261
1283
  this.emit("component:startup-rollback", { name });
1262
1284
  }
1263
- signalShutdown(method) {
1264
- this.emit("signal:shutdown", { method });
1285
+ signalShutdown(method, isAlreadyShuttingDown = false) {
1286
+ this.emit("signal:shutdown", { method, isAlreadyShuttingDown });
1265
1287
  }
1266
1288
  signalReload() {
1267
1289
  this.emit("signal:reload", void 0);
@@ -1437,6 +1459,25 @@ var lifecycleManagerErrCodes = {
1437
1459
  StopTimeout: "StopTimeout"
1438
1460
  };
1439
1461
 
1462
+ // src/lib/lifecycle-manager/constants.ts
1463
+ var LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS = "Cannot unregister during bulk operation";
1464
+ var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND = "Component not found";
1465
+ var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING = "Component not running";
1466
+ var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED = "Component is stalled";
1467
+ var LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS = "Bulk startup in progress";
1468
+ var LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS = "Shutdown in progress";
1469
+ var LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR = "Unknown error";
1470
+ var LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP = "Auto-detaching process signals on last component stop";
1471
+ var LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN = "Logger exit called during shutdown, waiting...";
1472
+ var LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED = "Message handler failed: {{error.message}}";
1473
+ var LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT = "Graceful shutdown timed out";
1474
+ var LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT = "Force shutdown timed out";
1475
+ var LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP = "Optional component stopped unexpectedly during startup, continuing: {{error.message}}";
1476
+ var LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP = "Required component stopped unexpectedly during startup: {{error.message}}";
1477
+ var LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS = "Cannot register component while shutdown is in progress (isShuttingDown=true).";
1478
+ var LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP = "Cannot register component during startup when it is a required dependency for other components.";
1479
+ var LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE = "Component instance is already registered.";
1480
+
1440
1481
  // src/lib/process-signal-manager.ts
1441
1482
  var import_ulid = require("ulid");
1442
1483
  var import_readline = __toESM(require("readline"), 1);
@@ -1971,6 +2012,7 @@ var LifecycleManager = class extends EventEmitterProtected {
1971
2012
  attachSignalsBeforeStartup;
1972
2013
  attachSignalsOnStart;
1973
2014
  detachSignalsOnStop;
2015
+ repeatedShutdownRequestPolicy;
1974
2016
  // Component management
1975
2017
  components = [];
1976
2018
  runningComponents = /* @__PURE__ */ new Set();
@@ -1980,8 +2022,15 @@ var LifecycleManager = class extends EventEmitterProtected {
1980
2022
  componentTimestamps = /* @__PURE__ */ new Map();
1981
2023
  componentErrors = /* @__PURE__ */ new Map();
1982
2024
  componentStartAttemptTokens = /* @__PURE__ */ new Map();
2025
+ // Use per-stop ULIDs instead of incrementing counters because a stalled
2026
+ // component can be unregistered and replaced by a same-name instance before
2027
+ // the old floating stop promise settles.
2028
+ componentStopAttemptTokens = /* @__PURE__ */ new Map();
2029
+ pendingForceStopWaiters = /* @__PURE__ */ new Map();
2030
+ unexpectedStopsDuringStartup = /* @__PURE__ */ new Map();
1983
2031
  // State flags
1984
2032
  isStarting = false;
2033
+ autoAttachedSignalsDuringStartup = false;
1985
2034
  isStarted = false;
1986
2035
  isShuttingDown = false;
1987
2036
  // Unique token used to detect shutdowns that happened during async start().
@@ -1990,6 +2039,17 @@ var LifecycleManager = class extends EventEmitterProtected {
1990
2039
  pendingLoggerExitResolve = null;
1991
2040
  shutdownMethod = null;
1992
2041
  lastShutdownResult = null;
2042
+ repeatedShutdownExpiryTimer = null;
2043
+ repeatedShutdownRequestState = {
2044
+ requestCount: 0,
2045
+ firstMethod: null,
2046
+ latestMethod: null,
2047
+ firstRequestAt: null,
2048
+ latestRequestAt: null,
2049
+ repeatedWindowStartedAt: null,
2050
+ hasTriggeredForceShutdown: false,
2051
+ remainsArmedUntil: null
2052
+ };
1993
2053
  // Signal management
1994
2054
  processSignalManager = null;
1995
2055
  onReloadRequested;
@@ -2016,6 +2076,37 @@ var LifecycleManager = class extends EventEmitterProtected {
2016
2076
  this.attachSignalsBeforeStartup = options.attachSignalsBeforeStartup ?? false;
2017
2077
  this.attachSignalsOnStart = options.attachSignalsOnStart ?? false;
2018
2078
  this.detachSignalsOnStop = options.detachSignalsOnStop ?? false;
2079
+ const repeatedShutdownRequestPolicy = options.repeatedShutdownRequestPolicy;
2080
+ if (repeatedShutdownRequestPolicy === void 0) {
2081
+ this.repeatedShutdownRequestPolicy = void 0;
2082
+ } else {
2083
+ const hasFiniteExplicitArmedAfterFailureMS = Number.isFinite(
2084
+ repeatedShutdownRequestPolicy.armedAfterFailureMS
2085
+ );
2086
+ const forceAfterCount = finiteClampMin(
2087
+ repeatedShutdownRequestPolicy.forceAfterCount,
2088
+ 1,
2089
+ 3
2090
+ );
2091
+ const withinMS = finiteClampMin(
2092
+ repeatedShutdownRequestPolicy.withinMS,
2093
+ 0,
2094
+ 2e3
2095
+ );
2096
+ const armedAfterFailureMS = finiteClampMin(
2097
+ repeatedShutdownRequestPolicy.armedAfterFailureMS,
2098
+ 0,
2099
+ withinMS * forceAfterCount
2100
+ );
2101
+ this.repeatedShutdownRequestPolicy = {
2102
+ forceAfterCount,
2103
+ withinMS,
2104
+ armedAfterFailureMS,
2105
+ countManualRetriesTowardEscalation: repeatedShutdownRequestPolicy.countManualRetriesTowardEscalation ?? false,
2106
+ hasExplicitArmedAfterFailureMS: hasFiniteExplicitArmedAfterFailureMS,
2107
+ onForceShutdown: repeatedShutdownRequestPolicy.onForceShutdown
2108
+ };
2109
+ }
2019
2110
  this.onReloadRequested = options.onReloadRequested;
2020
2111
  this.onInfoRequested = options.onInfoRequested;
2021
2112
  this.onDebugRequested = options.onDebugRequested;
@@ -2088,7 +2179,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2088
2179
  */
2089
2180
  async unregisterComponent(name, options) {
2090
2181
  if (this.isStarting || this.isShuttingDown) {
2091
- this.logger.entity(name).warn("Cannot unregister during bulk operation", {
2182
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS, {
2092
2183
  params: {
2093
2184
  isStarting: this.isStarting,
2094
2185
  isShuttingDown: this.isShuttingDown
@@ -2097,7 +2188,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2097
2188
  return {
2098
2189
  success: false,
2099
2190
  componentName: name,
2100
- reason: "Cannot unregister during bulk operation",
2191
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS,
2101
2192
  code: "bulk_operation_in_progress",
2102
2193
  wasStopped: false,
2103
2194
  wasRegistered: this.hasComponent(name)
@@ -2105,11 +2196,11 @@ var LifecycleManager = class extends EventEmitterProtected {
2105
2196
  }
2106
2197
  const component = this.getComponent(name);
2107
2198
  if (!component) {
2108
- this.logger.entity(name).warn("Component not found");
2199
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND);
2109
2200
  return {
2110
2201
  success: false,
2111
2202
  componentName: name,
2112
- reason: "Component not found",
2203
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
2113
2204
  code: "component_not_found",
2114
2205
  wasStopped: false,
2115
2206
  wasRegistered: false
@@ -2122,7 +2213,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2122
2213
  return {
2123
2214
  success: false,
2124
2215
  componentName: name,
2125
- reason: "Component is stalled",
2216
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
2126
2217
  code: "stop_failed",
2127
2218
  stopFailureReason: "stalled",
2128
2219
  wasStopped: false,
@@ -2174,10 +2265,13 @@ var LifecycleManager = class extends EventEmitterProtected {
2174
2265
  wasStopped = true;
2175
2266
  }
2176
2267
  this.components = this.components.filter((c) => c.getName() !== name);
2268
+ component._clearUnexpectedStopHandler();
2177
2269
  this.componentStates.delete(name);
2178
2270
  this.componentTimestamps.delete(name);
2179
2271
  this.componentErrors.delete(name);
2180
2272
  this.componentStartAttemptTokens.delete(name);
2273
+ this.componentStopAttemptTokens.delete(name);
2274
+ this.pendingForceStopWaiters.delete(name);
2181
2275
  this.stalledComponents.delete(name);
2182
2276
  this.runningComponents.delete(name);
2183
2277
  this.updateStartedFlag();
@@ -2524,7 +2618,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2524
2618
  startedComponents: [],
2525
2619
  failedOptionalComponents: [],
2526
2620
  skippedDueToDependency: [],
2527
- reason: "Shutdown in progress",
2621
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2528
2622
  code: "shutdown_in_progress",
2529
2623
  durationMS: Date.now() - startTime
2530
2624
  };
@@ -2584,6 +2678,9 @@ var LifecycleManager = class extends EventEmitterProtected {
2584
2678
  };
2585
2679
  }
2586
2680
  this.isStarting = true;
2681
+ this.autoAttachedSignalsDuringStartup = false;
2682
+ this.unexpectedStopsDuringStartup.clear();
2683
+ this.resetRepeatedShutdownRequestState();
2587
2684
  this.shutdownMethod = null;
2588
2685
  this.lastShutdownResult = null;
2589
2686
  const didAutoAttachSignalsForBulkStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("bulk startup") : false;
@@ -2715,13 +2812,49 @@ var LifecycleManager = class extends EventEmitterProtected {
2715
2812
  error: result.error,
2716
2813
  durationMS: Date.now() - startTime
2717
2814
  };
2815
+ } else if (result.code === "component_unexpected_stop") {
2816
+ this.unexpectedStopsDuringStartup.delete(name);
2817
+ const error = result.error || new Error(
2818
+ result.reason || `Component "${name}" stopped unexpectedly`
2819
+ );
2820
+ if (component.isOptional()) {
2821
+ if (!failedOptionalComponents.some((entry) => entry.name === name)) {
2822
+ failedOptionalComponents.push({ name, error });
2823
+ }
2824
+ this.logger.entity(name).warn(
2825
+ LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
2826
+ {
2827
+ params: { error }
2828
+ }
2829
+ );
2830
+ } else {
2831
+ this.logger.entity(name).error(
2832
+ LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
2833
+ {
2834
+ params: { error }
2835
+ }
2836
+ );
2837
+ await this.rollbackStartup(startedComponents);
2838
+ return {
2839
+ success: false,
2840
+ startedComponents: [],
2841
+ failedOptionalComponents,
2842
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2843
+ reason: error.message,
2844
+ code: "component_unexpected_stop",
2845
+ error,
2846
+ durationMS: Date.now() - startTime
2847
+ };
2848
+ }
2718
2849
  } else {
2719
2850
  if (component.isOptional()) {
2720
2851
  this.logger.entity(name).warn(
2721
2852
  "Optional component failed to start, continuing: {{error.message}}",
2722
2853
  {
2723
2854
  params: {
2724
- error: result.error || new Error(result.reason || "Unknown error")
2855
+ error: result.error || new Error(
2856
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2857
+ )
2725
2858
  }
2726
2859
  }
2727
2860
  );
@@ -2735,14 +2868,18 @@ var LifecycleManager = class extends EventEmitterProtected {
2735
2868
  }
2736
2869
  failedOptionalComponents.push({
2737
2870
  name,
2738
- error: result.error || new Error(result.reason || "Unknown error")
2871
+ error: result.error || new Error(
2872
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2873
+ )
2739
2874
  });
2740
2875
  } else {
2741
2876
  this.logger.entity(name).error(
2742
2877
  "Required component failed to start, rolling back: {{error.message}}",
2743
2878
  {
2744
2879
  params: {
2745
- error: result.error || new Error(result.reason || "Unknown error")
2880
+ error: result.error || new Error(
2881
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2882
+ )
2746
2883
  }
2747
2884
  }
2748
2885
  );
@@ -2759,6 +2896,25 @@ var LifecycleManager = class extends EventEmitterProtected {
2759
2896
  };
2760
2897
  }
2761
2898
  }
2899
+ const unexpectedStopResult2 = this.consumeUnexpectedStopsDuringStartup(
2900
+ startedComponents,
2901
+ failedOptionalComponents
2902
+ );
2903
+ startedComponents.splice(0, startedComponents.length);
2904
+ startedComponents.push(...unexpectedStopResult2.startedComponents);
2905
+ if (unexpectedStopResult2.requiredFailure) {
2906
+ await this.rollbackStartup(startedComponents);
2907
+ return {
2908
+ success: false,
2909
+ startedComponents: [],
2910
+ failedOptionalComponents,
2911
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2912
+ reason: unexpectedStopResult2.requiredFailure.error.message,
2913
+ code: "component_unexpected_stop",
2914
+ error: unexpectedStopResult2.requiredFailure.error,
2915
+ durationMS: Date.now() - startTime
2916
+ };
2917
+ }
2762
2918
  }
2763
2919
  if (hasTimedOut) {
2764
2920
  const durationMS2 = Date.now() - startTime;
@@ -2782,6 +2938,25 @@ var LifecycleManager = class extends EventEmitterProtected {
2782
2938
  code: "startup_timeout"
2783
2939
  };
2784
2940
  }
2941
+ const unexpectedStopResult = this.consumeUnexpectedStopsDuringStartup(
2942
+ startedComponents,
2943
+ failedOptionalComponents
2944
+ );
2945
+ startedComponents.splice(0, startedComponents.length);
2946
+ startedComponents.push(...unexpectedStopResult.startedComponents);
2947
+ if (unexpectedStopResult.requiredFailure) {
2948
+ await this.rollbackStartup(startedComponents);
2949
+ return {
2950
+ success: false,
2951
+ startedComponents: [],
2952
+ failedOptionalComponents,
2953
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2954
+ reason: unexpectedStopResult.requiredFailure.error.message,
2955
+ code: "component_unexpected_stop",
2956
+ error: unexpectedStopResult.requiredFailure.error,
2957
+ durationMS: Date.now() - startTime
2958
+ };
2959
+ }
2785
2960
  this.updateStartedFlag();
2786
2961
  const skippedComponentsArray = [
2787
2962
  ...Array.from(skippedDueToDependency),
@@ -2813,10 +2988,12 @@ var LifecycleManager = class extends EventEmitterProtected {
2813
2988
  if (timeoutHandle) {
2814
2989
  clearTimeout(timeoutHandle);
2815
2990
  }
2816
- if (didAutoAttachSignalsForBulkStartup) {
2991
+ this.isStarting = false;
2992
+ if (didAutoAttachSignalsForBulkStartup || this.autoAttachedSignalsDuringStartup) {
2817
2993
  this.autoDetachSignalsIfIdle("failed bulk startup");
2818
2994
  }
2819
- this.isStarting = false;
2995
+ this.autoAttachedSignalsDuringStartup = false;
2996
+ this.unexpectedStopsDuringStartup.clear();
2820
2997
  }
2821
2998
  }
2822
2999
  /**
@@ -2880,7 +3057,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2880
3057
  return {
2881
3058
  success: false,
2882
3059
  componentName: name,
2883
- reason: "Bulk startup in progress",
3060
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS,
2884
3061
  code: "startup_in_progress"
2885
3062
  };
2886
3063
  }
@@ -2891,7 +3068,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2891
3068
  return {
2892
3069
  success: false,
2893
3070
  componentName: name,
2894
- reason: "Shutdown in progress",
3071
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2895
3072
  code: "shutdown_in_progress"
2896
3073
  };
2897
3074
  }
@@ -2925,7 +3102,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2925
3102
  return {
2926
3103
  success: false,
2927
3104
  componentName: name,
2928
- reason: this.isStarting ? "Bulk startup in progress" : "Shutdown in progress",
3105
+ reason: this.isStarting ? LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS : LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2929
3106
  code: this.isStarting ? "startup_in_progress" : "shutdown_in_progress"
2930
3107
  };
2931
3108
  }
@@ -3024,6 +3201,51 @@ var LifecycleManager = class extends EventEmitterProtected {
3024
3201
  shutdownMethod: this.shutdownMethod
3025
3202
  };
3026
3203
  }
3204
+ /**
3205
+ * Get status information about repeated shutdown escalation configuration and runtime state.
3206
+ */
3207
+ getShutdownEscalationStatus() {
3208
+ if (this.repeatedShutdownRequestPolicy === void 0) {
3209
+ return {
3210
+ configured: false,
3211
+ isShuttingDown: this.isShuttingDown,
3212
+ isArmed: false,
3213
+ forceAfterCount: null,
3214
+ withinMS: null,
3215
+ armedAfterFailureMS: null,
3216
+ armedAfterFailureMSSource: null,
3217
+ requestCount: 0,
3218
+ firstMethod: null,
3219
+ latestMethod: null,
3220
+ firstRequestAt: null,
3221
+ latestRequestAt: null,
3222
+ repeatedWindowStartedAt: null,
3223
+ armedUntil: null,
3224
+ hasTriggeredForceShutdown: false
3225
+ };
3226
+ }
3227
+ this.normalizeRepeatedShutdownRequestStateArmedStatus();
3228
+ const armedUntil = this.repeatedShutdownRequestState.remainsArmedUntil;
3229
+ const isArmed = armedUntil !== null;
3230
+ return {
3231
+ configured: true,
3232
+ isShuttingDown: this.isShuttingDown,
3233
+ isArmed,
3234
+ forceAfterCount: this.repeatedShutdownRequestPolicy.forceAfterCount,
3235
+ withinMS: this.repeatedShutdownRequestPolicy.withinMS,
3236
+ armedAfterFailureMS: this.repeatedShutdownRequestPolicy.armedAfterFailureMS,
3237
+ armedAfterFailureMSSource: this.repeatedShutdownRequestPolicy.hasExplicitArmedAfterFailureMS ? "explicit" : "derived",
3238
+ countManualRetriesTowardEscalation: this.repeatedShutdownRequestPolicy.countManualRetriesTowardEscalation,
3239
+ requestCount: this.repeatedShutdownRequestState.requestCount,
3240
+ firstMethod: this.repeatedShutdownRequestState.firstMethod,
3241
+ latestMethod: this.repeatedShutdownRequestState.latestMethod,
3242
+ firstRequestAt: this.repeatedShutdownRequestState.firstRequestAt,
3243
+ latestRequestAt: this.repeatedShutdownRequestState.latestRequestAt,
3244
+ repeatedWindowStartedAt: this.repeatedShutdownRequestState.repeatedWindowStartedAt,
3245
+ armedUntil: isArmed ? armedUntil : null,
3246
+ hasTriggeredForceShutdown: this.repeatedShutdownRequestState.hasTriggeredForceShutdown
3247
+ };
3248
+ }
3027
3249
  /**
3028
3250
  * Enable Logger exit hook integration
3029
3251
  *
@@ -3062,7 +3284,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3062
3284
  if (this.isShuttingDown) {
3063
3285
  if (isFirstExit && this.pendingLoggerExitResolve === null) {
3064
3286
  this.logger.debug(
3065
- "Logger exit called during shutdown, waiting...",
3287
+ LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN,
3066
3288
  {
3067
3289
  params: { exitCode }
3068
3290
  }
@@ -3071,7 +3293,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3071
3293
  this.pendingLoggerExitResolve = resolve;
3072
3294
  });
3073
3295
  }
3074
- this.logger.debug("Logger exit called during shutdown, waiting...", {
3296
+ this.logger.debug(LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN, {
3075
3297
  params: { exitCode }
3076
3298
  });
3077
3299
  return { action: "wait" };
@@ -3161,7 +3383,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3161
3383
  return {
3162
3384
  name,
3163
3385
  healthy: false,
3164
- message: "Component not found",
3386
+ message: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
3165
3387
  checkedAt: startTime,
3166
3388
  durationMS: 0,
3167
3389
  error: null,
@@ -3174,7 +3396,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3174
3396
  return {
3175
3397
  name,
3176
3398
  healthy: false,
3177
- message: isStalled ? "Component is stalled" : "Component not running",
3399
+ message: isStalled ? LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED : LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
3178
3400
  checkedAt: startTime,
3179
3401
  durationMS: Date.now() - startTime,
3180
3402
  error: null,
@@ -3390,7 +3612,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3390
3612
  result = component.onMessage(payload, from);
3391
3613
  } catch (error) {
3392
3614
  const err = error instanceof Error ? error : new Error(String(error));
3393
- this.logger.entity(componentName).error("Message handler failed: {{error.message}}", {
3615
+ this.logger.entity(componentName).error(LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED, {
3394
3616
  params: { error: err, from }
3395
3617
  });
3396
3618
  this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
@@ -3450,7 +3672,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3450
3672
  };
3451
3673
  } catch (error) {
3452
3674
  const err = error instanceof Error ? error : new Error(String(error));
3453
- this.logger.entity(componentName).error("Message handler failed: {{error.message}}", {
3675
+ this.logger.entity(componentName).error(LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED, {
3454
3676
  params: { error: err, from, timeoutMS }
3455
3677
  });
3456
3678
  this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
@@ -3723,7 +3945,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3723
3945
  this.lifecycleEvents.componentRegistrationRejected({
3724
3946
  name: componentName,
3725
3947
  reason: "shutdown_in_progress",
3726
- message: "Cannot register component while shutdown is in progress (isShuttingDown=true).",
3948
+ message: LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS,
3727
3949
  registrationIndexBefore,
3728
3950
  registrationIndexAfter: registrationIndexBefore,
3729
3951
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3735,7 +3957,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3735
3957
  targetComponentName,
3736
3958
  registrationIndexBefore,
3737
3959
  code: "shutdown_in_progress",
3738
- reason: "Cannot register component while shutdown is in progress (isShuttingDown=true).",
3960
+ reason: LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS,
3739
3961
  targetFound: void 0
3740
3962
  });
3741
3963
  }
@@ -3746,7 +3968,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3746
3968
  this.lifecycleEvents.componentRegistrationRejected({
3747
3969
  name: componentName,
3748
3970
  reason: "startup_in_progress",
3749
- message: "Cannot register component during startup when it is a required dependency for other components.",
3971
+ message: LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP,
3750
3972
  registrationIndexBefore,
3751
3973
  registrationIndexAfter: registrationIndexBefore,
3752
3974
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3758,7 +3980,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3758
3980
  targetComponentName,
3759
3981
  registrationIndexBefore,
3760
3982
  code: "startup_in_progress",
3761
- reason: "Cannot register component during startup when it is a required dependency for other components.",
3983
+ reason: LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP,
3762
3984
  targetFound: void 0
3763
3985
  });
3764
3986
  }
@@ -3767,7 +3989,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3767
3989
  this.lifecycleEvents.componentRegistrationRejected({
3768
3990
  name: componentName,
3769
3991
  reason: "duplicate_instance",
3770
- message: "Component instance is already registered.",
3992
+ message: LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE,
3771
3993
  registrationIndexBefore,
3772
3994
  registrationIndexAfter: registrationIndexBefore,
3773
3995
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3779,7 +4001,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3779
4001
  targetComponentName,
3780
4002
  registrationIndexBefore,
3781
4003
  code: "duplicate_instance",
3782
- reason: "Component instance is already registered.",
4004
+ reason: LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE,
3783
4005
  targetFound: void 0
3784
4006
  });
3785
4007
  }
@@ -4049,9 +4271,26 @@ var LifecycleManager = class extends EventEmitterProtected {
4049
4271
  code: "already_in_progress"
4050
4272
  };
4051
4273
  }
4274
+ this.normalizeRepeatedShutdownRequestStateArmedStatus();
4275
+ const repeatedShutdownPolicy = this.repeatedShutdownRequestPolicy;
4276
+ const isManualRetryWhileArmed = repeatedShutdownPolicy !== void 0 && method === "manual" && this.repeatedShutdownRequestState.firstRequestAt !== null && this.repeatedShutdownRequestState.remainsArmedUntil !== null;
4277
+ if (isManualRetryWhileArmed) {
4278
+ if (repeatedShutdownPolicy.countManualRetriesTowardEscalation) {
4279
+ this.handleRepeatedShutdownRequest(method);
4280
+ } else {
4281
+ this.resetRepeatedShutdownRequestState();
4282
+ }
4283
+ }
4284
+ if (this.repeatedShutdownRequestState.remainsArmedUntil !== null) {
4285
+ this.clearRepeatedShutdownExpiryTimer();
4286
+ this.repeatedShutdownRequestState.remainsArmedUntil = null;
4287
+ }
4052
4288
  this.isShuttingDown = true;
4053
4289
  this.shutdownToken = (0, import_ulid2.ulid)();
4054
4290
  this.shutdownMethod = method;
4291
+ if (this.repeatedShutdownRequestPolicy && this.repeatedShutdownRequestState.firstRequestAt === null) {
4292
+ this.seedRepeatedShutdownRequestState(method);
4293
+ }
4055
4294
  const isDuringStartup = this.isStarting;
4056
4295
  this.logger.info("Stopping all components", { params: { method } });
4057
4296
  this.lifecycleEvents.lifecycleManagerShutdownInitiated(
@@ -4076,8 +4315,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4076
4315
  const runningComponentsToStop = shutdownOrder.filter(
4077
4316
  (name) => this.isComponentRunning(name) || shouldRetryStalled && stalledComponentNames.has(name)
4078
4317
  );
4079
- const stoppedComponents = [];
4080
- const stalledComponents = [];
4318
+ const stoppedComponents = /* @__PURE__ */ new Set();
4081
4319
  let hasTimedOut = false;
4082
4320
  let timeoutHandle;
4083
4321
  try {
@@ -4108,34 +4346,37 @@ var LifecycleManager = class extends EventEmitterProtected {
4108
4346
  this.logger.entity(name).info("Stopping component");
4109
4347
  const isRunning = this.isComponentRunning(name);
4110
4348
  const isStalled = stalledComponentNames.has(name);
4349
+ const currentState = this.componentStates.get(name);
4350
+ if (currentState === "stopped") {
4351
+ stoppedComponents.add(name);
4352
+ continue;
4353
+ }
4111
4354
  const result2 = isRunning ? await this.stopComponentInternal(name) : shouldRetryStalled && isStalled ? await this.retryStalledComponent(name) : isStalled ? {
4112
4355
  success: false,
4113
4356
  componentName: name,
4114
- reason: "Component is stalled",
4357
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4115
4358
  code: "component_stalled",
4116
4359
  status: this.getComponentStatus(name)
4117
4360
  } : {
4118
4361
  success: false,
4119
4362
  componentName: name,
4120
- reason: "Component not running",
4363
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4121
4364
  code: "component_not_running",
4122
4365
  status: this.getComponentStatus(name)
4123
4366
  };
4124
4367
  if (result2.success) {
4125
- stoppedComponents.push(name);
4368
+ stoppedComponents.add(name);
4126
4369
  } else {
4127
4370
  this.logger.entity(name).error(
4128
4371
  "Component failed to stop, continuing with others: {{error.message}}",
4129
4372
  {
4130
4373
  params: {
4131
- error: result2.error || new Error(result2.reason || "Unknown error")
4374
+ error: result2.error || new Error(
4375
+ result2.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
4376
+ )
4132
4377
  }
4133
4378
  }
4134
4379
  );
4135
- const stallInfo = this.stalledComponents.get(name);
4136
- if (stallInfo) {
4137
- stalledComponents.push(stallInfo);
4138
- }
4139
4380
  if (shouldHaltOnStall) {
4140
4381
  this.logger.warn(
4141
4382
  "Halting shutdown after stall (haltOnStall=true)",
@@ -4151,18 +4392,40 @@ var LifecycleManager = class extends EventEmitterProtected {
4151
4392
  } else {
4152
4393
  await shutdownOperation();
4153
4394
  }
4395
+ const finalStalledNames = /* @__PURE__ */ new Set();
4396
+ for (const name of runningComponentsToStop) {
4397
+ if (this.stalledComponents.has(name)) {
4398
+ finalStalledNames.add(name);
4399
+ }
4400
+ }
4401
+ if (!shouldRetryStalled) {
4402
+ for (const name of stalledComponentNames) {
4403
+ if (this.stalledComponents.has(name)) {
4404
+ finalStalledNames.add(name);
4405
+ }
4406
+ }
4407
+ }
4408
+ for (const name of runningComponentsToStop) {
4409
+ if (!finalStalledNames.has(name) && this.componentStates.get(name) === "stopped") {
4410
+ stoppedComponents.add(name);
4411
+ }
4412
+ }
4413
+ const stalledComponents = Array.from(finalStalledNames).map((name) => this.stalledComponents.get(name)).filter((stallInfo) => !!stallInfo);
4154
4414
  const durationMS = Date.now() - startTime;
4155
4415
  const isSuccess = !hasTimedOut && stalledComponents.length === 0;
4156
- this.logger[isSuccess ? "success" : "warn"]("Shutdown completed", {
4157
- params: {
4158
- stopped: stoppedComponents.length,
4159
- stalled: stalledComponents.length,
4160
- durationMS
4416
+ this.logger[isSuccess ? "success" : "warn"](
4417
+ isSuccess ? "Shutdown completed successfully" : "Shutdown attempt completed with stalled components or timeout",
4418
+ {
4419
+ params: {
4420
+ stopped: stoppedComponents.size,
4421
+ stalled: stalledComponents.length,
4422
+ durationMS
4423
+ }
4161
4424
  }
4162
- });
4425
+ );
4163
4426
  const result = {
4164
4427
  success: isSuccess,
4165
- stoppedComponents,
4428
+ stoppedComponents: Array.from(stoppedComponents),
4166
4429
  stalledComponents,
4167
4430
  durationMS,
4168
4431
  timedOut: hasTimedOut || void 0,
@@ -4177,6 +4440,11 @@ var LifecycleManager = class extends EventEmitterProtected {
4177
4440
  method,
4178
4441
  duringStartup: isDuringStartup
4179
4442
  });
4443
+ if (isSuccess) {
4444
+ this.resetRepeatedShutdownRequestState();
4445
+ } else {
4446
+ this.armRepeatedShutdownAfterFailure();
4447
+ }
4180
4448
  return result;
4181
4449
  } finally {
4182
4450
  if (timeoutHandle) {
@@ -4208,7 +4476,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4208
4476
  return {
4209
4477
  success: false,
4210
4478
  componentName: name,
4211
- reason: "Component not found",
4479
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4212
4480
  code: "component_not_found"
4213
4481
  };
4214
4482
  }
@@ -4219,12 +4487,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4219
4487
  return {
4220
4488
  success: false,
4221
4489
  componentName: name,
4222
- reason: "Component not running",
4490
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4223
4491
  code: "component_not_running",
4224
4492
  status: this.getComponentStatus(name)
4225
4493
  };
4226
4494
  }
4227
4495
  this.logger.entity(name).warn("Retrying stalled component shutdown (force phase)");
4496
+ if (component.onShutdownForce) {
4497
+ this.issueStopAttemptToken(name);
4498
+ }
4228
4499
  return this.shutdownComponentForce(name, component, {
4229
4500
  gracefulPhaseRan: false,
4230
4501
  gracefulTimedOut: false,
@@ -4244,7 +4515,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4244
4515
  return {
4245
4516
  success: false,
4246
4517
  componentName: name,
4247
- reason: "Shutdown in progress",
4518
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
4248
4519
  code: "shutdown_in_progress"
4249
4520
  };
4250
4521
  }
@@ -4256,7 +4527,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4256
4527
  return {
4257
4528
  success: false,
4258
4529
  componentName: name,
4259
- reason: "Bulk startup in progress",
4530
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS,
4260
4531
  code: "startup_in_progress"
4261
4532
  };
4262
4533
  }
@@ -4266,7 +4537,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4266
4537
  return {
4267
4538
  success: false,
4268
4539
  componentName: name,
4269
- reason: "Component not found",
4540
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4270
4541
  code: "component_not_found"
4271
4542
  };
4272
4543
  }
@@ -4275,7 +4546,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4275
4546
  return {
4276
4547
  success: false,
4277
4548
  componentName: name,
4278
- reason: "Component is stalled",
4549
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4279
4550
  code: "component_stalled",
4280
4551
  status: this.getComponentStatus(name)
4281
4552
  };
@@ -4339,6 +4610,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4339
4610
  const timeoutMS = component.startupTimeoutMS;
4340
4611
  const startAttemptToken = (0, import_ulid2.ulid)();
4341
4612
  this.componentStartAttemptTokens.set(name, startAttemptToken);
4613
+ component._setUnexpectedStopHandler(
4614
+ (error) => this.handleComponentUnexpectedStop(name, startAttemptToken, error)
4615
+ );
4342
4616
  const shutdownTokenAtStart = this.shutdownToken;
4343
4617
  const didAutoAttachSignalsForComponentStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("component startup") : false;
4344
4618
  let timeoutHandle;
@@ -4381,6 +4655,18 @@ var LifecycleManager = class extends EventEmitterProtected {
4381
4655
  } else {
4382
4656
  await startPromise;
4383
4657
  }
4658
+ if (this.componentStartAttemptTokens.get(name) === startAttemptToken && this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
4659
+ component._clearUnexpectedStopHandler();
4660
+ const error = this.componentErrors.get(name) ?? new Error(`Component "${name}" stopped unexpectedly during startup`);
4661
+ return {
4662
+ success: false,
4663
+ componentName: name,
4664
+ reason: error.message,
4665
+ code: "component_unexpected_stop",
4666
+ error,
4667
+ status: this.getComponentStatus(name)
4668
+ };
4669
+ }
4384
4670
  if (this.isShuttingDown || shutdownTokenAtStart !== this.shutdownToken) {
4385
4671
  this.componentStates.set(name, "running");
4386
4672
  this.runningComponents.add(name);
@@ -4408,6 +4694,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4408
4694
  this.componentStates.set(name, "running");
4409
4695
  this.runningComponents.add(name);
4410
4696
  this.stalledComponents.delete(name);
4697
+ if (shouldForceStalled) {
4698
+ this.issueStopAttemptToken(name);
4699
+ }
4411
4700
  this.updateStartedFlag();
4412
4701
  if (this.attachSignalsOnStart && this.runningComponents.size === 1) {
4413
4702
  this.autoAttachSignals("first component start");
@@ -4427,9 +4716,24 @@ var LifecycleManager = class extends EventEmitterProtected {
4427
4716
  status: this.getComponentStatus(name)
4428
4717
  };
4429
4718
  } catch (error) {
4719
+ component._clearUnexpectedStopHandler();
4430
4720
  const err = error instanceof Error ? error : new Error(String(error));
4721
+ const isStartupTimeout = err instanceof ComponentStartTimeoutError && err.additionalInfo.componentName === name;
4722
+ const unexpectedStopError = this.componentErrors.get(name);
4723
+ if (this.componentStartAttemptTokens.get(name) === startAttemptToken && this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name) && (isStartupTimeout || unexpectedStopError instanceof Error)) {
4724
+ return {
4725
+ success: false,
4726
+ componentName: name,
4727
+ reason: unexpectedStopError?.message || `Component "${name}" stopped unexpectedly during startup`,
4728
+ code: "component_unexpected_stop",
4729
+ error: unexpectedStopError || new Error(
4730
+ `Component "${name}" stopped unexpectedly during startup`
4731
+ ),
4732
+ status: this.getComponentStatus(name)
4733
+ };
4734
+ }
4431
4735
  this.componentErrors.set(name, err);
4432
- if (err instanceof ComponentStartTimeoutError && err.additionalInfo.componentName === name) {
4736
+ if (isStartupTimeout) {
4433
4737
  this.componentStates.set(name, "starting-timed-out");
4434
4738
  this.logger.entity(name).error("Component startup timed out: {{error.message}}", {
4435
4739
  params: { error: err }
@@ -4474,7 +4778,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4474
4778
  return {
4475
4779
  success: false,
4476
4780
  componentName: name,
4477
- reason: "Component not found",
4781
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4478
4782
  code: "component_not_found"
4479
4783
  };
4480
4784
  }
@@ -4482,7 +4786,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4482
4786
  return {
4483
4787
  success: false,
4484
4788
  componentName: name,
4485
- reason: "Component is stalled",
4789
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4486
4790
  code: "component_stalled",
4487
4791
  status: this.getComponentStatus(name)
4488
4792
  };
@@ -4491,7 +4795,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4491
4795
  return {
4492
4796
  success: false,
4493
4797
  componentName: name,
4494
- reason: "Component not running",
4798
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4495
4799
  code: "component_not_running",
4496
4800
  status: this.getComponentStatus(name)
4497
4801
  };
@@ -4507,6 +4811,10 @@ var LifecycleManager = class extends EventEmitterProtected {
4507
4811
  };
4508
4812
  }
4509
4813
  if (options?.forceImmediate) {
4814
+ if (component.onShutdownForce) {
4815
+ this.issueStopAttemptToken(name);
4816
+ }
4817
+ component._clearUnexpectedStopHandler();
4510
4818
  return this.shutdownComponentForce(name, component, {
4511
4819
  gracefulPhaseRan: false,
4512
4820
  gracefulTimedOut: false,
@@ -4643,9 +4951,11 @@ var LifecycleManager = class extends EventEmitterProtected {
4643
4951
  * Calls stop() with timeout
4644
4952
  */
4645
4953
  async shutdownComponentGraceful(name, component, options) {
4954
+ component._clearUnexpectedStopHandler();
4646
4955
  this.componentStates.set(name, "stopping");
4647
4956
  this.logger.entity(name).info("Graceful shutdown started");
4648
4957
  this.lifecycleEvents.componentStopping(name);
4958
+ const stopAttemptToken = this.issueStopAttemptToken(name);
4649
4959
  const timeoutMS = options?.timeout ?? component.shutdownGracefulTimeoutMS;
4650
4960
  let timeoutHandle;
4651
4961
  try {
@@ -4666,7 +4976,16 @@ var LifecycleManager = class extends EventEmitterProtected {
4666
4976
  );
4667
4977
  }
4668
4978
  }
4669
- Promise.resolve(stopPromise).catch(() => {
4979
+ Promise.resolve(stopPromise).then(
4980
+ () => this.handleLateStopResolution(
4981
+ name,
4982
+ stopAttemptToken,
4983
+ "graceful"
4984
+ ),
4985
+ () => {
4986
+ }
4987
+ // Intentionally ignore errors after timeout
4988
+ ).catch(() => {
4670
4989
  });
4671
4990
  reject(
4672
4991
  new ComponentStopTimeoutError({
@@ -4685,9 +5004,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4685
5004
  this.stalledComponents.delete(name);
4686
5005
  this.updateStartedFlag();
4687
5006
  if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
4688
- this.logger.info(
4689
- "Auto-detaching process signals on last component stop"
4690
- );
5007
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
4691
5008
  this.detachSignals();
4692
5009
  }
4693
5010
  const timestamps = this.componentTimestamps.get(name) ?? {
@@ -4710,15 +5027,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4710
5027
  const err = error instanceof Error ? error : new Error(String(error));
4711
5028
  this.componentErrors.set(name, err);
4712
5029
  if (err instanceof ComponentStopTimeoutError && err.additionalInfo.componentName === name) {
4713
- this.logger.entity(name).warn("Graceful shutdown timed out");
5030
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT);
4714
5031
  this.lifecycleEvents.componentStopTimeout(name, err, {
4715
5032
  timeoutMS,
4716
- reason: "Graceful shutdown timed out"
5033
+ reason: LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT
4717
5034
  });
4718
5035
  return {
4719
5036
  success: false,
4720
5037
  componentName: name,
4721
- reason: "Graceful shutdown timed out",
5038
+ reason: LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT,
4722
5039
  code: "component_shutdown_timeout",
4723
5040
  error: err,
4724
5041
  status: this.getComponentStatus(name)
@@ -4761,8 +5078,6 @@ var LifecycleManager = class extends EventEmitterProtected {
4761
5078
  gracefulTimedOut: context.gracefulTimedOut
4762
5079
  }
4763
5080
  });
4764
- const timeoutMS = component.shutdownForceTimeoutMS;
4765
- let timeoutHandle;
4766
5081
  if (!component.onShutdownForce) {
4767
5082
  const stallInfo = {
4768
5083
  name,
@@ -4796,6 +5111,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4796
5111
  status: this.getComponentStatus(name)
4797
5112
  };
4798
5113
  }
5114
+ const timeoutMS = component.shutdownForceTimeoutMS;
5115
+ const { promise: stoppedDuringForcePromise, cleanup: cleanupForceWaiter } = this.createPendingForceStopWaiter(name);
5116
+ let timeoutHandle;
4799
5117
  try {
4800
5118
  const forcePromise = component.onShutdownForce();
4801
5119
  if (timeoutMS > 0) {
@@ -4814,23 +5132,44 @@ var LifecycleManager = class extends EventEmitterProtected {
4814
5132
  );
4815
5133
  }
4816
5134
  }
4817
- Promise.resolve(forcePromise).catch(() => {
5135
+ const forceAttemptToken = this.componentStopAttemptTokens.get(name) ?? (0, import_ulid2.ulid)();
5136
+ Promise.resolve(forcePromise).then(
5137
+ () => this.handleLateStopResolution(
5138
+ name,
5139
+ forceAttemptToken,
5140
+ "force"
5141
+ ),
5142
+ () => {
5143
+ }
5144
+ // Intentionally ignore errors after timeout
5145
+ ).catch(() => {
4818
5146
  });
4819
- reject(new Error("Force shutdown timed out"));
5147
+ reject(
5148
+ new Error(LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT)
5149
+ );
4820
5150
  }, timeoutMS);
4821
5151
  });
4822
- await Promise.race([forcePromise, timeoutPromise]);
5152
+ await Promise.race([
5153
+ forcePromise,
5154
+ timeoutPromise,
5155
+ stoppedDuringForcePromise
5156
+ ]);
4823
5157
  } else {
4824
- await forcePromise;
5158
+ await Promise.race([forcePromise, stoppedDuringForcePromise]);
5159
+ }
5160
+ if (this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
5161
+ return {
5162
+ success: true,
5163
+ componentName: name,
5164
+ status: this.getComponentStatus(name)
5165
+ };
4825
5166
  }
4826
5167
  this.componentStates.set(name, "stopped");
4827
5168
  this.runningComponents.delete(name);
4828
5169
  this.stalledComponents.delete(name);
4829
5170
  this.updateStartedFlag();
4830
5171
  if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
4831
- this.logger.info(
4832
- "Auto-detaching process signals on last component stop"
4833
- );
5172
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
4834
5173
  this.detachSignals();
4835
5174
  }
4836
5175
  const timestamps = this.componentTimestamps.get(name) ?? {
@@ -4851,8 +5190,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4851
5190
  status: this.getComponentStatus(name)
4852
5191
  };
4853
5192
  } catch (error) {
5193
+ if (this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
5194
+ return {
5195
+ success: true,
5196
+ componentName: name,
5197
+ status: this.getComponentStatus(name)
5198
+ };
5199
+ }
4854
5200
  const err = error instanceof Error ? error : new Error(String(error));
4855
- const isTimeout = err.message === "Force shutdown timed out";
5201
+ const isTimeout = err.message === LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT;
4856
5202
  const stallInfo = {
4857
5203
  name,
4858
5204
  phase: "force",
@@ -4883,12 +5229,13 @@ var LifecycleManager = class extends EventEmitterProtected {
4883
5229
  return {
4884
5230
  success: false,
4885
5231
  componentName: name,
4886
- reason: isTimeout ? "Force shutdown timed out" : err.message,
5232
+ reason: isTimeout ? LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT : err.message,
4887
5233
  code: isTimeout ? "component_shutdown_timeout" : "unknown_error",
4888
5234
  error: err,
4889
5235
  status: this.getComponentStatus(name)
4890
5236
  };
4891
5237
  } finally {
5238
+ cleanupForceWaiter();
4892
5239
  if (timeoutHandle) {
4893
5240
  clearTimeout(timeoutHandle);
4894
5241
  }
@@ -4965,7 +5312,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4965
5312
  "Failed to stop component during rollback, continuing: {{error.message}}",
4966
5313
  {
4967
5314
  params: {
4968
- error: result.error || new Error(result.reason || "Unknown error")
5315
+ error: result.error || new Error(
5316
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
5317
+ )
4969
5318
  }
4970
5319
  }
4971
5320
  );
@@ -4979,10 +5328,13 @@ var LifecycleManager = class extends EventEmitterProtected {
4979
5328
  }
4980
5329
  this.logger.info(`Auto-attaching process signals on ${trigger}`);
4981
5330
  this.attachSignals();
5331
+ if (this.isStarting) {
5332
+ this.autoAttachedSignalsDuringStartup = true;
5333
+ }
4982
5334
  return true;
4983
5335
  }
4984
5336
  autoDetachSignalsIfIdle(trigger) {
4985
- if (!this.detachSignalsOnStop || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
5337
+ if (!this.detachSignalsOnStop || this.isStarting || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
4986
5338
  return;
4987
5339
  }
4988
5340
  this.logger.info(`Auto-detaching process signals after ${trigger}`);
@@ -5024,6 +5376,210 @@ var LifecycleManager = class extends EventEmitterProtected {
5024
5376
  }).catch(() => {
5025
5377
  });
5026
5378
  }
5379
+ consumeUnexpectedStopsDuringStartup(startedComponents, failedOptionalComponents) {
5380
+ if (this.unexpectedStopsDuringStartup.size === 0) {
5381
+ return { startedComponents: [...startedComponents] };
5382
+ }
5383
+ const remainingStartedComponents = [];
5384
+ let requiredFailure;
5385
+ for (const name of startedComponents) {
5386
+ const startupStopError = this.unexpectedStopsDuringStartup.get(name);
5387
+ if (startupStopError === void 0) {
5388
+ remainingStartedComponents.push(name);
5389
+ continue;
5390
+ }
5391
+ this.unexpectedStopsDuringStartup.delete(name);
5392
+ const error = startupStopError ?? new Error(`Component "${name}" stopped unexpectedly during startup`);
5393
+ const component = this.getComponent(name);
5394
+ if (component?.isOptional()) {
5395
+ if (!failedOptionalComponents.some((entry) => entry.name === name)) {
5396
+ failedOptionalComponents.push({ name, error });
5397
+ }
5398
+ this.logger.entity(name).warn(
5399
+ LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
5400
+ {
5401
+ params: { error }
5402
+ }
5403
+ );
5404
+ continue;
5405
+ }
5406
+ this.logger.entity(name).error(
5407
+ LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
5408
+ {
5409
+ params: { error }
5410
+ }
5411
+ );
5412
+ requiredFailure ??= { name, error };
5413
+ }
5414
+ return {
5415
+ startedComponents: remainingStartedComponents,
5416
+ requiredFailure
5417
+ };
5418
+ }
5419
+ /**
5420
+ * Issues and returns a unique stop attempt token for a component.
5421
+ *
5422
+ * Each stop attempt (graceful or force-retry) gets a unique token.
5423
+ * The late-resolution handler captures this token in its closure so it can
5424
+ * skip any stall entries that were created by a *later* stop attempt — e.g. a
5425
+ * force-retry that also timed out after the original graceful promise floated
5426
+ * in the background.
5427
+ */
5428
+ issueStopAttemptToken(name) {
5429
+ const next = (0, import_ulid2.ulid)();
5430
+ this.componentStopAttemptTokens.set(name, next);
5431
+ return next;
5432
+ }
5433
+ createPendingForceStopWaiter(name) {
5434
+ let isResolved = false;
5435
+ let waiters = this.pendingForceStopWaiters.get(name);
5436
+ if (!waiters) {
5437
+ waiters = /* @__PURE__ */ new Set();
5438
+ this.pendingForceStopWaiters.set(name, waiters);
5439
+ }
5440
+ let resolveWaiter;
5441
+ const promise = new Promise((resolve) => {
5442
+ resolveWaiter = () => {
5443
+ if (isResolved) {
5444
+ return;
5445
+ }
5446
+ isResolved = true;
5447
+ resolve();
5448
+ };
5449
+ });
5450
+ waiters.add(resolveWaiter);
5451
+ return {
5452
+ promise,
5453
+ cleanup: () => {
5454
+ const pending = this.pendingForceStopWaiters.get(name);
5455
+ if (!pending) {
5456
+ return;
5457
+ }
5458
+ pending.delete(resolveWaiter);
5459
+ if (pending.size === 0) {
5460
+ this.pendingForceStopWaiters.delete(name);
5461
+ }
5462
+ }
5463
+ };
5464
+ }
5465
+ resolvePendingForceStopWaiters(name) {
5466
+ const waiters = this.pendingForceStopWaiters.get(name);
5467
+ if (!waiters || waiters.size === 0) {
5468
+ return;
5469
+ }
5470
+ this.pendingForceStopWaiters.delete(name);
5471
+ for (const resolve of waiters) {
5472
+ resolve();
5473
+ }
5474
+ }
5475
+ /**
5476
+ * Called when a stop promise eventually resolves after its timeout path already fired.
5477
+ *
5478
+ * Usually this means a previously stalled component's original stop() or
5479
+ * onShutdownForce() promise finally resolved, so the manager can clear the
5480
+ * stall and transition the component to stopped without a manual retry.
5481
+ *
5482
+ * There is one extra overlap case for graceful stop(): stop() can resolve
5483
+ * after the graceful timeout but before onShutdownForce() itself times out.
5484
+ * In that window no stall entry exists yet, but the component still finished
5485
+ * stopping cleanly, so we finalize it here and let the later force-timeout
5486
+ * path observe the already-stopped state and no-op. This overlap fix is
5487
+ * scoped to the same stop token and will not cross a later retry attempt.
5488
+ *
5489
+ * Two guards prevent stale floating promises from incorrectly clearing state:
5490
+ *
5491
+ * 1. token guard — if a newer stop attempt (e.g. a retryStalled
5492
+ * force-retry) has started since this promise was launched, its token
5493
+ * won't match and we bail out immediately.
5494
+ *
5495
+ * 2. state/stall guard — if the component was unregistered, restarted, or
5496
+ * already cleared by another path, there will be neither a matching stall
5497
+ * entry nor the force-phase overlap state, so we bail out.
5498
+ */
5499
+ handleLateStopResolution(name, token, source) {
5500
+ if (this.componentStopAttemptTokens.get(name) !== token) {
5501
+ return;
5502
+ }
5503
+ const currentState = this.componentStates.get(name);
5504
+ const stallInfo = this.stalledComponents.get(name);
5505
+ const isCompletedDuringForcePhase = source === "graceful" && !stallInfo && currentState === "force-stopping";
5506
+ if (stallInfo && currentState !== "stalled") {
5507
+ this.stalledComponents.delete(name);
5508
+ return;
5509
+ }
5510
+ if (!stallInfo && !isCompletedDuringForcePhase) {
5511
+ return;
5512
+ }
5513
+ const stalledDurationMS = stallInfo ? Date.now() - stallInfo.stalledAt : void 0;
5514
+ if (stallInfo) {
5515
+ this.stalledComponents.delete(name);
5516
+ }
5517
+ this.componentStates.set(name, "stopped");
5518
+ this.runningComponents.delete(name);
5519
+ this.componentErrors.set(name, null);
5520
+ this.updateStartedFlag();
5521
+ this.resolvePendingForceStopWaiters(name);
5522
+ if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
5523
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
5524
+ this.detachSignals();
5525
+ }
5526
+ const timestamps = this.componentTimestamps.get(name) ?? {
5527
+ startedAt: null,
5528
+ stoppedAt: null
5529
+ };
5530
+ timestamps.stoppedAt = Date.now();
5531
+ this.componentTimestamps.set(name, timestamps);
5532
+ this.logger.entity(name).info(
5533
+ stallInfo ? "Stalled component completed stop late, stall cleared" : "Graceful stop completed after force phase started",
5534
+ stalledDurationMS ? { params: { stalledDurationMS } } : void 0
5535
+ );
5536
+ if (source === "force") {
5537
+ this.lifecycleEvents.componentShutdownForceCompleted(name);
5538
+ }
5539
+ if (stallInfo && stalledDurationMS !== void 0) {
5540
+ this.lifecycleEvents.componentStalledResolved(
5541
+ name,
5542
+ stallInfo,
5543
+ stalledDurationMS
5544
+ );
5545
+ }
5546
+ this.lifecycleEvents.componentStopped(name, this.getComponentStatus(name));
5547
+ }
5548
+ handleComponentUnexpectedStop(name, startAttemptToken, error) {
5549
+ const currentState = this.componentStates.get(name);
5550
+ if (
5551
+ // Startup-time self-stops are valid too: start() may still be awaiting
5552
+ // some async work while an internal listener has already observed that
5553
+ // the component died and reported it.
5554
+ currentState !== "starting" && currentState !== "running" || this.componentStartAttemptTokens.get(name) !== startAttemptToken
5555
+ ) {
5556
+ return false;
5557
+ }
5558
+ this.runningComponents.delete(name);
5559
+ this.componentStates.set(name, "stopped");
5560
+ this.componentErrors.set(name, error ?? null);
5561
+ if (this.isStarting) {
5562
+ this.unexpectedStopsDuringStartup.set(name, error ?? null);
5563
+ }
5564
+ this.updateStartedFlag();
5565
+ if (this.detachSignalsOnStop && !this.isStarting && this.runningComponents.size === 0 && this.processSignalManager) {
5566
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
5567
+ this.detachSignals();
5568
+ }
5569
+ const timestamps = this.componentTimestamps.get(name) ?? {
5570
+ startedAt: null,
5571
+ stoppedAt: null
5572
+ };
5573
+ timestamps.stoppedAt = Date.now();
5574
+ this.componentTimestamps.set(name, timestamps);
5575
+ this.logger.entity(name).warn(
5576
+ error ? `Component stopped unexpectedly: ${error.message}` : "Component stopped unexpectedly",
5577
+ { params: { error } }
5578
+ );
5579
+ this.lifecycleEvents.componentUnexpectedStop(name, error);
5580
+ this.lifecycleEvents.componentStopped(name, this.getComponentStatus(name));
5581
+ return true;
5582
+ }
5027
5583
  /**
5028
5584
  * Safe emit wrapper - prevents event handler errors from breaking lifecycle
5029
5585
  */
@@ -5305,121 +5861,377 @@ var LifecycleManager = class extends EventEmitterProtected {
5305
5861
  }
5306
5862
  /**
5307
5863
  * Handle shutdown signal - initiates stopAllComponents().
5308
- * Double signal protection: if already shutting down, log warning and ignore.
5864
+ *
5865
+ * Four cases depending on the current shutdown state:
5866
+ *
5867
+ * 1. **Active shutdown** (`isShuttingDown = true`): escalate through the
5868
+ * repeated-shutdown policy if configured, otherwise log and discard.
5869
+ * Emits `signal:shutdown` with `isAlreadyShuttingDown: true` and returns
5870
+ * without starting another shutdown.
5871
+ *
5872
+ * 2. **Armed post-failure** (previous shutdown finished, armed window still
5873
+ * open): count the request toward the escalation window, emit
5874
+ * `signal:shutdown` with `isAlreadyShuttingDown: false`, then start a
5875
+ * new `stopAllComponents()` run to retry.
5876
+ *
5877
+ * 3. **Armed post-failure expired** (armed window opened but has since
5878
+ * elapsed): expire the stale state, treat the request as a fresh
5879
+ * shutdown (falls through to case 4).
5880
+ *
5881
+ * 4. **Fresh shutdown** (no prior shutdown state): seed escalation tracking
5882
+ * if policy is configured, emit `signal:shutdown` with
5883
+ * `isAlreadyShuttingDown: false`, and start `stopAllComponents()`.
5884
+ *
5885
+ * In all cases `signal:shutdown` is emitted exactly once.
5309
5886
  */
5310
5887
  handleShutdownRequest(method) {
5311
5888
  if (this.isShuttingDown) {
5312
- this.logger.warn("Shutdown already in progress, ignoring signal", {
5313
- params: { method }
5314
- });
5315
- return;
5889
+ this.lifecycleEvents.signalShutdown(method, true);
5890
+ if (this.handleRepeatedShutdownRequest(method)) {
5891
+ return;
5892
+ }
5893
+ }
5894
+ let didEmitShutdownSignal = false;
5895
+ let shouldSeedRepeatedShutdownState = this.repeatedShutdownRequestPolicy !== void 0;
5896
+ if (this.repeatedShutdownRequestPolicy && this.repeatedShutdownRequestState.firstRequestAt !== null && this.normalizeRepeatedShutdownRequestStateArmedStatus()) {
5897
+ this.lifecycleEvents.signalShutdown(method, false);
5898
+ didEmitShutdownSignal = true;
5899
+ shouldSeedRepeatedShutdownState = false;
5900
+ this.handleRepeatedShutdownRequest(method);
5901
+ }
5902
+ if (shouldSeedRepeatedShutdownState) {
5903
+ this.seedRepeatedShutdownRequestState(method);
5316
5904
  }
5317
5905
  this.logger.info("Shutdown signal received", { params: { method } });
5318
- this.lifecycleEvents.signalShutdown(method);
5906
+ if (!didEmitShutdownSignal) {
5907
+ this.lifecycleEvents.signalShutdown(method, false);
5908
+ }
5319
5909
  void this.stopAllComponentsInternal(method, {
5320
5910
  ...this.shutdownOptions
5321
5911
  });
5322
5912
  }
5323
5913
  /**
5324
- * Handle reload request - calls custom callback or broadcasts to components.
5325
- *
5326
- * When called from signal handlers (source='signal'), the Promise is started
5327
- * but not awaited due to Node.js signal handler constraints. Components are
5328
- * still notified and the work completes, but return values are not accessible.
5329
- *
5330
- * When called from manual triggers (source='trigger'), the Promise is awaited
5331
- * and results are returned for programmatic use.
5914
+ * Tracks repeated shutdown requests during an active shutdown and optionally
5915
+ * invokes the configured force shutdown callback when the threshold is reached.
5332
5916
  *
5333
- * @param source - Whether triggered from signal manager or manual trigger
5917
+ * @returns true when the request was consumed as part of the repeated-shutdown
5918
+ * escalation flow, false when the caller should treat it as a fresh shutdown request
5334
5919
  */
5335
- async handleReloadRequest(source = "trigger") {
5336
- this.logger.info("Reload request received", { params: { source } });
5337
- this.lifecycleEvents.signalReload();
5338
- if (this.onReloadRequested) {
5339
- const broadcastFn = () => this.broadcastReload();
5340
- const result = this.onReloadRequested(broadcastFn);
5341
- if (isPromise(result)) {
5342
- await result;
5343
- }
5344
- return {
5345
- signal: "reload",
5346
- results: [],
5347
- timedOut: false,
5348
- code: "ok"
5349
- };
5920
+ handleRepeatedShutdownRequest(method) {
5921
+ const policy = this.repeatedShutdownRequestPolicy;
5922
+ if (!policy) {
5923
+ this.logger.warn("Shutdown already in progress, ignoring signal", {
5924
+ params: { method }
5925
+ });
5926
+ return true;
5350
5927
  }
5351
- return this.broadcastReload();
5928
+ const now = Date.now();
5929
+ const state = this.repeatedShutdownRequestState;
5930
+ if (state.remainsArmedUntil !== null) {
5931
+ if (now >= state.remainsArmedUntil) {
5932
+ this.expireRepeatedShutdownRequestState();
5933
+ return false;
5934
+ }
5935
+ this.refreshRepeatedShutdownArmedWindow(now);
5936
+ }
5937
+ const shouldStartNewWindow = state.repeatedWindowStartedAt === null || now - state.repeatedWindowStartedAt > policy.withinMS;
5938
+ if (shouldStartNewWindow) {
5939
+ state.requestCount = 1;
5940
+ state.repeatedWindowStartedAt = now;
5941
+ } else {
5942
+ state.requestCount++;
5943
+ }
5944
+ state.latestMethod = method;
5945
+ state.latestRequestAt = now;
5946
+ this.logger.warn(
5947
+ this.isShuttingDown ? "Shutdown already in progress, tracking repeated signal" : "Previous shutdown attempt finished with stalled components or timeout, escalation window still armed, tracking repeated request",
5948
+ {
5949
+ params: {
5950
+ method,
5951
+ requestCount: state.requestCount,
5952
+ firstMethod: state.firstMethod,
5953
+ latestMethod: state.latestMethod,
5954
+ firstRequestAt: state.firstRequestAt,
5955
+ latestRequestAt: state.latestRequestAt,
5956
+ repeatedWindowStartedAt: state.repeatedWindowStartedAt,
5957
+ remainsArmedUntil: state.remainsArmedUntil,
5958
+ withinMS: policy.withinMS,
5959
+ forceAfterCount: policy.forceAfterCount
5960
+ }
5961
+ }
5962
+ );
5963
+ if (
5964
+ // Force escalation is single-fire per shutdown cycle. Later requests are
5965
+ // still logged but do not re-enter user force-shutdown logic.
5966
+ state.hasTriggeredForceShutdown || state.requestCount < policy.forceAfterCount || state.firstMethod === null || state.firstRequestAt === null || state.latestMethod === null || state.latestRequestAt === null
5967
+ ) {
5968
+ return true;
5969
+ }
5970
+ state.hasTriggeredForceShutdown = true;
5971
+ const context = {
5972
+ requestCount: state.requestCount,
5973
+ firstMethod: state.firstMethod,
5974
+ latestMethod: state.latestMethod,
5975
+ firstRequestAt: state.firstRequestAt,
5976
+ latestRequestAt: state.latestRequestAt,
5977
+ isShuttingDown: this.isShuttingDown,
5978
+ wasArmedAfterFailure: state.remainsArmedUntil !== null
5979
+ };
5980
+ this.logger.warn(
5981
+ "Repeated shutdown request threshold reached, invoking force shutdown handler",
5982
+ {
5983
+ params: {
5984
+ method,
5985
+ requestCount: context.requestCount,
5986
+ firstMethod: context.firstMethod,
5987
+ latestMethod: context.latestMethod,
5988
+ firstRequestAt: context.firstRequestAt,
5989
+ latestRequestAt: context.latestRequestAt,
5990
+ repeatedWindowStartedAt: state.repeatedWindowStartedAt,
5991
+ remainsArmedUntil: state.remainsArmedUntil,
5992
+ withinMS: policy.withinMS,
5993
+ forceAfterCount: policy.forceAfterCount
5994
+ }
5995
+ }
5996
+ );
5997
+ safeHandleCallback(
5998
+ "repeatedShutdownRequestPolicy.onForceShutdown",
5999
+ policy.onForceShutdown,
6000
+ context
6001
+ );
6002
+ this.lifecycleEvents.lifecycleManagerShutdownEscalationForced({
6003
+ firstMethod: context.firstMethod,
6004
+ latestMethod: context.latestMethod,
6005
+ requestCount: context.requestCount,
6006
+ firstRequestAt: context.firstRequestAt,
6007
+ latestRequestAt: context.latestRequestAt,
6008
+ wasArmedAfterFailure: context.wasArmedAfterFailure
6009
+ });
6010
+ return true;
5352
6011
  }
5353
6012
  /**
5354
- * Handle info request - calls custom callback or broadcasts to components.
5355
- *
5356
- * When called from signal handlers, the Promise executes but return values
5357
- * are not accessible due to Node.js signal handler constraints.
6013
+ * Clears repeated shutdown request tracking so a new shutdown cycle starts fresh.
6014
+ */
6015
+ resetRepeatedShutdownRequestState() {
6016
+ this.clearRepeatedShutdownExpiryTimer();
6017
+ this.repeatedShutdownRequestState = {
6018
+ requestCount: 0,
6019
+ firstMethod: null,
6020
+ latestMethod: null,
6021
+ firstRequestAt: null,
6022
+ latestRequestAt: null,
6023
+ repeatedWindowStartedAt: null,
6024
+ hasTriggeredForceShutdown: false,
6025
+ remainsArmedUntil: null
6026
+ };
6027
+ }
6028
+ /**
6029
+ * Clear any pending expiration timer for the post-failure escalation window.
6030
+ */
6031
+ clearRepeatedShutdownExpiryTimer() {
6032
+ if (this.repeatedShutdownExpiryTimer === null) {
6033
+ return;
6034
+ }
6035
+ clearTimeout(this.repeatedShutdownExpiryTimer);
6036
+ this.repeatedShutdownExpiryTimer = null;
6037
+ }
6038
+ /**
6039
+ * Returns whether post-failure escalation remains armed after first
6040
+ * normalizing any stale timer-backed state.
5358
6041
  *
5359
- * @param source - Whether triggered from signal manager or manual trigger
6042
+ * The method can expire old armed windows as a side effect because the timer
6043
+ * callback may not have run yet on a delayed event loop. Callers use this
6044
+ * when they need the effective runtime truth, not just the last timer write.
5360
6045
  */
5361
- async handleInfoRequest(source = "trigger") {
5362
- this.logger.info("Info request received", { params: { source } });
5363
- this.lifecycleEvents.signalInfo();
5364
- if (this.onInfoRequested) {
5365
- const broadcastFn = () => this.broadcastInfo();
5366
- const result = this.onInfoRequested(broadcastFn);
5367
- if (isPromise(result)) {
5368
- await result;
6046
+ normalizeRepeatedShutdownRequestStateArmedStatus(now = Date.now()) {
6047
+ const armedUntil = this.repeatedShutdownRequestState.remainsArmedUntil;
6048
+ if (armedUntil === null) {
6049
+ return false;
6050
+ }
6051
+ if (now >= armedUntil) {
6052
+ this.expireRepeatedShutdownRequestState();
6053
+ return false;
6054
+ }
6055
+ return true;
6056
+ }
6057
+ /**
6058
+ * Transition armed post-failure escalation state into its expired/reset state.
6059
+ */
6060
+ expireRepeatedShutdownRequestState() {
6061
+ const policy = this.repeatedShutdownRequestPolicy;
6062
+ const state = this.repeatedShutdownRequestState;
6063
+ if (!policy || state.remainsArmedUntil === null) {
6064
+ return;
6065
+ }
6066
+ const armedUntil = state.remainsArmedUntil;
6067
+ this.clearRepeatedShutdownExpiryTimer();
6068
+ const expiredState = {
6069
+ firstMethod: state.firstMethod,
6070
+ latestMethod: state.latestMethod,
6071
+ requestCount: state.requestCount,
6072
+ armedUntil
6073
+ };
6074
+ this.logger.warn(
6075
+ "Repeated shutdown escalation window expired, clearing previous shutdown state",
6076
+ {
6077
+ params: {
6078
+ remainsArmedUntil: armedUntil,
6079
+ withinMS: policy.withinMS,
6080
+ forceAfterCount: policy.forceAfterCount
6081
+ }
5369
6082
  }
5370
- return {
5371
- signal: "info",
5372
- results: [],
5373
- timedOut: false,
5374
- code: "ok"
5375
- };
6083
+ );
6084
+ if (expiredState.firstMethod !== null) {
6085
+ this.lifecycleEvents.lifecycleManagerShutdownEscalationExpired({
6086
+ firstMethod: expiredState.firstMethod,
6087
+ latestMethod: expiredState.latestMethod,
6088
+ requestCount: expiredState.requestCount,
6089
+ armedUntil: expiredState.armedUntil
6090
+ });
5376
6091
  }
5377
- return this.broadcastInfo();
6092
+ this.resetRepeatedShutdownRequestState();
5378
6093
  }
5379
6094
  /**
5380
- * Handle debug request - calls custom callback or broadcasts to components.
6095
+ * Arms or refreshes the post-failure escalation window and its expiration timer.
6096
+ */
6097
+ refreshRepeatedShutdownArmedWindow(now = Date.now()) {
6098
+ const policy = this.repeatedShutdownRequestPolicy;
6099
+ if (!policy) {
6100
+ return;
6101
+ }
6102
+ this.clearRepeatedShutdownExpiryTimer();
6103
+ const armedUntil = now + policy.armedAfterFailureMS;
6104
+ this.repeatedShutdownRequestState.remainsArmedUntil = armedUntil;
6105
+ this.repeatedShutdownExpiryTimer = setTimeout(() => {
6106
+ this.expireRepeatedShutdownRequestState();
6107
+ }, policy.armedAfterFailureMS);
6108
+ this.repeatedShutdownExpiryTimer.unref();
6109
+ }
6110
+ /**
6111
+ * Seeds shutdown escalation tracking for a new shutdown cycle.
5381
6112
  *
5382
- * When called from signal handlers, the Promise executes but return values
5383
- * are not accessible due to Node.js signal handler constraints.
6113
+ * The first shutdown trigger starts graceful shutdown and arms escalation with
6114
+ * an effective post-start count of 0. Later shutdown requests can then count
6115
+ * toward the configured force threshold regardless of whether the shutdown
6116
+ * started from a signal, keyboard shortcut, or direct API call.
6117
+ */
6118
+ seedRepeatedShutdownRequestState(method) {
6119
+ const now = Date.now();
6120
+ this.repeatedShutdownRequestState = {
6121
+ requestCount: 0,
6122
+ firstMethod: method,
6123
+ latestMethod: method,
6124
+ firstRequestAt: now,
6125
+ latestRequestAt: now,
6126
+ repeatedWindowStartedAt: null,
6127
+ hasTriggeredForceShutdown: false,
6128
+ remainsArmedUntil: null
6129
+ };
6130
+ }
6131
+ /**
6132
+ * Preserves a short-lived post-failure escalation window after shutdown
6133
+ * returns unsuccessfully so operators can keep pressing shutdown without
6134
+ * losing the existing force count the moment the graceful attempt finishes.
6135
+ */
6136
+ armRepeatedShutdownAfterFailure() {
6137
+ const policy = this.repeatedShutdownRequestPolicy;
6138
+ const state = this.repeatedShutdownRequestState;
6139
+ if (!policy || policy.armedAfterFailureMS <= 0 || // armedAfterFailureMS = 0 disables post-failure arming
6140
+ state.firstRequestAt === null || state.hasTriggeredForceShutdown) {
6141
+ return;
6142
+ }
6143
+ this.refreshRepeatedShutdownArmedWindow();
6144
+ const armedUntil = state.remainsArmedUntil;
6145
+ if (state.firstMethod !== null && armedUntil !== null) {
6146
+ this.lifecycleEvents.lifecycleManagerShutdownEscalationArmed({
6147
+ firstMethod: state.firstMethod,
6148
+ requestCount: state.requestCount,
6149
+ armedUntil
6150
+ });
6151
+ }
6152
+ }
6153
+ /**
6154
+ * Shared dispatch path for reload/info/debug requests. Logs the dispatch,
6155
+ * emits the signal event, then either invokes the user-supplied callback
6156
+ * (passing the broadcast function so the user controls when/whether to
6157
+ * broadcast) or broadcasts directly when no callback is configured.
5384
6158
  *
5385
- * @param source - Whether triggered from signal manager or manual trigger
6159
+ * When called from signal handlers (source='signal'), the Promise is started
6160
+ * but not awaited — Node.js signal handlers cannot return values, so results
6161
+ * are not accessible. Components are still notified and the work completes.
6162
+ * When called from manual triggers (source='trigger'), the Promise is awaited
6163
+ * and results are returned for programmatic use.
5386
6164
  */
5387
- async handleDebugRequest(source = "trigger") {
5388
- this.logger.info("Debug request received", { params: { source } });
5389
- this.lifecycleEvents.signalDebug();
5390
- if (this.onDebugRequested) {
5391
- const broadcastFn = () => this.broadcastDebug();
5392
- const result = this.onDebugRequested(broadcastFn);
6165
+ async handleSignalRequest(descriptor, source) {
6166
+ this.logger.info(descriptor.dispatchedLogLabel, { params: { source } });
6167
+ descriptor.emitSignal();
6168
+ if (descriptor.customCallback) {
6169
+ const result = descriptor.customCallback(descriptor.broadcast);
5393
6170
  if (isPromise(result)) {
5394
6171
  await result;
5395
6172
  }
5396
6173
  return {
5397
- signal: "debug",
6174
+ signal: descriptor.signal,
5398
6175
  results: [],
5399
6176
  timedOut: false,
5400
6177
  code: "ok"
5401
6178
  };
5402
6179
  }
5403
- return this.broadcastDebug();
6180
+ return descriptor.broadcast();
6181
+ }
6182
+ async handleReloadRequest(source = "trigger") {
6183
+ return this.handleSignalRequest(
6184
+ {
6185
+ signal: "reload",
6186
+ dispatchedLogLabel: "Reload dispatched",
6187
+ emitSignal: () => this.lifecycleEvents.signalReload(),
6188
+ customCallback: this.onReloadRequested,
6189
+ broadcast: () => this.broadcastReload()
6190
+ },
6191
+ source
6192
+ );
6193
+ }
6194
+ async handleInfoRequest(source = "trigger") {
6195
+ return this.handleSignalRequest(
6196
+ {
6197
+ signal: "info",
6198
+ dispatchedLogLabel: "Info dispatched",
6199
+ emitSignal: () => this.lifecycleEvents.signalInfo(),
6200
+ customCallback: this.onInfoRequested,
6201
+ broadcast: () => this.broadcastInfo()
6202
+ },
6203
+ source
6204
+ );
6205
+ }
6206
+ async handleDebugRequest(source = "trigger") {
6207
+ return this.handleSignalRequest(
6208
+ {
6209
+ signal: "debug",
6210
+ dispatchedLogLabel: "Debug dispatched",
6211
+ emitSignal: () => this.lifecycleEvents.signalDebug(),
6212
+ customCallback: this.onDebugRequested,
6213
+ broadcast: () => this.broadcastDebug()
6214
+ },
6215
+ source
6216
+ );
5404
6217
  }
5405
6218
  /**
5406
- * Broadcast reload signal to all running components.
5407
- * Calls onReload() on components that implement it.
5408
- * Continues on errors - collects all results.
6219
+ * Shared signal broadcast pipeline used by reload/info/debug.
6220
+ * Iterates running components, runs the picked handler with timeout, and
6221
+ * aggregates per-component results into a SignalBroadcastResult.
5409
6222
  */
5410
- async broadcastReload() {
6223
+ async runSignalBroadcast(descriptor) {
5411
6224
  const results = [];
5412
- const componentsToReload = this.components.filter(
6225
+ const targets = this.components.filter(
5413
6226
  (component) => this.runningComponents.has(component.getName())
5414
6227
  );
5415
6228
  if (this.isStarting) {
5416
- this.logger.info(
5417
- "Reload during startup: only reloading already-started components"
5418
- );
6229
+ this.logger.info(descriptor.startupLog);
5419
6230
  }
5420
- for (const component of componentsToReload) {
6231
+ for (const component of targets) {
5421
6232
  const name = component.getName();
5422
- if (!component.onReload) {
6233
+ const handler = descriptor.pickHandler(component);
6234
+ if (!handler) {
5423
6235
  results.push({
5424
6236
  name,
5425
6237
  called: false,
@@ -5429,13 +6241,13 @@ var LifecycleManager = class extends EventEmitterProtected {
5429
6241
  });
5430
6242
  continue;
5431
6243
  }
5432
- this.lifecycleEvents.componentReloadStarted(name);
6244
+ descriptor.emitStarted(name);
5433
6245
  const timeoutMS = component.signalTimeoutMS;
5434
6246
  let timeoutHandle;
5435
6247
  const timeoutResult = { timedOut: true };
5436
6248
  try {
5437
- const result = component.onReload();
5438
- const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
6249
+ const handlerResult = handler();
6250
+ const handlerPromise = isPromise(handlerResult) ? handlerResult : Promise.resolve(handlerResult);
5439
6251
  const outcome = timeoutMS > 0 ? await Promise.race([
5440
6252
  handlerPromise,
5441
6253
  new Promise((resolve) => {
@@ -5445,7 +6257,7 @@ var LifecycleManager = class extends EventEmitterProtected {
5445
6257
  })
5446
6258
  ]) : await handlerPromise;
5447
6259
  if (outcome === timeoutResult) {
5448
- this.logger.entity(name).warn("Reload handler timed out", {
6260
+ this.logger.entity(name).warn(descriptor.timeoutLog, {
5449
6261
  params: { timeoutMS }
5450
6262
  });
5451
6263
  Promise.resolve(handlerPromise).catch(() => {
@@ -5458,7 +6270,7 @@ var LifecycleManager = class extends EventEmitterProtected {
5458
6270
  code: "timeout"
5459
6271
  });
5460
6272
  } else {
5461
- this.lifecycleEvents.componentReloadCompleted(name);
6273
+ descriptor.emitCompleted(name);
5462
6274
  results.push({
5463
6275
  name,
5464
6276
  called: true,
@@ -5469,10 +6281,10 @@ var LifecycleManager = class extends EventEmitterProtected {
5469
6281
  }
5470
6282
  } catch (error) {
5471
6283
  const err = error instanceof Error ? error : new Error(String(error));
5472
- this.logger.entity(name).error("Reload failed: {{error.message}}", {
6284
+ this.logger.entity(name).error(descriptor.errorLog, {
5473
6285
  params: { error: err }
5474
6286
  });
5475
- this.lifecycleEvents.componentReloadFailed(name, err);
6287
+ descriptor.emitFailed(name, err);
5476
6288
  results.push({
5477
6289
  name,
5478
6290
  called: true,
@@ -5493,108 +6305,45 @@ var LifecycleManager = class extends EventEmitterProtected {
5493
6305
  const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
5494
6306
  const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
5495
6307
  return {
5496
- signal: "reload",
6308
+ signal: descriptor.signal,
5497
6309
  results,
5498
6310
  timedOut: hasTimeout,
5499
6311
  code
5500
6312
  };
5501
6313
  }
6314
+ /**
6315
+ * Broadcast reload signal to all running components.
6316
+ * Calls onReload() on components that implement it.
6317
+ * Continues on errors - collects all results.
6318
+ */
6319
+ async broadcastReload() {
6320
+ return this.runSignalBroadcast({
6321
+ signal: "reload",
6322
+ pickHandler: (component) => component.onReload?.bind(component),
6323
+ startupLog: "Reload during startup: only reloading already-started components",
6324
+ timeoutLog: "Reload handler timed out",
6325
+ errorLog: "Reload failed: {{error.message}}",
6326
+ emitStarted: (name) => this.lifecycleEvents.componentReloadStarted(name),
6327
+ emitCompleted: (name) => this.lifecycleEvents.componentReloadCompleted(name),
6328
+ emitFailed: (name, error) => this.lifecycleEvents.componentReloadFailed(name, error)
6329
+ });
6330
+ }
5502
6331
  /**
5503
6332
  * Broadcast info signal to all running components.
5504
6333
  * Calls onInfo() on components that implement it.
5505
6334
  * Continues on errors - collects all results.
5506
6335
  */
5507
6336
  async broadcastInfo() {
5508
- const results = [];
5509
- const componentsToNotify = this.components.filter(
5510
- (component) => this.runningComponents.has(component.getName())
5511
- );
5512
- if (this.isStarting) {
5513
- this.logger.info(
5514
- "Info during startup: only notifying already-started components"
5515
- );
5516
- }
5517
- for (const component of componentsToNotify) {
5518
- const name = component.getName();
5519
- if (!component.onInfo) {
5520
- results.push({
5521
- name,
5522
- called: false,
5523
- error: null,
5524
- timedOut: false,
5525
- code: "no_handler"
5526
- });
5527
- continue;
5528
- }
5529
- this.lifecycleEvents.componentInfoStarted(name);
5530
- const timeoutMS = component.signalTimeoutMS;
5531
- let timeoutHandle;
5532
- const timeoutResult = { timedOut: true };
5533
- try {
5534
- const result = component.onInfo();
5535
- const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
5536
- const outcome = timeoutMS > 0 ? await Promise.race([
5537
- handlerPromise,
5538
- new Promise((resolve) => {
5539
- timeoutHandle = setTimeout(() => {
5540
- resolve(timeoutResult);
5541
- }, timeoutMS);
5542
- })
5543
- ]) : await handlerPromise;
5544
- if (outcome === timeoutResult) {
5545
- this.logger.entity(name).warn("Info handler timed out", {
5546
- params: { timeoutMS }
5547
- });
5548
- Promise.resolve(handlerPromise).catch(() => {
5549
- });
5550
- results.push({
5551
- name,
5552
- called: true,
5553
- error: null,
5554
- timedOut: true,
5555
- code: "timeout"
5556
- });
5557
- } else {
5558
- this.lifecycleEvents.componentInfoCompleted(name);
5559
- results.push({
5560
- name,
5561
- called: true,
5562
- error: null,
5563
- timedOut: false,
5564
- code: "called"
5565
- });
5566
- }
5567
- } catch (error) {
5568
- const err = error instanceof Error ? error : new Error(String(error));
5569
- this.logger.entity(name).error("Info handler failed: {{error.message}}", {
5570
- params: { error: err }
5571
- });
5572
- this.lifecycleEvents.componentInfoFailed(name, err);
5573
- results.push({
5574
- name,
5575
- called: true,
5576
- error: err,
5577
- timedOut: false,
5578
- code: "error"
5579
- });
5580
- } finally {
5581
- if (timeoutHandle) {
5582
- clearTimeout(timeoutHandle);
5583
- }
5584
- }
5585
- }
5586
- const calledResults = results.filter((result) => result.called);
5587
- const hasError = calledResults.some((result) => result.error);
5588
- const isAllError = calledResults.length > 0 && calledResults.every((result) => result.error);
5589
- const hasTimeout = calledResults.some((result) => result.timedOut);
5590
- const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
5591
- const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
5592
- return {
6337
+ return this.runSignalBroadcast({
5593
6338
  signal: "info",
5594
- results,
5595
- timedOut: hasTimeout,
5596
- code
5597
- };
6339
+ pickHandler: (component) => component.onInfo?.bind(component),
6340
+ startupLog: "Info during startup: only notifying already-started components",
6341
+ timeoutLog: "Info handler timed out",
6342
+ errorLog: "Info handler failed: {{error.message}}",
6343
+ emitStarted: (name) => this.lifecycleEvents.componentInfoStarted(name),
6344
+ emitCompleted: (name) => this.lifecycleEvents.componentInfoCompleted(name),
6345
+ emitFailed: (name, error) => this.lifecycleEvents.componentInfoFailed(name, error)
6346
+ });
5598
6347
  }
5599
6348
  /**
5600
6349
  * Broadcast debug signal to all running components.
@@ -5602,96 +6351,16 @@ var LifecycleManager = class extends EventEmitterProtected {
5602
6351
  * Continues on errors - collects all results.
5603
6352
  */
5604
6353
  async broadcastDebug() {
5605
- const results = [];
5606
- const componentsToNotify = this.components.filter(
5607
- (component) => this.runningComponents.has(component.getName())
5608
- );
5609
- if (this.isStarting) {
5610
- this.logger.info(
5611
- "Debug during startup: only notifying already-started components"
5612
- );
5613
- }
5614
- for (const component of componentsToNotify) {
5615
- const name = component.getName();
5616
- if (!component.onDebug) {
5617
- results.push({
5618
- name,
5619
- called: false,
5620
- error: null,
5621
- timedOut: false,
5622
- code: "no_handler"
5623
- });
5624
- continue;
5625
- }
5626
- this.lifecycleEvents.componentDebugStarted(name);
5627
- const timeoutMS = component.signalTimeoutMS;
5628
- let timeoutHandle;
5629
- const timeoutResult = { timedOut: true };
5630
- try {
5631
- const result = component.onDebug();
5632
- const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
5633
- const outcome = timeoutMS > 0 ? await Promise.race([
5634
- handlerPromise,
5635
- new Promise((resolve) => {
5636
- timeoutHandle = setTimeout(() => {
5637
- resolve(timeoutResult);
5638
- }, timeoutMS);
5639
- })
5640
- ]) : await handlerPromise;
5641
- if (outcome === timeoutResult) {
5642
- this.logger.entity(name).warn("Debug handler timed out", {
5643
- params: { timeoutMS }
5644
- });
5645
- Promise.resolve(handlerPromise).catch(() => {
5646
- });
5647
- results.push({
5648
- name,
5649
- called: true,
5650
- error: null,
5651
- timedOut: true,
5652
- code: "timeout"
5653
- });
5654
- } else {
5655
- this.lifecycleEvents.componentDebugCompleted(name);
5656
- results.push({
5657
- name,
5658
- called: true,
5659
- error: null,
5660
- timedOut: false,
5661
- code: "called"
5662
- });
5663
- }
5664
- } catch (error) {
5665
- const err = error instanceof Error ? error : new Error(String(error));
5666
- this.logger.entity(name).error("Debug handler failed: {{error.message}}", {
5667
- params: { error: err }
5668
- });
5669
- this.lifecycleEvents.componentDebugFailed(name, err);
5670
- results.push({
5671
- name,
5672
- called: true,
5673
- error: err,
5674
- timedOut: false,
5675
- code: "error"
5676
- });
5677
- } finally {
5678
- if (timeoutHandle) {
5679
- clearTimeout(timeoutHandle);
5680
- }
5681
- }
5682
- }
5683
- const calledResults = results.filter((result) => result.called);
5684
- const hasError = calledResults.some((result) => result.error);
5685
- const isAllError = calledResults.length > 0 && calledResults.every((result) => result.error);
5686
- const hasTimeout = calledResults.some((result) => result.timedOut);
5687
- const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
5688
- const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
5689
- return {
6354
+ return this.runSignalBroadcast({
5690
6355
  signal: "debug",
5691
- results,
5692
- timedOut: hasTimeout,
5693
- code
5694
- };
6356
+ pickHandler: (component) => component.onDebug?.bind(component),
6357
+ startupLog: "Debug during startup: only notifying already-started components",
6358
+ timeoutLog: "Debug handler timed out",
6359
+ errorLog: "Debug handler failed: {{error.message}}",
6360
+ emitStarted: (name) => this.lifecycleEvents.componentDebugStarted(name),
6361
+ emitCompleted: (name) => this.lifecycleEvents.componentDebugCompleted(name),
6362
+ emitFailed: (name, error) => this.lifecycleEvents.componentDebugFailed(name, error)
6363
+ });
5695
6364
  }
5696
6365
  };
5697
6366
 
@@ -5717,6 +6386,10 @@ var BaseComponent = class {
5717
6386
  name;
5718
6387
  /** Reference to component-scoped lifecycle (set by manager when registered) */
5719
6388
  lifecycle;
6389
+ /** @internal Set by LifecycleManager while the component is running. */
6390
+ _unexpectedStopHandler;
6391
+ /** @internal Incremented whenever the unexpected-stop handler is re-armed or cleared. */
6392
+ _unexpectedStopGeneration = 0;
5720
6393
  /**
5721
6394
  * Create a new component
5722
6395
  *
@@ -5751,6 +6424,24 @@ var BaseComponent = class {
5751
6424
  // Default if undefined/null/non-finite
5752
6425
  );
5753
6426
  }
6427
+ /** @internal Called by LifecycleManager after a successful start. */
6428
+ _setUnexpectedStopHandler(handler) {
6429
+ this._unexpectedStopGeneration += 1;
6430
+ this._unexpectedStopHandler = handler;
6431
+ const generation = this._unexpectedStopGeneration;
6432
+ this.reportUnexpectedStop = (error) => {
6433
+ if (this._unexpectedStopGeneration !== generation) {
6434
+ return false;
6435
+ }
6436
+ return this._unexpectedStopHandler?.(error) ?? false;
6437
+ };
6438
+ }
6439
+ /** @internal Called by LifecycleManager when stop begins or component is unregistered. */
6440
+ _clearUnexpectedStopHandler() {
6441
+ this._unexpectedStopGeneration += 1;
6442
+ this._unexpectedStopHandler = void 0;
6443
+ this.reportUnexpectedStop = () => false;
6444
+ }
5754
6445
  /**
5755
6446
  * Get component name
5756
6447
  */
@@ -5769,6 +6460,38 @@ var BaseComponent = class {
5769
6460
  isOptional() {
5770
6461
  return this.optional;
5771
6462
  }
6463
+ /**
6464
+ * Run-scoped unexpected-stop callback. Rebound by LifecycleManager on each
6465
+ * successful start so captured references from older runs go stale.
6466
+ */
6467
+ reportUnexpectedStop = () => false;
6468
+ /**
6469
+ * Get this component's own status from the manager's perspective.
6470
+ *
6471
+ * Equivalent to `this.lifecycle.getComponentStatus(this.getName())` but without
6472
+ * needing to pass the name. Returns `undefined` if the component is not registered.
6473
+ *
6474
+ * Check `status?.state === 'running'` to test whether the component is currently running.
6475
+ */
6476
+ getSelfStatus() {
6477
+ return this.lifecycle?.getComponentStatus(this.name);
6478
+ }
6479
+ /**
6480
+ * Capture a run-scoped unexpected-stop reporter for async listeners created during start().
6481
+ *
6482
+ * Unlike calling `this.reportUnexpectedStop()` later, the returned callback becomes a no-op
6483
+ * once the component is stopped, unregistered, or restarted. This prevents stale listeners
6484
+ * from a previous run from stopping a newer run of the same component instance.
6485
+ */
6486
+ getUnexpectedStopReporter() {
6487
+ const generation = this._unexpectedStopGeneration;
6488
+ return (error) => {
6489
+ if (this._unexpectedStopGeneration !== generation) {
6490
+ return false;
6491
+ }
6492
+ return this._unexpectedStopHandler?.(error) ?? false;
6493
+ };
6494
+ }
5772
6495
  };
5773
6496
  // Annotate the CommonJS export names for ESM import in node:
5774
6497
  0 && (module.exports = {