Skip to content

Conversation

@CypherpunkSamurai
Copy link

@CypherpunkSamurai CypherpunkSamurai commented Jan 13, 2026

feature: add more rotation to account

added more rotation techniques for better account rotation

why

  • prevent account limiting

Summary by CodeRabbit

  • New Features

    • Added "priority-queue" account selection and optional health_score and token_bucket configuration.
  • Improvements

    • Default selection changed from "sticky" to "hybrid" for better distribution.
    • Token consume/refund semantics added for requests, with integrated health tracking to improve rate-limit handling, backoff, and recovery.
    • Account ranking now factors health, token availability, and recency.
  • Tests

    • Added extensive unit tests for health, token bucket, selection, jitter, and delay behaviors.

✏️ Tip: You can customize this high-level summary in your review settings.

add more rotation method like hybrid

Signed-off-by: CypherpunkSamurai <CypherpunkSamurai@users.noreply.github.com>
add tests for rotation

Signed-off-by: CypherpunkSamurai <CypherpunkSamurai@users.noreply.github.com>
Copilot AI review requested due to automatic review settings January 13, 2026 20:15
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 13, 2026

Warning

Rate limit exceeded

@CypherpunkSamurai has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 5 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between fbcf56d and 411b4a5.

📒 Files selected for processing (1)
  • src/plugin/config/schema.ts

Walkthrough

Adds a rotation subsystem (health-score and token-bucket trackers), integrates priority-queue selection into account manager and request flow, initializes trackers on plugin startup, consumes/refunds tokens during request lifecycle, and records successes, rate-limits, and failures for account health.

Changes

Cohort / File(s) Summary
Configuration Schema
src/plugin/config/schema.ts
Added priority-queue to AccountSelectionStrategy; changed default account_selection_strategy to hybrid; introduced optional health_score and token_bucket config blocks and added their defaults to DEFAULT_CONFIG.
Rotation Core Module
src/plugin/rotation.ts
New rotation implementation: HealthScoreTracker, TokenBucketTracker, jitter/randomDelay utilities, AccountWithMetrics, sortByLruWithHealth, selectHybridAccount, selectPriorityQueueAccount, priority scoring, and singleton init/get helpers.
Rotation Tests
src/plugin/rotation.test.ts
Added extensive unit tests covering health tracker, token bucket, jitter/delay, LRU+health sorting, hybrid selection, and priority-queue selection behavior and determinism.
Account Manager Integration
src/plugin/accounts.ts
Imported rotation utilities; added priority-queue selection path using health/token trackers and AccountWithMetrics; updated hybrid to use health-aware LRU sorting; selection updates lastUsed, quota touch, and per-family index.
Plugin Core Integration
src/plugin.ts
Initialize health/token trackers at startup when configured; consume tokens at request start for priority-queue flow; refund tokens on 429/network/API failures; record success/rate-limit/failure events to health tracker; integrated with existing retry, backoff, and quota switching logic.
Account Manager Tests
src/plugin/accounts.test.ts
Added tests for priority-queue strategy validating selection, skipping rate-limited/cooling accounts, fallback behavior, and state updates.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant Plugin
    participant AccountManager
    participant Rotation
    participant Upstream

    Client->>Plugin: Send request
    Plugin->>Rotation: evaluate candidates (health, tokens, rate-limit)
    Rotation-->>Plugin: selected account index
    Plugin->>Rotation: consume(accountIndex, cost)
    Rotation-->>Plugin: token consumed
    Plugin->>AccountManager: set current account
    Plugin->>Upstream: execute request using selected account

    alt Success
        Upstream-->>Plugin: 200 OK
        Plugin->>Rotation: recordSuccess(accountIndex)
    else Rate Limit (429)
        Upstream-->>Plugin: 429
        Plugin->>Rotation: recordRateLimit(accountIndex)
        Plugin->>Rotation: refund(accountIndex, cost)
        Plugin->>Plugin: apply backoff / quota switching
    else Network/API Error
        Upstream-->>Plugin: Error
        Plugin->>Rotation: recordFailure(accountIndex)
        Plugin->>Rotation: refund(accountIndex, cost)
    end

    Plugin-->>Client: return response
