create-byan-agent 2.9.4 → 2.9.6

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.
Files changed (98) hide show
  1. package/install/bin/byan-cleanup.js +156 -0
  2. package/install/bin/byan-kanban.js +159 -0
  3. package/install/bin/byan-ledger.js +45 -0
  4. package/install/bin/create-byan-agent-v2.js +15 -1
  5. package/install/lib/cleanup/detector.js +154 -0
  6. package/install/lib/cleanup/executor.js +72 -0
  7. package/install/lib/staging-consent.js +149 -0
  8. package/install/lib/subagent-generator.js +208 -0
  9. package/install/lib/token-ledger.js +131 -0
  10. package/install/templates/.claude/agents/bmad-bmad-master.md +14 -0
  11. package/install/templates/.claude/agents/bmad-bmb-agent-builder.md +14 -0
  12. package/install/templates/.claude/agents/bmad-bmb-module-builder.md +14 -0
  13. package/install/templates/.claude/agents/bmad-bmb-workflow-builder.md +14 -0
  14. package/install/templates/.claude/agents/bmad-bmm-analyst.md +14 -0
  15. package/install/templates/.claude/agents/bmad-bmm-architect.md +14 -0
  16. package/install/templates/.claude/agents/bmad-bmm-dev.md +14 -0
  17. package/install/templates/.claude/agents/bmad-bmm-pm.md +14 -0
  18. package/install/templates/.claude/agents/bmad-bmm-quick-flow-solo-dev.md +14 -0
  19. package/install/templates/.claude/agents/bmad-bmm-quinn.md +14 -0
  20. package/install/templates/.claude/agents/bmad-bmm-sm.md +14 -0
  21. package/install/templates/.claude/agents/bmad-bmm-tech-writer.md +14 -0
  22. package/install/templates/.claude/agents/bmad-bmm-ux-designer.md +14 -0
  23. package/install/templates/.claude/agents/bmad-byan-v2.md +14 -0
  24. package/install/templates/.claude/agents/bmad-byan.md +152 -0
  25. package/install/templates/.claude/agents/bmad-carmack.md +14 -0
  26. package/install/templates/.claude/agents/bmad-cis-brainstorming-coach.md +14 -0
  27. package/install/templates/.claude/agents/bmad-cis-creative-problem-solver.md +14 -0
  28. package/install/templates/.claude/agents/bmad-cis-design-thinking-coach.md +14 -0
  29. package/install/templates/.claude/agents/bmad-cis-innovation-strategist.md +14 -0
  30. package/install/templates/.claude/agents/bmad-cis-presentation-master.md +14 -0
  31. package/install/templates/.claude/agents/bmad-cis-storyteller.md +14 -0
  32. package/install/templates/.claude/agents/bmad-claude.md +26 -0
  33. package/install/templates/.claude/agents/bmad-codex.md +26 -0
  34. package/install/templates/.claude/agents/bmad-compliance.md +68 -0
  35. package/install/templates/.claude/agents/bmad-drawio.md +25 -0
  36. package/install/templates/.claude/agents/bmad-expert-merise-agile.md +54 -0
  37. package/install/templates/.claude/agents/bmad-fact-checker.md +14 -0
  38. package/install/templates/.claude/agents/bmad-forgeron.md +14 -0
  39. package/install/templates/.claude/agents/bmad-hermes.md +59 -0
  40. package/install/templates/.claude/agents/bmad-marc.md +25 -0
  41. package/install/templates/.claude/agents/bmad-patnote.md +26 -0
  42. package/install/templates/.claude/agents/bmad-rachid.md +25 -0
  43. package/install/templates/.claude/agents/bmad-tao.md +14 -0
  44. package/install/templates/.claude/agents/bmad-tea-tea.md +14 -0
  45. package/install/templates/.claude/agents/bmad-yanstaller.md +47 -0
  46. package/install/templates/.claude/hooks/fact-check-absolutes.js +185 -0
  47. package/install/templates/.claude/hooks/fd-phase-guard.js +87 -0
  48. package/install/templates/.claude/hooks/fd-response-check.js +92 -0
  49. package/install/templates/.claude/hooks/lib/failure-detector.js +14 -0
  50. package/install/templates/.claude/hooks/pre-compact-save.js +148 -0
  51. package/install/templates/.claude/hooks/stage-to-byan.js +119 -0
  52. package/install/templates/.claude/hooks/tool-failure-guard.js +6 -0
  53. package/install/templates/.claude/hooks/tool-transparency.js +4 -0
  54. package/install/templates/.claude/settings.json +27 -0
  55. package/install/templates/.claude/skills/byan-byan/SKILL.md +115 -163
  56. package/install/templates/.claude/skills/byan-orchestrate/SKILL.md +100 -0
  57. package/install/templates/.githooks/pre-commit +75 -0
  58. package/install/templates/.github/extensions/byan-staging/extension.mjs +169 -0
  59. package/install/templates/.github/extensions/byan-staging/package.json +8 -0
  60. package/install/templates/_byan/mcp/byan-mcp-server/lib/copilot.js +148 -0
  61. package/install/templates/_byan/mcp/byan-mcp-server/lib/fd-state.js +163 -0
  62. package/install/templates/_byan/mcp/byan-mcp-server/lib/kanban.js +226 -0
  63. package/install/templates/_byan/mcp/byan-mcp-server/lib/peer-review.js +187 -0
  64. package/install/templates/_byan/mcp/byan-mcp-server/server.js +463 -0
  65. package/install/templates/detector.js +154 -0
  66. package/package.json +6 -7
  67. package/src/loadbalancer/capability-matrix.js +157 -0
  68. package/src/loadbalancer/config.js +141 -0
  69. package/src/loadbalancer/graceful-degradation.js +212 -0
  70. package/src/loadbalancer/health-probe.js +151 -0
  71. package/src/loadbalancer/hooks/claude-hooks.js +53 -0
  72. package/src/loadbalancer/hooks/copilot-hooks.js +74 -0
  73. package/src/loadbalancer/index.js +81 -0
  74. package/src/loadbalancer/loadbalancer.default.yaml +65 -0
  75. package/src/loadbalancer/loadbalancer.js +324 -0
  76. package/src/loadbalancer/mcp-server.js +304 -0
  77. package/src/loadbalancer/metrics.js +146 -0
  78. package/src/loadbalancer/native/claude-integration.js +64 -0
  79. package/src/loadbalancer/native/copilot-integration.js +59 -0
  80. package/src/loadbalancer/pressure-score.js +102 -0
  81. package/src/loadbalancer/providers/base-provider.js +80 -0
  82. package/src/loadbalancer/providers/byan-api-provider.js +132 -0
  83. package/src/loadbalancer/providers/claude-provider.js +113 -0
  84. package/src/loadbalancer/providers/copilot-provider.js +104 -0
  85. package/src/loadbalancer/rate-limit-tracker.js +216 -0
  86. package/src/loadbalancer/session-bridge.js +179 -0
  87. package/src/loadbalancer/state/db.js +211 -0
  88. package/src/loadbalancer/state/migrations/001-initial.sql +50 -0
  89. package/src/loadbalancer/tools/index.js +123 -0
  90. package/src/loadbalancer/velocity-estimator.js +147 -0
  91. package/src/staging/staging.js +394 -0
  92. package/update-byan-agent/bin/update-byan-agent.js +27 -2
  93. package/API-BYAN-V2.md +0 -741
  94. package/BMAD-QUICK-REFERENCE.md +0 -370
  95. package/CHANGELOG-v2.1.0.md +0 -371
  96. package/MIGRATION-v2.0-to-v2.1.md +0 -430
  97. package/README-BYAN-V2.md +0 -446
  98. package/TEST-GUIDE-v2.3.2.md +0 -161
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Copilot Hooks — Rate Limit Detection for GitHub Copilot SDK
3
+ *
4
+ * Intercepts Copilot SDK session events and HTTP responses
5
+ * to detect 429 errors and X-RateLimit headers.
6
+ */
7
+
8
+ /**
9
+ * Attach rate limit detection to a Copilot SDK session.
10
+ * @param {object} session - Copilot SDK session object
11
+ * @param {import('../rate-limit-tracker').RateLimitTracker} tracker
12
+ */
13
+ function attachCopilotHooks(session, tracker) {
14
+ if (!session || !session.on) return;
15
+
16
+ session.on('error', (err) => {
17
+ const is429 = err.status === 429 || err.statusCode === 429 ||
18
+ (err.message && err.message.includes('rate limit'));
19
+
20
+ if (is429) {
21
+ const headers = err.headers || err.response?.headers || {};
22
+ tracker.record429({
23
+ source: 'copilot-event',
24
+ headers: {
25
+ 'x-ratelimit-remaining': headers['x-ratelimit-remaining'],
26
+ 'x-ratelimit-reset': headers['x-ratelimit-reset'],
27
+ 'retry-after': headers['retry-after'],
28
+ },
29
+ });
30
+ }
31
+ });
32
+
33
+ session.on('response', (res) => {
34
+ const headers = res.headers || {};
35
+ if (headers['x-ratelimit-remaining'] || headers['retry-after']) {
36
+ tracker.recordHeaders(headers);
37
+ }
38
+ if (!res.rateLimited) {
39
+ tracker.recordSuccess();
40
+ }
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Wrap a Copilot SDK sendAndWait call with rate limit detection.
46
+ * @param {Function} sendFn - Original sendAndWait function
47
+ * @param {import('../rate-limit-tracker').RateLimitTracker} tracker
48
+ * @returns {Function} Wrapped function
49
+ */
50
+ function wrapCopilotSend(sendFn, tracker) {
51
+ return async function wrappedSend(...args) {
52
+ try {
53
+ const result = await sendFn.apply(this, args);
54
+ tracker.recordSuccess();
55
+ return result;
56
+ } catch (err) {
57
+ const is429 = err.status === 429 || err.statusCode === 429 ||
58
+ (err.message && err.message.includes('rate limit'));
59
+
60
+ if (is429) {
61
+ const headers = err.headers || err.response?.headers || {};
62
+ tracker.record429({
63
+ source: 'copilot-send',
64
+ headers,
65
+ });
66
+ tracker.recordHeaders(headers);
67
+ }
68
+
69
+ throw err;
70
+ }
71
+ };
72
+ }
73
+
74
+ module.exports = { attachCopilotHooks, wrapCopilotSend };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * BYAN LoadBalancer — Module Index
3
+ *
4
+ * Public API for the loadbalancer module.
5
+ *
6
+ * Usage:
7
+ * const { LoadBalancer, loadConfig } = require('./src/loadbalancer');
8
+ * const config = loadConfig(projectRoot);
9
+ * const lb = new LoadBalancer({ config, providers: {...} });
10
+ */
11
+
12
+ const { LoadBalancer } = require('./loadbalancer');
13
+ const { loadConfig, getEnabledProviders, getProviderOrder } = require('./config');
14
+ const { RateLimitTracker, STATES } = require('./rate-limit-tracker');
15
+ const { SessionBridge } = require('./session-bridge');
16
+ const { SharedStateStore } = require('./state/db');
17
+ const { BaseProvider } = require('./providers/base-provider');
18
+ const { CopilotProvider } = require('./providers/copilot-provider');
19
+ const { ClaudeProvider } = require('./providers/claude-provider');
20
+ const { CopilotIntegration } = require('./native/copilot-integration');
21
+ const { ClaudeIntegration } = require('./native/claude-integration');
22
+ const { ByanApiProvider } = require('./providers/byan-api-provider');
23
+ const { HealthProbe } = require('./health-probe');
24
+ const { GracefulDegradation, PRIORITY } = require('./graceful-degradation');
25
+ const { CapabilityMatrix, DEFAULT_CAPABILITIES } = require('./capability-matrix');
26
+ const { Metrics } = require('./metrics');
27
+ const { VelocityEstimator, TREND } = require('./velocity-estimator');
28
+ const { calculatePressure, formatPressureSummary, STATE_SCORES, DEFAULT_WEIGHTS } = require('./pressure-score');
29
+
30
+ // MCP server lazy-loaded — requires @modelcontextprotocol/sdk (optional dep)
31
+ let _mcp = null;
32
+ function getMcp() {
33
+ if (!_mcp) _mcp = require('./mcp-server');
34
+ return _mcp;
35
+ }
36
+
37
+ module.exports = {
38
+ // Core
39
+ LoadBalancer,
40
+ loadConfig,
41
+ getEnabledProviders,
42
+ getProviderOrder,
43
+
44
+ // Rate limits
45
+ RateLimitTracker,
46
+ STATES,
47
+
48
+ // Session management
49
+ SessionBridge,
50
+ SharedStateStore,
51
+
52
+ // Providers
53
+ BaseProvider,
54
+ CopilotProvider,
55
+ ClaudeProvider,
56
+ ByanApiProvider,
57
+
58
+ // Native integrations
59
+ CopilotIntegration,
60
+ ClaudeIntegration,
61
+
62
+ // Robustness (Sprint 3)
63
+ HealthProbe,
64
+ GracefulDegradation,
65
+ PRIORITY,
66
+ CapabilityMatrix,
67
+ DEFAULT_CAPABILITIES,
68
+ Metrics,
69
+
70
+ // Quota Monitoring (lb-quota-realtime)
71
+ VelocityEstimator,
72
+ TREND,
73
+ calculatePressure,
74
+ formatPressureSummary,
75
+ STATE_SCORES,
76
+ DEFAULT_WEIGHTS,
77
+
78
+ // MCP Server (lazy — requires @modelcontextprotocol/sdk)
79
+ get startServer() { return getMcp().startServer; },
80
+ get VERSION() { return getMcp().VERSION; },
81
+ };
@@ -0,0 +1,65 @@
1
+ # BYAN LoadBalancer — Default Configuration
2
+ # Copilot CLI <-> Claude Code automatic failover
3
+
4
+ providers:
5
+ copilot:
6
+ enabled: true
7
+ sdk: "@github/copilot-sdk"
8
+ transport: "json-rpc"
9
+ auth_env: "COPILOT_GITHUB_TOKEN"
10
+ models:
11
+ worker: "gpt-4.1-mini"
12
+ agent: "gpt-4.1"
13
+
14
+ claude:
15
+ enabled: true
16
+ sdk: "@anthropic-ai/claude-agent-sdk"
17
+ transport: "native"
18
+ auth_env: "ANTHROPIC_API_KEY"
19
+ models:
20
+ worker: "claude-haiku-4.5"
21
+ agent: "claude-sonnet-4.5"
22
+
23
+ byan_api:
24
+ enabled: false
25
+ url: "http://localhost:3737"
26
+ auth_env: "BYAN_API_TOKEN"
27
+
28
+ primary: "claude"
29
+ fallback_order: ["copilot", "byan_api"]
30
+
31
+ rate_limits:
32
+ window_ms: 60000
33
+ max_429_in_window: 3
34
+ throttle_threshold: 2
35
+ block_threshold: 3
36
+ recovery_probe_interval_ms: 10000
37
+ half_open_max_requests: 2
38
+
39
+ sessions:
40
+ sticky_timeout_ms: 300000
41
+ context_summary_max_tokens: 2000
42
+ auto_switch_on_rate_limit: true
43
+
44
+ quota:
45
+ velocity_window_ms: 120000
46
+ warning_threshold_per_min: 10
47
+ max_requests_before_limit: 30
48
+ preemptive_threshold: 75
49
+ preemptive_enabled: true
50
+
51
+ health_probe:
52
+ enabled: true
53
+ interval_ms: 30000
54
+ timeout_ms: 5000
55
+ cheap_model: true
56
+
57
+ mcp_server:
58
+ transport: "http"
59
+ port: 3838
60
+ host: "127.0.0.1"
61
+
62
+ logging:
63
+ level: "info"
64
+ switchover_events: true
65
+ rate_limit_events: true
@@ -0,0 +1,324 @@
1
+ /**
2
+ * LoadBalancer Core — Failover Routing Engine
3
+ *
4
+ * Routes requests to the healthiest provider. Handles:
5
+ * - Sticky sessions (prefer same provider for ongoing conversation)
6
+ * - Auto-failover on rate limit (THROTTLED/BLOCKED triggers switch)
7
+ * - Manual provider switch with context transfer
8
+ * - Integration with RateLimitTracker + SharedStateStore + SessionBridge
9
+ *
10
+ * Sits ABOVE existing BYAN routers:
11
+ * LoadBalancer (picks PLATFORM) → ExecutionRouter (picks STRATEGY) → Dispatcher (picks MODEL)
12
+ */
13
+
14
+ const { EventEmitter } = require('events');
15
+ const { RateLimitTracker, STATES } = require('./rate-limit-tracker');
16
+ const { getProviderOrder } = require('./config');
17
+
18
+ class LoadBalancer extends EventEmitter {
19
+ /**
20
+ * @param {object} opts
21
+ * @param {object} opts.config - Loaded loadbalancer config
22
+ * @param {object} opts.providers - { name: ProviderAdapter } map
23
+ * @param {import('./state/db').SharedStateStore} [opts.store]
24
+ * @param {import('./session-bridge').SessionBridge} [opts.bridge]
25
+ */
26
+ constructor(opts) {
27
+ super();
28
+ this.config = opts.config;
29
+ this.providers = opts.providers || {};
30
+ this.store = opts.store || null;
31
+ this.bridge = opts.bridge || null;
32
+
33
+ this.trackers = {};
34
+ this.activeProvider = this.config.primary;
35
+ this.sessions = new Map();
36
+
37
+ this._initTrackers();
38
+ }
39
+
40
+ _initTrackers() {
41
+ const rlConfig = this.config.rate_limits || {};
42
+
43
+ for (const name of Object.keys(this.providers)) {
44
+ const tracker = new RateLimitTracker(name, rlConfig);
45
+
46
+ tracker.on('state_change', (event) => {
47
+ this.emit('rate_limit_change', event);
48
+
49
+ if (this.store) {
50
+ this.store.logRateLimit({
51
+ provider: event.provider,
52
+ eventType: '429',
53
+ stateBefore: event.from,
54
+ stateAfter: event.to,
55
+ meta: { reason: event.reason },
56
+ });
57
+ }
58
+
59
+ if (
60
+ this.config.sessions?.auto_switch_on_rate_limit &&
61
+ event.provider === this.activeProvider &&
62
+ (event.to === STATES.BLOCKED || event.to === STATES.THROTTLED)
63
+ ) {
64
+ this._autoFailover(event);
65
+ }
66
+ });
67
+
68
+ this.trackers[name] = tracker;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Get overall loadbalancer status.
74
+ */
75
+ getStatus() {
76
+ const providerStates = {};
77
+ for (const [name, tracker] of Object.entries(this.trackers)) {
78
+ providerStates[name] = tracker.getState();
79
+ }
80
+
81
+ return {
82
+ activeProvider: this.activeProvider,
83
+ primary: this.config.primary,
84
+ fallbackOrder: this.config.fallback_order,
85
+ providers: providerStates,
86
+ activeSessions: this.sessions.size,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Get detailed rate limit info for all providers.
92
+ */
93
+ getRateLimitDetails() {
94
+ const details = {};
95
+ for (const [name, tracker] of Object.entries(this.trackers)) {
96
+ details[name] = tracker.getState();
97
+ }
98
+ return details;
99
+ }
100
+
101
+ /**
102
+ * Get switchover history from store.
103
+ */
104
+ getSwitchoverHistory(limit = 20) {
105
+ if (!this.store) return { events: [], total: 0 };
106
+ const events = this.store.getSwitchoverHistory(limit);
107
+ return { events, total: events.length };
108
+ }
109
+
110
+ /**
111
+ * Route a request to the best available provider.
112
+ * @param {object} opts
113
+ * @param {string} opts.prompt
114
+ * @param {string} [opts.sessionId]
115
+ * @param {string} [opts.preferProvider] - 'auto', 'claude', 'copilot'
116
+ * @returns {Promise<object>} Provider response
117
+ */
118
+ async send(opts) {
119
+ const provider = this._selectProvider(opts);
120
+ const tracker = this.trackers[provider.name];
121
+
122
+ if (!tracker?.canAcceptRequest()) {
123
+ const fallback = this._findHealthyFallback(provider.name);
124
+ if (!fallback) {
125
+ return {
126
+ provider: null,
127
+ content: null,
128
+ error: 'All providers rate-limited. Please wait.',
129
+ rateLimited: true,
130
+ };
131
+ }
132
+ return this._sendToProvider(fallback, opts, tracker);
133
+ }
134
+
135
+ return this._sendToProvider(provider, opts, tracker);
136
+ }
137
+
138
+ async _sendToProvider(provider, opts) {
139
+ const tracker = this.trackers[provider.name];
140
+ const start = Date.now();
141
+
142
+ try {
143
+ const result = await provider.send(opts);
144
+ const latencyMs = Date.now() - start;
145
+
146
+ if (result.rateLimited) {
147
+ tracker.record429(result.rateLimitHeaders || {});
148
+
149
+ if (result.rateLimitHeaders) {
150
+ tracker.recordHeaders(result.rateLimitHeaders);
151
+ }
152
+
153
+ const fallback = this._findHealthyFallback(provider.name);
154
+ if (fallback) {
155
+ this.emit('failover', {
156
+ from: provider.name,
157
+ to: fallback.name,
158
+ reason: 'rate_limited',
159
+ });
160
+ return this._sendToProvider(fallback, opts);
161
+ }
162
+
163
+ return result;
164
+ }
165
+
166
+ tracker.recordSuccess();
167
+
168
+ if (this.store) {
169
+ this.store.recordRequest(provider.name, latencyMs);
170
+ }
171
+
172
+ if (opts.sessionId) {
173
+ this.sessions.set(opts.sessionId, {
174
+ provider: provider.name,
175
+ lastUsed: Date.now(),
176
+ });
177
+ }
178
+
179
+ return result;
180
+ } catch (err) {
181
+ const is429 = err.status === 429 || err.statusCode === 429;
182
+ if (is429) {
183
+ tracker.record429({ source: 'send_error' });
184
+ const fallback = this._findHealthyFallback(provider.name);
185
+ if (fallback) {
186
+ return this._sendToProvider(fallback, opts);
187
+ }
188
+ }
189
+ throw err;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Force switch to a specific provider.
195
+ */
196
+ async switchProvider(opts) {
197
+ const prev = this.activeProvider;
198
+ const target = opts.target;
199
+
200
+ let injectionPrompt = null;
201
+ let contextTransferred = false;
202
+
203
+ if (this.bridge && opts.sessionId) {
204
+ const sourceProvider = this.providers[prev];
205
+ if (sourceProvider) {
206
+ const transfer = await this.bridge.transfer(
207
+ sourceProvider, target, opts.sessionId, opts.reason || 'manual_switch'
208
+ );
209
+ injectionPrompt = transfer.injectionPrompt;
210
+ contextTransferred = true;
211
+ }
212
+ }
213
+
214
+ this.activeProvider = target;
215
+
216
+ this.emit('switch', {
217
+ from: prev,
218
+ to: target,
219
+ reason: opts.reason || 'manual_switch',
220
+ contextTransferred,
221
+ });
222
+
223
+ return {
224
+ switched: true,
225
+ from: prev,
226
+ to: target,
227
+ reason: opts.reason,
228
+ contextTransferred,
229
+ injectionPrompt,
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Get session context in portable format.
235
+ */
236
+ async getSessionContext(sessionId) {
237
+ if (!this.bridge || !sessionId) {
238
+ return {
239
+ sessionId: sessionId || 'none',
240
+ provider: this.activeProvider,
241
+ context: null,
242
+ };
243
+ }
244
+
245
+ const provider = this.providers[this.activeProvider];
246
+ const context = await this.bridge.extract(provider, sessionId);
247
+ return { sessionId, provider: this.activeProvider, context };
248
+ }
249
+
250
+ _selectProvider(opts) {
251
+ if (opts.preferProvider && opts.preferProvider !== 'auto') {
252
+ const preferred = this.providers[opts.preferProvider];
253
+ if (preferred && this.trackers[opts.preferProvider]?.canAcceptRequest()) {
254
+ return preferred;
255
+ }
256
+ }
257
+
258
+ if (opts.sessionId) {
259
+ const sticky = this.sessions.get(opts.sessionId);
260
+ if (sticky) {
261
+ const stickyTimeout = this.config.sessions?.sticky_timeout_ms || 300000;
262
+ if (Date.now() - sticky.lastUsed < stickyTimeout) {
263
+ const provider = this.providers[sticky.provider];
264
+ if (provider && this.trackers[sticky.provider]?.canAcceptRequest()) {
265
+ return provider;
266
+ }
267
+ }
268
+ }
269
+ }
270
+
271
+ const active = this.providers[this.activeProvider];
272
+ if (active && this.trackers[this.activeProvider]?.canAcceptRequest()) {
273
+ return active;
274
+ }
275
+
276
+ const fallback = this._findHealthyFallback(this.activeProvider);
277
+ return fallback || this.providers[this.activeProvider];
278
+ }
279
+
280
+ _findHealthyFallback(excludeProvider) {
281
+ const order = getProviderOrder(this.config);
282
+
283
+ for (const name of order) {
284
+ if (name === excludeProvider) continue;
285
+ const tracker = this.trackers[name];
286
+ if (tracker?.canAcceptRequest() && this.providers[name]) {
287
+ return this.providers[name];
288
+ }
289
+ }
290
+ return null;
291
+ }
292
+
293
+ _autoFailover(event) {
294
+ const fallback = this._findHealthyFallback(event.provider);
295
+ if (!fallback) return;
296
+
297
+ const prev = this.activeProvider;
298
+ this.activeProvider = fallback.name;
299
+
300
+ this.emit('auto_failover', {
301
+ from: prev,
302
+ to: fallback.name,
303
+ reason: `${event.provider} ${event.to}: ${event.reason}`,
304
+ trigger: 'rate_limit',
305
+ });
306
+ }
307
+
308
+ /**
309
+ * Get tracker for a provider (for hooks integration).
310
+ */
311
+ getTracker(providerName) {
312
+ return this.trackers[providerName];
313
+ }
314
+
315
+ destroy() {
316
+ for (const tracker of Object.values(this.trackers)) {
317
+ tracker.destroy();
318
+ }
319
+ this.sessions.clear();
320
+ this.removeAllListeners();
321
+ }
322
+ }
323
+
324
+ module.exports = { LoadBalancer };