@supaku/agentfactory-linear 0.7.18 → 0.7.20
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.
- package/dist/src/agent-client-project-repo.test.js +2 -0
- package/dist/src/agent-client.d.ts +16 -4
- package/dist/src/agent-client.d.ts.map +1 -1
- package/dist/src/agent-client.js +280 -109
- package/dist/src/circuit-breaker.d.ts +76 -0
- package/dist/src/circuit-breaker.d.ts.map +1 -0
- package/dist/src/circuit-breaker.js +229 -0
- package/dist/src/circuit-breaker.test.d.ts +2 -0
- package/dist/src/circuit-breaker.test.d.ts.map +1 -0
- package/dist/src/circuit-breaker.test.js +292 -0
- package/dist/src/errors.d.ts +12 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +18 -0
- package/dist/src/index.d.ts +7 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -1
- package/dist/src/issue-tracker-proxy.d.ts +140 -0
- package/dist/src/issue-tracker-proxy.d.ts.map +1 -0
- package/dist/src/issue-tracker-proxy.js +10 -0
- package/dist/src/proxy-client.d.ts +103 -0
- package/dist/src/proxy-client.d.ts.map +1 -0
- package/dist/src/proxy-client.js +191 -0
- package/dist/src/types.d.ts +50 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit Breaker for Linear API calls
|
|
3
|
+
*
|
|
4
|
+
* Prevents wasting rate limit quota on requests that are guaranteed to fail
|
|
5
|
+
* (e.g., expired OAuth tokens, revoked access). Implements the standard
|
|
6
|
+
* closed → open → half-open state machine.
|
|
7
|
+
*
|
|
8
|
+
* State machine:
|
|
9
|
+
* closed → all calls proceed; auth failures increment counter;
|
|
10
|
+
* at threshold → open
|
|
11
|
+
* open → all calls throw CircuitOpenError immediately (zero quota);
|
|
12
|
+
* after resetTimeoutMs → half-open
|
|
13
|
+
* half-open → one probe call allowed; success → closed;
|
|
14
|
+
* failure → open (with exponential backoff on reset timeout)
|
|
15
|
+
*/
|
|
16
|
+
import { CircuitOpenError } from './errors.js';
|
|
17
|
+
import type { CircuitBreakerConfig, CircuitBreakerStrategy } from './types.js';
|
|
18
|
+
export type CircuitState = 'closed' | 'open' | 'half-open';
|
|
19
|
+
export declare const DEFAULT_CIRCUIT_BREAKER_CONFIG: Required<CircuitBreakerConfig>;
|
|
20
|
+
export declare class CircuitBreaker implements CircuitBreakerStrategy {
|
|
21
|
+
private _state;
|
|
22
|
+
private consecutiveFailures;
|
|
23
|
+
private openedAt;
|
|
24
|
+
private currentResetTimeoutMs;
|
|
25
|
+
private probeInFlight;
|
|
26
|
+
private readonly config;
|
|
27
|
+
constructor(config?: Partial<CircuitBreakerConfig>);
|
|
28
|
+
get state(): CircuitState;
|
|
29
|
+
/**
|
|
30
|
+
* Check if a call is allowed to proceed.
|
|
31
|
+
* In half-open state, only one probe call is allowed at a time.
|
|
32
|
+
*/
|
|
33
|
+
canProceed(): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Record a successful API call. Resets the circuit to closed.
|
|
36
|
+
*/
|
|
37
|
+
recordSuccess(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Record an auth failure. May trip the circuit to open.
|
|
40
|
+
* Called after isAuthError() returns true, so the error is already vetted.
|
|
41
|
+
*/
|
|
42
|
+
recordAuthFailure(_statusCode?: number): void;
|
|
43
|
+
/**
|
|
44
|
+
* Check if an error is an auth/rate-limit error that should count as a circuit failure.
|
|
45
|
+
*
|
|
46
|
+
* Detects:
|
|
47
|
+
* - HTTP status codes in authErrorCodes (400, 401, 403)
|
|
48
|
+
* - Linear GraphQL RATELIMITED error code in response body
|
|
49
|
+
* - Linear SDK error objects with nested error details
|
|
50
|
+
*/
|
|
51
|
+
isAuthError(error: unknown): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Extract the status code from an auth error, or 0 if not determinable.
|
|
54
|
+
*/
|
|
55
|
+
extractStatusCode(error: unknown): number;
|
|
56
|
+
/**
|
|
57
|
+
* Reset the circuit breaker to closed state.
|
|
58
|
+
*/
|
|
59
|
+
reset(): void;
|
|
60
|
+
/**
|
|
61
|
+
* Get diagnostic info for logging/monitoring.
|
|
62
|
+
*/
|
|
63
|
+
getStatus(): {
|
|
64
|
+
state: CircuitState;
|
|
65
|
+
consecutiveFailures: number;
|
|
66
|
+
currentResetTimeoutMs: number;
|
|
67
|
+
msSinceOpened: number | null;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Create a CircuitOpenError with current diagnostic info.
|
|
71
|
+
*/
|
|
72
|
+
createOpenError(): CircuitOpenError;
|
|
73
|
+
private trip;
|
|
74
|
+
private shouldTransitionToHalfOpen;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=circuit-breaker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.d.ts","sourceRoot":"","sources":["../../src/circuit-breaker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAE9E,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAA;AAE1D,eAAO,MAAM,8BAA8B,EAAE,QAAQ,CAAC,oBAAoB,CAMzE,CAAA;AAED,qBAAa,cAAe,YAAW,sBAAsB;IAC3D,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,mBAAmB,CAAI;IAC/B,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,qBAAqB,CAAQ;IACrC,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;gBAE3C,MAAM,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC;IAKlD,IAAI,KAAK,IAAI,YAAY,CAOxB;IAED;;;OAGG;IACH,UAAU,IAAI,OAAO;IAgBrB;;OAEG;IACH,aAAa,IAAI,IAAI;IASrB;;;OAGG;IACH,iBAAiB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI;IAmB7C;;;;;;;OAOG;IACH,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;IAyBpC;;OAEG;IACH,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM;IAKzC;;OAEG;IACH,KAAK,IAAI,IAAI;IAOb;;OAEG;IACH,SAAS,IAAI;QACX,KAAK,EAAE,YAAY,CAAA;QACnB,mBAAmB,EAAE,MAAM,CAAA;QAC3B,qBAAqB,EAAE,MAAM,CAAA;QAC7B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;KAC7B;IASD;;OAEG;IACH,eAAe,IAAI,gBAAgB;IAgBnC,OAAO,CAAC,IAAI;IAKZ,OAAO,CAAC,0BAA0B;CAGnC"}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit Breaker for Linear API calls
|
|
3
|
+
*
|
|
4
|
+
* Prevents wasting rate limit quota on requests that are guaranteed to fail
|
|
5
|
+
* (e.g., expired OAuth tokens, revoked access). Implements the standard
|
|
6
|
+
* closed → open → half-open state machine.
|
|
7
|
+
*
|
|
8
|
+
* State machine:
|
|
9
|
+
* closed → all calls proceed; auth failures increment counter;
|
|
10
|
+
* at threshold → open
|
|
11
|
+
* open → all calls throw CircuitOpenError immediately (zero quota);
|
|
12
|
+
* after resetTimeoutMs → half-open
|
|
13
|
+
* half-open → one probe call allowed; success → closed;
|
|
14
|
+
* failure → open (with exponential backoff on reset timeout)
|
|
15
|
+
*/
|
|
16
|
+
import { CircuitOpenError } from './errors.js';
|
|
17
|
+
export const DEFAULT_CIRCUIT_BREAKER_CONFIG = {
|
|
18
|
+
failureThreshold: 2,
|
|
19
|
+
resetTimeoutMs: 60_000,
|
|
20
|
+
maxResetTimeoutMs: 300_000,
|
|
21
|
+
backoffMultiplier: 2,
|
|
22
|
+
authErrorCodes: [400, 401, 403],
|
|
23
|
+
};
|
|
24
|
+
export class CircuitBreaker {
|
|
25
|
+
_state = 'closed';
|
|
26
|
+
consecutiveFailures = 0;
|
|
27
|
+
openedAt = 0;
|
|
28
|
+
currentResetTimeoutMs;
|
|
29
|
+
probeInFlight = false;
|
|
30
|
+
config;
|
|
31
|
+
constructor(config) {
|
|
32
|
+
this.config = { ...DEFAULT_CIRCUIT_BREAKER_CONFIG, ...config };
|
|
33
|
+
this.currentResetTimeoutMs = this.config.resetTimeoutMs;
|
|
34
|
+
}
|
|
35
|
+
get state() {
|
|
36
|
+
// Check if open circuit should transition to half-open
|
|
37
|
+
if (this._state === 'open' && this.shouldTransitionToHalfOpen()) {
|
|
38
|
+
this._state = 'half-open';
|
|
39
|
+
this.probeInFlight = false;
|
|
40
|
+
}
|
|
41
|
+
return this._state;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if a call is allowed to proceed.
|
|
45
|
+
* In half-open state, only one probe call is allowed at a time.
|
|
46
|
+
*/
|
|
47
|
+
canProceed() {
|
|
48
|
+
const currentState = this.state; // triggers open → half-open check
|
|
49
|
+
switch (currentState) {
|
|
50
|
+
case 'closed':
|
|
51
|
+
return true;
|
|
52
|
+
case 'open':
|
|
53
|
+
return false;
|
|
54
|
+
case 'half-open':
|
|
55
|
+
// Allow exactly one probe call
|
|
56
|
+
if (this.probeInFlight)
|
|
57
|
+
return false;
|
|
58
|
+
this.probeInFlight = true;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Record a successful API call. Resets the circuit to closed.
|
|
64
|
+
*/
|
|
65
|
+
recordSuccess() {
|
|
66
|
+
this.consecutiveFailures = 0;
|
|
67
|
+
this.probeInFlight = false;
|
|
68
|
+
if (this._state !== 'closed') {
|
|
69
|
+
this._state = 'closed';
|
|
70
|
+
this.currentResetTimeoutMs = this.config.resetTimeoutMs;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Record an auth failure. May trip the circuit to open.
|
|
75
|
+
* Called after isAuthError() returns true, so the error is already vetted.
|
|
76
|
+
*/
|
|
77
|
+
recordAuthFailure(_statusCode) {
|
|
78
|
+
this.probeInFlight = false;
|
|
79
|
+
this.consecutiveFailures++;
|
|
80
|
+
if (this._state === 'half-open') {
|
|
81
|
+
// Probe failed — reopen with exponential backoff
|
|
82
|
+
this.trip();
|
|
83
|
+
this.currentResetTimeoutMs = Math.min(this.currentResetTimeoutMs * this.config.backoffMultiplier, this.config.maxResetTimeoutMs);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (this.consecutiveFailures >= this.config.failureThreshold) {
|
|
87
|
+
this.trip();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if an error is an auth/rate-limit error that should count as a circuit failure.
|
|
92
|
+
*
|
|
93
|
+
* Detects:
|
|
94
|
+
* - HTTP status codes in authErrorCodes (400, 401, 403)
|
|
95
|
+
* - Linear GraphQL RATELIMITED error code in response body
|
|
96
|
+
* - Linear SDK error objects with nested error details
|
|
97
|
+
*/
|
|
98
|
+
isAuthError(error) {
|
|
99
|
+
if (typeof error !== 'object' || error === null)
|
|
100
|
+
return false;
|
|
101
|
+
const err = error;
|
|
102
|
+
// Check HTTP status code
|
|
103
|
+
const statusCode = extractStatusCode(err);
|
|
104
|
+
if (statusCode !== null && this.config.authErrorCodes.includes(statusCode)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
// Check for Linear GraphQL RATELIMITED error
|
|
108
|
+
if (isGraphQLRateLimited(err)) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
// Check error message for known auth failure patterns
|
|
112
|
+
const message = err.message ?? '';
|
|
113
|
+
if (/access denied|unauthorized|forbidden/i.test(message)) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Extract the status code from an auth error, or 0 if not determinable.
|
|
120
|
+
*/
|
|
121
|
+
extractStatusCode(error) {
|
|
122
|
+
if (typeof error !== 'object' || error === null)
|
|
123
|
+
return 0;
|
|
124
|
+
return extractStatusCode(error) ?? 0;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Reset the circuit breaker to closed state.
|
|
128
|
+
*/
|
|
129
|
+
reset() {
|
|
130
|
+
this._state = 'closed';
|
|
131
|
+
this.consecutiveFailures = 0;
|
|
132
|
+
this.probeInFlight = false;
|
|
133
|
+
this.currentResetTimeoutMs = this.config.resetTimeoutMs;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get diagnostic info for logging/monitoring.
|
|
137
|
+
*/
|
|
138
|
+
getStatus() {
|
|
139
|
+
return {
|
|
140
|
+
state: this.state,
|
|
141
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
142
|
+
currentResetTimeoutMs: this.currentResetTimeoutMs,
|
|
143
|
+
msSinceOpened: this.openedAt > 0 ? Date.now() - this.openedAt : null,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Create a CircuitOpenError with current diagnostic info.
|
|
148
|
+
*/
|
|
149
|
+
createOpenError() {
|
|
150
|
+
const timeRemaining = Math.max(0, this.currentResetTimeoutMs - (Date.now() - this.openedAt));
|
|
151
|
+
return new CircuitOpenError(`Circuit breaker is open — Linear API calls blocked for ${Math.ceil(timeRemaining / 1000)}s. ` +
|
|
152
|
+
`${this.consecutiveFailures} consecutive auth failures detected.`, timeRemaining);
|
|
153
|
+
}
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Private
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
trip() {
|
|
158
|
+
this._state = 'open';
|
|
159
|
+
this.openedAt = Date.now();
|
|
160
|
+
}
|
|
161
|
+
shouldTransitionToHalfOpen() {
|
|
162
|
+
return Date.now() - this.openedAt >= this.currentResetTimeoutMs;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Helpers
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
/**
|
|
169
|
+
* Extract HTTP status code from various error shapes
|
|
170
|
+
*/
|
|
171
|
+
function extractStatusCode(err) {
|
|
172
|
+
// Direct status/statusCode property
|
|
173
|
+
if (typeof err.status === 'number')
|
|
174
|
+
return err.status;
|
|
175
|
+
if (typeof err.statusCode === 'number')
|
|
176
|
+
return err.statusCode;
|
|
177
|
+
// Nested in response
|
|
178
|
+
const response = err.response;
|
|
179
|
+
if (response) {
|
|
180
|
+
if (typeof response.status === 'number')
|
|
181
|
+
return response.status;
|
|
182
|
+
if (typeof response.statusCode === 'number')
|
|
183
|
+
return response.statusCode;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Check if the error contains a Linear GraphQL RATELIMITED error code.
|
|
189
|
+
*
|
|
190
|
+
* Linear returns HTTP 200 with a GraphQL error body when rate-limited:
|
|
191
|
+
* { errors: [{ extensions: { code: 'RATELIMITED' } }] }
|
|
192
|
+
*/
|
|
193
|
+
function isGraphQLRateLimited(err) {
|
|
194
|
+
// Check error.extensions.code directly (Linear SDK error shape)
|
|
195
|
+
const extensions = err.extensions;
|
|
196
|
+
if (extensions?.code === 'RATELIMITED')
|
|
197
|
+
return true;
|
|
198
|
+
// Check nested errors array (raw GraphQL response shape)
|
|
199
|
+
const errors = err.errors;
|
|
200
|
+
if (Array.isArray(errors)) {
|
|
201
|
+
for (const gqlError of errors) {
|
|
202
|
+
const ext = gqlError.extensions;
|
|
203
|
+
if (ext?.code === 'RATELIMITED')
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Check response body for GraphQL errors
|
|
208
|
+
const response = err.response;
|
|
209
|
+
if (response) {
|
|
210
|
+
const body = response.body;
|
|
211
|
+
const data = response.data;
|
|
212
|
+
const target = body ?? data;
|
|
213
|
+
if (target) {
|
|
214
|
+
const bodyErrors = target.errors;
|
|
215
|
+
if (Array.isArray(bodyErrors)) {
|
|
216
|
+
for (const gqlError of bodyErrors) {
|
|
217
|
+
const ext = gqlError.extensions;
|
|
218
|
+
if (ext?.code === 'RATELIMITED')
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Check error message as last resort
|
|
225
|
+
const message = err.message ?? '';
|
|
226
|
+
if (message.includes('RATELIMITED'))
|
|
227
|
+
return true;
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.test.d.ts","sourceRoot":"","sources":["../../src/circuit-breaker.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { CircuitBreaker, DEFAULT_CIRCUIT_BREAKER_CONFIG } from './circuit-breaker.js';
|
|
3
|
+
import { CircuitOpenError } from './errors.js';
|
|
4
|
+
describe('CircuitBreaker', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.useFakeTimers();
|
|
7
|
+
});
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.useRealTimers();
|
|
10
|
+
});
|
|
11
|
+
// ========================================================================
|
|
12
|
+
// Construction & defaults
|
|
13
|
+
// ========================================================================
|
|
14
|
+
it('starts in closed state', () => {
|
|
15
|
+
const cb = new CircuitBreaker();
|
|
16
|
+
expect(cb.state).toBe('closed');
|
|
17
|
+
});
|
|
18
|
+
it('uses default config when none provided', () => {
|
|
19
|
+
const cb = new CircuitBreaker();
|
|
20
|
+
const status = cb.getStatus();
|
|
21
|
+
expect(status.state).toBe('closed');
|
|
22
|
+
expect(status.consecutiveFailures).toBe(0);
|
|
23
|
+
expect(status.currentResetTimeoutMs).toBe(DEFAULT_CIRCUIT_BREAKER_CONFIG.resetTimeoutMs);
|
|
24
|
+
});
|
|
25
|
+
it('accepts custom config', () => {
|
|
26
|
+
const cb = new CircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30_000 });
|
|
27
|
+
// Trip it 4 times — should still be closed
|
|
28
|
+
for (let i = 0; i < 4; i++) {
|
|
29
|
+
cb.recordAuthFailure(401);
|
|
30
|
+
}
|
|
31
|
+
expect(cb.state).toBe('closed');
|
|
32
|
+
// 5th failure trips it
|
|
33
|
+
cb.recordAuthFailure(401);
|
|
34
|
+
expect(cb.state).toBe('open');
|
|
35
|
+
});
|
|
36
|
+
// ========================================================================
|
|
37
|
+
// canProceed
|
|
38
|
+
// ========================================================================
|
|
39
|
+
it('allows calls when closed', () => {
|
|
40
|
+
const cb = new CircuitBreaker();
|
|
41
|
+
expect(cb.canProceed()).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
it('blocks calls when open', () => {
|
|
44
|
+
const cb = new CircuitBreaker({ failureThreshold: 1 });
|
|
45
|
+
cb.recordAuthFailure(401);
|
|
46
|
+
expect(cb.state).toBe('open');
|
|
47
|
+
expect(cb.canProceed()).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it('allows one probe call when half-open', () => {
|
|
50
|
+
const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 1000 });
|
|
51
|
+
cb.recordAuthFailure(401);
|
|
52
|
+
expect(cb.state).toBe('open');
|
|
53
|
+
// Advance past reset timeout
|
|
54
|
+
vi.advanceTimersByTime(1001);
|
|
55
|
+
expect(cb.state).toBe('half-open');
|
|
56
|
+
// First call should be allowed (probe)
|
|
57
|
+
expect(cb.canProceed()).toBe(true);
|
|
58
|
+
// Second call should be blocked (probe already in-flight)
|
|
59
|
+
expect(cb.canProceed()).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
// ========================================================================
|
|
62
|
+
// State transitions: closed → open
|
|
63
|
+
// ========================================================================
|
|
64
|
+
it('trips after consecutive auth failures reach threshold', () => {
|
|
65
|
+
const cb = new CircuitBreaker({ failureThreshold: 2 });
|
|
66
|
+
cb.recordAuthFailure(400);
|
|
67
|
+
expect(cb.state).toBe('closed');
|
|
68
|
+
cb.recordAuthFailure(401);
|
|
69
|
+
expect(cb.state).toBe('open');
|
|
70
|
+
});
|
|
71
|
+
it('always counts failures regardless of status code (isAuthError already vetted)', () => {
|
|
72
|
+
const cb = new CircuitBreaker({ failureThreshold: 2 });
|
|
73
|
+
cb.recordAuthFailure(200); // e.g., GraphQL 200 with auth error in body
|
|
74
|
+
expect(cb.getStatus().consecutiveFailures).toBe(1);
|
|
75
|
+
cb.recordAuthFailure(); // no status code at all
|
|
76
|
+
expect(cb.state).toBe('open');
|
|
77
|
+
});
|
|
78
|
+
it('resets failure count on success', () => {
|
|
79
|
+
const cb = new CircuitBreaker({ failureThreshold: 3 });
|
|
80
|
+
cb.recordAuthFailure(401); // 1
|
|
81
|
+
cb.recordAuthFailure(403); // 2
|
|
82
|
+
cb.recordSuccess(); // reset to 0
|
|
83
|
+
cb.recordAuthFailure(400); // 1 — should NOT trip
|
|
84
|
+
expect(cb.state).toBe('closed');
|
|
85
|
+
});
|
|
86
|
+
// ========================================================================
|
|
87
|
+
// State transitions: open → half-open
|
|
88
|
+
// ========================================================================
|
|
89
|
+
it('transitions from open to half-open after resetTimeoutMs', () => {
|
|
90
|
+
const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 5000 });
|
|
91
|
+
cb.recordAuthFailure(401);
|
|
92
|
+
expect(cb.state).toBe('open');
|
|
93
|
+
// Not enough time
|
|
94
|
+
vi.advanceTimersByTime(4999);
|
|
95
|
+
expect(cb.state).toBe('open');
|
|
96
|
+
// Exactly enough time
|
|
97
|
+
vi.advanceTimersByTime(1);
|
|
98
|
+
expect(cb.state).toBe('half-open');
|
|
99
|
+
});
|
|
100
|
+
// ========================================================================
|
|
101
|
+
// State transitions: half-open → closed (probe success)
|
|
102
|
+
// ========================================================================
|
|
103
|
+
it('closes circuit on successful probe in half-open', () => {
|
|
104
|
+
const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 1000 });
|
|
105
|
+
cb.recordAuthFailure(401);
|
|
106
|
+
vi.advanceTimersByTime(1001);
|
|
107
|
+
expect(cb.state).toBe('half-open');
|
|
108
|
+
// Probe succeeds
|
|
109
|
+
cb.canProceed(); // acquire probe
|
|
110
|
+
cb.recordSuccess();
|
|
111
|
+
expect(cb.state).toBe('closed');
|
|
112
|
+
expect(cb.getStatus().consecutiveFailures).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
// ========================================================================
|
|
115
|
+
// State transitions: half-open → open (probe failure + exponential backoff)
|
|
116
|
+
// ========================================================================
|
|
117
|
+
it('reopens circuit on failed probe with exponential backoff', () => {
|
|
118
|
+
const cb = new CircuitBreaker({
|
|
119
|
+
failureThreshold: 1,
|
|
120
|
+
resetTimeoutMs: 1000,
|
|
121
|
+
maxResetTimeoutMs: 16000,
|
|
122
|
+
backoffMultiplier: 2,
|
|
123
|
+
});
|
|
124
|
+
// First trip
|
|
125
|
+
cb.recordAuthFailure(401);
|
|
126
|
+
expect(cb.state).toBe('open');
|
|
127
|
+
expect(cb.getStatus().currentResetTimeoutMs).toBe(1000);
|
|
128
|
+
// Wait for half-open
|
|
129
|
+
vi.advanceTimersByTime(1001);
|
|
130
|
+
expect(cb.state).toBe('half-open');
|
|
131
|
+
// Probe fails → reopen with backoff
|
|
132
|
+
cb.canProceed();
|
|
133
|
+
cb.recordAuthFailure(401);
|
|
134
|
+
expect(cb.state).toBe('open');
|
|
135
|
+
expect(cb.getStatus().currentResetTimeoutMs).toBe(2000); // 1000 * 2
|
|
136
|
+
// Wait 2000ms for next half-open
|
|
137
|
+
vi.advanceTimersByTime(2001);
|
|
138
|
+
expect(cb.state).toBe('half-open');
|
|
139
|
+
// Probe fails again → further backoff
|
|
140
|
+
cb.canProceed();
|
|
141
|
+
cb.recordAuthFailure(401);
|
|
142
|
+
expect(cb.state).toBe('open');
|
|
143
|
+
expect(cb.getStatus().currentResetTimeoutMs).toBe(4000); // 2000 * 2
|
|
144
|
+
});
|
|
145
|
+
it('caps reset timeout at maxResetTimeoutMs', () => {
|
|
146
|
+
const cb = new CircuitBreaker({
|
|
147
|
+
failureThreshold: 1,
|
|
148
|
+
resetTimeoutMs: 1000,
|
|
149
|
+
maxResetTimeoutMs: 3000,
|
|
150
|
+
backoffMultiplier: 2,
|
|
151
|
+
});
|
|
152
|
+
// Trip → open (1000ms)
|
|
153
|
+
cb.recordAuthFailure(401);
|
|
154
|
+
vi.advanceTimersByTime(1001);
|
|
155
|
+
// Probe fail → 2000ms
|
|
156
|
+
cb.canProceed();
|
|
157
|
+
cb.recordAuthFailure(401);
|
|
158
|
+
vi.advanceTimersByTime(2001);
|
|
159
|
+
// Probe fail → 3000ms (capped at max)
|
|
160
|
+
cb.canProceed();
|
|
161
|
+
cb.recordAuthFailure(401);
|
|
162
|
+
expect(cb.getStatus().currentResetTimeoutMs).toBe(3000);
|
|
163
|
+
vi.advanceTimersByTime(3001);
|
|
164
|
+
// Probe fail → still 3000ms (capped)
|
|
165
|
+
cb.canProceed();
|
|
166
|
+
cb.recordAuthFailure(401);
|
|
167
|
+
expect(cb.getStatus().currentResetTimeoutMs).toBe(3000);
|
|
168
|
+
});
|
|
169
|
+
it('resets backoff on successful recovery', () => {
|
|
170
|
+
const cb = new CircuitBreaker({
|
|
171
|
+
failureThreshold: 1,
|
|
172
|
+
resetTimeoutMs: 1000,
|
|
173
|
+
maxResetTimeoutMs: 16000,
|
|
174
|
+
backoffMultiplier: 2,
|
|
175
|
+
});
|
|
176
|
+
// Trip and backoff to 2000ms
|
|
177
|
+
cb.recordAuthFailure(401);
|
|
178
|
+
vi.advanceTimersByTime(1001);
|
|
179
|
+
cb.canProceed();
|
|
180
|
+
cb.recordAuthFailure(401);
|
|
181
|
+
expect(cb.getStatus().currentResetTimeoutMs).toBe(2000);
|
|
182
|
+
// Recover
|
|
183
|
+
vi.advanceTimersByTime(2001);
|
|
184
|
+
cb.canProceed();
|
|
185
|
+
cb.recordSuccess();
|
|
186
|
+
expect(cb.state).toBe('closed');
|
|
187
|
+
expect(cb.getStatus().currentResetTimeoutMs).toBe(1000); // reset to original
|
|
188
|
+
});
|
|
189
|
+
// ========================================================================
|
|
190
|
+
// reset()
|
|
191
|
+
// ========================================================================
|
|
192
|
+
it('reset() returns to closed from any state', () => {
|
|
193
|
+
const cb = new CircuitBreaker({ failureThreshold: 1 });
|
|
194
|
+
cb.recordAuthFailure(401);
|
|
195
|
+
expect(cb.state).toBe('open');
|
|
196
|
+
cb.reset();
|
|
197
|
+
expect(cb.state).toBe('closed');
|
|
198
|
+
expect(cb.getStatus().consecutiveFailures).toBe(0);
|
|
199
|
+
expect(cb.getStatus().currentResetTimeoutMs).toBe(DEFAULT_CIRCUIT_BREAKER_CONFIG.resetTimeoutMs);
|
|
200
|
+
});
|
|
201
|
+
// ========================================================================
|
|
202
|
+
// isAuthError
|
|
203
|
+
// ========================================================================
|
|
204
|
+
it('detects auth errors by HTTP status code', () => {
|
|
205
|
+
const cb = new CircuitBreaker();
|
|
206
|
+
expect(cb.isAuthError({ status: 400 })).toBe(true);
|
|
207
|
+
expect(cb.isAuthError({ status: 401 })).toBe(true);
|
|
208
|
+
expect(cb.isAuthError({ status: 403 })).toBe(true);
|
|
209
|
+
expect(cb.isAuthError({ statusCode: 401 })).toBe(true);
|
|
210
|
+
expect(cb.isAuthError({ response: { status: 403 } })).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
it('does not flag non-auth status codes', () => {
|
|
213
|
+
const cb = new CircuitBreaker();
|
|
214
|
+
expect(cb.isAuthError({ status: 200 })).toBe(false);
|
|
215
|
+
expect(cb.isAuthError({ status: 404 })).toBe(false);
|
|
216
|
+
expect(cb.isAuthError({ status: 500 })).toBe(false);
|
|
217
|
+
expect(cb.isAuthError({ status: 429 })).toBe(false); // rate limit is handled separately
|
|
218
|
+
});
|
|
219
|
+
it('detects GraphQL RATELIMITED error code', () => {
|
|
220
|
+
const cb = new CircuitBreaker();
|
|
221
|
+
// Direct extensions.code
|
|
222
|
+
expect(cb.isAuthError({ extensions: { code: 'RATELIMITED' } })).toBe(true);
|
|
223
|
+
// Nested errors array
|
|
224
|
+
expect(cb.isAuthError({
|
|
225
|
+
errors: [{ extensions: { code: 'RATELIMITED' } }],
|
|
226
|
+
})).toBe(true);
|
|
227
|
+
// In response body
|
|
228
|
+
expect(cb.isAuthError({
|
|
229
|
+
response: {
|
|
230
|
+
body: {
|
|
231
|
+
errors: [{ extensions: { code: 'RATELIMITED' } }],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
})).toBe(true);
|
|
235
|
+
// In response data (alternative shape)
|
|
236
|
+
expect(cb.isAuthError({
|
|
237
|
+
response: {
|
|
238
|
+
data: {
|
|
239
|
+
errors: [{ extensions: { code: 'RATELIMITED' } }],
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
})).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
it('detects auth errors by message pattern', () => {
|
|
245
|
+
const cb = new CircuitBreaker();
|
|
246
|
+
expect(cb.isAuthError({ message: 'Access denied - Only app users can create agent activities' })).toBe(true);
|
|
247
|
+
expect(cb.isAuthError({ message: 'Unauthorized request' })).toBe(true);
|
|
248
|
+
expect(cb.isAuthError({ message: 'Forbidden: insufficient permissions' })).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
it('rejects non-error inputs', () => {
|
|
251
|
+
const cb = new CircuitBreaker();
|
|
252
|
+
expect(cb.isAuthError(null)).toBe(false);
|
|
253
|
+
expect(cb.isAuthError(undefined)).toBe(false);
|
|
254
|
+
expect(cb.isAuthError('string')).toBe(false);
|
|
255
|
+
expect(cb.isAuthError(42)).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
it('detects RATELIMITED in error message', () => {
|
|
258
|
+
const cb = new CircuitBreaker();
|
|
259
|
+
expect(cb.isAuthError({ message: 'GraphQL Error: RATELIMITED' })).toBe(true);
|
|
260
|
+
});
|
|
261
|
+
// ========================================================================
|
|
262
|
+
// createOpenError
|
|
263
|
+
// ========================================================================
|
|
264
|
+
it('creates a CircuitOpenError with remaining time info', () => {
|
|
265
|
+
const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 10_000 });
|
|
266
|
+
cb.recordAuthFailure(401);
|
|
267
|
+
// 3 seconds have passed
|
|
268
|
+
vi.advanceTimersByTime(3000);
|
|
269
|
+
const error = cb.createOpenError();
|
|
270
|
+
expect(error).toBeInstanceOf(CircuitOpenError);
|
|
271
|
+
expect(error.code).toBe('CIRCUIT_OPEN');
|
|
272
|
+
expect(error.retryAfterMs).toBeGreaterThan(0);
|
|
273
|
+
expect(error.retryAfterMs).toBeLessThanOrEqual(7000);
|
|
274
|
+
expect(error.message).toMatch(/Circuit breaker is open/);
|
|
275
|
+
});
|
|
276
|
+
// ========================================================================
|
|
277
|
+
// getStatus diagnostic info
|
|
278
|
+
// ========================================================================
|
|
279
|
+
it('provides diagnostic status info', () => {
|
|
280
|
+
const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 5000 });
|
|
281
|
+
let status = cb.getStatus();
|
|
282
|
+
expect(status.state).toBe('closed');
|
|
283
|
+
expect(status.consecutiveFailures).toBe(0);
|
|
284
|
+
expect(status.msSinceOpened).toBeNull();
|
|
285
|
+
cb.recordAuthFailure(401);
|
|
286
|
+
vi.advanceTimersByTime(2000);
|
|
287
|
+
status = cb.getStatus();
|
|
288
|
+
expect(status.state).toBe('open');
|
|
289
|
+
expect(status.consecutiveFailures).toBe(1);
|
|
290
|
+
expect(status.msSinceOpened).toBeGreaterThanOrEqual(2000);
|
|
291
|
+
});
|
|
292
|
+
});
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -54,6 +54,14 @@ export declare class LinearStatusTransitionError extends LinearAgentError {
|
|
|
54
54
|
readonly toStatus: string;
|
|
55
55
|
constructor(message: string, issueId: string, fromStatus: string, toStatus: string);
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Error thrown when the circuit breaker is open.
|
|
59
|
+
* All API calls are blocked to prevent wasting rate limit quota.
|
|
60
|
+
*/
|
|
61
|
+
export declare class CircuitOpenError extends LinearAgentError {
|
|
62
|
+
readonly retryAfterMs: number;
|
|
63
|
+
constructor(message: string, retryAfterMs: number);
|
|
64
|
+
}
|
|
57
65
|
/**
|
|
58
66
|
* Error thrown when agent spawning fails
|
|
59
67
|
*/
|
|
@@ -72,6 +80,10 @@ export declare function isLinearAgentError(error: unknown): error is LinearAgent
|
|
|
72
80
|
* Type guard to check if an error is an AgentSpawnError
|
|
73
81
|
*/
|
|
74
82
|
export declare function isAgentSpawnError(error: unknown): error is AgentSpawnError;
|
|
83
|
+
/**
|
|
84
|
+
* Type guard to check if an error is a CircuitOpenError
|
|
85
|
+
*/
|
|
86
|
+
export declare function isCircuitOpenError(error: unknown): error is CircuitOpenError;
|
|
75
87
|
/**
|
|
76
88
|
* Type guard to check if an error is retryable
|
|
77
89
|
*/
|
package/dist/src/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;aAGvB,IAAI,EAAE,MAAM;aACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;gBAFjD,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YAAA;CAQpD;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,gBAAgB;aAGhC,UAAU,EAAE,MAAM;aAClB,QAAQ,CAAC,EAAE,OAAO;gBAFlC,OAAO,EAAE,MAAM,EACC,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,OAAO,YAAA;CAKrC;AAED;;GAEG;AACH,qBAAa,yBAA0B,SAAQ,gBAAgB;aAG3C,QAAQ,EAAE,MAAM;aAChB,SAAS,EAAE,KAAK;gBAFhC,OAAO,EAAE,MAAM,EACC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,KAAK;CAQnC;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,gBAAgB;aAGpC,SAAS,CAAC,EAAE,MAAM;aAClB,OAAO,CAAC,EAAE,MAAM;gBAFhC,OAAO,EAAE,MAAM,EACC,SAAS,CAAC,EAAE,MAAM,YAAA,EAClB,OAAO,CAAC,EAAE,MAAM,YAAA;CAKnC;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,gBAAgB;aAGrC,YAAY,EAAE,MAAM;aACpB,SAAS,CAAC,EAAE,MAAM;gBAFlC,OAAO,EAAE,MAAM,EACC,YAAY,EAAE,MAAM,EACpB,SAAS,CAAC,EAAE,MAAM,YAAA;CAKrC;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,gBAAgB;aACN,SAAS,CAAC,EAAE,MAAM;gBAAnD,OAAO,EAAE,MAAM,EAAkB,SAAS,CAAC,EAAE,MAAM,YAAA;CAIhE;AAED;;GAEG;AACH,qBAAa,2BAA4B,SAAQ,gBAAgB;aAG7C,OAAO,EAAE,MAAM;aACf,UAAU,EAAE,MAAM;aAClB,QAAQ,EAAE,MAAM;gBAHhC,OAAO,EAAE,MAAM,EACC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM;CASnC;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,gBAAgB;aAGjC,OAAO,EAAE,MAAM;aACf,SAAS,CAAC,EAAE,MAAM;aAClB,WAAW,EAAE,OAAO;aACpB,KAAK,CAAC,EAAE,KAAK;gBAJ7B,OAAO,EAAE,MAAM,EACC,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,YAAA,EAClB,WAAW,GAAE,OAAe,EAC5B,KAAK,CAAC,EAAE,KAAK,YAAA;CAUhC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAE5E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAE1E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,OAAO,EACd,oBAAoB,GAAE,MAAM,EAA8B,GACzD,OAAO,CAkBT"}
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;aAGvB,IAAI,EAAE,MAAM;aACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;gBAFjD,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YAAA;CAQpD;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,gBAAgB;aAGhC,UAAU,EAAE,MAAM;aAClB,QAAQ,CAAC,EAAE,OAAO;gBAFlC,OAAO,EAAE,MAAM,EACC,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,OAAO,YAAA;CAKrC;AAED;;GAEG;AACH,qBAAa,yBAA0B,SAAQ,gBAAgB;aAG3C,QAAQ,EAAE,MAAM;aAChB,SAAS,EAAE,KAAK;gBAFhC,OAAO,EAAE,MAAM,EACC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,KAAK;CAQnC;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,gBAAgB;aAGpC,SAAS,CAAC,EAAE,MAAM;aAClB,OAAO,CAAC,EAAE,MAAM;gBAFhC,OAAO,EAAE,MAAM,EACC,SAAS,CAAC,EAAE,MAAM,YAAA,EAClB,OAAO,CAAC,EAAE,MAAM,YAAA;CAKnC;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,gBAAgB;aAGrC,YAAY,EAAE,MAAM;aACpB,SAAS,CAAC,EAAE,MAAM;gBAFlC,OAAO,EAAE,MAAM,EACC,YAAY,EAAE,MAAM,EACpB,SAAS,CAAC,EAAE,MAAM,YAAA;CAKrC;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,gBAAgB;aACN,SAAS,CAAC,EAAE,MAAM;gBAAnD,OAAO,EAAE,MAAM,EAAkB,SAAS,CAAC,EAAE,MAAM,YAAA;CAIhE;AAED;;GAEG;AACH,qBAAa,2BAA4B,SAAQ,gBAAgB;aAG7C,OAAO,EAAE,MAAM;aACf,UAAU,EAAE,MAAM;aAClB,QAAQ,EAAE,MAAM;gBAHhC,OAAO,EAAE,MAAM,EACC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM;CASnC;AAED;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,gBAAgB;aAGlC,YAAY,EAAE,MAAM;gBADpC,OAAO,EAAE,MAAM,EACC,YAAY,EAAE,MAAM;CAKvC;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,gBAAgB;aAGjC,OAAO,EAAE,MAAM;aACf,SAAS,CAAC,EAAE,MAAM;aAClB,WAAW,EAAE,OAAO;aACpB,KAAK,CAAC,EAAE,KAAK;gBAJ7B,OAAO,EAAE,MAAM,EACC,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,YAAA,EAClB,WAAW,GAAE,OAAe,EAC5B,KAAK,CAAC,EAAE,KAAK,YAAA;CAUhC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAE5E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAE1E;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAE5E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,OAAO,EACd,oBAAoB,GAAE,MAAM,EAA8B,GACzD,OAAO,CAkBT"}
|
package/dist/src/errors.js
CHANGED
|
@@ -99,6 +99,18 @@ export class LinearStatusTransitionError extends LinearAgentError {
|
|
|
99
99
|
this.name = 'LinearStatusTransitionError';
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Error thrown when the circuit breaker is open.
|
|
104
|
+
* All API calls are blocked to prevent wasting rate limit quota.
|
|
105
|
+
*/
|
|
106
|
+
export class CircuitOpenError extends LinearAgentError {
|
|
107
|
+
retryAfterMs;
|
|
108
|
+
constructor(message, retryAfterMs) {
|
|
109
|
+
super(message, 'CIRCUIT_OPEN', { retryAfterMs });
|
|
110
|
+
this.retryAfterMs = retryAfterMs;
|
|
111
|
+
this.name = 'CircuitOpenError';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
102
114
|
/**
|
|
103
115
|
* Error thrown when agent spawning fails
|
|
104
116
|
*/
|
|
@@ -133,6 +145,12 @@ export function isLinearAgentError(error) {
|
|
|
133
145
|
export function isAgentSpawnError(error) {
|
|
134
146
|
return error instanceof AgentSpawnError;
|
|
135
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Type guard to check if an error is a CircuitOpenError
|
|
150
|
+
*/
|
|
151
|
+
export function isCircuitOpenError(error) {
|
|
152
|
+
return error instanceof CircuitOpenError;
|
|
153
|
+
}
|
|
136
154
|
/**
|
|
137
155
|
* Type guard to check if an error is retryable
|
|
138
156
|
*/
|