Loading
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "Feat/better account rotation" accurately summarizes the main change: introducing improved account rotation techniques including health tracking, token bucket management, and new selection strategies (priority-queue and enhanced hybrid).
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @src/plugin/rotation.ts:
- Around line 416-419: The score calculation is using a hardcoded 50 instead of
the tracker's configured max tokens; update the call in the candidates mapping
so calculatePriorityScore(acc, ...) uses the TokenBucketTracker's actual
maxTokens (e.g., call tracker.getMaxTokens() or access the
tracker.config.maxTokens) rather than 50; if maxTokens is private, add a
TokenBucketTracker.getMaxTokens() accessor and use that in the mapping to
compute each candidate's score.
🧹 Nitpick comments (4)
src/plugin/accounts.ts (1)

287-313: Priority-queue strategy lacks fallback to default sticky behavior.

When selectPriorityQueueAccount returns null (all accounts filtered out), the code silently falls through without returning. This means the function continues to the sticky strategy code below (line 341+), which may not be intentional behavior. Consider either:

  1. Returning null explicitly to indicate no account available
  2. Adding a comment clarifying this is intentional fallback behavior

Also, the comment on line 306 mentions token consumption is deferred to plugin.ts, which aligns with the implementation in that file.

💡 Suggestion: Make fallback behavior explicit
       const selectedIndex = selectPriorityQueueAccount(accountsWithMetrics, tokenTracker);
       if (selectedIndex !== null) {
         const selected = this.accounts[selectedIndex];
         if (selected) {
           // Token consumption is deferred to request execution in plugin.ts
           selected.lastUsed = nowMs();
           this.markTouchedForQuota(selected, quotaKey);
           this.currentAccountIndexByFamily[family] = selected.index;
           return selected;
         }
       }
+      // Fall through to sticky strategy when priority-queue finds no candidates
     }
src/plugin/rotation.test.ts (1)

397-474: selectPriorityQueueAccount tests cover key scenarios.

Tests verify:

  • Empty input handling
  • Filtering of rate-limited accounts
  • Filtering of accounts without tokens
  • Filtering of unhealthy accounts
  • Single candidate scenarios
  • Valid index returned from multiple candidates

Consider adding a test for the weighted random distribution to verify that higher-scored accounts are selected more frequently over many iterations.

💡 Optional: Add weighted distribution test
it("favors higher-scored candidates in weighted selection", () => {
  const tokenTracker = new TokenBucketTracker();
  const accounts: AccountWithMetrics[] = [
    { index: 0, lastUsed: 1000, healthScore: 100, isRateLimited: false, isCoolingDown: false },
    { index: 1, lastUsed: 1000, healthScore: 50, isRateLimited: false, isCoolingDown: false },
  ];

  const counts = new Map<number, number>();
  for (let i = 0; i < 1000; i++) {
    const result = selectPriorityQueueAccount(accounts, tokenTracker);
    counts.set(result!, (counts.get(result!) ?? 0) + 1);
  }

  // Higher health score should be selected more often
  expect(counts.get(0)!).toBeGreaterThan(counts.get(1)!);
});
src/plugin/rotation.ts (2)

438-440: Fallback chain returns 0 as last resort.

The fallback ?? 0 on line 439 would return index 0 even if no candidates exist. However, this code path should be unreachable since we already return null on line 412 when candidates.length === 0.

The fallback is defensive but could mask bugs if somehow reached.

💡 Consider removing unreachable fallback
-  // Fallback to top score
-  return topCandidates[0]?.acc.index ?? candidates[0]?.index ?? 0;
+  // Fallback to top score (topCandidates is guaranteed non-empty at this point)
+  return topCandidates[0]!.acc.index;

446-480: Singleton pattern for global trackers.

The init* functions overwrite existing trackers without warning. This is fine for plugin startup but could cause issues if called multiple times. The get* functions safely create with defaults if not initialized.

Consider adding a warning log if re-initializing an existing tracker to help debug unexpected behavior.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 83ea312 and 8bc5d8c.

📒 Files selected for processing (5)
  • src/plugin.ts
  • src/plugin/accounts.ts
  • src/plugin/config/schema.ts
  • src/plugin/rotation.test.ts
  • src/plugin/rotation.ts
🧰 Additional context used
🧬 Code graph analysis (3)
src/plugin.ts (1)
src/plugin/rotation.ts (4)
  • initHealthTracker (477-480)
  • initTokenTracker (455-458)
  • getHealthTracker (466-471)
  • getTokenTracker (448-453)
