Skip to content

fix(photon): prevent sidecar death spiral with health-check server and restart counter reset - #53197

Closed
cjboy007 wants to merge 2 commits into
NousResearch:mainfrom
cjboy007:fix/photon-sidecar-death-spiral-49858
Closed

fix(photon): prevent sidecar death spiral with health-check server and restart counter reset#53197
cjboy007 wants to merge 2 commits into
NousResearch:mainfrom
cjboy007:fix/photon-sidecar-death-spiral-49858

Conversation

@cjboy007

@cjboy007 cjboy007 commented Jun 26, 2026

Copy link
Copy Markdown

Summary

Fixes #49858

Root Cause

Two bugs in the Photon adapter and sidecar caused a death spiral when the sidecar crashed:

  1. Adapter restart counter gives up permanently — After 5 restart attempts within 10 minutes, the adapter never tried again. No recovery without manual gateway restart.
  2. Sidecar init crashes on 429 rate limitSpectrum() initialization had no error handling for rate limits. If Photon returned 429, the sidecar exited immediately, and the adapter couldn't detect it was alive (port not yet open).

Changes

adapter.py — stop giving up permanently

  • Reset restart counter after 30 min of stable sidecar operation
  • When counter exhausts, wait 5 min cooldown then retry (instead of permanent lockout)

sidecar/index.mjs — survive rate limits during init

  • Start a temporary health-check HTTP server on port 8789 before Spectrum() init
  • Initialize Spectrum() asynchronously with 429 retry logic (wait 60s, max 5 attempts)
  • Swap to full server after successful init
  • This prevents the adapter from killing the sidecar while it's waiting out a rate limit

Key insight

The sidecar's for(;;) reconnection loop in index.mjs was already correct. The bugs were in the adapter's restart logic and the sidecar's init sequence, which prevented proper recovery from transient failures.

cjboy007 added 2 commits June 24, 2026 09:16
The Feishu/Lark file upload API returns error 99992402 when the
multipart file field lacks a Content-Type header. This fix uses
mimetypes.guess_type() to infer the MIME type from the filename
and passes it as a (filename, file, mime_type) tuple to requests,
falling back to application/octet-stream when the type is unknown.
…d restart counter reset

Fixes NousResearch#49858

Root cause: 5 compounding bugs created a permanent death spiral:

1. Sidecar initialization blocks on rate limit before opening port
   - Spectrum() init could hang on 429, but HTTP server was defined after
   - Adapter checks port, finds nothing, kills sidecar, retries
   - Creates kill loop that burns through rate limit quota faster

2. Adapter restart counter gives up permanently after 5 attempts
   - Once _RESTART_MAX_ATTEMPTS exhausted, never retries
   - No recovery without manual gateway restart

Fixes:
- Start temporary health-check HTTP server on port 8789 FIRST
- Then initialize Spectrum() asynchronously with 429 retry logic (wait 60s, max 5 attempts)
- Swap to full server after successful init
- Reset restart counter after 30 min stability
- Retry after 5 min cooldown instead of permanent give-up

This ensures adapter can detect sidecar is alive even during slow init,
and prevents permanent lockout after transient failures.
@alt-glitch alt-glitch added type/bug Something isn't working comp/plugins Plugin system and bundled plugins comp/gateway Gateway runner, session dispatch, delivery sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages P3 Low — cosmetic, nice to have labels Jun 26, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed Photon recovery investigation. Current main already handles the sidecar-exit portion through gateway-managed retry (plugins/platforms/photon/adapter.py:1022-1034, gateway/run.py:7841-7925), but the remaining initialization-rate-limit idea needs rework.

Problems

  • plugins/platforms/photon/sidecar/index.mjs:145 exposes GET /health, while the unchanged readiness probe sends authenticated POST /healthz and accepts only 200 (plugins/platforms/photon/adapter.py:831-836). The temporary server returns 503, so it cannot keep startup alive during initialization.
  • The 30-minute reset cannot run: timestamps older than 600 seconds are pruned at plugins/platforms/photon/adapter.py:887-890 before the > 1800 check at :894-902.
  • The diff adds no regression tests for the 429, readiness, restart, or Feishu MIME paths.

Suggested changes

  • Rework the Photon change on current main around the existing gateway reconnect owner, and make any temporary endpoint match the actual authenticated /healthz readiness contract.
  • Add deterministic tests for initialization retries and stable-runtime reset semantics; split and test the unrelated Feishu upload change separately.

Automated hermes-sweeper review.


// Temporary health-check server (just responds to /health)
const healthServer = http.createServer((req, res) => {
if (req.url === '/health') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The adapter still probes authenticated POST /healthz and accepts only HTTP 200 (adapter.py:831-836), but this temporary server returns 200 only for /health. During initialization it will return 503 to every readiness probe, so the proposed server does not prevent the 15-second startup failure.

now = time.time()

# Prune old timestamps outside the window
self._restart_timestamps = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pruning removes all timestamps older than 10 minutes before the later stable_duration > 1800 check. Any timestamp that survives is necessarily newer than 10 minutes, so the advertised 30-minute stability-reset branch is unreachable.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 15, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Closing — sidecar-exit recovery is gateway-owned on current main (supervisor + reconnect queue), the unauthenticated GET /health duplicated the authenticated POST /healthz probe, and the 30-min restart counter reset was unreachable (pruned at 600s). The problem you targeted was real; the mechanism landed differently via #73563. Thanks for the thorough attempt.

@teknium1 teknium1 closed this Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Photon iMessage: sidecar death causes silent reconnect death spiral (no respawn)

3 participants