Files
hermes-agent/gateway
Teknium 6879a621b1 fix(gateway): live foreign token lock at startup exits 78 instead of retry-queueing forever
BasePlatformAdapter._acquire_platform_lock emits `{scope}_lock` with
retryable=True on purpose (#54167): a MID-RUN reconnect must be able to
recover once the live holder exits or a stale record is cleared. The
startup router keyed solely off that flag, so a live foreign holder of the
bot token at zero-connected startup landed in `_failed_platforms` with
gateway_state=running — alive, deaf, and retry-storming the token every
backoff — instead of the exit-78 (EX_CONFIG / startup_failed) contract
that #51228 established for single-writer conflicts.

Minimal class fix, salvaged from #83183 (@alexgunsberg) against current
main:

- gateway/restart.py: `is_global_startup_conflict(error_code)` — matches
  the `*_lock` / `lock_conflict` code families every adapter emits for
  scoped-lock and identity conflicts. Code only, never message text.
- gateway/run.py primary startup routing: a lock-conflict failure is
  routed as non-retryable (parked `fatal`, not queued). Nothing else
  connected → exit 78; alongside a transient peer → NS-609 mixed mode,
  gateway stays alive and only the peer retries.
- gateway/run.py `_schedule_secondary_profile_startup_reconnect`: the same
  contract for multiplex secondaries — park `<profile>:<platform>` fatal
  like `duplicate_credential` instead of scheduling a reconnect storm.
- Mid-run behavior is untouched: `_handle_adapter_fatal_error_impl` and
  the reconnect watcher still treat `*_lock` as retryable (#54167).

Not carried over from #83183 (superseded on main or out of scope): the
`degraded` lifecycle write only fires on the all-retryable path and the
runner immediately overwrites it with `running` (so busy/drain already
see `running`); the secondary retry bridge landed separately in
96489f3c1b (#92064); Buzz/IRC/LINE lock-tuple unpack and the reconnect
ownership registry are separate class fixes.

Live repro (real GatewayRunner.start(), isolated HERMES_HOME + lock dir,
live holder subprocess owning the lock via production
acquire_scoped_lock): before — exit_code=None, gateway_state=running,
telegram `retrying`, queued in _failed_platforms; after — exit_code=78,
gateway_state=startup_failed, telegram `fatal`, _failed_platforms={}.

Co-authored-by: alexgunsberg <alex@gunsberg.fi>
2026-09-02 00:17:54 -07:00
..