src/plugin/rotation.test.ts (1)
src/plugin/rotation.ts (8)
  • HealthScoreTracker (54-163)
  • TokenBucketTracker (283-345)
  • addJitter (177-181)
  • randomDelay (190-192)
  • AccountWithMetrics (198-204)
  • sortByLruWithHealth (215-229)
  • selectHybridAccount (241-253)
  • selectPriorityQueueAccount (393-440)
src/plugin/accounts.ts (1)
src/plugin/rotation.ts (5)
  • getHealthTracker (466-471)
  • getTokenTracker (448-453)
  • AccountWithMetrics (198-204)
  • selectPriorityQueueAccount (393-440)
  • sortByLruWithHealth (215-229)
🔇 Additional comments (23)
src/plugin/accounts.ts (2)

5-5: LGTM on imports.

The new imports from ./rotation align with the strategy implementations below and follow the module's public API.


315-339: Hybrid strategy also lacks explicit fallback handling.

Same observation as priority-queue: when sortByLruWithHealth returns an empty array, the code falls through to sticky behavior. The logic is correct, but documenting the intentional fallback would improve maintainability.

The implementation correctly uses sortByLruWithHealth and selects the first candidate (LRU with health tiebreaker).

src/plugin.ts (6)

42-42: LGTM on imports.

The imports correctly include both getter and initializer functions for the trackers.


666-686: Tracker initialization depends on optional config blocks.

The trackers are only explicitly initialized when their config sections exist. However, getHealthTracker() and getTokenTracker() are called unconditionally in the request flow (lines 988, 1037, 1063, 1229, 1242, 1280, 1437, 1539). These getter functions create trackers with default config if not initialized, which is safe but could lead to unexpected default behavior.

This is acceptable since the defaults are sensible, but consider documenting this behavior or always initializing with defaults to make it explicit.


1226-1230: Token consumption only for priority-queue strategy - verify alignment with accounts.ts.

Token is consumed here before the request, but in accounts.ts the comment says "Token consumption is deferred to request execution in plugin.ts". This aligns correctly.

However, ensure that the consume call here and the hasTokens check in selectPriorityQueueAccount (rotation.ts line 404) are consistent - currently both use default cost of 1.


1240-1243: Token refund correctly handles 429 and network errors.

The refund logic properly returns the token when:

  1. Rate limit (429) is encountered
  2. Network/API errors occur in the catch block

This ensures tokens aren't lost due to failed requests. Good defensive pattern.

Also applies to: 1537-1540


988-988: Health tracker records failures on auth and project errors.

These recordFailure calls appropriately penalize accounts that encounter authentication or project context errors. The tracker is obtained via getHealthTracker() which creates with defaults if not initialized.

Also applies to: 1037-1037, 1063-1063


1280-1280: Health events recorded appropriately on rate limit and success.

  • recordRateLimit on 429 applies moderate penalty
  • recordSuccess on successful response improves health

This aligns with the health score design in rotation.ts.

Also applies to: 1437-1437

src/plugin/rotation.test.ts (5)

1-12: LGTM on test setup and imports.

Clean import of all public exports from the rotation module. Using vitest with real timers reset is appropriate for most tests.


14-154: Good coverage for HealthScoreTracker.

Tests cover:

  • Initial state and custom config
  • Success recording with score cap
  • Rate limit and failure penalties with floor at 0
  • Consecutive failures tracking
  • Usability threshold
  • Reset and snapshot functionality

156-242: Good coverage for TokenBucketTracker.

Tests cover:

  • Initial state and custom config
  • Token consumption with success/failure cases
  • Exact balance consumption
  • Multiple consumes
  • hasTokens with custom cost
  • Refund with cap at maxTokens

244-301: Good statistical tests for jitter utilities.

Using 100 iterations to verify range bounds is appropriate for testing random functions. Tests correctly verify:

  • Range bounds for addJitter
  • Default jitter factor
  • Non-negative results
  • Integer results
  • randomDelay range and edge case (min === max)

303-395: Comprehensive tests for selection functions.

Tests for sortByLruWithHealth and selectHybridAccount cover:

  • Filtering rate-limited, cooling-down, and unhealthy accounts
  • LRU sorting (oldest first)
  • Health score as tiebreaker
  • Empty results handling
  • Deterministic selection of best candidate
src/plugin/config/schema.ts (4)

20-22: LGTM on strategy schema extension.

Good documentation for the new priority-queue strategy. The enum extension is backward compatible.


242-242: Breaking change: Default strategy changed from 'sticky' to 'hybrid'.

This changes the default behavior for all users who haven't explicitly configured account_selection_strategy. The hybrid strategy will now touch all fresh accounts to sync reset timers before becoming sticky.

