feat(mgc-config + state): planner.useRendezvous schema + remaining-transition reads

Three coherent additions that the MGC rendezvous planner depends on:

- machineGroupControl.json: new `planner.useRendezvous` boolean (default
  true). Used by both `_optimalControl` and `equalFlowControl` (via the
  shared `_dispatchFlowDistribution` helper) to gate same-time-landing.

- state.js: external aborts (returnToOperational=false) bump a monotonic
  `sequenceAbortToken`. executeSequence captures it at entry and bails
  out of its for-loop if it advances mid-sequence, so a shutdown that's
  past its ramp-down step doesn't barge through stopping → coolingdown
  when a fresher demand re-engages the pump.

- stateManager.js: new `getRemainingTransitionS()` returns the seconds
  remaining in a timed state by reading the wall-clock entry timestamp.
  buildProfile() reads it so the planner can compute exact eta for a
  child that's currently mid-ladder (warmingup / starting / cooling).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-17 19:43:21 +02:00
parent f8f71a4f1c
commit af02d36b07
4 changed files with 130 additions and 3 deletions

View File

@@ -39,6 +39,11 @@
class stateManager {
constructor(config, logger) {
this.currentState = config.state.current;
// Wall-clock entry timestamp into currentState. Used by
// getRemainingTransitionS() so callers (e.g. MGC movement planner)
// can compute exact remaining time for timed states without
// approximating from the full configured duration.
this.stateEnteredAt = Date.now();
this.availableStates = config.state.available;
this.descriptions = config.state.descriptions;
this.logger = logger;
@@ -63,7 +68,18 @@ class stateManager {
getCurrentState() {
return this.currentState;
}
// Seconds remaining in the current timed state (warmingup, coolingdown,
// starting, stopping, …). Returns 0 for untimed states or once the
// configured duration has elapsed. The MGC movement planner uses this to
// compute exact rendezvous time for protected (non-interruptible) states.
getRemainingTransitionS() {
const d = this.transitionTimes?.[this.currentState] || 0;
if (d <= 0) return 0;
const elapsed = (Date.now() - this.stateEnteredAt) / 1000;
return Math.max(0, d - elapsed);
}
transitionTo(newState,signal) {
return new Promise((resolve, reject) => {
if (signal && signal.aborted) {
@@ -89,6 +105,7 @@ class stateManager {
if (transitionDuration > 0) {
const timeoutId = setTimeout(() => {
this.currentState = newState;
this.stateEnteredAt = Date.now();
resolve(`Transition from ${this.currentState} to ${newState} completed in ${transitionDuration}s.`);
}, transitionDuration * 1000);
if (signal) {
@@ -99,6 +116,7 @@ class stateManager {
}
} else {
this.currentState = newState;
this.stateEnteredAt = Date.now();
resolve(`Immediate transition to ${this.currentState} completed.`);
}
});