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.
@@ -956,6 +956,9 @@ var ComponentLifecycle = class {
956
956
  getSignalStatus() {
957
957
  return this.manager.getSignalStatus();
958
958
  }
959
+ getShutdownEscalationStatus() {
960
+ return this.manager.getShutdownEscalationStatus();
961
+ }
959
962
  triggerReload() {
960
963
  return this.manager.triggerReload();
961
964
  }
@@ -1134,6 +1137,15 @@ var LifecycleManagerEvents = class {
1134
1137
  lifecycleManagerShutdownCompleted(input) {
1135
1138
  this.emit("lifecycle-manager:shutdown-completed", input);
1136
1139
  }
1140
+ lifecycleManagerShutdownEscalationArmed(input) {
1141
+ this.emit("lifecycle-manager:shutdown-escalation-armed", input);
1142
+ }
1143
+ lifecycleManagerShutdownEscalationExpired(input) {
1144
+ this.emit("lifecycle-manager:shutdown-escalation-expired", input);
1145
+ }
1146
+ lifecycleManagerShutdownEscalationForced(input) {
1147
+ this.emit("lifecycle-manager:shutdown-escalation-forced", input);
1148
+ }
1137
1149
  componentStarting(name) {
1138
1150
  this.emit("component:starting", { name });
1139
1151
  }
@@ -1201,6 +1213,16 @@ var LifecycleManagerEvents = class {
1201
1213
  code: info?.code
1202
1214
  });
1203
1215
  }
1216
+ componentStalledResolved(name, stallInfo, stalledDurationMS) {
1217
+ this.emit("component:stalled-resolved", {
1218
+ name,
1219
+ stallInfo,
1220
+ stalledDurationMS
1221
+ });
1222
+ }
1223
+ componentUnexpectedStop(name, error) {
1224
+ this.emit("component:unexpected-stop", { name, error });
1225
+ }
1204
1226
  componentShutdownForceCompleted(name) {
1205
1227
  this.emit("component:shutdown-force-completed", { name });
1206
1228
  }