While hybrid may be better for multi-account setups, this could surprise users expecting sticky behavior (preserves prompt cache). Consider:

  1. Documenting this change prominently in release notes
  2. Or keeping 'sticky' as default for backward compatibility

266-288: Well-structured config blocks for health score and token bucket.

Good validation ranges:

  • Health scores: 0-100 with appropriate sub-ranges for penalties/rewards
  • Token bucket: reasonable defaults (50 tokens, 6/min regeneration)

The optional() wrapper allows these to be omitted, letting the code use defaults.


333-346: DEFAULT_CONFIG includes full health_score and token_bucket objects.

This ensures consistent defaults whether config is loaded from file or not. The values match the Zod schema defaults.

src/plugin/rotation.ts (6)

1-11: Good module documentation.

Clear overview of the rotation system's purpose and strategies.


54-163: HealthScoreTracker implementation is sound.

Key observations:

  • Time-based recovery in getScore() correctly applies passive healing
  • recordSuccess resets consecutive failures
  • recordRateLimit and recordFailure properly increment consecutive failures
  • Score is clamped between 0 and maxScore
  • getSnapshot() provides debugging capability

177-192: Jitter utilities are correctly implemented.

  • addJitter: Properly generates value in ±jitterFactor range, ensures non-negative
  • randomDelay: Simple uniform distribution between min and max

215-253: LRU selection functions are well-designed.

sortByLruWithHealth:

  • Correctly filters unavailable accounts
  • Primary sort by lastUsed (oldest first)
  • Tiebreaker by health score (higher wins)

selectHybridAccount:

  • Simple wrapper that returns first sorted candidate
  • Properly returns null for empty results

283-345: TokenBucketTracker implements standard token bucket algorithm.

  • getTokens: Correctly applies time-based regeneration
  • hasTokens: Simple threshold check with default cost
  • consume: Atomic check-and-decrement
  • refund: Properly caps at maxTokens

363-377: Priority score formula weights tokens heavily over health.

The formula: health*2 + (tokens/max)*500 + freshness*0.1

With default 50 tokens and 100 health:

  • Health component: 0-200 points
  • Token component: 0-500 points
  • Freshness component: 0-360 points (capped at 1 hour)

Tokens dominate the score, which may cause high-token accounts to always win even with lower health. This could be intentional (prioritize accounts with capacity) but worth verifying this matches the design intent.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements an advanced account rotation system to prevent account limiting and bans. The system introduces health scoring to track account wellness, a token bucket algorithm for client-side rate limiting, and sophisticated selection strategies (hybrid and priority-queue) that consider account health, token availability, and LRU (Least Recently Used) metrics.

Changes:

  • Added rotation.ts with HealthScoreTracker, TokenBucketTracker, LRU selection, and weighted random selection algorithms
  • Added comprehensive test suite in rotation.test.ts covering core functionality
  • Updated config schema to support new 'priority-queue' strategy and changed default from 'sticky' to 'hybrid'
  • Integrated health tracking and token management into accounts.ts and plugin.ts

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/plugin/rotation.ts Core rotation system with health scoring, token bucket, LRU selection, and priority queue algorithms
src/plugin/rotation.test.ts Comprehensive test suite for rotation functionality
src/plugin/config/schema.ts Schema updates to support new strategies, health score config, and token bucket config with default changed to 'hybrid'
src/plugin/accounts.ts Integration of hybrid and priority-queue strategies into account selection logic
src/plugin.ts Initialization of trackers and integration of health/token tracking throughout request lifecycle

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

CypherpunkSamurai and others added 3 commits January 14, 2026 01:53
use max tokens instead of hardcoded values

Signed-off-by: CypherpunkSamurai <CypherpunkSamurai@users.noreply.github.com>
update default value

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
update default value for rotation

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @src/plugin/rotation.ts:
- Around line 436-444: The fallback returns a hardcoded 0 which can be invalid;
update the final return in the random selection block so it never falls back to
literal 0: return topCandidates[0].acc.index ?? candidates[0].index (or simply
return topCandidates[0].acc.index since topCandidates is guaranteed non-empty).
Change the expression in rotation.ts (the loop using topCandidates and
randomValue) to remove "?? 0" and rely on the existing candidate objects
(topCandidates[0] or candidates[0]) as the safe fallback.
🧹 Nitpick comments (3)
src/plugin/rotation.ts (3)

