@goodandready/dsh-clinebot 0.5.17 → 0.5.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -130,6 +130,30 @@ export function getAccountUsageMetric(acc) {
130
130
  return 50
131
131
  }
132
132
 
133
+ let roundRobinCursor = 0
134
+
135
+ export function getRoundRobinCursor() {
136
+ return roundRobinCursor
137
+ }
138
+
139
+ export function resetRoundRobinCursor() {
140
+ roundRobinCursor = 0
141
+ }
142
+
143
+ export function getNextRoundRobinAccount(accounts = []) {
144
+ if (!Array.isArray(accounts) || accounts.length === 0) {
145
+ return null
146
+ }
147
+ const available = accounts.filter(acc => acc && acc.present !== false && !isAccountBlocked(acc))
148
+ const pool = available.length > 0 ? available : accounts.filter(acc => acc && acc.present !== false)
149
+ if (pool.length === 0) {
150
+ return accounts[0] || null
151
+ }
152
+ const chosen = pool[Math.abs(roundRobinCursor) % pool.length]
153
+ roundRobinCursor = (roundRobinCursor + 1) % pool.length
154
+ return chosen
155
+ }
156
+
133
157
  export function selectLeastUsedAccount(accounts = []) {
134
158
  if (!Array.isArray(accounts) || accounts.length === 0) {
135
159
  return null
@@ -164,7 +188,7 @@ export function selectLeastUsedAccount(accounts = []) {
164
188
  * @param {string} [defaultActiveId] Explicit active account ID, envName or 'auto'
165
189
  * @returns {{ account: Object|null, isReassigned: boolean, reason: string }}
166
190
  */
167
- export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId = '') {
191
+ export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId = '', accountMode = 'least_used') {
168
192
  const safeAccounts = Array.isArray(accounts) ? accounts.filter(a => a && a.present !== false) : []
169
193
  if (safeAccounts.length === 0) {
170
194
  const fallback = Array.isArray(accounts) && accounts.length > 0 ? accounts[0] : null
@@ -185,13 +209,24 @@ export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId
185
209
 
186
210
  const pinnedAvailable = pinnedAccount && !isAccountBlocked(pinnedAccount) ? pinnedAccount : null
187
211
 
212
+ const pickCandidate = () => {
213
+ if (accountMode === 'round_robin') {
214
+ return getNextRoundRobinAccount(safeAccounts) || safeAccounts[0]
215
+ }
216
+ return selectLeastUsedAccount(safeAccounts) || safeAccounts[0]
217
+ }
218
+
188
219
  // If no sessionId provided:
189
220
  if (!sessionId) {
190
221
  if (pinnedAvailable) {
191
222
  return { account: pinnedAvailable, isReassigned: false, reason: 'pinned_active' }
192
223
  }
193
- const best = selectLeastUsedAccount(safeAccounts) || safeAccounts[0]
194
- return { account: best, isReassigned: false, reason: pinnedAccount ? 'pinned_in_cooldown_fallback' : 'no_session_id' }
224
+ const chosen = pickCandidate()
225
+ return {
226
+ account: chosen,
227
+ isReassigned: false,
228
+ reason: pinnedAccount ? 'pinned_in_cooldown_fallback' : (accountMode === 'round_robin' ? 'round_robin_ad_hoc' : 'no_session_id')
229
+ }
195
230
  }
196
231
 
197
232
  const now = Date.now()
@@ -235,8 +270,8 @@ export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId
235
270
  // Otherwise, failover is needed
236
271
  }
237
272
 
238
- // Pick least-used available account
239
- const chosen = selectLeastUsedAccount(safeAccounts) || safeAccounts[0]
273
+ // Pick candidate according to accountMode
274
+ const chosen = pickCandidate()
240
275
  if (chosen) {
241
276
  sessions.set(sessionId, {
242
277
  accountId: chosen.apiKeyEnv || chosen.id,
@@ -248,7 +283,7 @@ export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId
248
283
  return {
249
284
  account: chosen,
250
285
  isReassigned: Boolean(existing),
251
- reason: existing ? 'mid_session_failover' : (pinnedAccount ? 'pinned_in_cooldown_fallback' : 'new_session_assigned')
286
+ reason: existing ? 'mid_session_failover' : (pinnedAccount ? 'pinned_in_cooldown_fallback' : (accountMode === 'round_robin' ? 'round_robin_assigned' : 'new_session_assigned'))
252
287
  }
253
288
  }
254
289
 
@@ -269,6 +304,7 @@ export function releaseSession(sessionId) {
269
304
  export function resetSessionRouter() {
270
305
  sessions.clear()
271
306
  cooldowns.clear()
307
+ roundRobinCursor = 0
272
308
  }
273
309
 
274
310
  /**
@@ -279,6 +315,7 @@ export function getSessionRouterStatus() {
279
315
  activeSessionsCount: sessions.size,
280
316
  maxSessions: MAX_SESSIONS,
281
317
  sessionTtlMs: SESSION_TTL_MS,
318
+ roundRobinCursor,
282
319
  cooldownsCount: cooldowns.size,
283
320
  cooldowns: Object.fromEntries(
284
321
  Array.from(cooldowns.entries()).map(([k, v]) => [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-clinebot",
3
- "version": "0.5.17",
3
+ "version": "0.5.19",
4
4
  "description": "DeepSeek Harness companion for ClineBot / ClinePass: dynamic subscription models sync, quota exhaustion warnings, session metrics, dedicated settings page, live usage limits, and /cline slash-command.",
5
5
  "license": "MIT",
6
6
  "type": "module",