@@ -1210,8 +1232,8 @@ var LifecycleManagerEvents = class {
1210
1232
  componentStartupRollback(name) {
1211
1233
  this.emit("component:startup-rollback", { name });
1212
1234
  }
1213
- signalShutdown(method) {
1214
- this.emit("signal:shutdown", { method });
1235
+ signalShutdown(method, isAlreadyShuttingDown = false) {
1236
+ this.emit("signal:shutdown", { method, isAlreadyShuttingDown });
1215
1237
  }
1216
1238
  signalReload() {
1217
1239
  this.emit("signal:reload", void 0);
@@ -1387,6 +1409,25 @@ var lifecycleManagerErrCodes = {
1387
1409
  StopTimeout: "StopTimeout"
1388
1410
  };
1389
1411
 
1412
+ // src/lib/lifecycle-manager/constants.ts
1413
+ var LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS = "Cannot unregister during bulk operation";
1414
+ var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND = "Component not found";
1415
+ var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING = "Component not running";
1416
+ var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED = "Component is stalled";
1417
+ var LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS = "Bulk startup in progress";
1418
+ var LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS = "Shutdown in progress";
1419
+ var LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR = "Unknown error";
1420
+ var LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP = "Auto-detaching process signals on last component stop";
1421
+ var LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN = "Logger exit called during shutdown, waiting...";
1422
+ var LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED = "Message handler failed: {{error.message}}";
1423
+ var LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT = "Graceful shutdown timed out";
1424
+ var LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT = "Force shutdown timed out";
1425
+ var LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP = "Optional component stopped unexpectedly during startup, continuing: {{error.message}}";
1426
+ var LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP = "Required component stopped unexpectedly during startup: {{error.message}}";
1427
+ var LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS = "Cannot register component while shutdown is in progress (isShuttingDown=true).";
1428
+ var LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP = "Cannot register component during startup when it is a required dependency for other components.";
1429
+ var LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE = "Component instance is already registered.";
1430
+
1390
1431
  // src/lib/process-signal-manager.ts
1391
1432
  import { ulid } from "ulid";
1392
1433
  import readline from "readline";
@@ -1921,6 +1962,7 @@ var LifecycleManager = class extends EventEmitterProtected {
1921
1962
  attachSignalsBeforeStartup;
1922
1963
  attachSignalsOnStart;
1923
1964
  detachSignalsOnStop;
1965
+ repeatedShutdownRequestPolicy;
1924
1966
  // Component management
1925
1967
  components = [];
1926
1968
  runningComponents = /* @__PURE__ */ new Set();
@@ -1930,8 +1972,15 @@ var LifecycleManager = class extends EventEmitterProtected {
1930
1972
  componentTimestamps = /* @__PURE__ */ new Map();
1931
1973
  componentErrors = /* @__PURE__ */ new Map();
1932
1974
  componentStartAttemptTokens = /* @__PURE__ */ new Map();
1975
+ // Use per-stop ULIDs instead of incrementing counters because a stalled
1976
+ // component can be unregistered and replaced by a same-name instance before
1977
+ // the old floating stop promise settles.
1978
+ componentStopAttemptTokens = /* @__PURE__ */ new Map();
1979
+ pendingForceStopWaiters = /* @__PURE__ */ new Map();
1980
+ unexpectedStopsDuringStartup = /* @__PURE__ */ new Map();
1933
1981
  // State flags
1934
1982
  isStarting = false;
1983
+ autoAttachedSignalsDuringStartup = false;
1935
1984
  isStarted = false;
1936
1985
  isShuttingDown = false;
1937
1986
  // Unique token used to detect shutdowns that happened during async start().
@@ -1940,6 +1989,17 @@ var LifecycleManager = class extends EventEmitterProtected {
1940
1989
  pendingLoggerExitResolve = null;
1941
1990
  shutdownMethod = null;
1942
1991
  lastShutdownResult = null;
1992
+ repeatedShutdownExpiryTimer = null;
1993
+ repeatedShutdownRequestState = {
1994
+ requestCount: 0,
1995
+ firstMethod: null,
1996
+ latestMethod: null,
1997
+ firstRequestAt: null,
1998
+ latestRequestAt: null,
1999
+ repeatedWindowStartedAt: null,
2000
+ hasTriggeredForceShutdown: false,
2001
+ remainsArmedUntil: null
2002
+ };
1943
2003
  // Signal management
1944
2004
  processSignalManager = null;
1945
2005
  onReloadRequested;
@@ -1966,6 +2026,37 @@ var LifecycleManager = class extends EventEmitterProtected {
1966
2026
  this.attachSignalsBeforeStartup = options.attachSignalsBeforeStartup ?? false;
1967
2027
  this.attachSignalsOnStart = options.attachSignalsOnStart ?? false;
1968
2028
  this.detachSignalsOnStop = options.detachSignalsOnStop ?? false;
2029
+ const repeatedShutdownRequestPolicy = options.repeatedShutdownRequestPolicy;
2030
+ if (repeatedShutdownRequestPolicy === void 0) {
2031
+ this.repeatedShutdownRequestPolicy = void 0;
2032
+ } else {
2033
+ const hasFiniteExplicitArmedAfterFailureMS = Number.isFinite(
2034
+ repeatedShutdownRequestPolicy.armedAfterFailureMS
2035
+ );
2036
+ const forceAfterCount = finiteClampMin(
2037
+ repeatedShutdownRequestPolicy.forceAfterCount,
2038
+ 1,
2039
+ 3
2040
+ );
2041
+ const withinMS = finiteClampMin(
2042
+ repeatedShutdownRequestPolicy.withinMS,
2043
+ 0,
2044
+ 2e3
2045
+ );
2046
+ const armedAfterFailureMS = finiteClampMin(
2047
+ repeatedShutdownRequestPolicy.armedAfterFailureMS,
2048
+ 0,
2049
+ withinMS * forceAfterCount
2050
+ );
2051
+ this.repeatedShutdownRequestPolicy = {
2052
+ forceAfterCount,
2053
+ withinMS,
2054
+ armedAfterFailureMS,
2055
+ countManualRetriesTowardEscalation: repeatedShutdownRequestPolicy.countManualRetriesTowardEscalation ?? false,
2056
+ hasExplicitArmedAfterFailureMS: hasFiniteExplicitArmedAfterFailureMS,
2057
+ onForceShutdown: repeatedShutdownRequestPolicy.onForceShutdown
2058
+ };
2059
+ }
1969
2060
  this.onReloadRequested = options.onReloadRequested;
1970
2061
  this.onInfoRequested = options.onInfoRequested;
1971
2062
  this.onDebugRequested = options.onDebugRequested;
@@ -2038,7 +2129,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2038
2129
  */
2039
2130
  async unregisterComponent(name, options) {
2040
2131
  if (this.isStarting || this.isShuttingDown) {
2041
- this.logger.entity(name).warn("Cannot unregister during bulk operation", {
2132
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS, {
2042
2133
  params: {
2043
2134
  isStarting: this.isStarting,
2044
2135
  isShuttingDown: this.isShuttingDown
@@ -2047,7 +2138,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2047
2138
  return {
2048
2139
  success: false,
2049
2140
  componentName: name,
2050
- reason: "Cannot unregister during bulk operation",
2141
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS,
2051
2142
  code: "bulk_operation_in_progress",
2052
2143
  wasStopped: false,
2053
2144
  wasRegistered: this.hasComponent(name)
@@ -2055,11 +2146,11 @@ var LifecycleManager = class extends EventEmitterProtected {
2055
2146
  }
2056
2147
  const component = this.getComponent(name);
2057
2148
  if (!component) {
2058
- this.logger.entity(name).warn("Component not found");
2149
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND);
2059
2150
  return {
2060
2151
  success: false,
2061
2152
  componentName: name,
2062
- reason: "Component not found",
2153
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
2063
2154
  code: "component_not_found",
2064
2155
  wasStopped: false,
2065
2156
  wasRegistered: false
@@ -2072,7 +2163,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2072
2163
  return {
2073
2164
  success: false,
2074
2165
  componentName: name,
2075
- reason: "Component is stalled",
2166
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
2076
2167
  code: "stop_failed",
2077
2168
  stopFailureReason: "stalled",
2078
2169
  wasStopped: false,
@@ -2124,10 +2215,13 @@ var LifecycleManager = class extends EventEmitterProtected {
2124
2215
  wasStopped = true;
2125
2216
  }
2126
2217
  this.components = this.components.filter((c) => c.getName() !== name);
2218
+ component._clearUnexpectedStopHandler();
2127
2219
  this.componentStates.delete(name);
2128
2220
  this.componentTimestamps.delete(name);
2129
2221
  this.componentErrors.delete(name);
2130
2222
  this.componentStartAttemptTokens.delete(name);
2223
+ this.componentStopAttemptTokens.delete(name);
2224
+ this.pendingForceStopWaiters.delete(name);
2131
2225
  this.stalledComponents.delete(name);
2132
2226
  this.runningComponents.delete(name);
2133
2227
  this.updateStartedFlag();
@@ -2474,7 +2568,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2474
2568
  startedComponents: [],
2475
2569
  failedOptionalComponents: [],
2476
2570
  skippedDueToDependency: [],
2477
- reason: "Shutdown in progress",
2571
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2478
2572
  code: "shutdown_in_progress",
2479
2573
  durationMS: Date.now() - startTime
2480
2574
  };
@@ -2534,6 +2628,9 @@ var LifecycleManager = class extends EventEmitterProtected {
2534
2628
  };
2535
2629
  }
2536
2630
  this.isStarting = true;
2631
+ this.autoAttachedSignalsDuringStartup = false;
2632
+ this.unexpectedStopsDuringStartup.clear();
2633
+ this.resetRepeatedShutdownRequestState();
2537
2634
  this.shutdownMethod = null;
2538
2635
  this.lastShutdownResult = null;
2539
2636
  const didAutoAttachSignalsForBulkStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("bulk startup") : false;
@@ -2665,13 +2762,49 @@ var LifecycleManager = class extends EventEmitterProtected {
2665
2762
  error: result.error,
2666
2763
  durationMS: Date.now() - startTime
2667
2764
  };
2765
+ } else if (result.code === "component_unexpected_stop") {
2766
+ this.unexpectedStopsDuringStartup.delete(name);
2767
+ const error = result.error || new Error(
2768
+ result.reason || `Component "${name}" stopped unexpectedly`
2769
+ );
2770
+ if (component.isOptional()) {
2771
+ if (!failedOptionalComponents.some((entry) => entry.name === name)) {
2772
+ failedOptionalComponents.push({ name, error });
2773
+ }
2774
+ this.logger.entity(name).warn(
2775
+ LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
2776
+ {
2777
+ params: { error }
2778
+ }
2779
+ );
2780
+ } else {
2781
+ this.logger.entity(name).error(
2782
+ LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
2783
+ {
2784
+ params: { error }
2785
+ }
2786
+ );
2787
+ await this.rollbackStartup(startedComponents);
2788
+ return {
2789
+ success: false,
2790
+ startedComponents: [],
2791
+ failedOptionalComponents,
2792
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2793
+ reason: error.message,
2794
+ code: "component_unexpected_stop",
2795
+ error,
2796
+ durationMS: Date.now() - startTime
2797
+ };
2798
+ }
2668
2799
  } else {
2669
2800
  if (component.isOptional()) {
2670
2801
  this.logger.entity(name).warn(
2671
2802
  "Optional component failed to start, continuing: {{error.message}}",
2672
2803
  {
2673
2804
  params: {
2674
- error: result.error || new Error(result.reason || "Unknown error")
2805
+ error: result.error || new Error(
2806
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2807
+ )
2675
2808
  }
2676
2809
  }
2677
2810
  );
@@ -2685,14 +2818,18 @@ var LifecycleManager = class extends EventEmitterProtected {
2685
2818
  }
2686
2819
  failedOptionalComponents.push({
2687
2820
  name,
2688
- error: result.error || new Error(result.reason || "Unknown error")
2821
+ error: result.error || new Error(
2822
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2823
+ )
2689
2824
  });
2690
2825
  } else {
2691
2826
  this.logger.entity(name).error(
2692
2827
  "Required component failed to start, rolling back: {{error.message}}",
2693
2828
  {
2694
2829
  params: {
2695
- error: result.error || new Error(result.reason || "Unknown error")
2830
+ error: result.error || new Error(
2831
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2832
+ )
2696
2833
  }
2697
2834
  }
2698
2835
  );
@@ -2709,6 +2846,25 @@ var LifecycleManager = class extends EventEmitterProtected {
2709
2846
  };
2710
2847
  }
2711
2848
  }
2849
+ const unexpectedStopResult2 = this.consumeUnexpectedStopsDuringStartup(
2850
+ startedComponents,
2851
+ failedOptionalComponents
2852
+ );
2853
+ startedComponents.splice(0, startedComponents.length);
2854
+ startedComponents.push(...unexpectedStopResult2.startedComponents);
2855
+ if (unexpectedStopResult2.requiredFailure) {
2856
+ await this.rollbackStartup(startedComponents);
2857
+ return {
2858
+ success: false,
2859
+ startedComponents: [],
2860
+ failedOptionalComponents,
2861
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2862
+ reason: unexpectedStopResult2.requiredFailure.error.message,
2863
+ code: "component_unexpected_stop",
2864
+ error: unexpectedStopResult2.requiredFailure.error,
2865
+ durationMS: Date.now() - startTime
2866
+ };
2867
+ }
2712
2868
  }
2713
2869
  if (hasTimedOut) {
2714
2870
  const durationMS2 = Date.now() - startTime;
@@ -2732,6 +2888,25 @@ var LifecycleManager = class extends EventEmitterProtected {
2732
2888
  code: "startup_timeout"
2733
2889
  };
2734
2890
  }
2891
+ const unexpectedStopResult = this.consumeUnexpectedStopsDuringStartup(
2892
+ startedComponents,
2893
+ failedOptionalComponents
2894
+ );
2895
+ startedComponents.splice(0, startedComponents.length);
2896
+ startedComponents.push(...unexpectedStopResult.startedComponents);
2897
+ if (unexpectedStopResult.requiredFailure) {
2898
+ await this.rollbackStartup(startedComponents);
2899
+ return {
2900
+ success: false,
2901
+ startedComponents: [],
2902
+ failedOptionalComponents,
2903
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2904
+ reason: unexpectedStopResult.requiredFailure.error.message,
2905
+ code: "component_unexpected_stop",
2906
+ error: unexpectedStopResult.requiredFailure.error,
2907
+ durationMS: Date.now() - startTime
2908
+ };
2909
+ }
2735
2910
  this.updateStartedFlag();
2736
2911
  const skippedComponentsArray = [
2737
2912
  ...Array.from(skippedDueToDependency),
@@ -2763,10 +2938,12 @@ var LifecycleManager = class extends EventEmitterProtected {
2763
2938
  if (timeoutHandle) {
2764
2939
  clearTimeout(timeoutHandle);
2765
2940
  }
2766
- if (didAutoAttachSignalsForBulkStartup) {
2941
+ this.isStarting = false;
2942
+ if (didAutoAttachSignalsForBulkStartup || this.autoAttachedSignalsDuringStartup) {
2767
2943
  this.autoDetachSignalsIfIdle("failed bulk startup");
2768
2944
  }
2769
- this.isStarting = false;
2945
+ this.autoAttachedSignalsDuringStartup = false;
2946
+ this.unexpectedStopsDuringStartup.clear();
2770
2947
  }
2771
2948
  }
2772
2949
  /**
@@ -2830,7 +3007,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2830
3007
  return {
2831
3008
  success: false,
2832
3009
  componentName: name,
2833
- reason: "Bulk startup in progress",
3010
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS,
2834
3011
  code: "startup_in_progress"
2835
3012
  };
2836
3013
  }
@@ -2841,7 +3018,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2841
3018
  return {
2842
3019
  success: false,
2843
3020
  componentName: name,
2844
- reason: "Shutdown in progress",
3021
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2845
3022
  code: "shutdown_in_progress"
2846
3023
  };
2847
3024
  }
@@ -2875,7 +3052,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2875
3052
  return {
2876
3053
  success: false,
2877
3054
  componentName: name,
2878
- reason: this.isStarting ? "Bulk startup in progress" : "Shutdown in progress",
3055
+ reason: this.isStarting ? LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS : LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2879
3056
  code: this.isStarting ? "startup_in_progress" : "shutdown_in_progress"
2880
3057
  };
2881
3058
  }
@@ -2974,6 +3151,51 @@ var LifecycleManager = class extends EventEmitterProtected {
2974
3151
  shutdownMethod: this.shutdownMethod
2975
3152
  };
2976
3153
  }
3154
+ /**
3155
+ * Get status information about repeated shutdown escalation configuration and runtime state.
3156
+ */
3157
+ getShutdownEscalationStatus() {
3158
+ if (this.repeatedShutdownRequestPolicy === void 0) {
3159
+ return {
3160
+ configured: false,
3161
+ isShuttingDown: this.isShuttingDown,
3162
+ isArmed: false,
3163
+ forceAfterCount: null,
3164
+ withinMS: null,
3165
+ armedAfterFailureMS: null,
3166
+ armedAfterFailureMSSource: null,
3167
+ requestCount: 0,
3168
+ firstMethod: null,
3169
+ latestMethod: null,
3170
+ firstRequestAt: null,
3171
+ latestRequestAt: null,
3172
+ repeatedWindowStartedAt: null,
3173
+ armedUntil: null,
3174
+ hasTriggeredForceShutdown: false
3175
+ };
3176
+ }
3177
+ this.normalizeRepeatedShutdownRequestStateArmedStatus();
3178
+ const armedUntil = this.repeatedShutdownRequestState.remainsArmedUntil;
3179
+ const isArmed = armedUntil !== null;
3180
+ return {
3181
+ configured: true,
3182
+ isShuttingDown: this.isShuttingDown,
3183
+ isArmed,
3184
+ forceAfterCount: this.repeatedShutdownRequestPolicy.forceAfterCount,
3185
+ withinMS: this.repeatedShutdownRequestPolicy.withinMS,
3186
+ armedAfterFailureMS: this.repeatedShutdownRequestPolicy.armedAfterFailureMS,
3187
+ armedAfterFailureMSSource: this.repeatedShutdownRequestPolicy.hasExplicitArmedAfterFailureMS ? "explicit" : "derived",
3188
+ countManualRetriesTowardEscalation: this.repeatedShutdownRequestPolicy.countManualRetriesTowardEscalation,
3189
+ requestCount: this.repeatedShutdownRequestState.requestCount,
3190
+ firstMethod: this.repeatedShutdownRequestState.firstMethod,
3191
+ latestMethod: this.repeatedShutdownRequestState.latestMethod,
3192
+ firstRequestAt: this.repeatedShutdownRequestState.firstRequestAt,
3193
+ latestRequestAt: this.repeatedShutdownRequestState.latestRequestAt,
3194
+ repeatedWindowStartedAt: this.repeatedShutdownRequestState.repeatedWindowStartedAt,
3195
+ armedUntil: isArmed ? armedUntil : null,
3196
+ hasTriggeredForceShutdown: this.repeatedShutdownRequestState.hasTriggeredForceShutdown
3197
+ };
3198
+ }
2977
3199
  /**
2978
3200
  * Enable Logger exit hook integration
2979
3201
  *
@@ -3012,7 +3234,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3012
3234
  if (this.isShuttingDown) {
3013
3235
  if (isFirstExit && this.pendingLoggerExitResolve === null) {
3014
3236
  this.logger.debug(
3015
- "Logger exit called during shutdown, waiting...",
3237
+ LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN,
3016
3238
  {
3017
3239
  params: { exitCode }
3018
3240
  }
@@ -3021,7 +3243,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3021
3243
  this.pendingLoggerExitResolve = resolve;
3022
3244
  });
3023
3245
  }
3024
- this.logger.debug("Logger exit called during shutdown, waiting...", {
3246
+ this.logger.debug(LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN, {
3025
3247
  params: { exitCode }
3026
3248
  });
3027
3249
  return { action: "wait" };
@@ -3111,7 +3333,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3111
3333
  return {
3112
3334
  name,
3113
3335
  healthy: false,
3114
- message: "Component not found",
3336
+ message: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
3115
3337
  checkedAt: startTime,
3116
3338
  durationMS: 0,
3117
3339
  error: null,
@@ -3124,7 +3346,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3124
3346
  return {
3125
3347
  name,
3126
3348
  healthy: false,
3127
- message: isStalled ? "Component is stalled" : "Component not running",
3349
+ message: isStalled ? LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED : LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
3128
3350
  checkedAt: startTime,
3129
3351
  durationMS: Date.now() - startTime,
3130
3352
  error: null,
@@ -3340,7 +3562,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3340
3562
  result = component.onMessage(payload, from);
3341
3563
  } catch (error) {
3342
3564
  const err = error instanceof Error ? error : new Error(String(error));
3343
- this.logger.entity(componentName).error("Message handler failed: {{error.message}}", {
3565
+ this.logger.entity(componentName).error(LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED, {
3344
3566
  params: { error: err, from }
3345
3567
  });
3346
3568
  this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
@@ -3400,7 +3622,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3400
3622
  };
3401
3623
  } catch (error) {
3402
3624
  const err = error instanceof Error ? error : new Error(String(error));
3403
- this.logger.entity(componentName).error("Message handler failed: {{error.message}}", {
3625
+ this.logger.entity(componentName).error(LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED, {
3404
3626
  params: { error: err, from, timeoutMS }
3405
3627
  });
3406
3628
  this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
@@ -3673,7 +3895,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3673
3895
  this.lifecycleEvents.componentRegistrationRejected({
3674
3896
  name: componentName,
3675
3897
  reason: "shutdown_in_progress",
3676
- message: "Cannot register component while shutdown is in progress (isShuttingDown=true).",
3898
+ message: LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS,
3677
3899
  registrationIndexBefore,
3678
3900
  registrationIndexAfter: registrationIndexBefore,
3679
3901
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3685,7 +3907,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3685
3907
  targetComponentName,
3686
3908
  registrationIndexBefore,
3687
3909
  code: "shutdown_in_progress",
3688
- reason: "Cannot register component while shutdown is in progress (isShuttingDown=true).",
3910
+ reason: LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS,
3689
3911
  targetFound: void 0
3690
3912
  });
3691
3913
  }
@@ -3696,7 +3918,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3696
3918
  this.lifecycleEvents.componentRegistrationRejected({
3697
3919
  name: componentName,
3698
3920
  reason: "startup_in_progress",
3699
- message: "Cannot register component during startup when it is a required dependency for other components.",
3921
+ message: LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP,
3700
3922
  registrationIndexBefore,
3701
3923
  registrationIndexAfter: registrationIndexBefore,
3702
3924
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3708,7 +3930,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3708
3930
  targetComponentName,
3709
3931
  registrationIndexBefore,
3710
3932
  code: "startup_in_progress",
3711
- reason: "Cannot register component during startup when it is a required dependency for other components.",
3933
+ reason: LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP,
3712
3934
  targetFound: void 0
3713
3935
  });
3714
3936
  }
@@ -3717,7 +3939,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3717
3939
  this.lifecycleEvents.componentRegistrationRejected({
3718
3940
  name: componentName,
3719
3941
  reason: "duplicate_instance",
3720
- message: "Component instance is already registered.",
3942
+ message: LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE,
3721
3943
  registrationIndexBefore,
3722
3944
  registrationIndexAfter: registrationIndexBefore,
3723
3945
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3729,7 +3951,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3729
3951
  targetComponentName,
3730
3952
  registrationIndexBefore,
3731
3953
  code: "duplicate_instance",
3732
- reason: "Component instance is already registered.",
3954
+ reason: LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE,
3733
3955
  targetFound: void 0
3734
3956
  });
3735
3957
  }
@@ -3999,9 +4221,26 @@ var LifecycleManager = class extends EventEmitterProtected {
3999
4221
  code: "already_in_progress"
4000
4222
  };
4001
4223
  }
4224
+ this.normalizeRepeatedShutdownRequestStateArmedStatus();
4225
+ const repeatedShutdownPolicy = this.repeatedShutdownRequestPolicy;
4226
+ const isManualRetryWhileArmed = repeatedShutdownPolicy !== void 0 && method === "manual" && this.repeatedShutdownRequestState.firstRequestAt !== null && this.repeatedShutdownRequestState.remainsArmedUntil !== null;
4227
+ if (isManualRetryWhileArmed) {
4228
+ if (repeatedShutdownPolicy.countManualRetriesTowardEscalation) {
4229
+ this.handleRepeatedShutdownRequest(method);
4230
+ } else {
4231
+ this.resetRepeatedShutdownRequestState();
4232
+ }
4233
+ }
4234
+ if (this.repeatedShutdownRequestState.remainsArmedUntil !== null) {
4235
+ this.clearRepeatedShutdownExpiryTimer();
4236
+ this.repeatedShutdownRequestState.remainsArmedUntil = null;
4237
+ }
4002
4238
  this.isShuttingDown = true;
4003
4239
  this.shutdownToken = ulid2();
4004
4240
  this.shutdownMethod = method;
4241
+ if (this.repeatedShutdownRequestPolicy && this.repeatedShutdownRequestState.firstRequestAt === null) {
4242
+ this.seedRepeatedShutdownRequestState(method);
4243
+ }
4005
4244
  const isDuringStartup = this.isStarting;
4006
4245
  this.logger.info("Stopping all components", { params: { method } });
4007
4246
  this.lifecycleEvents.lifecycleManagerShutdownInitiated(
@@ -4026,8 +4265,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4026
4265
  const runningComponentsToStop = shutdownOrder.filter(
4027
4266
  (name) => this.isComponentRunning(name) || shouldRetryStalled && stalledComponentNames.has(name)
4028
4267
  );
4029
- const stoppedComponents = [];
4030
- const stalledComponents = [];
4268
+ const stoppedComponents = /* @__PURE__ */ new Set();
4031
4269
  let hasTimedOut = false;
4032
4270
  let timeoutHandle;
4033
4271
  try {
@@ -4058,34 +4296,37 @@ var LifecycleManager = class extends EventEmitterProtected {
4058
4296
  this.logger.entity(name).info("Stopping component");
4059
4297
  const isRunning = this.isComponentRunning(name);
4060
4298
  const isStalled = stalledComponentNames.has(name);
4299
+ const currentState = this.componentStates.get(name);
4300
+ if (currentState === "stopped") {
4301
+ stoppedComponents.add(name);
4302
+ continue;
4303
+ }
4061
4304
  const result2 = isRunning ? await this.stopComponentInternal(name) : shouldRetryStalled && isStalled ? await this.retryStalledComponent(name) : isStalled ? {
4062
4305
  success: false,
4063
4306
  componentName: name,
4064
- reason: "Component is stalled",
4307
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4065
4308
  code: "component_stalled",
4066
4309
  status: this.getComponentStatus(name)
4067
4310
  } : {
4068
4311
  success: false,
4069
4312
  componentName: name,
4070
- reason: "Component not running",
4313
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4071
4314
  code: "component_not_running",
4072
4315
  status: this.getComponentStatus(name)
4073
4316
  };
4074
4317
  if (result2.success) {
4075
- stoppedComponents.push(name);
4318
+ stoppedComponents.add(name);
4076
4319
  } else {
4077
4320
  this.logger.entity(name).error(
4078
4321
  "Component failed to stop, continuing with others: {{error.message}}",
4079
4322
  {
4080
4323
  params: {
4081
- error: result2.error || new Error(result2.reason || "Unknown error")
4324
+ error: result2.error || new Error(
4325
+ result2.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
4326
+ )
4082
4327
  }
4083
4328
  }
4084
4329
  );
4085
- const stallInfo = this.stalledComponents.get(name);
4086
- if (stallInfo) {
4087
- stalledComponents.push(stallInfo);
4088
- }
4089
4330
  if (shouldHaltOnStall) {
4090
4331
  this.logger.warn(
4091
4332
  "Halting shutdown after stall (haltOnStall=true)",
@@ -4101,18 +4342,40 @@ var LifecycleManager = class extends EventEmitterProtected {
4101
4342
  } else {
4102
4343
  await shutdownOperation();
4103
4344
  }
4345
+ const finalStalledNames = /* @__PURE__ */ new Set();
4346
+ for (const name of runningComponentsToStop) {
4347
+ if (this.stalledComponents.has(name)) {
4348
+ finalStalledNames.add(name);
4349
+ }
4350
+ }
4351
+ if (!shouldRetryStalled) {
4352
+ for (const name of stalledComponentNames) {
4353
+ if (this.stalledComponents.has(name)) {
4354
+ finalStalledNames.add(name);
4355
+ }
4356
+ }
4357
+ }
4358
+ for (const name of runningComponentsToStop) {
4359
+ if (!finalStalledNames.has(name) && this.componentStates.get(name) === "stopped") {
4360
+ stoppedComponents.add(name);
4361
+ }
4362
+ }
4363
+ const stalledComponents = Array.from(finalStalledNames).map((name) => this.stalledComponents.get(name)).filter((stallInfo) => !!stallInfo);
4104
4364
  const durationMS = Date.now() - startTime;
4105
4365
  const isSuccess = !hasTimedOut && stalledComponents.length === 0;
4106
- this.logger[isSuccess ? "success" : "warn"]("Shutdown completed", {
4107
- params: {
4108
- stopped: stoppedComponents.length,
4109
- stalled: stalledComponents.length,
4110
- durationMS
4366
+ this.logger[isSuccess ? "success" : "warn"](
4367
+ isSuccess ? "Shutdown completed successfully" : "Shutdown attempt completed with stalled components or timeout",
4368
+ {
4369
+ params: {
4370
+ stopped: stoppedComponents.size,
4371
+ stalled: stalledComponents.length,
4372
+ durationMS
4373
+ }
4111
4374
  }
4112
- });
4375
+ );
4113
4376
  const result = {
4114
4377
  success: isSuccess,
4115
- stoppedComponents,
4378
+ stoppedComponents: Array.from(stoppedComponents),
4116
4379
  stalledComponents,
4117
4380
  durationMS,
4118
4381
  timedOut: hasTimedOut || void 0,
@@ -4127,6 +4390,11 @@ var LifecycleManager = class extends EventEmitterProtected {
4127
4390
  method,
4128
4391
  duringStartup: isDuringStartup
4129
4392
  });
4393
+ if (isSuccess) {
4394
+ this.resetRepeatedShutdownRequestState();
4395
+ } else {
4396
+ this.armRepeatedShutdownAfterFailure();
4397
+ }
4130
4398
  return result;
4131
4399
  } finally {
4132
4400
  if (timeoutHandle) {
@@ -4158,7 +4426,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4158
4426
  return {
4159
4427
  success: false,
4160
4428
  componentName: name,
4161
- reason: "Component not found",
4429
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4162
4430
  code: "component_not_found"
4163
4431
  };
4164
4432
  }
@@ -4169,12 +4437,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4169
4437
  return {
4170
4438
  success: false,
4171
4439
  componentName: name,
4172
- reason: "Component not running",
4440
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4173
4441
  code: "component_not_running",
4174
4442
  status: this.getComponentStatus(name)
4175
4443
  };
4176
4444
  }
4177
4445
  this.logger.entity(name).warn("Retrying stalled component shutdown (force phase)");
4446
+ if (component.onShutdownForce) {
4447
+ this.issueStopAttemptToken(name);
4448
+ }
4178
4449
  return this.shutdownComponentForce(name, component, {
4179
4450
  gracefulPhaseRan: false,
4180
4451
  gracefulTimedOut: false,
@@ -4194,7 +4465,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4194
4465
  return {
4195
4466
  success: false,
4196
4467
  componentName: name,
4197
- reason: "Shutdown in progress",
4468
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
4198
4469
  code: "shutdown_in_progress"
4199
4470
  };
4200
4471
  }
@@ -4206,7 +4477,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4206
4477
  return {
4207
4478
  success: false,
4208
4479
  componentName: name,
4209
- reason: "Bulk startup in progress",
4480
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS,
4210
4481
  code: "startup_in_progress"
4211
4482
  };
4212
4483
  }
@@ -4216,7 +4487,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4216
4487
  return {
4217
4488
  success: false,
4218
4489
  componentName: name,
4219
- reason: "Component not found",
4490
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4220
4491
  code: "component_not_found"
4221
4492
  };
4222
4493
  }
@@ -4225,7 +4496,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4225
4496
  return {
4226
4497
  success: false,
4227
4498
  componentName: name,
4228
- reason: "Component is stalled",
4499
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4229
4500
  code: "component_stalled",
4230
4501
  status: this.getComponentStatus(name)
4231
4502
  };
@@ -4289,6 +4560,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4289
4560
  const timeoutMS = component.startupTimeoutMS;
4290
4561
  const startAttemptToken = ulid2();
4291
4562
  this.componentStartAttemptTokens.set(name, startAttemptToken);
4563
+ component._setUnexpectedStopHandler(
4564
+ (error) => this.handleComponentUnexpectedStop(name, startAttemptToken, error)
4565
+ );
4292
4566
  const shutdownTokenAtStart = this.shutdownToken;
4293
4567
  const didAutoAttachSignalsForComponentStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("component startup") : false;
4294
4568
  let timeoutHandle;
@@ -4331,6 +4605,18 @@ var LifecycleManager = class extends EventEmitterProtected {
4331
4605
  } else {
4332
4606
  await startPromise;
4333
4607
  }
4608
+ if (this.componentStartAttemptTokens.get(name) === startAttemptToken && this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
4609
+ component._clearUnexpectedStopHandler();
4610
+ const error = this.componentErrors.get(name) ?? new Error(`Component "${name}" stopped unexpectedly during startup`);
4611
+ return {
4612
+ success: false,
4613
+ componentName: name,
4614
+ reason: error.message,
4615
+ code: "component_unexpected_stop",
4616
+ error,
4617
+ status: this.getComponentStatus(name)
4618
+ };
4619
+ }
4334
4620
  if (this.isShuttingDown || shutdownTokenAtStart !== this.shutdownToken) {
4335
4621
  this.componentStates.set(name, "running");
4336
4622
  this.runningComponents.add(name);
@@ -4358,6 +4644,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4358
4644
  this.componentStates.set(name, "running");
4359
4645
  this.runningComponents.add(name);
4360
4646
  this.stalledComponents.delete(name);
4647
+ if (shouldForceStalled) {
4648
+ this.issueStopAttemptToken(name);
4649
+ }
4361
4650
  this.updateStartedFlag();
4362
4651
  if (this.attachSignalsOnStart && this.runningComponents.size === 1) {
4363
4652
  this.autoAttachSignals("first component start");
@@ -4377,9 +4666,24 @@ var LifecycleManager = class extends EventEmitterProtected {
4377
4666
  status: this.getComponentStatus(name)
4378
4667
  };
4379
4668
  } catch (error) {
4669
+ component._clearUnexpectedStopHandler();
4380
4670
  const err = error instanceof Error ? error : new Error(String(error));
4671
+ const isStartupTimeout = err instanceof ComponentStartTimeoutError && err.additionalInfo.componentName === name;
4672
+ const unexpectedStopError = this.componentErrors.get(name);
4673
+ if (this.componentStartAttemptTokens.get(name) === startAttemptToken && this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name) && (isStartupTimeout || unexpectedStopError instanceof Error)) {
4674
+ return {
4675
+ success: false,
4676
+ componentName: name,
4677
+ reason: unexpectedStopError?.message || `Component "${name}" stopped unexpectedly during startup`,
4678
+ code: "component_unexpected_stop",
4679
+ error: unexpectedStopError || new Error(
4680
+ `Component "${name}" stopped unexpectedly during startup`
4681
+ ),
4682
+ status: this.getComponentStatus(name)
4683
+ };
4684
+ }
4381
4685
  this.componentErrors.set(name, err);
4382
- if (err instanceof ComponentStartTimeoutError && err.additionalInfo.componentName === name) {
4686
+ if (isStartupTimeout) {
4383
4687
  this.componentStates.set(name, "starting-timed-out");
4384
4688
  this.logger.entity(name).error("Component startup timed out: {{error.message}}", {
4385
4689
  params: { error: err }
@@ -4424,7 +4728,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4424
4728
  return {
4425
4729
  success: false,
4426
4730
  componentName: name,
4427
- reason: "Component not found",
4731
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4428
4732
  code: "component_not_found"
4429
4733
  };
4430
4734
  }
@@ -4432,7 +4736,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4432
4736
  return {
4433
4737
  success: false,
4434
4738
  componentName: name,
4435
- reason: "Component is stalled",
4739
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4436
4740
  code: "component_stalled",
4437
4741
  status: this.getComponentStatus(name)
4438
4742
  };
@@ -4441,7 +4745,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4441
4745
  return {
4442
4746
  success: false,
4443
4747
  componentName: name,
4444
- reason: "Component not running",
4748
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4445
4749
  code: "component_not_running",
4446
4750
  status: this.getComponentStatus(name)
4447
4751
  };
@@ -4457,6 +4761,10 @@ var LifecycleManager = class extends EventEmitterProtected {
4457
4761
  };
4458
4762
  }
4459
4763
  if (options?.forceImmediate) {
4764
+ if (component.onShutdownForce) {
4765
+ this.issueStopAttemptToken(name);
4766
+ }
4767
+ component._clearUnexpectedStopHandler();
4460
4768
  return this.shutdownComponentForce(name, component, {
4461
4769
  gracefulPhaseRan: false,
4462
4770
  gracefulTimedOut: false,
@@ -4593,9 +4901,11 @@ var LifecycleManager = class extends EventEmitterProtected {
4593
4901
  * Calls stop() with timeout
4594
4902
  */
4595
4903
  async shutdownComponentGraceful(name, component, options) {
4904
+ component._clearUnexpectedStopHandler();
4596
4905
  this.componentStates.set(name, "stopping");
4597
4906
  this.logger.entity(name).info("Graceful shutdown started");
4598
4907
  this.lifecycleEvents.componentStopping(name);
4908
+ const stopAttemptToken = this.issueStopAttemptToken(name);
4599
4909
  const timeoutMS = options?.timeout ?? component.shutdownGracefulTimeoutMS;
4600
4910
  let timeoutHandle;
4601
4911
  try {
@@ -4616,7 +4926,16 @@ var LifecycleManager = class extends EventEmitterProtected {
4616
4926
  );
4617
4927
  }
4618
4928
  }
4619
- Promise.resolve(stopPromise).catch(() => {
4929
+ Promise.resolve(stopPromise).then(
4930
+ () => this.handleLateStopResolution(
4931
+ name,
4932
+ stopAttemptToken,
4933
+ "graceful"
4934
+ ),
4935
+ () => {
4936
+ }
4937
+ // Intentionally ignore errors after timeout
4938
+ ).catch(() => {
4620
4939
  });
4621
4940
  reject(
4622
4941
  new ComponentStopTimeoutError({
@@ -4635,9 +4954,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4635
4954
  this.stalledComponents.delete(name);
4636
4955
  this.updateStartedFlag();
4637
4956
  if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
4638
- this.logger.info(
4639
- "Auto-detaching process signals on last component stop"
4640
- );
4957
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
4641
4958
  this.detachSignals();
4642
4959
  }
4643
4960
  const timestamps = this.componentTimestamps.get(name) ?? {
@@ -4660,15 +4977,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4660
4977
  const err = error instanceof Error ? error : new Error(String(error));
4661
4978
  this.componentErrors.set(name, err);
4662
4979
  if (err instanceof ComponentStopTimeoutError && err.additionalInfo.componentName === name) {
4663
- this.logger.entity(name).warn("Graceful shutdown timed out");
4980
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT);
4664
4981
  this.lifecycleEvents.componentStopTimeout(name, err, {
4665
4982
  timeoutMS,
4666
- reason: "Graceful shutdown timed out"
4983
+ reason: LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT
4667
4984
  });
4668
4985
  return {
4669
4986
  success: false,
4670
4987
  componentName: name,
4671
- reason: "Graceful shutdown timed out",
4988
+ reason: LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT,
4672
4989
  code: "component_shutdown_timeout",
4673
4990
  error: err,
4674
4991
  status: this.getComponentStatus(name)
@@ -4711,8 +5028,6 @@ var LifecycleManager = class extends EventEmitterProtected {
4711
5028
  gracefulTimedOut: context.gracefulTimedOut
4712
5029
  }
4713
5030
  });
4714
- const timeoutMS = component.shutdownForceTimeoutMS;
4715
- let timeoutHandle;
4716
5031
  if (!component.onShutdownForce) {
4717
5032
  const stallInfo = {
4718
5033
  name,
@@ -4746,6 +5061,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4746
5061
  status: this.getComponentStatus(name)
4747
5062
  };
4748
5063
  }
5064
+ const timeoutMS = component.shutdownForceTimeoutMS;
5065
+ const { promise: stoppedDuringForcePromise, cleanup: cleanupForceWaiter } = this.createPendingForceStopWaiter(name);
5066
+ let timeoutHandle;
4749
5067
  try {
4750
5068
  const forcePromise = component.onShutdownForce();
4751
5069
  if (timeoutMS > 0) {
@@ -4764,23 +5082,44 @@ var LifecycleManager = class extends EventEmitterProtected {
4764
5082
  );
4765
5083
  }
4766
5084
  }
4767
- Promise.resolve(forcePromise).catch(() => {
5085
+ const forceAttemptToken = this.componentStopAttemptTokens.get(name) ?? ulid2();
5086
+ Promise.resolve(forcePromise).then(
5087
+ () => this.handleLateStopResolution(
5088
+ name,
5089
+ forceAttemptToken,
5090
+ "force"
5091
+ ),
5092
+ () => {
5093
+ }
5094
+ // Intentionally ignore errors after timeout
5095
+ ).catch(() => {
4768
5096
  });
4769
- reject(new Error("Force shutdown timed out"));
5097
+ reject(
5098
+ new Error(LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT)
5099
+ );
4770
5100
  }, timeoutMS);
4771
5101
  });
4772
- await Promise.race([forcePromise, timeoutPromise]);
5102
+ await Promise.race([
5103
+ forcePromise,
5104
+ timeoutPromise,
5105
+ stoppedDuringForcePromise
5106
+ ]);
4773
5107
  } else {
4774
- await forcePromise;
5108
+ await Promise.race([forcePromise, stoppedDuringForcePromise]);
5109
+ }
5110
+ if (this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
5111
+ return {
5112
+ success: true,
5113
+ componentName: name,
5114
+ status: this.getComponentStatus(name)
5115
+ };
4775
5116
  }
4776
5117
  this.componentStates.set(name, "stopped");
4777
5118
  this.runningComponents.delete(name);
4778
5119
  this.stalledComponents.delete(name);
4779
5120
  this.updateStartedFlag();
4780
5121
  if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
4781
- this.logger.info(
4782
- "Auto-detaching process signals on last component stop"
4783
- );
5122
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
4784
5123
  this.detachSignals();
4785
5124
  }
4786
5125
  const timestamps = this.componentTimestamps.get(name) ?? {
@@ -4801,8 +5140,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4801
5140
  status: this.getComponentStatus(name)
4802
5141
  };
4803
5142
  } catch (error) {
5143
+ if (this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
5144
+ return {
5145
+ success: true,
5146
+ componentName: name,
5147
+ status: this.getComponentStatus(name)
5148
+ };
5149
+ }
4804
5150
  const err = error instanceof Error ? error : new Error(String(error));
4805
- const isTimeout = err.message === "Force shutdown timed out";
5151
+ const isTimeout = err.message === LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT;
4806
5152
  const stallInfo = {
4807
5153
  name,
4808
5154
  phase: "force",
@@ -4833,12 +5179,13 @@ var LifecycleManager = class extends EventEmitterProtected {
4833
5179
  return {
4834
5180
  success: false,
4835
5181
  componentName: name,
4836
- reason: isTimeout ? "Force shutdown timed out" : err.message,
5182
+ reason: isTimeout ? LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT : err.message,
4837
5183
  code: isTimeout ? "component_shutdown_timeout" : "unknown_error",
4838
5184
  error: err,
4839
5185
  status: this.getComponentStatus(name)
4840
5186
  };
4841
5187
  } finally {
5188
+ cleanupForceWaiter();
4842
5189
  if (timeoutHandle) {
4843
5190
  clearTimeout(timeoutHandle);
4844
5191
  }
@@ -4915,7 +5262,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4915
5262
  "Failed to stop component during rollback, continuing: {{error.message}}",
4916
5263
  {
4917
5264
  params: {
4918
- error: result.error || new Error(result.reason || "Unknown error")
5265
+ error: result.error || new Error(
5266
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
5267
+ )
4919
5268
  }
4920
5269
  }
4921
5270
  );
@@ -4929,10 +5278,13 @@ var LifecycleManager = class extends EventEmitterProtected {
4929
5278
  }
4930
5279
  this.logger.info(`Auto-attaching process signals on ${trigger}`);
4931
5280
  this.attachSignals();
5281
+ if (this.isStarting) {
5282
+ this.autoAttachedSignalsDuringStartup = true;
5283
+ }
4932
5284
  return true;
4933
5285
  }
4934
5286
  autoDetachSignalsIfIdle(trigger) {
4935
- if (!this.detachSignalsOnStop || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
5287
+ if (!this.detachSignalsOnStop || this.isStarting || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
4936
5288
  return;
4937
5289
  }
4938
5290
  this.logger.info(`Auto-detaching process signals after ${trigger}`);
@@ -4974,6 +5326,210 @@ var LifecycleManager = class extends EventEmitterProtected {
4974
5326
  }).catch(() => {
4975
5327
  });
4976
5328
  }
5329
+ consumeUnexpectedStopsDuringStartup(startedComponents, failedOptionalComponents) {
5330
+ if (this.unexpectedStopsDuringStartup.size === 0) {
5331
+ return { startedComponents: [...startedComponents] };
5332
+ }
5333
+ const remainingStartedComponents = [];
5334
+ let requiredFailure;
5335
+ for (const name of startedComponents) {
5336
+ const startupStopError = this.unexpectedStopsDuringStartup.get(name);
5337
+ if (startupStopError === void 0) {
5338
+ remainingStartedComponents.push(name);
5339
+ continue;
5340
+ }
5341
+ this.unexpectedStopsDuringStartup.delete(name);
5342
+ const error = startupStopError ?? new Error(`Component "${name}" stopped unexpectedly during startup`);
5343
+ const component = this.getComponent(name);
5344
+ if (component?.isOptional()) {
5345
+ if (!failedOptionalComponents.some((entry) => entry.name === name)) {
5346
+ failedOptionalComponents.push({ name, error });
5347
+ }
5348
+ this.logger.entity(name).warn(
5349
+ LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
5350
+ {
5351
+ params: { error }
5352
+ }
5353
+ );
5354
+ continue;
5355
+ }
5356
+ this.logger.entity(name).error(
5357
+ LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
5358
+ {
5359
+ params: { error }
5360
+ }
5361
+ );
5362
+ requiredFailure ??= { name, error };
5363
+ }
5364
+ return {
5365
+ startedComponents: remainingStartedComponents,
5366
+ requiredFailure
5367
+ };
5368
+ }
5369
+ /**
5370
+ * Issues and returns a unique stop attempt token for a component.
5371
+ *
5372
+ * Each stop attempt (graceful or force-retry) gets a unique token.
5373
+ * The late-resolution handler captures this token in its closure so it can
5374
+ * skip any stall entries that were created by a *later* stop attempt — e.g. a
5375
+ * force-retry that also timed out after the original graceful promise floated
5376
+ * in the background.
5377
+ */
5378
+ issueStopAttemptToken(name) {
5379
+ const next = ulid2();
5380
+ this.componentStopAttemptTokens.set(name, next);
5381
+ return next;
5382
+ }
5383
+ createPendingForceStopWaiter(name) {
5384
+ let isResolved = false;
5385
+ let waiters = this.pendingForceStopWaiters.get(name);
5386
+ if (!waiters) {
5387
+ waiters = /* @__PURE__ */ new Set();
5388
+ this.pendingForceStopWaiters.set(name, waiters);
5389
+ }
5390
+ let resolveWaiter;
5391
+ const promise = new Promise((resolve) => {
5392
+ resolveWaiter = () => {
5393
+ if (isResolved) {
5394
+ return;
5395
+ }
5396
+ isResolved = true;
5397
+ resolve();
5398
+ };
5399
+ });
5400
+ waiters.add(resolveWaiter);
5401
+ return {
5402
+ promise,
5403
+ cleanup: () => {
5404
+ const pending = this.pendingForceStopWaiters.get(name);
5405
+ if (!pending) {
5406
+ return;
5407
+ }
5408
+ pending.delete(resolveWaiter);
5409
+ if (pending.size === 0) {
5410
+ this.pendingForceStopWaiters.delete(name);
5411
+ }
5412
+ }
5413
+ };
5414
+ }
5415
+ resolvePendingForceStopWaiters(name) {
5416
+ const waiters = this.pendingForceStopWaiters.get(name);
5417
+ if (!waiters || waiters.size === 0) {
5418
+ return;
5419
+ }
5420
+ this.pendingForceStopWaiters.delete(name);
5421
+ for (const resolve of waiters) {
5422
+ resolve();
5423
+ }
5424
+ }
5425
+ /**
5426
+ * Called when a stop promise eventually resolves after its timeout path already fired.
5427
+ *
5428
+ * Usually this means a previously stalled component's original stop() or
5429
+ * onShutdownForce() promise finally resolved, so the manager can clear the
5430
+ * stall and transition the component to stopped without a manual retry.
5431
+ *
5432
+ * There is one extra overlap case for graceful stop(): stop() can resolve
5433
+ * after the graceful timeout but before onShutdownForce() itself times out.
5434
+ * In that window no stall entry exists yet, but the component still finished
5435
+ * stopping cleanly, so we finalize it here and let the later force-timeout
5436
+ * path observe the already-stopped state and no-op. This overlap fix is
5437
+ * scoped to the same stop token and will not cross a later retry attempt.
5438
+ *
5439
+ * Two guards prevent stale floating promises from incorrectly clearing state:
5440
+ *
5441
+ * 1. token guard — if a newer stop attempt (e.g. a retryStalled
5442
+ * force-retry) has started since this promise was launched, its token
5443
+ * won't match and we bail out immediately.
5444
+ *
5445
+ * 2. state/stall guard — if the component was unregistered, restarted, or
5446
+ * already cleared by another path, there will be neither a matching stall
5447
+ * entry nor the force-phase overlap state, so we bail out.
5448
+ */
5449
+ handleLateStopResolution(name, token, source) {
5450
+ if (this.componentStopAttemptTokens.get(name) !== token) {
5451
+ return;
5452
+ }
5453
+ const currentState = this.componentStates.get(name);
5454
+ const stallInfo = this.stalledComponents.get(name);
5455
+ const isCompletedDuringForcePhase = source === "graceful" && !stallInfo && currentState === "force-stopping";
5456
+ if (stallInfo && currentState !== "stalled") {
5457
+ this.stalledComponents.delete(name);
5458
+ return;
5459
+ }
5460
+ if (!stallInfo && !isCompletedDuringForcePhase) {
5461
+ return;
5462
+ }
5463
+ const stalledDurationMS = stallInfo ? Date.now() - stallInfo.stalledAt : void 0;
5464
+ if (stallInfo) {
5465
+ this.stalledComponents.delete(name);
5466
+ }
5467
+ this.componentStates.set(name, "stopped");
5468
+ this.runningComponents.delete(name);
5469
+ this.componentErrors.set(name, null);
5470
+ this.updateStartedFlag();
5471
+ this.resolvePendingForceStopWaiters(name);
5472
+ if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
5473
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
5474
+ this.detachSignals();
5475
+ }
5476
+ const timestamps = this.componentTimestamps.get(name) ?? {
5477
+ startedAt: null,
5478
+ stoppedAt: null
5479
+ };
5480
+ timestamps.stoppedAt = Date.now();
5481
+ this.componentTimestamps.set(name, timestamps);
5482
+ this.logger.entity(name).info(
5483
+ stallInfo ? "Stalled component completed stop late, stall cleared" : "Graceful stop completed after force phase started",
5484
+ stalledDurationMS ? { params: { stalledDurationMS } } : void 0
5485
+ );
5486
+ if (source === "force") {
5487
+ this.lifecycleEvents.componentShutdownForceCompleted(name);
5488
+ }
5489
+ if (stallInfo && stalledDurationMS !== void 0) {
5490
+ this.lifecycleEvents.componentStalledResolved(
5491
+ name,
5492
+ stallInfo,
5493
+ stalledDurationMS
5494
+ );
5495
+ }
5496
+ this.lifecycleEvents.componentStopped(name, this.getComponentStatus(name));
5497
+ }
5498
+ handleComponentUnexpectedStop(name, startAttemptToken, error) {
5499
+ const currentState = this.componentStates.get(name);
5500
+ if (
5501
+ // Startup-time self-stops are valid too: start() may still be awaiting
5502
+ // some async work while an internal listener has already observed that
5503
+ // the component died and reported it.
5504
+ currentState !== "starting" && currentState !== "running" || this.componentStartAttemptTokens.get(name) !== startAttemptToken
5505
+ ) {
5506
+ return false;
5507
+ }
5508
+ this.runningComponents.delete(name);
5509
+ this.componentStates.set(name, "stopped");
5510
+ this.componentErrors.set(name, error ?? null);
5511
+ if (this.isStarting) {
5512
+ this.unexpectedStopsDuringStartup.set(name, error ?? null);
5513
+ }
5514
+ this.updateStartedFlag();
5515
+ if (this.detachSignalsOnStop && !this.isStarting && this.runningComponents.size === 0 && this.processSignalManager) {
5516
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
5517
+ this.detachSignals();
5518
+ }
5519
+ const timestamps = this.componentTimestamps.get(name) ?? {
5520
+ startedAt: null,
5521
+ stoppedAt: null
5522
+ };
5523
+ timestamps.stoppedAt = Date.now();
5524
+ this.componentTimestamps.set(name, timestamps);
5525
+ this.logger.entity(name).warn(
5526
+ error ? `Component stopped unexpectedly: ${error.message}` : "Component stopped unexpectedly",
5527
+ { params: { error } }
5528
+ );
5529
+ this.lifecycleEvents.componentUnexpectedStop(name, error);
5530
+ this.lifecycleEvents.componentStopped(name, this.getComponentStatus(name));
5531
+ return true;
5532
+ }
4977
5533
  /**
4978
5534
  * Safe emit wrapper - prevents event handler errors from breaking lifecycle
4979
5535
  */
@@ -5255,121 +5811,377 @@ var LifecycleManager = class extends EventEmitterProtected {
5255
5811
  }
5256
5812
  /**
5257
5813
  * Handle shutdown signal - initiates stopAllComponents().
5258
- * Double signal protection: if already shutting down, log warning and ignore.
5814
+ *
5815
+ * Four cases depending on the current shutdown state:
5816
+ *
5817
+ * 1. **Active shutdown** (`isShuttingDown = true`): escalate through the
5818
+ * repeated-shutdown policy if configured, otherwise log and discard.
5819
+ * Emits `signal:shutdown` with `isAlreadyShuttingDown: true` and returns
5820
+ * without starting another shutdown.
5821
+ *
5822
+ * 2. **Armed post-failure** (previous shutdown finished, armed window still
5823
+ * open): count the request toward the escalation window, emit
5824
+ * `signal:shutdown` with `isAlreadyShuttingDown: false`, then start a
5825
+ * new `stopAllComponents()` run to retry.
5826
+ *
5827
+ * 3. **Armed post-failure expired** (armed window opened but has since
5828
+ * elapsed): expire the stale state, treat the request as a fresh
5829
+ * shutdown (falls through to case 4).
5830
+ *
5831
+ * 4. **Fresh shutdown** (no prior shutdown state): seed escalation tracking
5832
+ * if policy is configured, emit `signal:shutdown` with
5833
+ * `isAlreadyShuttingDown: false`, and start `stopAllComponents()`.
5834
+ *
5835
+ * In all cases `signal:shutdown` is emitted exactly once.
5259
5836
  */
5260
5837
  handleShutdownRequest(method) {
5261
5838
  if (this.isShuttingDown) {
5262
- this.logger.warn("Shutdown already in progress, ignoring signal", {
5263
- params: { method }
5264
- });
5265
- return;
5839
+ this.lifecycleEvents.signalShutdown(method, true);
5840
+ if (this.handleRepeatedShutdownRequest(method)) {
5841
+ return;
5842
+ }
5843
+ }
5844
+ let didEmitShutdownSignal = false;
5845
+ let shouldSeedRepeatedShutdownState = this.repeatedShutdownRequestPolicy !== void 0;
5846
+ if (this.repeatedShutdownRequestPolicy && this.repeatedShutdownRequestState.firstRequestAt !== null && this.normalizeRepeatedShutdownRequestStateArmedStatus()) {
5847
+ this.lifecycleEvents.signalShutdown(method, false);
5848
+ didEmitShutdownSignal = true;
5849
+ shouldSeedRepeatedShutdownState = false;
5850
+ this.handleRepeatedShutdownRequest(method);
5851
+ }
5852
+ if (shouldSeedRepeatedShutdownState) {
5853
+ this.seedRepeatedShutdownRequestState(method);
5266
5854
  }
5267
5855
  this.logger.info("Shutdown signal received", { params: { method } });
5268
- this.lifecycleEvents.signalShutdown(method);
5856
+ if (!didEmitShutdownSignal) {
5857
+ this.lifecycleEvents.signalShutdown(method, false);
5858
+ }
5269
5859
  void this.stopAllComponentsInternal(method, {
5270
5860
  ...this.shutdownOptions
5271
5861
  });
5272
5862
  }
5273
5863
  /**
5274
- * Handle reload request - calls custom callback or broadcasts to components.
5275
- *
5276
- * When called from signal handlers (source='signal'), the Promise is started
5277
- * but not awaited due to Node.js signal handler constraints. Components are
5278
- * still notified and the work completes, but return values are not accessible.
5279
- *
5280
- * When called from manual triggers (source='trigger'), the Promise is awaited
5281
- * and results are returned for programmatic use.
5864
+ * Tracks repeated shutdown requests during an active shutdown and optionally
5865
+ * invokes the configured force shutdown callback when the threshold is reached.
5282
5866
  *
5283
- * @param source - Whether triggered from signal manager or manual trigger
5867
+ * @returns true when the request was consumed as part of the repeated-shutdown
5868
+ * escalation flow, false when the caller should treat it as a fresh shutdown request
5284
5869
  */
5285
- async handleReloadRequest(source = "trigger") {
5286
- this.logger.info("Reload request received", { params: { source } });
5287
- this.lifecycleEvents.signalReload();
5288
- if (this.onReloadRequested) {
5289
- const broadcastFn = () => this.broadcastReload();
5290
- const result = this.onReloadRequested(broadcastFn);
5291
- if (isPromise(result)) {
5292
- await result;
5293
- }
5294
- return {
5295
- signal: "reload",
5296
- results: [],
5297
- timedOut: false,
5298
- code: "ok"
5299
- };
5870
+ handleRepeatedShutdownRequest(method) {
5871
+ const policy = this.repeatedShutdownRequestPolicy;
5872
+ if (!policy) {
5873
+ this.logger.warn("Shutdown already in progress, ignoring signal", {
5874
+ params: { method }
5875
+ });
5876
+ return true;
5300
5877
  }
5301
- return this.broadcastReload();
5878
+ const now = Date.now();
5879
+ const state = this.repeatedShutdownRequestState;
5880
+ if (state.remainsArmedUntil !== null) {
5881
+ if (now >= state.remainsArmedUntil) {
5882
+ this.expireRepeatedShutdownRequestState();
5883
+ return false;
5884
+ }
5885
+ this.refreshRepeatedShutdownArmedWindow(now);
5886
+ }
5887
+ const shouldStartNewWindow = state.repeatedWindowStartedAt === null || now - state.repeatedWindowStartedAt > policy.withinMS;
5888
+ if (shouldStartNewWindow) {
5889
+ state.requestCount = 1;
5890
+ state.repeatedWindowStartedAt = now;
5891
+ } else {
5892
+ state.requestCount++;
5893
+ }
5894
+ state.latestMethod = method;
5895
+ state.latestRequestAt = now;
5896
+ this.logger.warn(
5897
+ 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",
5898
+ {
5899
+ params: {
5900
+ method,
5901
+ requestCount: state.requestCount,
5902
+ firstMethod: state.firstMethod,
5903
+ latestMethod: state.latestMethod,
5904
+ firstRequestAt: state.firstRequestAt,
5905
+ latestRequestAt: state.latestRequestAt,
5906
+ repeatedWindowStartedAt: state.repeatedWindowStartedAt,
5907
+ remainsArmedUntil: state.remainsArmedUntil,
5908
+ withinMS: policy.withinMS,
5909
+ forceAfterCount: policy.forceAfterCount
5910
+ }
5911
+ }
5912
+ );
5913
+ if (
5914
+ // Force escalation is single-fire per shutdown cycle. Later requests are
5915
+ // still logged but do not re-enter user force-shutdown logic.
5916
+ state.hasTriggeredForceShutdown || state.requestCount < policy.forceAfterCount || state.firstMethod === null || state.firstRequestAt === null || state.latestMethod === null || state.latestRequestAt === null
5917
+ ) {
5918
+ return true;
5919
+ }
5920
+ state.hasTriggeredForceShutdown = true;
5921
+ const context = {
5922
+ requestCount: state.requestCount,
5923
+ firstMethod: state.firstMethod,
5924
+ latestMethod: state.latestMethod,
5925
+ firstRequestAt: state.firstRequestAt,
5926
+ latestRequestAt: state.latestRequestAt,
5927
+ isShuttingDown: this.isShuttingDown,
5928
+ wasArmedAfterFailure: state.remainsArmedUntil !== null
5929
+ };
5930
+ this.logger.warn(
5931
+ "Repeated shutdown request threshold reached, invoking force shutdown handler",
5932
+ {
5933
+ params: {
5934
+ method,
5935
+ requestCount: context.requestCount,
5936
+ firstMethod: context.firstMethod,
5937
+ latestMethod: context.latestMethod,
5938
+ firstRequestAt: context.firstRequestAt,
5939
+ latestRequestAt: context.latestRequestAt,
5940
+ repeatedWindowStartedAt: state.repeatedWindowStartedAt,
5941
+ remainsArmedUntil: state.remainsArmedUntil,
5942
+ withinMS: policy.withinMS,
5943
+ forceAfterCount: policy.forceAfterCount
5944
+ }
5945
+ }
5946
+ );
5947
+ safeHandleCallback(
5948
+ "repeatedShutdownRequestPolicy.onForceShutdown",
5949
+ policy.onForceShutdown,
5950
+ context
5951
+ );
5952
+ this.lifecycleEvents.lifecycleManagerShutdownEscalationForced({
5953
+ firstMethod: context.firstMethod,
5954
+ latestMethod: context.latestMethod,
5955
+ requestCount: context.requestCount,
5956
+ firstRequestAt: context.firstRequestAt,
5957
+ latestRequestAt: context.latestRequestAt,
5958
+ wasArmedAfterFailure: context.wasArmedAfterFailure
5959
+ });
5960
+ return true;
5302
5961
  }
5303
5962
  /**
5304
- * Handle info request - calls custom callback or broadcasts to components.
5305
- *
5306
- * When called from signal handlers, the Promise executes but return values
5307
- * are not accessible due to Node.js signal handler constraints.
5963
+ * Clears repeated shutdown request tracking so a new shutdown cycle starts fresh.
5964
+ */
5965
+ resetRepeatedShutdownRequestState() {
5966
+ this.clearRepeatedShutdownExpiryTimer();
5967
+ this.repeatedShutdownRequestState = {
5968
+ requestCount: 0,
5969
+ firstMethod: null,
5970
+ latestMethod: null,
5971
+ firstRequestAt: null,
5972
+ latestRequestAt: null,
5973
+ repeatedWindowStartedAt: null,
5974
+ hasTriggeredForceShutdown: false,
5975
+ remainsArmedUntil: null
5976
+ };
5977
+ }
5978
+ /**
5979
+ * Clear any pending expiration timer for the post-failure escalation window.
5980
+ */
5981
+ clearRepeatedShutdownExpiryTimer() {
5982
+ if (this.repeatedShutdownExpiryTimer === null) {
5983
+ return;
5984
+ }
5985
+ clearTimeout(this.repeatedShutdownExpiryTimer);
5986
+ this.repeatedShutdownExpiryTimer = null;
5987
+ }
5988
+ /**
5989
+ * Returns whether post-failure escalation remains armed after first
5990
+ * normalizing any stale timer-backed state.
5308
5991
  *
5309
- * @param source - Whether triggered from signal manager or manual trigger
5992
+ * The method can expire old armed windows as a side effect because the timer
5993
+ * callback may not have run yet on a delayed event loop. Callers use this
5994
+ * when they need the effective runtime truth, not just the last timer write.
5310
5995
  */
5311
- async handleInfoRequest(source = "trigger") {
5312
- this.logger.info("Info request received", { params: { source } });
5313
- this.lifecycleEvents.signalInfo();
5314
- if (this.onInfoRequested) {
5315
- const broadcastFn = () => this.broadcastInfo();
5316
- const result = this.onInfoRequested(broadcastFn);
5317
- if (isPromise(result)) {
5318
- await result;
5996
+ normalizeRepeatedShutdownRequestStateArmedStatus(now = Date.now()) {
5997
+ const armedUntil = this.repeatedShutdownRequestState.remainsArmedUntil;
5998
+ if (armedUntil === null) {
5999
+ return false;
6000
+ }
6001
+ if (now >= armedUntil) {
6002
+ this.expireRepeatedShutdownRequestState();
6003
+ return false;
6004
+ }
6005
+ return true;
6006
+ }
6007
+ /**
6008
+ * Transition armed post-failure escalation state into its expired/reset state.
6009
+ */
6010
+ expireRepeatedShutdownRequestState() {
6011
+ const policy = this.repeatedShutdownRequestPolicy;
6012
+ const state = this.repeatedShutdownRequestState;
6013
+ if (!policy || state.remainsArmedUntil === null) {
6014
+ return;
6015
+ }
6016
+ const armedUntil = state.remainsArmedUntil;
6017
+ this.clearRepeatedShutdownExpiryTimer();
6018
+ const expiredState = {
6019
+ firstMethod: state.firstMethod,
6020
+ latestMethod: state.latestMethod,
6021
+ requestCount: state.requestCount,
6022
+ armedUntil
6023
+ };
6024
+ this.logger.warn(
6025
+ "Repeated shutdown escalation window expired, clearing previous shutdown state",
6026
+ {
6027
+ params: {
6028
+ remainsArmedUntil: armedUntil,
6029
+ withinMS: policy.withinMS,
6030
+ forceAfterCount: policy.forceAfterCount
6031
+ }
5319
6032
  }
5320
- return {
5321
- signal: "info",
5322
- results: [],
5323
- timedOut: false,
5324
- code: "ok"
5325
- };
6033
+ );
6034
+ if (expiredState.firstMethod !== null) {
6035
+ this.lifecycleEvents.lifecycleManagerShutdownEscalationExpired({
6036
+ firstMethod: expiredState.firstMethod,
6037
+ latestMethod: expiredState.latestMethod,
6038
+ requestCount: expiredState.requestCount,
6039
+ armedUntil: expiredState.armedUntil
6040
+ });
5326
6041
  }
5327
- return this.broadcastInfo();
6042
+ this.resetRepeatedShutdownRequestState();
5328
6043
  }
5329
6044
  /**
5330
- * Handle debug request - calls custom callback or broadcasts to components.
6045
+ * Arms or refreshes the post-failure escalation window and its expiration timer.
6046
+ */
6047
+ refreshRepeatedShutdownArmedWindow(now = Date.now()) {
6048
+ const policy = this.repeatedShutdownRequestPolicy;
6049
+ if (!policy) {
6050
+ return;
6051
+ }
6052
+ this.clearRepeatedShutdownExpiryTimer();
6053
+ const armedUntil = now + policy.armedAfterFailureMS;
6054
+ this.repeatedShutdownRequestState.remainsArmedUntil = armedUntil;
6055
+ this.repeatedShutdownExpiryTimer = setTimeout(() => {
6056
+ this.expireRepeatedShutdownRequestState();
6057
+ }, policy.armedAfterFailureMS);
6058
+ this.repeatedShutdownExpiryTimer.unref();
6059
+ }
6060
+ /**
6061
+ * Seeds shutdown escalation tracking for a new shutdown cycle.
5331
6062
  *
5332
- * When called from signal handlers, the Promise executes but return values
5333
- * are not accessible due to Node.js signal handler constraints.
6063
+ * The first shutdown trigger starts graceful shutdown and arms escalation with
6064
+ * an effective post-start count of 0. Later shutdown requests can then count
6065
+ * toward the configured force threshold regardless of whether the shutdown
6066
+ * started from a signal, keyboard shortcut, or direct API call.
6067
+ */
6068
+ seedRepeatedShutdownRequestState(method) {
6069
+ const now = Date.now();
6070
+ this.repeatedShutdownRequestState = {
6071
+ requestCount: 0,
6072
+ firstMethod: method,
6073
+ latestMethod: method,
6074
+ firstRequestAt: now,
6075
+ latestRequestAt: now,
6076
+ repeatedWindowStartedAt: null,
6077
+ hasTriggeredForceShutdown: false,
6078
+ remainsArmedUntil: null
6079
+ };
6080
+ }
6081
+ /**
6082
+ * Preserves a short-lived post-failure escalation window after shutdown
6083
+ * returns unsuccessfully so operators can keep pressing shutdown without
6084
+ * losing the existing force count the moment the graceful attempt finishes.
6085
+ */
6086
+ armRepeatedShutdownAfterFailure() {
6087
+ const policy = this.repeatedShutdownRequestPolicy;
6088
+ const state = this.repeatedShutdownRequestState;
6089
+ if (!policy || policy.armedAfterFailureMS <= 0 || // armedAfterFailureMS = 0 disables post-failure arming
6090
+ state.firstRequestAt === null || state.hasTriggeredForceShutdown) {
6091
+ return;
6092
+ }
6093
+ this.refreshRepeatedShutdownArmedWindow();
6094
+ const armedUntil = state.remainsArmedUntil;
6095
+ if (state.firstMethod !== null && armedUntil !== null) {
6096
+ this.lifecycleEvents.lifecycleManagerShutdownEscalationArmed({
6097
+ firstMethod: state.firstMethod,
6098
+ requestCount: state.requestCount,
6099
+ armedUntil
6100
+ });
6101
+ }
6102
+ }
6103
+ /**
6104
+ * Shared dispatch path for reload/info/debug requests. Logs the dispatch,
6105
+ * emits the signal event, then either invokes the user-supplied callback
6106
+ * (passing the broadcast function so the user controls when/whether to
6107
+ * broadcast) or broadcasts directly when no callback is configured.
5334
6108
  *
5335
- * @param source - Whether triggered from signal manager or manual trigger
6109
+ * When called from signal handlers (source='signal'), the Promise is started
6110
+ * but not awaited — Node.js signal handlers cannot return values, so results
6111
+ * are not accessible. Components are still notified and the work completes.
6112
+ * When called from manual triggers (source='trigger'), the Promise is awaited
6113
+ * and results are returned for programmatic use.
5336
6114
  */
5337
- async handleDebugRequest(source = "trigger") {
5338
- this.logger.info("Debug request received", { params: { source } });
5339
- this.lifecycleEvents.signalDebug();
5340
- if (this.onDebugRequested) {
5341
- const broadcastFn = () => this.broadcastDebug();
5342
- const result = this.onDebugRequested(broadcastFn);
6115
+ async handleSignalRequest(descriptor, source) {
6116
+ this.logger.info(descriptor.dispatchedLogLabel, { params: { source } });
6117
+ descriptor.emitSignal();
6118
+ if (descriptor.customCallback) {
6119
+ const result = descriptor.customCallback(descriptor.broadcast);
5343
6120
  if (isPromise(result)) {
5344
6121
  await result;
5345
6122
  }
5346
6123
  return {
5347
- signal: "debug",
6124
+ signal: descriptor.signal,
5348
6125
  results: [],
5349
6126
  timedOut: false,
5350
6127
  code: "ok"
5351
6128
  };
5352
6129
  }
5353
- return this.broadcastDebug();
6130
+ return descriptor.broadcast();
6131
+ }
6132
+ async handleReloadRequest(source = "trigger") {
6133
+ return this.handleSignalRequest(
6134
+ {
6135
+ signal: "reload",
6136
+ dispatchedLogLabel: "Reload dispatched",
6137
+ emitSignal: () => this.lifecycleEvents.signalReload(),
6138
+ customCallback: this.onReloadRequested,
6139
+ broadcast: () => this.broadcastReload()
6140
+ },
6141
+ source
6142
+ );
6143
+ }
6144
+ async handleInfoRequest(source = "trigger") {
6145
+ return this.handleSignalRequest(
6146
+ {
6147
+ signal: "info",
6148
+ dispatchedLogLabel: "Info dispatched",
6149
+ emitSignal: () => this.lifecycleEvents.signalInfo(),
6150
+ customCallback: this.onInfoRequested,
6151
+ broadcast: () => this.broadcastInfo()
6152
+ },
6153
+ source
6154
+ );
6155
+ }
6156
+ async handleDebugRequest(source = "trigger") {
6157
+ return this.handleSignalRequest(
6158
+ {
6159
+ signal: "debug",
6160
+ dispatchedLogLabel: "Debug dispatched",
6161
+ emitSignal: () => this.lifecycleEvents.signalDebug(),
6162
+ customCallback: this.onDebugRequested,
6163
+ broadcast: () => this.broadcastDebug()
6164
+ },
6165
+ source
6166
+ );
5354
6167
  }
5355
6168
  /**
5356
- * Broadcast reload signal to all running components.
5357
- * Calls onReload() on components that implement it.
5358
- * Continues on errors - collects all results.
6169
+ * Shared signal broadcast pipeline used by reload/info/debug.
6170
+ * Iterates running components, runs the picked handler with timeout, and
6171
+ * aggregates per-component results into a SignalBroadcastResult.
5359
6172
  */
5360
- async broadcastReload() {
6173
+ async runSignalBroadcast(descriptor) {
5361
6174
  const results = [];
5362
- const componentsToReload = this.components.filter(
6175
+ const targets = this.components.filter(
5363
6176
  (component) => this.runningComponents.has(component.getName())
5364
6177
  );
5365
6178
  if (this.isStarting) {
5366
- this.logger.info(
5367
- "Reload during startup: only reloading already-started components"
5368
- );
6179
+ this.logger.info(descriptor.startupLog);
5369
6180
  }
5370
- for (const component of componentsToReload) {
6181
+ for (const component of targets) {
5371
6182
  const name = component.getName();
5372
- if (!component.onReload) {
6183
+ const handler = descriptor.pickHandler(component);
6184
+ if (!handler) {
5373
6185
  results.push({
5374
6186
  name,
5375
6187
  called: false,
@@ -5379,13 +6191,13 @@ var LifecycleManager = class extends EventEmitterProtected {
5379
6191
  });
5380
6192
  continue;
5381
6193
  }
5382
- this.lifecycleEvents.componentReloadStarted(name);
6194
+ descriptor.emitStarted(name);
5383
6195
  const timeoutMS = component.signalTimeoutMS;
5384
6196
  let timeoutHandle;
5385
6197
  const timeoutResult = { timedOut: true };
5386
6198
  try {
5387
- const result = component.onReload();
5388
- const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
6199
+ const handlerResult = handler();
6200
+ const handlerPromise = isPromise(handlerResult) ? handlerResult : Promise.resolve(handlerResult);
5389
6201
  const outcome = timeoutMS > 0 ? await Promise.race([
5390
6202
  handlerPromise,
5391
6203
  new Promise((resolve) => {
@@ -5395,7 +6207,7 @@ var LifecycleManager = class extends EventEmitterProtected {
5395
6207
  })
5396
6208
  ]) : await handlerPromise;
5397
6209
  if (outcome === timeoutResult) {
5398
- this.logger.entity(name).warn("Reload handler timed out", {
6210
+ this.logger.entity(name).warn(descriptor.timeoutLog, {
5399
6211
  params: { timeoutMS }
5400
6212
  });
5401
6213
  Promise.resolve(handlerPromise).catch(() => {
@@ -5408,7 +6220,7 @@ var LifecycleManager = class extends EventEmitterProtected {
5408
6220
  code: "timeout"
5409
6221
  });
5410
6222
  } else {
5411
- this.lifecycleEvents.componentReloadCompleted(name);
6223
+ descriptor.emitCompleted(name);
5412
6224
  results.push({
5413
6225
  name,
5414
6226
  called: true,
@@ -5419,10 +6231,10 @@ var LifecycleManager = class extends EventEmitterProtected {
5419
6231
  }
5420
6232
  } catch (error) {
5421
6233
  const err = error instanceof Error ? error : new Error(String(error));
5422
- this.logger.entity(name).error("Reload failed: {{error.message}}", {
6234
+ this.logger.entity(name).error(descriptor.errorLog, {
5423
6235
  params: { error: err }
5424
6236
  });
5425
- this.lifecycleEvents.componentReloadFailed(name, err);
6237
+ descriptor.emitFailed(name, err);
5426
6238
  results.push({
5427
6239
  name,
5428
6240
  called: true,
@@ -5443,108 +6255,45 @@ var LifecycleManager = class extends EventEmitterProtected {
5443
6255
  const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
5444
6256
  const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
5445
6257
  return {
5446
- signal: "reload",
6258
+ signal: descriptor.signal,
5447
6259
  results,
5448
6260
  timedOut: hasTimeout,
5449
6261
  code
5450
6262
  };
5451
6263
  }
6264
+ /**
6265
+ * Broadcast reload signal to all running components.
6266
+ * Calls onReload() on components that implement it.
6267
+ * Continues on errors - collects all results.
6268
+ */
6269
+ async broadcastReload() {
6270
+ return this.runSignalBroadcast({
6271
+ signal: "reload",
6272
+ pickHandler: (component) => component.onReload?.bind(component),
6273
+ startupLog: "Reload during startup: only reloading already-started components",
6274
+ timeoutLog: "Reload handler timed out",
6275
+ errorLog: "Reload failed: {{error.message}}",
6276
+ emitStarted: (name) => this.lifecycleEvents.componentReloadStarted(name),
6277
+ emitCompleted: (name) => this.lifecycleEvents.componentReloadCompleted(name),
6278
+ emitFailed: (name, error) => this.lifecycleEvents.componentReloadFailed(name, error)
6279
+ });
6280
+ }
5452
6281
  /**
5453
6282
  * Broadcast info signal to all running components.
5454
6283
  * Calls onInfo() on components that implement it.
5455
6284
  * Continues on errors - collects all results.
5456
6285
  */
5457
6286
  async broadcastInfo() {
5458
- const results = [];
5459
- const componentsToNotify = this.components.filter(
5460
- (component) => this.runningComponents.has(component.getName())
5461
- );
5462
- if (this.isStarting) {
5463
- this.logger.info(
5464
- "Info during startup: only notifying already-started components"
5465
- );
5466
- }
5467
- for (const component of componentsToNotify) {
5468
- const name = component.getName();
5469
- if (!component.onInfo) {
5470
- results.push({
5471
- name,
5472
- called: false,
5473
- error: null,
5474
- timedOut: false,
5475
- code: "no_handler"
5476
- });
5477
- continue;
5478
- }
5479
- this.lifecycleEvents.componentInfoStarted(name);
5480
- const timeoutMS = component.signalTimeoutMS;
5481
- let timeoutHandle;
5482
- const timeoutResult = { timedOut: true };
5483
- try {
5484
- const result = component.onInfo();
5485
- const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
5486
- const outcome = timeoutMS > 0 ? await Promise.race([
5487
- handlerPromise,
5488
- new Promise((resolve) => {
5489
- timeoutHandle = setTimeout(() => {
5490
- resolve(timeoutResult);
5491
- }, timeoutMS);
5492
- })
5493
- ]) : await handlerPromise;
5494
- if (outcome === timeoutResult) {
5495
- this.logger.entity(name).warn("Info handler timed out", {
5496
- params: { timeoutMS }
5497
- });
5498
- Promise.resolve(handlerPromise).catch(() => {
5499
- });
5500
- results.push({
5501
- name,
5502
- called: true,
5503
- error: null,
5504
- timedOut: true,
5505
- code: "timeout"
5506
- });
5507
- } else {
5508
- this.lifecycleEvents.componentInfoCompleted(name);
5509
- results.push({
5510
- name,
5511
- called: true,
5512
- error: null,
5513
- timedOut: false,
5514
- code: "called"
5515
- });
5516
- }
5517
- } catch (error) {
5518
- const err = error instanceof Error ? error : new Error(String(error));
5519
- this.logger.entity(name).error("Info handler failed: {{error.message}}", {
5520
- params: { error: err }
5521
- });
5522
- this.lifecycleEvents.componentInfoFailed(name, err);
5523
- results.push({
5524
- name,
5525
- called: true,
5526
- error: err,
5527
- timedOut: false,
5528
- code: "error"
5529
- });
5530
- } finally {
5531
- if (timeoutHandle) {
5532
- clearTimeout(timeoutHandle);
5533
- }
5534
- }
5535
- }
5536
- const calledResults = results.filter((result) => result.called);
5537
- const hasError = calledResults.some((result) => result.error);
5538
- const isAllError = calledResults.length > 0 && calledResults.every((result) => result.error);
5539
- const hasTimeout = calledResults.some((result) => result.timedOut);
5540
- const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
5541
- const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
5542
- return {
6287
+ return this.runSignalBroadcast({
5543
6288
  signal: "info",
5544
- results,
5545
- timedOut: hasTimeout,
5546
- code
5547
- };
6289
+ pickHandler: (component) => component.onInfo?.bind(component),
6290
+ startupLog: "Info during startup: only notifying already-started components",
6291
+ timeoutLog: "Info handler timed out",
6292
+ errorLog: "Info handler failed: {{error.message}}",
6293
+ emitStarted: (name) => this.lifecycleEvents.componentInfoStarted(name),
6294
+ emitCompleted: (name) => this.lifecycleEvents.componentInfoCompleted(name),
6295
+ emitFailed: (name, error) => this.lifecycleEvents.componentInfoFailed(name, error)
6296
+ });
5548
6297
  }
5549
6298
  /**
5550
6299
  * Broadcast debug signal to all running components.
@@ -5552,96 +6301,16 @@ var LifecycleManager = class extends EventEmitterProtected {
5552
6301
  * Continues on errors - collects all results.
5553
6302
  */
5554
6303
  async broadcastDebug() {
5555
- const results = [];
5556
- const componentsToNotify = this.components.filter(
5557
- (component) => this.runningComponents.has(component.getName())
5558
- );
5559
- if (this.isStarting) {
5560
- this.logger.info(
5561
- "Debug during startup: only notifying already-started components"
5562
- );
5563
- }
5564
- for (const component of componentsToNotify) {
5565
- const name = component.getName();
5566
- if (!component.onDebug) {
5567
- results.push({
5568
- name,
5569
- called: false,
5570
- error: null,
5571
- timedOut: false,
5572
- code: "no_handler"
5573
- });
5574
- continue;
5575
- }
5576
- this.lifecycleEvents.componentDebugStarted(name);
5577
- const timeoutMS = component.signalTimeoutMS;
5578
- let timeoutHandle;
5579
- const timeoutResult = { timedOut: true };
5580
- try {
5581
- const result = component.onDebug();
5582
- const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
5583
- const outcome = timeoutMS > 0 ? await Promise.race([
5584
- handlerPromise,
5585
- new Promise((resolve) => {
5586
- timeoutHandle = setTimeout(() => {
5587
- resolve(timeoutResult);
5588
- }, timeoutMS);
5589
- })
5590
- ]) : await handlerPromise;
5591
- if (outcome === timeoutResult) {
5592
- this.logger.entity(name).warn("Debug handler timed out", {
5593
- params: { timeoutMS }
5594
- });
5595
- Promise.resolve(handlerPromise).catch(() => {
5596
- });
5597
- results.push({
5598
- name,
5599
- called: true,
5600
- error: null,
5601
- timedOut: true,
5602
- code: "timeout"
5603
- });
5604
- } else {
5605
- this.lifecycleEvents.componentDebugCompleted(name);
5606
- results.push({
5607
- name,
5608
- called: true,
5609
- error: null,
5610
- timedOut: false,
5611
- code: "called"
5612
- });
5613
- }
5614
- } catch (error) {
5615
- const err = error instanceof Error ? error : new Error(String(error));
5616
- this.logger.entity(name).error("Debug handler failed: {{error.message}}", {
5617
- params: { error: err }
5618
- });
5619
- this.lifecycleEvents.componentDebugFailed(name, err);
5620
- results.push({
5621
- name,
5622
- called: true,
5623
- error: err,
5624
- timedOut: false,
5625
- code: "error"
5626
- });
5627
- } finally {
5628
- if (timeoutHandle) {
5629
- clearTimeout(timeoutHandle);
5630
- }
5631
- }
5632
- }
5633
- const calledResults = results.filter((result) => result.called);
5634
- const hasError = calledResults.some((result) => result.error);
5635
- const isAllError = calledResults.length > 0 && calledResults.every((result) => result.error);
5636
- const hasTimeout = calledResults.some((result) => result.timedOut);
5637
- const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
5638
- const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
5639
- return {
6304
+ return this.runSignalBroadcast({
5640
6305
  signal: "debug",
5641
- results,
5642
- timedOut: hasTimeout,
5643
- code
5644
- };
6306
+ pickHandler: (component) => component.onDebug?.bind(component),
6307
+ startupLog: "Debug during startup: only notifying already-started components",
6308
+ timeoutLog: "Debug handler timed out",
6309
+ errorLog: "Debug handler failed: {{error.message}}",
6310
+ emitStarted: (name) => this.lifecycleEvents.componentDebugStarted(name),
6311
+ emitCompleted: (name) => this.lifecycleEvents.componentDebugCompleted(name),
6312
+ emitFailed: (name, error) => this.lifecycleEvents.componentDebugFailed(name, error)
6313
+ });
5645
6314
  }
5646
6315
  };
5647
6316
 
@@ -5667,6 +6336,10 @@ var BaseComponent = class {
5667
6336
  name;
5668
6337
  /** Reference to component-scoped lifecycle (set by manager when registered) */
5669
6338
  lifecycle;
6339
+ /** @internal Set by LifecycleManager while the component is running. */
6340
+ _unexpectedStopHandler;
6341
+ /** @internal Incremented whenever the unexpected-stop handler is re-armed or cleared. */
6342
+ _unexpectedStopGeneration = 0;
5670
6343
  /**
5671
6344
  * Create a new component
5672
6345
  *
@@ -5701,6 +6374,24 @@ var BaseComponent = class {
5701
6374
  // Default if undefined/null/non-finite
5702
6375
  );
5703
6376
  }
6377
+ /** @internal Called by LifecycleManager after a successful start. */
6378
+ _setUnexpectedStopHandler(handler) {
6379
+ this._unexpectedStopGeneration += 1;
6380
+ this._unexpectedStopHandler = handler;
6381
+ const generation = this._unexpectedStopGeneration;
6382
+ this.reportUnexpectedStop = (error) => {
6383
+ if (this._unexpectedStopGeneration !== generation) {
6384
+ return false;
6385
+ }
6386
+ return this._unexpectedStopHandler?.(error) ?? false;
6387
+ };
6388
+ }
6389
+ /** @internal Called by LifecycleManager when stop begins or component is unregistered. */
6390
+ _clearUnexpectedStopHandler() {
6391
+ this._unexpectedStopGeneration += 1;
6392
+ this._unexpectedStopHandler = void 0;
6393
+ this.reportUnexpectedStop = () => false;
6394
+ }
5704
6395
  /**
5705
6396
  * Get component name
5706
6397
  */
@@ -5719,6 +6410,38 @@ var BaseComponent = class {
5719
6410
  isOptional() {
5720
6411
  return this.optional;
5721
6412
  }
6413
+ /**
6414
+ * Run-scoped unexpected-stop callback. Rebound by LifecycleManager on each
6415
+ * successful start so captured references from older runs go stale.
6416
+ */
6417
+ reportUnexpectedStop = () => false;
6418
+ /**
6419
+ * Get this component's own status from the manager's perspective.
6420
+ *
6421
+ * Equivalent to `this.lifecycle.getComponentStatus(this.getName())` but without
6422
+ * needing to pass the name. Returns `undefined` if the component is not registered.
6423
+ *
6424
+ * Check `status?.state === 'running'` to test whether the component is currently running.
6425
+ */
6426
+ getSelfStatus() {
6427
+ return this.lifecycle?.getComponentStatus(this.name);
6428
+ }
6429
+ /**
6430
+ * Capture a run-scoped unexpected-stop reporter for async listeners created during start().
6431
+ *
6432
+ * Unlike calling `this.reportUnexpectedStop()` later, the returned callback becomes a no-op
6433
+ * once the component is stopped, unregistered, or restarted. This prevents stale listeners
6434
+ * from a previous run from stopping a newer run of the same component instance.
6435
+ */
6436
+ getUnexpectedStopReporter() {
6437
+ const generation = this._unexpectedStopGeneration;
6438
+ return (error) => {
6439
+ if (this._unexpectedStopGeneration !== generation) {
6440
+ return false;
6441
+ }
6442
+ return this._unexpectedStopHandler?.(error) ?? false;
6443
+ };
6444
+ }
5722
6445
  };
5723
6446
  export {
5724
6447
  BaseComponent,