294-308: Minor inconsistency: Token regeneration produces fractional values.

Unlike HealthScoreTracker.getScore() which uses Math.floor for recovered points (line 74), getTokens() doesn't floor the recovered tokens, resulting in fractional token counts. This works correctly but is inconsistent between the two trackers.

Consider whether fractional tokens are intentional or if Math.floor should be applied for consistency:

     const minutesSinceUpdate = (now - state.lastUpdated) / (1000 * 60);
-    const recoveredTokens = minutesSinceUpdate * this.config.regenerationRatePerMinute;
+    const recoveredTokens = Math.floor(minutesSinceUpdate * this.config.regenerationRatePerMinute);

453-476: Config parameter is silently ignored after first initialization.

In getTokenTracker and getHealthTracker, the config parameter is only used when creating a new instance. If a tracker already exists, subsequent calls with different configs will silently use the existing tracker, potentially leading to unexpected behavior.

This is a common singleton pattern trade-off. The current design relies on calling initTokenTracker/initHealthTracker at startup with the desired config. Consider adding a warning or documentation note for callers who might expect config to be applied on every call.

Optional: Add runtime warning when config is ignored
 export function getTokenTracker(config?: Partial<TokenBucketConfig>): TokenBucketTracker {
   if (!globalTokenTracker) {
     globalTokenTracker = new TokenBucketTracker(config);
+  } else if (config !== undefined) {
+    console.warn('getTokenTracker: config ignored, tracker already initialized. Use initTokenTracker to reconfigure.');
   }
   return globalTokenTracker;
 }

54-56: Consider adding cleanup for long-running processes.

Both HealthScoreTracker and TokenBucketTracker use Map structures that grow as accounts are tracked but only shrink via explicit reset() calls (health tracker only). For long-running processes with account churn, consider:

  1. Adding a reset(accountIndex) method to TokenBucketTracker for parity.
  2. Optionally, implementing a cleanup mechanism for stale entries (e.g., accounts not used for N hours).

This is unlikely to be an issue for typical usage patterns but worth considering for operational resilience.

Also applies to: 283-285

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8bc5d8c and 8f569fe.

📒 Files selected for processing (2)
  • src/plugin/rotation.test.ts
  • src/plugin/rotation.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/plugin/rotation.test.ts
🔇 Additional comments (4)
src/plugin/rotation.ts (4)

16-41: LGTM!

The configuration interface and default values are well-structured with clear documentation. The scoring parameters provide a reasonable balance between penalizing failures and allowing recovery.


54-163: LGTM!

The HealthScoreTracker class is well-implemented:

  • Time-based recovery in getScore() is correctly calculated.
  • The state management properly handles both known and unknown accounts.
  • Consecutive failure tracking provides useful insights for monitoring.

177-192: LGTM!

The jitter utilities are correctly implemented. addJitter properly handles the symmetric jitter distribution and clamps to non-negative values. randomDelay provides a clean uniform distribution.


215-253: LGTM!

The LRU selection logic is well-implemented:

  • Filtering correctly excludes rate-limited, cooling-down, and unhealthy accounts.
  • The sort order (oldest first, then healthiest) aligns with the documented priority.
  • The null-safety handling is robust.

CypherpunkSamurai and others added 11 commits January 14, 2026 01:55
ref: update default value for rotation

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
ref: update default value for rotation

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: CypherpunkSamurai <CypherpunkSamurai@users.noreply.github.com>
Signed-off-by: CypherpunkSamurai <CypherpunkSamurai@users.noreply.github.com>
Signed-off-by: CypherpunkSamurai <CypherpunkSamurai@users.noreply.github.com>
update hybrid doc

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
update default value for rotation

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
clamp using `Math.Max`

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: CypherpunkSamurai <CypherpunkSamurai@users.noreply.github.com>
Signed-off-by: CypherpunkSamurai <CypherpunkSamurai@users.noreply.github.com>
Signed-off-by: CypherpunkSamurai <CypherpunkSamurai@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @src/plugin/accounts.test.ts:
- Around line 828-844: The test named "falls back to sticky when all accounts
unavailable" never marks the single account as unavailable, so it doesn't
exercise the fallback; modify the stored AccountStorageV3 used to construct
AccountManager so the sole account is actually unavailable at time 0 (e.g., add
a rate-limiting/cooldown field such as rateLimitedUntil or cooldownUntil set >
0, e.g., 1000, or use the AccountManager/API helper that marks accounts
unavailable) and then call manager.getCurrentOrNextForFamily("claude", null,
"priority-queue") and assert the fallback behavior (sticky index returned or
null depending on expected semantics).
🧹 Nitpick comments (4)
src/plugin/rotation.ts (2)

