#21728: fix(boot): deduplicate startup notification within restart cycle
gateway
size: S
Cluster:
Hook and Gateway Improvements
## Problem
When the gateway recovers from a repeated config-invalid restart loop, multiple boot sessions may have been queued (one per restart cycle). All of them can complete in a short window, and each independently sends the startup notification defined in `BOOT.md`. The user ends up receiving duplicate online messages.
**Log evidence** (2026-02-20 08:10–08:15 config-invalid loop, recovered at 08:16):
- `boot-2026-02-20_08-16-21-094-3b733d68` → sent notification ✓
- `boot-2026-02-20_08-16-58-928-b27bd200` → sent duplicate notification ✗
## Root Cause
`startGatewaySidecars` is called on every gateway restart. Each call fires a `gateway:startup` hook event that is handled by `runBootChecklist` → `runBootOnce`. There was no guard to prevent `runBootOnce` from executing more than once per OS-process lifetime.
## Fix Approach
Added a module-level boolean flag `_bootNotificationSent` in `src/gateway/boot.ts`.
- The flag is checked **synchronously** at the top of `runBootOnce`. If it is already set, the function returns immediately with `{ status: "skipped", reason: "already-ran" }`.
- The flag is set **synchronously** (before any `await`) once we decide to proceed, making the read-then-write atomic at the JavaScript level (Node.js is single-threaded). This prevents race conditions from concurrent event-loop ticks.
- Because the flag lives in module scope it is **automatically reset** when the gateway daemon process exits and restarts — the right granularity for this dedup.
- A test-only `_resetBootDedup()` export allows unit tests to reset the flag between cases.
**Files changed:**
| File | Change |
|------|--------|
| `src/gateway/boot.ts` | Add dedup flag + guard in `runBootOnce`; update `BootRunResult` type |
| `src/gateway/boot.test.ts` | Add `_resetBootDedup` to `beforeEach`; add 3 dedup-specific test cases |
## Testing
Unit tests added in `src/gateway/boot.test.ts` under a new `startup notification deduplication` describe block:
1. **First run sends notification** — `agentCommand` is called once.
2. **Second run within same process is skipped** — `agentCommand` is **not** called a second time; result is `{ status: "skipped", reason: "already-ran" }`.
3. **`_resetBootDedup` restores normal behaviour** — simulates a fresh OS-process restart; the next run is allowed through.
All 10 tests pass (`pnpm vitest run src/gateway/boot.test.ts`). `pnpm check` (format + type-check + lint) exits clean.
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Prevents duplicate startup notifications when the gateway recovers from repeated restart cycles by adding a module-level deduplication flag. The flag is checked synchronously at the top of `runBootOnce` and set before any async operations, ensuring atomicity in Node.js's single-threaded event loop. The flag naturally resets on full process restart, which is the correct granularity for this dedup. Implementation includes comprehensive test coverage for first run, subsequent runs within the same process, and reset behavior.
<h3>Confidence Score: 5/5</h3>
- Safe to merge - implements targeted fix with comprehensive test coverage
- The fix correctly addresses the duplicate notification issue with a simple, atomic deduplication mechanism. The flag is set synchronously before any async operations, preventing race conditions. Test coverage is thorough, including reset behavior for unit tests. The module-level scope ensures automatic cleanup on process restart, which is the correct granularity for this use case.
- No files require special attention
<sub>Last reviewed commit: d0a3502</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#9172: Fix: Add rate limiting to boot-md hook to prevent spam during rapid...
by vishaltandale00 · 2026-02-04
78.7%
#13178: fix: dedup mapped hook dispatches to prevent Gmail Pub/Sub retries
by striking · 2026-02-10
73.8%
#22424: fix: prevent crash when onUpdate is truthy but not callable (fixes ...
by mcaxtr · 2026-02-21
73.1%
#11470: fix: prevent gateway:watch race by passing --no-clean to tsdown
by apetresc · 2026-02-07
71.1%
#21591: fix(update): prevent double restart when refreshing service env
by irchelper · 2026-02-20
70.7%
#20541: fix(hooks): clear internal hooks before plugins register
by ramarnat · 2026-02-19
70.2%
#21944: feat(gateway): crash-loop protection with escalating backoff
by Protocol-zero-0 · 2026-02-20
70.2%
#10801: fix: eagerly initialize QMD memory backend on gateway startup
by 1kuna · 2026-02-07
69.6%
#21277: fix(browser): dedupe concurrent relay init and await shared startup...
by HOYALIM · 2026-02-19
69.3%
#9112: Fix: Prevent double SIGUSR1 restart on model switch
by vishaltandale00 · 2026-02-04
69.0%