Replace try/catch with fail() pattern with expect().rejects.toThrow()
which is the standard vitest/Jest pattern for async error expectations.
- Remove 'fail' from vitest imports (not exported in this version)
- Convert auth/billing cooldown tests to use expect().rejects.toThrow()
- All 34 tests still passing with proper async error handling
- Import 'fail' function from vitest for test assertions
- Fix TypeScript types: use AuthProfileFailureReason instead of unknown
- All tests passing with proper type safety
- Add logic to distinguish between rate_limit, auth, and billing cooldown reasons
- Rate limits: allow same-provider fallback attempts (different model may work)
- Auth/billing issues: block all attempts for that provider (affects whole provider)
- Add comprehensive test suite for cooldown behavior distinctions
- Preserve existing probe logic and backward compatibility
- Smart handling of providers without auth profiles based on context
Fixes issue where all cooldown types were treated identically, preventing
appropriate fallback strategies for different failure scenarios.
Fixes#19249 - Model failover does not activate on rate limit
This addresses TWO independent bugs in the model fallback system:
**Bug A: Session model overrides skip fallbacks**
- Changed comparison from exact model strings to provider-only comparison
- Session overrides within same provider now preserve fallback protection
- Allows: claude-opus-4-6 vs claude-sonnet-4-20250514 (same provider)
- Blocks: claude-opus vs gpt-4.1-mini (cross-provider, as intended)
**Bug B: Provider cooldowns block same-provider fallbacks**
- Modified cooldown logic to allow fallback attempts even during cooldown
- Rate limits are often model-specific, not provider-wide
- Primary models respect existing probe logic during cooldown
- Fallback models always attempted despite provider cooldown
**Test Coverage:**
- All 32 tests passing (0 skipped)
- Added comprehensive test cases for both scenarios
- Backwards compatibility preserved with @deprecated function
- Includes cross-provider cooldown scenarios and auth profile mocking
**Impact:**
This resolves the frustrating experience where configured fallbacks
don't work during quota management, model testing, or rate limit scenarios.
**Technical Details:**
- Preserves all existing fallback behavior for other scenarios
- Clean implementation with proper error handling
- No breaking changes to API or configuration
✅ All 30 tests now passing (0 skipped)
Key fixes:
1. Session model overrides preserve same-provider fallbacks
2. Cross-provider test fixed with proper credential error type
3. Backwards compatibility maintained with @deprecated function
4. Clean commit history without build artifacts
Core behavior:
- ✅ claude-sonnet vs claude-opus (same provider) → fallbacks work
- ✅ openai vs anthropic (cross-provider) → configured primary fallback
- ✅ All existing fallback scenarios preserved
- ✅ Proper error type handling for credential/auth failures
This resolves#19249 where users lose fallback protection during
quota management and model testing scenarios.
Fixes#19249 - Model failover does not activate on rate limit
Core fix:
- Changed comparison from exact model strings to provider-only comparison
- Session model overrides within same provider now preserve fallbacks
- Cross-provider blocking preserved as intended
Backwards compatibility:
- Restored sameModelCandidate() function marked as @deprecated
- Function preserved for any external usage but flagged for future removal
- Added eslint disable for intentionally unused backward compat function
Test coverage:
- Added comprehensive test cases for session override scenarios
- 29/30 tests passing (1 skipped cross-provider edge case for follow-up)
- All existing fallback behavior preserved
Technical details:
- Allows: claude-opus-4-6 vs claude-sonnet-4-20250514 (same provider)
- Allows: Model version differences within same provider
- Blocks: claude-opus vs gpt-4.1-mini (different providers, as intended)
This resolves the issue where users lose fallback protection when
switching models for quota management or testing.
Fixes#19249 - Model failover does not activate on rate limit
This addresses two independent bugs in the model fallback system:
**Bug A: Session model overrides skip fallbacks**
- Problem: sameModelCandidate() compared exact model strings, so any
session override (e.g. Sonnet vs Opus) would skip ALL fallbacks
- Impact: Users doing session model overrides for quota management
or testing would lose fallback safety net entirely
- Fix: Change from model-specific to provider-specific comparison
- Allow: claude-opus-4-6 vs claude-sonnet-4-20250514 (same provider)
- Block: claude-opus vs gpt-4.1-mini (different providers)
**Bug B: Provider cooldowns block same-provider fallbacks**
- Problem: Rate limits often model-specific, but cooldown was
provider-wide. When primary hits quota, fallbacks from same
provider were skipped without attempts
- Impact: Users with same-provider fallbacks (common case) never
got to try alternative models that might work
- Fix: Always attempt fallback models even during provider cooldown
- Logic: Rate limits are typically per-model, not per-provider
**Test Coverage**
- Added comprehensive test cases for both scenarios
- Includes reproduction case for exact GitHub issue config
- Tests cross-provider, same-provider, version differences
- Tests cooldown behavior with auth profile mocking
**Backward Compatibility**
- Preserves existing cross-provider blocking behavior
- No breaking changes to API or config
- More permissive fallback attempts improve reliability
When grammY's runner exceeds maxRetryTime during a network outage,
runner.task() resolves cleanly. Previously, the polling loop treated
this as an intentional stop and exited permanently — killing Telegram
polling for the lifetime of the gateway process.
Now the outer loop detects this case and restarts with exponential
backoff, so polling recovers once connectivity is restored.
Also bumps maxRetryTime from 5 minutes to 60 minutes so the runner
itself survives longer outages (e.g. scheduled internet downtime)
without needing the outer loop restart path.
The followup runner (used for queued messages, inter-agent sends,
heartbeat followups, etc.) only called typing.markRunComplete() in
its finally block. The typing controller requires BOTH markRunComplete
AND markDispatchIdle to trigger cleanup — but markDispatchIdle was
only wired through the buffered dispatcher path, which followup turns
bypass entirely.
This caused the typing indicator to persist indefinitely on channels
like Telegram when the agent replied with NO_REPLY or produced empty
payloads, because the keepalive loop was never stopped.
Adds markDispatchIdle() alongside markRunComplete() in the followup
runner's finally block, and four test cases covering NO_REPLY, empty
payloads, agent errors, and successful delivery.
Complements #26295 which addressed the channel-level callback layer.
Fixes#26595
Co-authored-by: Samantha <samantha@Samanthas-Mac-mini.local>
The existing `closed` flag in `createTypingCallbacks` guards
`onReplyStart` but not `fireStart` itself. If a keepalive tick is
already in-flight when `fireStop` sets `closed = true` and calls
`keepaliveLoop.stop()`, the running `onTick → fireStart` callback
still completes and sends a stale `sendChatAction('typing')` after
the reply message has been delivered.
On Telegram (which has no cancel-typing API), this causes the typing
indicator to linger ~5 seconds after the bot's message appears.
Add a `closed` early-return in `fireStart` as defense-in-depth so
that even an in-flight tick is suppressed once cleanup has started.
* fix(test): stabilize low-mem parallel lane and cron session mock
* feat(android): make QR scanning first-class onboarding
* docs(android): update README for native Android workflow
* fix(android): stabilize chat composer ime and tab layout
* fix(android): stabilize chat ime insets and tab bar
* fix(android): remove tab bar gap above system nav
* fix(android): harden scanned setup code parsing
* test(android): cover non-string setupCode QR payload
* fix(test): add changelog note for low-mem test runner (#26324) (thanks @ngutman)
---------
Co-authored-by: Ayaan Zaidi <zaidi@uplause.io>