diff --git a/docs/plans/analysis/2026-02-16-codebase-audit-report.md b/docs/plans/analysis/2026-02-16-codebase-audit-report.md index f8b9e2d..ee34675 100644 --- a/docs/plans/analysis/2026-02-16-codebase-audit-report.md +++ b/docs/plans/analysis/2026-02-16-codebase-audit-report.md @@ -14,6 +14,7 @@ Scope: Production-risk-first audit of bugs, code improvements, and feature oppor - ✅ F-012 addressed: synthetic repeated-tool nudge no longer emits invalid `tool_result.tool_use_id`; nudge is injected as plain user text guidance. - ✅ F-009 addressed: gateway now enforces per-connection WebSocket ingress rate limits with deterministic throttle errors and close-on-repeated-violation behavior. - ✅ F-008 addressed: WhatsApp Chromium launch is now sandboxed by default; no-sandbox mode is behind explicit `whatsapp.no_sandbox: true` opt-in. +- ✅ F-014 addressed: `ModelRouter.setOnTierChange` now preserves existing listeners instead of replacing them, removing destructive listener-setter behavior. ## Executive Summary diff --git a/docs/plans/state.json b/docs/plans/state.json index e43a965..217e699 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -2512,6 +2512,18 @@ "docs/plans/analysis/2026-02-16-codebase-audit-report.md" ], "test_status": "pnpm test:run src/channels/whatsapp/adapter.test.ts src/config/schema.test.ts + pnpm typecheck passing" + }, + "audit-followup-modelrouter-listeners": { + "status": "completed", + "date": "2026-02-16", + "updated": "2026-02-16", + "summary": "Removed ModelRouter tier-change listener setter footgun: setOnTierChange now appends instead of replacing existing listeners, with regression test coverage.", + "files_modified": [ + "src/models/router.ts", + "src/models/router.test.ts", + "docs/plans/analysis/2026-02-16-codebase-audit-report.md" + ], + "test_status": "pnpm test:run src/models/router.test.ts + pnpm typecheck passing" } }, "overall_progress": { diff --git a/src/models/router.test.ts b/src/models/router.test.ts index 55f140d..0d2e59c 100644 --- a/src/models/router.test.ts +++ b/src/models/router.test.ts @@ -463,4 +463,22 @@ describe('setClient and labels', () => { expect(fallback.chat).not.toHaveBeenCalled(); expect(router.isTierStrict('default')).toBe(true); }); + + it('setOnTierChange does not replace existing listeners', () => { + const router = new ModelRouter({ + default: { chat: vi.fn() } as unknown as ModelClient, + fast: { chat: vi.fn() } as unknown as ModelClient, + fallbackChain: [], + }); + + const first = vi.fn(); + const second = vi.fn(); + router.addOnTierChange(first); + router.setOnTierChange(second); + + router.setTier('fast'); + + expect(first).toHaveBeenCalledWith('fast'); + expect(second).toHaveBeenCalledWith('fast'); + }); }); diff --git a/src/models/router.ts b/src/models/router.ts index 0724b4d..17fc355 100644 --- a/src/models/router.ts +++ b/src/models/router.ts @@ -67,7 +67,7 @@ export class ModelRouter implements ModelClient { } setOnTierChange(callback: (tier: ModelTier) => void): void { - this.tierChangeListeners = [callback]; + this.addOnTierChange(callback); } addOnTierChange(callback: (tier: ModelTier) => void): void {