367-381: Priority score formula favors freshness (older = higher score).

The freshnessComponent adds to the score for accounts that haven't been used recently (capped at 1 hour = 360 points). This is the inverse of typical "freshness" naming (where fresh usually means recent). The naming is slightly counterintuitive but the comment on line 374-375 clarifies the intent.

Consider renaming freshnessComponent to restPeriodBonus or lruBonus to better reflect that older/more-rested accounts get higher scores.


460-494: Singleton pattern for trackers lacks reset capability for testing.

The global singletons work correctly for production use, but there's no way to reset them between tests. This could lead to test pollution if tests share the same process.

Consider adding reset functions for testing purposes:

♻️ Suggested addition for test isolation
// For testing only - resets singleton state
export function resetTokenTracker(): void {
  globalTokenTracker = null;
}

export function resetHealthTracker(): void {
  globalHealthTracker = null;
}
src/plugin/config/schema.ts (1)

323-346: Inconsistent indentation in DEFAULT_CONFIG.

Lines 324-326 have inconsistent indentation compared to surrounding lines. This is a minor formatting issue.

🔧 Fix indentation
   account_selection_strategy: 'hybrid',
-pid_offset_enabled: false,
-   switch_on_first_rate_limit: true,
-   auto_update: true,
+  pid_offset_enabled: false,
+  switch_on_first_rate_limit: true,
+  auto_update: true,
src/plugin/accounts.test.ts (1)

773-790: Test doesn't verify health-based or token-based selection.

The test name suggests it validates selection based on "health and token availability," but all accounts have identical metrics (same lastUsed: 0, no rate limits, no cooldown). The assertion only confirms some account is returned, not that the priority-queue algorithm correctly prioritizes by health score or token availability.

Consider mocking or initializing the health/token trackers with different values per account and asserting that the account with the best metrics is selected.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8f569fe and fbcf56d.

📒 Files selected for processing (6)
  • src/plugin.ts
  • src/plugin/accounts.test.ts
  • src/plugin/accounts.ts
  • src/plugin/config/schema.ts
  • src/plugin/rotation.test.ts
  • src/plugin/rotation.ts
🧰 Additional context used
🧬 Code graph analysis (3)
src/plugin/accounts.test.ts (2)
src/plugin/storage.ts (1)
  • AccountStorageV3 (76-84)
src/plugin/accounts.ts (1)
  • AccountManager (107-586)
src/plugin.ts (1)
src/plugin/rotation.ts (4)
  • initHealthTracker (491-494)
  • initTokenTracker (469-472)
  • getHealthTracker (480-485)
  • getTokenTracker (462-467)
src/plugin/rotation.test.ts (1)
src/plugin/rotation.ts (8)
  • HealthScoreTracker (54-163)
  • TokenBucketTracker (283-349)
  • addJitter (177-181)
  • randomDelay (190-192)
  • AccountWithMetrics (198-204)
  • sortByLruWithHealth (215-229)
  • selectHybridAccount (241-253)
  • selectPriorityQueueAccount (397-454)
🔇 Additional comments (33)
src/plugin/rotation.ts (8)

16-41: LGTM - Health score configuration is well-designed.

The configuration interface provides sensible defaults and clear documentation. The penalty values being negative is intuitive for the additive score model.


54-80: Time-based recovery calculation looks correct.

The getScore method properly applies passive recovery based on elapsed time and caps at maxScore. The use of Math.floor for recovery points prevents fractional score accumulation.


85-127: Consider potential double-counting with time-based recovery.

When recordSuccess, recordRateLimit, or recordFailure are called, they fetch the current score via getScore() which includes time-based recovery, then store the adjusted score. This is correct behavior. However, be aware that if these methods are called in rapid succession, the time-based recovery between calls will be minimal (essentially zero), which is the intended behavior.


150-162: getSnapshot only returns accounts that have been explicitly tracked.

The snapshot won't include accounts that have never had any health events recorded (they'd return the initial score via getScore but won't appear in the Map). This is likely intentional for debugging purposes but worth noting.


177-192: Jitter utilities are correctly implemented.

Both functions handle edge cases appropriately - addJitter clamps to non-negative with Math.max(0, ...) and both return rounded integers. The randomness distribution is uniform which is appropriate for this use case.


