({ hubUrl, store, logger, getTaskMeta } = {})
| 446 | |
| 447 | class LifecycleManager { |
| 448 | constructor({ hubUrl, store, logger, getTaskMeta } = {}) { |
| 449 | this.hubUrl = (hubUrl || process.env.A2A_HUB_URL || '').replace(/\/+$/, ''); |
| 450 | this.store = store; |
| 451 | this.logger = logger || console; |
| 452 | this.getTaskMeta = getTaskMeta || null; |
| 453 | this._heartbeatTimer = null; |
| 454 | this._running = false; |
| 455 | this._startedAt = null; |
| 456 | this._lastHeartbeatTickAt = 0; |
| 457 | this._consecutiveFailures = 0; |
| 458 | this._reauthInProgress = false; |
| 459 | this._helloRateLimitUntil = 0; |
| 460 | this._reauthBackoffUntil = 0; |
| 461 | this._consecutiveReauthFailures = 0; |
| 462 | this._driftInterval = null; |
| 463 | this._lastDriftCheckAt = 0; |
| 464 | this._hubUnreachableFailures = 0; |
| 465 | this._hubUnreachableUntil = 0; |
| 466 | this._envSecretSuppressionMarker = normalizeNodeSecretEnvSuppression(this.store && this.store.getState |
| 467 | ? this.store.getState('node_secret_env_suppressed') |
| 468 | : null); |
| 469 | this._envSuppressionClearedForSecret = null; |
| 470 | this._suppressEnvSecret = Boolean(this._envSecretSuppressionMarker && getEnvNodeSecret()); |
| 471 | |
| 472 | // H4 fix: persist the legacy node_id file as soon as the in-memory |
| 473 | // node_id is known, NOT only after a successful hello(). The original |
| 474 | // code persisted only in hello() (see ~L390-398) — but proxy-mode boot |
| 475 | // can fire `reportForceUpdateOutcome` BEFORE hello() returns: |
| 476 | // |
| 477 | // - First-tick heartbeat hits 426 → executeForceUpdate() → exit(78) |
| 478 | // all happens BEFORE the hello() response is processed. |
| 479 | // - enrich.js force_update path can fire during the same window. |
| 480 | // |
| 481 | // `_shortNodeIdForStatePath` in a2aProtocol.js then picks the |
| 482 | // state-file suffix from `_cachedNodeId` (never set in proxy mode — |
| 483 | // only getNodeId() sets it, and proxy never calls getNodeId()) or the |
| 484 | // legacy ~/.evomap/node_id file. With both empty, it falls through |
| 485 | // to 'anon', and the outcome lands at `force_update_last.anon.json`. |
| 486 | // Next boot's hello() writes the real id, the heartbeat reads |
| 487 | // `force_update_last.<8hex>.json`, the anon file is orphaned and the |
| 488 | // outcome is silently lost. |
| 489 | // |
| 490 | // Persisting at construction closes the window. _persistLegacyNodeId |
| 491 | // early-returns on invalid input (NODE_ID_RE gate, same regex as the |
| 492 | // hello() path uses) so a malformed/empty store value is a no-op, |
| 493 | // and it is idempotent on matching content so the cost is one |
| 494 | // existsSync + one readFileSync per construction. We keep the |
| 495 | // existing post-hello call as a safety net in case hello() mints or |
| 496 | // mutates the id. |
| 497 | try { |
| 498 | const earlyNodeId = this.store && this.store.getState |
| 499 | ? this.store.getState('node_id') |
| 500 | : null; |
| 501 | if (earlyNodeId && NODE_ID_RE.test(earlyNodeId)) { |
| 502 | _persistLegacyNodeId(earlyNodeId); |
| 503 | } |
| 504 | } catch (e) { |
| 505 | // Best-effort: persistence failure must never break construction. |
nothing calls this directly
no test coverage detected