215-253: LRU selection and hybrid account selection look correct.

The filtering and sorting logic properly handles rate-limited, cooling-down, and unhealthy accounts. The defensive sorted[0]?.index ?? null handles the edge case where the array might be empty after filtering.


283-349: Token bucket implementation is correct.

The regeneration calculation and clamping logic are sound. Note that getTokens returns a floating-point value (regenerated tokens aren't floored), which is fine since hasTokens and consume use >= and < comparisons.


397-454: Priority queue selection has sound logic with good defensive coding.

The weighted random selection properly handles edge cases (empty candidates, zero total score). The top 30% selection with min/max bounds (1-5) prevents both degenerate cases and excessive randomization.

src/plugin/rotation.test.ts (6)

1-17: Test setup imports and timer management look correct.

Using vi.useRealTimers() in beforeEach ensures clean state. The imports cover all public APIs being tested.


129-190: Time-based recovery tests properly mock Date.now.

The tests correctly use vi.spyOn(Date, 'now') and restore mocks with vi.restoreAllMocks(). The test at line 169-189 validates the floor behavior for partial recovery points.


203-216: getSnapshot test validates account tracking.

The test confirms that accounts with recorded events appear in the snapshot with correct scores and failure counts.


316-354: Token regeneration tests have good coverage.

Tests verify both regeneration over time and the cap at maxTokens. The time mocking is handled correctly.


356-413: Jitter and delay tests use statistical approach.

Running 100 iterations to verify bounds is appropriate for testing randomized functions. The tests cover edge cases like small base values with high jitter factors and min === max.


509-586: selectPriorityQueueAccount tests cover filtering scenarios but could test weighted selection.

The tests verify filtering behavior well but don't explicitly test the weighted random distribution. Given the randomness, this is acceptable, but the test at lines 573-585 running 100 iterations provides some confidence.

src/plugin/config/schema.ts (2)

17-22: Strategy enum extended correctly.

The new priority-queue option and updated documentation for hybrid as the default are consistent with the implementation.


266-288: Health score and token bucket schemas have appropriate validation bounds.

The constraints are sensible:

  • Health scores bounded 0-100
  • Penalties are negative (min -100/-50 to max 0)
  • Token bucket allows up to 1000 tokens with regeneration 0.1-60/min

Both schemas are optional, which aligns with the initialization logic in plugin.ts that only initializes trackers when config is present.

src/plugin.ts (9)

42-42: Import statement for rotation module utilities.

The imports are correctly scoped to what's needed in this file.


666-686: Tracker initialization maps config keys correctly.

The snake_case config keys are properly mapped to camelCase tracker config. Note that trackers are only initialized when config blocks are present, otherwise defaults will be used via the lazy-init in getHealthTracker()/getTokenTracker().


988-988: Health failures recorded on auth/project errors.

The recordFailure calls are placed at appropriate error handling points. These degrade account health when authentication or project context fails.

Also applies to: 1037-1037, 1063-1063


1188-1234: Token consumption tracking initialized correctly.

The tokenConsumed flag is properly scoped to the endpoint retry loop and only set when strategy is priority-queue. This ensures tokens are only consumed once per request attempt.


1244-1248: Token refund on 429 is correctly implemented.

The refund happens before rate-limit handling, and the flag is reset to prevent double-refunds.


1285-1286: Rate limit recorded to health tracker.

This appropriately degrades account health when rate-limited, influencing future selection.


1440-1443: Success recorded after successful response.

The health tracker records success only when response.ok is true, which correctly rewards healthy accounts.


1541-1546: Token refund on network/API errors is correctly handled.

The refund only occurs if tokenConsumed is true, preventing refunds for requests that never consumed a token. The flag is reset after refund.


1229-1234: Token consumption/refund appears balanced across all paths.

Tokens are consumed once (line 1232), and refunded in two error scenarios:

  1. On 429 rate limit (line 1246)
  2. On network/API catch block (line 1544)

However, there's a potential edge case: if the request succeeds (line 1440-1442), the token is never "confirmed consumed" - it just stays consumed. This is the intended behavior (successful requests cost tokens).

Also applies to: 1244-1248, 1541-1546

src/plugin/accounts.ts (5)

5-5: Import statement correctly scoped.

Imports the necessary rotation utilities and types.


287-313: Priority-queue selection correctly integrates with rotation module.

The implementation:

  1. Builds metrics for all accounts with health scores from the tracker
  2. Delegates selection to selectPriorityQueueAccount
  3. Updates account state on selection

The comment on line 306 correctly notes that token consumption is deferred to plugin.ts request execution.


315-339: Hybrid strategy refactored to use health-aware LRU sorting.

The logic correctly uses sortByLruWithHealth from the rotation module, ensuring consistent filtering and sorting behavior. The null-safety check on sorted[0] and selected is appropriate.


341-367: Fallback to sticky selection preserves backward compatibility.

When priority-queue or hybrid strategies find no candidates (all filtered out), the code falls through to the sticky selection logic. This provides graceful degradation.


287-339: Both strategies update lastUsed timestamp consistently.

Lines 307 and 333 both update lastUsed = nowMs() on selection, ensuring the LRU tracking remains accurate across strategy changes.

src/plugin/accounts.test.ts (3)

792-808: LGTM!

The test correctly verifies that rate-limited accounts are skipped by the priority-queue strategy.


810-826: LGTM!

The test correctly verifies that cooling-down accounts are skipped by the priority-queue strategy.


846-865: LGTM!

The test properly verifies that the lastUsed timestamp and currentAccountIndexByFamily are updated when an account is selected via the priority-queue strategy.

Comment on lines +828 to +844
it("falls back to sticky when all accounts unavailable", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(0));

const stored: AccountStorageV3 = {
version: 3,
accounts: [
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
],
activeIndex: 0,
};

const manager = new AccountManager(undefined, stored);

const selected = manager.getCurrentOrNextForFamily("claude", null, "priority-queue");
expect(selected?.index).toBe(0);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test doesn't actually verify fallback behavior.

The test name states "falls back to sticky when all accounts unavailable," but the single account is never marked as unavailable (not rate-limited, not cooling down). This tests normal selection of an available account, not the fallback path.

To properly test fallback behavior, mark the account as rate-limited or cooling down, then verify the sticky fallback logic activates (or returns null if that's the expected behavior when all accounts are truly unavailable).

Suggested fix to actually test fallback
       it("falls back to sticky when all accounts unavailable", () => {
         vi.useFakeTimers();
         vi.setSystemTime(new Date(0));

         const stored: AccountStorageV3 = {
           version: 3,
           accounts: [
             { refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
+            { refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
           ],
           activeIndex: 0,
         };

         const manager = new AccountManager(undefined, stored);
+        const accounts = manager.getAccounts();
+        
+        // Mark all accounts as rate-limited to trigger fallback
+        manager.markRateLimited(accounts[0]!, 60000, "claude");
+        manager.markRateLimited(accounts[1]!, 60000, "claude");

         const selected = manager.getCurrentOrNextForFamily("claude", null, "priority-queue");
-        expect(selected?.index).toBe(0);
+        // Verify fallback behavior - either returns null or falls back to sticky
+        expect(selected).toBeNull();
       });
🤖 Prompt for AI Agents
In @src/plugin/accounts.test.ts around lines 828 - 844, The test named "falls
back to sticky when all accounts unavailable" never marks the single account as
unavailable, so it doesn't exercise the fallback; modify the stored
AccountStorageV3 used to construct AccountManager so the sole account is
actually unavailable at time 0 (e.g., add a rate-limiting/cooldown field such as
rateLimitedUntil or cooldownUntil set > 0, e.g., 1000, or use the
AccountManager/API helper that marks accounts unavailable) and then call
manager.getCurrentOrNextForFamily("claude", null, "priority-queue") and assert
the fallback behavior (sticky index returned or null depending on expected
semantics).

@tctinh tctinh changed the base branch from main to dev January 14, 2026 04:07
@tctinh tctinh merged commit 160e3bd into NoeFabris:dev Jan 14, 2026
1 check passed
@tctinh
Copy link
Collaborator

tctinh commented Jan 14, 2026

merged, thanks you for the help

@CypherpunkSamurai
Copy link
Author

CypherpunkSamurai commented Jan 14, 2026

this wasn't fully tested locally. i opened it hoping for a manual code review 😅

ah i noticed it's on dev. ok, will patch it if any bugs come forth.

@tctinh
Copy link
Collaborator

tctinh commented Jan 14, 2026

this wasn't fully tested locally. i opened it hoping for a manual code review 😅

ah i noticed it's on dev. ok, will patch it if any bugs come forth.

deployed on beta.3 You can test the release beta version, I'm testing it on my side as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants