@supaku/agentfactory-linear 0.7.13 → 0.7.14
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 +27 -1
- package/dist/src/agent-client.d.ts.map +1 -1
- package/dist/src/agent-client.js +68 -2
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/platform-adapter.d.ts +16 -3
- package/dist/src/platform-adapter.d.ts.map +1 -1
- package/dist/src/platform-adapter.js +45 -14
- package/dist/src/platform-adapter.test.js +56 -23
- package/dist/src/rate-limiter.d.ts +64 -0
- package/dist/src/rate-limiter.d.ts.map +1 -0
- package/dist/src/rate-limiter.js +163 -0
- package/dist/src/rate-limiter.test.d.ts +2 -0
- package/dist/src/rate-limiter.test.d.ts.map +1 -0
- package/dist/src/rate-limiter.test.js +217 -0
- package/dist/src/retry.d.ts +17 -1
- package/dist/src/retry.d.ts.map +1 -1
- package/dist/src/retry.js +11 -2
- package/dist/src/types.d.ts +2 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { LinearAgentClient } from './agent-client.js';
|
|
3
|
+
import { TokenBucket } from './rate-limiter.js';
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
4
5
|
// Mock helpers
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
@@ -28,6 +29,7 @@ function createClientWithProject(project) {
|
|
|
28
29
|
value: { maxRetries: 0, baseDelay: 0, maxDelay: 0 },
|
|
29
30
|
writable: false,
|
|
30
31
|
});
|
|
32
|
+
Object.defineProperty(client, 'rateLimiter', { value: new TokenBucket(), writable: false });
|
|
31
33
|
Object.defineProperty(client, 'statusCache', { value: new Map(), writable: false });
|
|
32
34
|
return client;
|
|
33
35
|
}
|
|
@@ -8,6 +8,7 @@ import type { LinearAgentClientConfig, LinearWorkflowStatus, StatusMapping, Agen
|
|
|
8
8
|
export declare class LinearAgentClient {
|
|
9
9
|
private readonly client;
|
|
10
10
|
private readonly retryConfig;
|
|
11
|
+
private readonly rateLimiter;
|
|
11
12
|
private statusCache;
|
|
12
13
|
constructor(config: LinearAgentClientConfig);
|
|
13
14
|
/**
|
|
@@ -15,7 +16,11 @@ export declare class LinearAgentClient {
|
|
|
15
16
|
*/
|
|
16
17
|
get linearClient(): LinearClient;
|
|
17
18
|
/**
|
|
18
|
-
* Execute an operation with retry logic
|
|
19
|
+
* Execute an operation with retry logic and rate limiting.
|
|
20
|
+
*
|
|
21
|
+
* On HTTP 429 (rate limited):
|
|
22
|
+
* - Uses the Retry-After header value for the wait delay (falls back to 60s)
|
|
23
|
+
* - Penalizes the token bucket so concurrent callers also back off
|
|
19
24
|
*/
|
|
20
25
|
private withRetry;
|
|
21
26
|
/**
|
|
@@ -158,6 +163,27 @@ export declare class LinearAgentClient {
|
|
|
158
163
|
* @returns True if the issue has a parent issue
|
|
159
164
|
*/
|
|
160
165
|
isChildIssue(issueIdOrIdentifier: string): Promise<boolean>;
|
|
166
|
+
/**
|
|
167
|
+
* Fetch all non-terminal issues in a project using a single GraphQL query.
|
|
168
|
+
*
|
|
169
|
+
* Replaces the N+1 pattern of fetching issues then lazy-loading state/labels/parent/project
|
|
170
|
+
* for each one. Returns pre-resolved data suitable for GovernorIssue construction.
|
|
171
|
+
*
|
|
172
|
+
* @param project - Linear project name
|
|
173
|
+
* @returns Array of issue data with childCount for parent detection
|
|
174
|
+
*/
|
|
175
|
+
listProjectIssues(project: string): Promise<Array<{
|
|
176
|
+
id: string;
|
|
177
|
+
identifier: string;
|
|
178
|
+
title: string;
|
|
179
|
+
description?: string;
|
|
180
|
+
status: string;
|
|
181
|
+
labels: string[];
|
|
182
|
+
createdAt: number;
|
|
183
|
+
parentId?: string;
|
|
184
|
+
project?: string;
|
|
185
|
+
childCount: number;
|
|
186
|
+
}>>;
|
|
161
187
|
/**
|
|
162
188
|
* Check if an issue has child issues (is a parent issue)
|
|
163
189
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-client.d.ts","sourceRoot":"","sources":["../../src/agent-client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAGb,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,KAAK,EACV,uBAAuB,EACvB,oBAAoB,EACpB,aAAa,EAEb,wBAAwB,EACxB,mBAAmB,EACnB,uBAAuB,EACvB,wBAAwB,EACxB,8BAA8B,EAC9B,wBAAwB,EACxB,wBAAwB,EACxB,mBAAmB,EACnB,wBAAwB,EAExB,oBAAoB,EACpB,iBAAiB,EAEjB,aAAa,EACb,cAAc,EACf,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"agent-client.d.ts","sourceRoot":"","sources":["../../src/agent-client.ts"],"names":[],"mappings":"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"}
|
package/dist/src/agent-client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LinearClient, AgentActivitySignal as LinearAgentActivitySignal, IssueRelationType as LinearIssueRelationType, } from '@linear/sdk';
|
|
2
2
|
import { LinearApiError, LinearStatusTransitionError } from './errors.js';
|
|
3
3
|
import { withRetry, DEFAULT_RETRY_CONFIG } from './retry.js';
|
|
4
|
+
import { TokenBucket, extractRetryAfterMs } from './rate-limiter.js';
|
|
4
5
|
/**
|
|
5
6
|
* Core Linear Agent Client
|
|
6
7
|
* Wraps @linear/sdk with retry logic and helper methods
|
|
@@ -8,6 +9,7 @@ import { withRetry, DEFAULT_RETRY_CONFIG } from './retry.js';
|
|
|
8
9
|
export class LinearAgentClient {
|
|
9
10
|
client;
|
|
10
11
|
retryConfig;
|
|
12
|
+
rateLimiter;
|
|
11
13
|
statusCache = new Map();
|
|
12
14
|
constructor(config) {
|
|
13
15
|
this.client = new LinearClient({
|
|
@@ -18,6 +20,7 @@ export class LinearAgentClient {
|
|
|
18
20
|
...DEFAULT_RETRY_CONFIG,
|
|
19
21
|
...config.retry,
|
|
20
22
|
};
|
|
23
|
+
this.rateLimiter = new TokenBucket(config.rateLimit);
|
|
21
24
|
}
|
|
22
25
|
/**
|
|
23
26
|
* Get the underlying LinearClient instance
|
|
@@ -26,11 +29,24 @@ export class LinearAgentClient {
|
|
|
26
29
|
return this.client;
|
|
27
30
|
}
|
|
28
31
|
/**
|
|
29
|
-
* Execute an operation with retry logic
|
|
32
|
+
* Execute an operation with retry logic and rate limiting.
|
|
33
|
+
*
|
|
34
|
+
* On HTTP 429 (rate limited):
|
|
35
|
+
* - Uses the Retry-After header value for the wait delay (falls back to 60s)
|
|
36
|
+
* - Penalizes the token bucket so concurrent callers also back off
|
|
30
37
|
*/
|
|
31
38
|
async withRetry(fn) {
|
|
32
|
-
return withRetry(
|
|
39
|
+
return withRetry(async () => {
|
|
40
|
+
await this.rateLimiter.acquire();
|
|
41
|
+
return fn();
|
|
42
|
+
}, {
|
|
33
43
|
config: this.retryConfig,
|
|
44
|
+
getRetryAfterMs: extractRetryAfterMs,
|
|
45
|
+
onRateLimited: (retryAfterMs) => {
|
|
46
|
+
const seconds = retryAfterMs / 1000;
|
|
47
|
+
console.warn(`[LinearAgentClient] Rate limited by Linear API, backing off ${seconds}s`);
|
|
48
|
+
this.rateLimiter.penalize(seconds);
|
|
49
|
+
},
|
|
34
50
|
onRetry: ({ attempt, delay }) => {
|
|
35
51
|
console.log(`[LinearAgentClient] Retry attempt ${attempt + 1}/${this.retryConfig.maxRetries}, ` +
|
|
36
52
|
`waiting ${delay}ms`);
|
|
@@ -422,6 +438,56 @@ export class LinearAgentClient {
|
|
|
422
438
|
return parent != null;
|
|
423
439
|
});
|
|
424
440
|
}
|
|
441
|
+
/**
|
|
442
|
+
* Fetch all non-terminal issues in a project using a single GraphQL query.
|
|
443
|
+
*
|
|
444
|
+
* Replaces the N+1 pattern of fetching issues then lazy-loading state/labels/parent/project
|
|
445
|
+
* for each one. Returns pre-resolved data suitable for GovernorIssue construction.
|
|
446
|
+
*
|
|
447
|
+
* @param project - Linear project name
|
|
448
|
+
* @returns Array of issue data with childCount for parent detection
|
|
449
|
+
*/
|
|
450
|
+
async listProjectIssues(project) {
|
|
451
|
+
await this.rateLimiter.acquire();
|
|
452
|
+
const query = `
|
|
453
|
+
query ListProjectIssues($filter: IssueFilter!) {
|
|
454
|
+
issues(filter: $filter, first: 250) {
|
|
455
|
+
nodes {
|
|
456
|
+
id
|
|
457
|
+
identifier
|
|
458
|
+
title
|
|
459
|
+
description
|
|
460
|
+
createdAt
|
|
461
|
+
state { name }
|
|
462
|
+
labels { nodes { name } }
|
|
463
|
+
parent { id }
|
|
464
|
+
project { name }
|
|
465
|
+
children { nodes { id } }
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
`;
|
|
470
|
+
const terminalStatuses = ['Accepted', 'Canceled', 'Duplicate'];
|
|
471
|
+
const result = await this.client.client.rawRequest(query, {
|
|
472
|
+
filter: {
|
|
473
|
+
project: { name: { eq: project } },
|
|
474
|
+
state: { name: { nin: terminalStatuses } },
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
const data = result.data;
|
|
478
|
+
return data.issues.nodes.map((node) => ({
|
|
479
|
+
id: node.id,
|
|
480
|
+
identifier: node.identifier,
|
|
481
|
+
title: node.title,
|
|
482
|
+
description: node.description ?? undefined,
|
|
483
|
+
status: node.state?.name ?? 'Backlog',
|
|
484
|
+
labels: node.labels.nodes.map((l) => l.name),
|
|
485
|
+
createdAt: new Date(node.createdAt).getTime(),
|
|
486
|
+
parentId: node.parent?.id ?? undefined,
|
|
487
|
+
project: node.project?.name ?? undefined,
|
|
488
|
+
childCount: node.children.nodes.length,
|
|
489
|
+
}));
|
|
490
|
+
}
|
|
425
491
|
/**
|
|
426
492
|
* Check if an issue has child issues (is a parent issue)
|
|
427
493
|
*
|
package/dist/src/index.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export { truncateText, buildCompletionComment, splitContentIntoComments, buildCo
|
|
|
10
10
|
export type { CommentChunk } from './utils.js';
|
|
11
11
|
export { parseCheckboxes, updateCheckbox, updateCheckboxByText, updateCheckboxes, hasCheckboxes, getCheckboxSummary, } from './checkbox-utils.js';
|
|
12
12
|
export type { CheckboxItem, CheckboxUpdate } from './checkbox-utils.js';
|
|
13
|
+
export { TokenBucket, DEFAULT_RATE_LIMIT_CONFIG, extractRetryAfterMs } from './rate-limiter.js';
|
|
14
|
+
export type { TokenBucketConfig } from './rate-limiter.js';
|
|
13
15
|
export { LinearAgentClient, createLinearAgentClient } from './agent-client.js';
|
|
14
16
|
export { AgentSession, createAgentSession } from './agent-session.js';
|
|
15
17
|
export * from './webhook-types.js';
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/src/index.js
CHANGED
|
@@ -10,6 +10,8 @@ export { LINEAR_COMMENT_MAX_LENGTH, TRUNCATION_MARKER, MAX_COMPLETION_COMMENTS,
|
|
|
10
10
|
export { truncateText, buildCompletionComment, splitContentIntoComments, buildCompletionComments, } from './utils.js';
|
|
11
11
|
// Checkbox utilities
|
|
12
12
|
export { parseCheckboxes, updateCheckbox, updateCheckboxByText, updateCheckboxes, hasCheckboxes, getCheckboxSummary, } from './checkbox-utils.js';
|
|
13
|
+
// Rate limiter
|
|
14
|
+
export { TokenBucket, DEFAULT_RATE_LIMIT_CONFIG, extractRetryAfterMs } from './rate-limiter.js';
|
|
13
15
|
// Client
|
|
14
16
|
export { LinearAgentClient, createLinearAgentClient } from './agent-client.js';
|
|
15
17
|
// Session
|
|
@@ -94,14 +94,27 @@ export declare class LinearPlatformAdapter extends LinearFrontendAdapter {
|
|
|
94
94
|
/**
|
|
95
95
|
* Scan a Linear project for all non-terminal issues.
|
|
96
96
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
97
|
+
* Uses a single GraphQL query via `listProjectIssues()` to fetch all
|
|
98
|
+
* issue data in one API call, eliminating the N+1 problem of lazy-loading
|
|
99
|
+
* state/labels/parent/project for each issue.
|
|
100
100
|
*
|
|
101
101
|
* @param project - Linear project name to scan
|
|
102
102
|
* @returns Array of GovernorIssue for all active issues
|
|
103
103
|
*/
|
|
104
104
|
scanProjectIssues(project: string): Promise<GovernorIssue[]>;
|
|
105
|
+
/**
|
|
106
|
+
* Scan a project and return both GovernorIssues and a set of parent issue IDs.
|
|
107
|
+
*
|
|
108
|
+
* The parent issue IDs are derived from `childCount > 0` in the single
|
|
109
|
+
* GraphQL query, allowing callers to skip per-issue `isParentIssue()` API calls.
|
|
110
|
+
*
|
|
111
|
+
* @param project - Linear project name to scan
|
|
112
|
+
* @returns Issues and a set of parent issue IDs
|
|
113
|
+
*/
|
|
114
|
+
scanProjectIssuesWithParents(project: string): Promise<{
|
|
115
|
+
issues: GovernorIssue[];
|
|
116
|
+
parentIssueIds: Set<string>;
|
|
117
|
+
}>;
|
|
105
118
|
/**
|
|
106
119
|
* Convert a Linear SDK Issue object to a GovernorIssue.
|
|
107
120
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-adapter.d.ts","sourceRoot":"","sources":["../../src/platform-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAY1D;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,iCAAiC;AACjC,KAAK,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAA;AAEhD;;;GAGG;AACH,UAAU,uBAAuB;IAC/B,IAAI,EAAE,sBAAsB,CAAA;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,aAAa,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,WAAW,CAAA;CACpB;AAED;;;GAGG;AACH,UAAU,iBAAiB;IACzB,IAAI,EAAE,eAAe,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,aAAa,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,WAAW,CAAA;CACpB;AAED,gDAAgD;AAChD,KAAK,aAAa,GAAG,uBAAuB,GAAG,iBAAiB,CAAA;AAmJhE;;;;;;;;;GASG;AACH,qBAAa,qBAAsB,SAAQ,qBAAqB;IAC9D;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAmB;gBAEzC,MAAM,EAAE,iBAAiB;IAOrC;;;;;;;;;;;;OAYG;IACH,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,EAAE,GAAG,IAAI;IA0D/D;;;;;;;;;OASG;IACG,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"platform-adapter.d.ts","sourceRoot":"","sources":["../../src/platform-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAY1D;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,iCAAiC;AACjC,KAAK,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAA;AAEhD;;;GAGG;AACH,UAAU,uBAAuB;IAC/B,IAAI,EAAE,sBAAsB,CAAA;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,aAAa,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,WAAW,CAAA;CACpB;AAED;;;GAGG;AACH,UAAU,iBAAiB;IACzB,IAAI,EAAE,eAAe,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,aAAa,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,WAAW,CAAA;CACpB;AAED,gDAAgD;AAChD,KAAK,aAAa,GAAG,uBAAuB,GAAG,iBAAiB,CAAA;AAmJhE;;;;;;;;;GASG;AACH,qBAAa,qBAAsB,SAAQ,qBAAqB;IAC9D;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAmB;gBAEzC,MAAM,EAAE,iBAAiB;IAOrC;;;;;;;;;;;;OAYG;IACH,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,EAAE,GAAG,IAAI;IA0D/D;;;;;;;;;OASG;IACG,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAgBlE;;;;;;;;OAQG;IACG,4BAA4B,CAChC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;QAAE,MAAM,EAAE,aAAa,EAAE,CAAC;QAAC,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;KAAE,CAAC;IA0BpE;;;;;;;;;;OAUG;IACG,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC;CAS/D"}
|
|
@@ -187,26 +187,57 @@ export class LinearPlatformAdapter extends LinearFrontendAdapter {
|
|
|
187
187
|
/**
|
|
188
188
|
* Scan a Linear project for all non-terminal issues.
|
|
189
189
|
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
190
|
+
* Uses a single GraphQL query via `listProjectIssues()` to fetch all
|
|
191
|
+
* issue data in one API call, eliminating the N+1 problem of lazy-loading
|
|
192
|
+
* state/labels/parent/project for each issue.
|
|
193
193
|
*
|
|
194
194
|
* @param project - Linear project name to scan
|
|
195
195
|
* @returns Array of GovernorIssue for all active issues
|
|
196
196
|
*/
|
|
197
197
|
async scanProjectIssues(project) {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
198
|
+
const issues = await this.linearAgentClient.listProjectIssues(project);
|
|
199
|
+
return issues.map((issue) => ({
|
|
200
|
+
id: issue.id,
|
|
201
|
+
identifier: issue.identifier,
|
|
202
|
+
title: issue.title,
|
|
203
|
+
description: issue.description,
|
|
204
|
+
status: issue.status,
|
|
205
|
+
labels: issue.labels,
|
|
206
|
+
createdAt: issue.createdAt,
|
|
207
|
+
parentId: issue.parentId,
|
|
208
|
+
project: issue.project,
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Scan a project and return both GovernorIssues and a set of parent issue IDs.
|
|
213
|
+
*
|
|
214
|
+
* The parent issue IDs are derived from `childCount > 0` in the single
|
|
215
|
+
* GraphQL query, allowing callers to skip per-issue `isParentIssue()` API calls.
|
|
216
|
+
*
|
|
217
|
+
* @param project - Linear project name to scan
|
|
218
|
+
* @returns Issues and a set of parent issue IDs
|
|
219
|
+
*/
|
|
220
|
+
async scanProjectIssuesWithParents(project) {
|
|
221
|
+
const rawIssues = await this.linearAgentClient.listProjectIssues(project);
|
|
222
|
+
const parentIssueIds = new Set();
|
|
223
|
+
const issues = [];
|
|
224
|
+
for (const issue of rawIssues) {
|
|
225
|
+
if (issue.childCount > 0) {
|
|
226
|
+
parentIssueIds.add(issue.id);
|
|
227
|
+
}
|
|
228
|
+
issues.push({
|
|
229
|
+
id: issue.id,
|
|
230
|
+
identifier: issue.identifier,
|
|
231
|
+
title: issue.title,
|
|
232
|
+
description: issue.description,
|
|
233
|
+
status: issue.status,
|
|
234
|
+
labels: issue.labels,
|
|
235
|
+
createdAt: issue.createdAt,
|
|
236
|
+
parentId: issue.parentId,
|
|
237
|
+
project: issue.project,
|
|
238
|
+
});
|
|
208
239
|
}
|
|
209
|
-
return
|
|
240
|
+
return { issues, parentIssueIds };
|
|
210
241
|
}
|
|
211
242
|
/**
|
|
212
243
|
* Convert a Linear SDK Issue object to a GovernorIssue.
|
|
@@ -51,6 +51,7 @@ function createMockClient() {
|
|
|
51
51
|
createAgentSessionOnIssue: vi.fn(),
|
|
52
52
|
updateAgentSession: vi.fn(),
|
|
53
53
|
createAgentActivity: vi.fn(),
|
|
54
|
+
listProjectIssues: vi.fn(),
|
|
54
55
|
};
|
|
55
56
|
const linearClient = {
|
|
56
57
|
issues: vi.fn(),
|
|
@@ -290,13 +291,28 @@ describe('LinearPlatformAdapter', () => {
|
|
|
290
291
|
// scanProjectIssues
|
|
291
292
|
// ========================================================================
|
|
292
293
|
describe('scanProjectIssues', () => {
|
|
294
|
+
/** Helper: create a listProjectIssues result entry */
|
|
295
|
+
function mockListProjectIssue(overrides = {}) {
|
|
296
|
+
return {
|
|
297
|
+
id: overrides.id ?? 'issue-uuid-1',
|
|
298
|
+
identifier: overrides.identifier ?? 'SUP-100',
|
|
299
|
+
title: overrides.title ?? 'Test Issue',
|
|
300
|
+
description: overrides.description ?? 'A test issue description',
|
|
301
|
+
status: overrides.status ?? 'Backlog',
|
|
302
|
+
labels: overrides.labels ?? ['Feature'],
|
|
303
|
+
createdAt: overrides.createdAt ?? Date.now(),
|
|
304
|
+
parentId: overrides.parentId,
|
|
305
|
+
project: overrides.project ?? 'MyProject',
|
|
306
|
+
childCount: overrides.childCount ?? 0,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
293
309
|
it('returns GovernorIssues for all non-terminal issues', async () => {
|
|
294
310
|
const issues = [
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
311
|
+
mockListProjectIssue({ id: 'i-1', identifier: 'SUP-1', status: 'Backlog' }),
|
|
312
|
+
mockListProjectIssue({ id: 'i-2', identifier: 'SUP-2', status: 'Started' }),
|
|
313
|
+
mockListProjectIssue({ id: 'i-3', identifier: 'SUP-3', status: 'Finished' }),
|
|
298
314
|
];
|
|
299
|
-
mocks.
|
|
315
|
+
mocks.listProjectIssues.mockResolvedValue(issues);
|
|
300
316
|
const result = await adapter.scanProjectIssues('MyProject');
|
|
301
317
|
expect(result).toHaveLength(3);
|
|
302
318
|
expect(result[0].id).toBe('i-1');
|
|
@@ -307,40 +323,57 @@ describe('LinearPlatformAdapter', () => {
|
|
|
307
323
|
expect(result[2].id).toBe('i-3');
|
|
308
324
|
expect(result[2].status).toBe('Finished');
|
|
309
325
|
});
|
|
310
|
-
it('
|
|
311
|
-
mocks.
|
|
326
|
+
it('delegates to listProjectIssues with project name', async () => {
|
|
327
|
+
mocks.listProjectIssues.mockResolvedValue([]);
|
|
312
328
|
await adapter.scanProjectIssues('TestProject');
|
|
313
|
-
expect(mocks.
|
|
314
|
-
filter: {
|
|
315
|
-
project: { name: { eq: 'TestProject' } },
|
|
316
|
-
state: { name: { nin: ['Accepted', 'Canceled', 'Duplicate'] } },
|
|
317
|
-
},
|
|
318
|
-
});
|
|
329
|
+
expect(mocks.listProjectIssues).toHaveBeenCalledWith('TestProject');
|
|
319
330
|
});
|
|
320
331
|
it('returns empty array when no issues found', async () => {
|
|
321
|
-
mocks.
|
|
332
|
+
mocks.listProjectIssues.mockResolvedValue([]);
|
|
322
333
|
const result = await adapter.scanProjectIssues('EmptyProject');
|
|
323
334
|
expect(result).toEqual([]);
|
|
324
335
|
});
|
|
325
|
-
it('
|
|
326
|
-
const issue =
|
|
336
|
+
it('maps issue properties correctly', async () => {
|
|
337
|
+
const issue = mockListProjectIssue({
|
|
327
338
|
labels: ['Bug', 'Urgent'],
|
|
328
339
|
parentId: 'parent-1',
|
|
329
|
-
|
|
340
|
+
project: 'MyProject',
|
|
330
341
|
});
|
|
331
|
-
mocks.
|
|
342
|
+
mocks.listProjectIssues.mockResolvedValue([issue]);
|
|
332
343
|
const result = await adapter.scanProjectIssues('MyProject');
|
|
333
344
|
expect(result[0].labels).toEqual(['Bug', 'Urgent']);
|
|
334
345
|
expect(result[0].parentId).toBe('parent-1');
|
|
335
346
|
expect(result[0].project).toBe('MyProject');
|
|
336
347
|
});
|
|
337
|
-
it('
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
mocks.linearClientIssues.mockResolvedValue({ nodes: [issue] });
|
|
348
|
+
it('preserves createdAt as epoch milliseconds', async () => {
|
|
349
|
+
const createdAt = new Date('2025-03-01T08:00:00Z').getTime();
|
|
350
|
+
const issue = mockListProjectIssue({ createdAt });
|
|
351
|
+
mocks.listProjectIssues.mockResolvedValue([issue]);
|
|
342
352
|
const result = await adapter.scanProjectIssues('MyProject');
|
|
343
|
-
expect(result[0].createdAt).toBe(
|
|
353
|
+
expect(result[0].createdAt).toBe(createdAt);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
// ========================================================================
|
|
357
|
+
// scanProjectIssuesWithParents
|
|
358
|
+
// ========================================================================
|
|
359
|
+
describe('scanProjectIssuesWithParents', () => {
|
|
360
|
+
it('returns issues and parent IDs set', async () => {
|
|
361
|
+
mocks.listProjectIssues.mockResolvedValue([
|
|
362
|
+
{
|
|
363
|
+
id: 'p-1', identifier: 'SUP-10', title: 'Parent',
|
|
364
|
+
status: 'Backlog', labels: [], createdAt: Date.now(),
|
|
365
|
+
project: 'MyProject', childCount: 3,
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
id: 'c-1', identifier: 'SUP-11', title: 'Child',
|
|
369
|
+
status: 'Backlog', labels: [], createdAt: Date.now(),
|
|
370
|
+
parentId: 'p-1', project: 'MyProject', childCount: 0,
|
|
371
|
+
},
|
|
372
|
+
]);
|
|
373
|
+
const { issues, parentIssueIds } = await adapter.scanProjectIssuesWithParents('MyProject');
|
|
374
|
+
expect(issues).toHaveLength(2);
|
|
375
|
+
expect(parentIssueIds.has('p-1')).toBe(true);
|
|
376
|
+
expect(parentIssueIds.has('c-1')).toBe(false);
|
|
344
377
|
});
|
|
345
378
|
});
|
|
346
379
|
// ========================================================================
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Bucket Rate Limiter
|
|
3
|
+
*
|
|
4
|
+
* Proactive rate limiting for Linear API calls. Uses a token bucket algorithm
|
|
5
|
+
* to throttle requests below Linear's ~100 req/min limit.
|
|
6
|
+
*
|
|
7
|
+
* Default: 80 burst capacity, 1.5 tokens/sec refill (~90 req/min sustained).
|
|
8
|
+
*/
|
|
9
|
+
export interface TokenBucketConfig {
|
|
10
|
+
/** Maximum tokens (burst capacity). Default: 80 */
|
|
11
|
+
maxTokens: number;
|
|
12
|
+
/** Tokens added per second. Default: 1.5 (~90/min) */
|
|
13
|
+
refillRate: number;
|
|
14
|
+
}
|
|
15
|
+
export declare const DEFAULT_RATE_LIMIT_CONFIG: TokenBucketConfig;
|
|
16
|
+
export declare class TokenBucket {
|
|
17
|
+
private tokens;
|
|
18
|
+
private readonly maxTokens;
|
|
19
|
+
private readonly refillRate;
|
|
20
|
+
private lastRefill;
|
|
21
|
+
private waitQueue;
|
|
22
|
+
constructor(config?: Partial<TokenBucketConfig>);
|
|
23
|
+
/** Refill tokens based on elapsed time since last refill. */
|
|
24
|
+
private refill;
|
|
25
|
+
/** Drain waiters that can be satisfied after a refill. */
|
|
26
|
+
private drainWaiters;
|
|
27
|
+
/**
|
|
28
|
+
* Acquire a single token. Resolves immediately if tokens are available,
|
|
29
|
+
* otherwise queues the caller until a token becomes available via refill.
|
|
30
|
+
*/
|
|
31
|
+
acquire(): Promise<void>;
|
|
32
|
+
/** Schedule a timer to refill and drain waiters. */
|
|
33
|
+
private refillTimer;
|
|
34
|
+
private scheduleRefillDrain;
|
|
35
|
+
/**
|
|
36
|
+
* Penalize the bucket after receiving a 429 rate limit response.
|
|
37
|
+
*
|
|
38
|
+
* Drains all tokens to 0 and shifts the refill baseline forward by
|
|
39
|
+
* `seconds` so no new tokens appear until the penalty expires.
|
|
40
|
+
* Any already-queued waiters will wait for the penalty period plus
|
|
41
|
+
* normal refill time.
|
|
42
|
+
*
|
|
43
|
+
* @param seconds - How long to pause before tokens start refilling (from Retry-After header)
|
|
44
|
+
*/
|
|
45
|
+
penalize(seconds: number): void;
|
|
46
|
+
/** Current number of available tokens (for testing/monitoring). */
|
|
47
|
+
get availableTokens(): number;
|
|
48
|
+
/** Number of callers waiting for tokens (for testing/monitoring). */
|
|
49
|
+
get pendingCount(): number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Extract a Retry-After delay (in milliseconds) from an error thrown by
|
|
53
|
+
* the Linear SDK or a raw HTTP 429 response.
|
|
54
|
+
*
|
|
55
|
+
* Checks (in order):
|
|
56
|
+
* 1. `error.response.headers.get('retry-after')` (fetch Response)
|
|
57
|
+
* 2. `error.response.headers['retry-after']` (plain object headers)
|
|
58
|
+
* 3. `error.headers?.['retry-after']` (error-level headers)
|
|
59
|
+
*
|
|
60
|
+
* The Retry-After value is parsed as seconds (integer). If no valid value
|
|
61
|
+
* is found, returns `null`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function extractRetryAfterMs(error: unknown): number | null;
|
|
64
|
+
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAA;IACjB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,eAAO,MAAM,yBAAyB,EAAE,iBAGvC,CAAA;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,SAAS,CAAwB;gBAE7B,MAAM,GAAE,OAAO,CAAC,iBAAiB,CAAM;IAQnD,6DAA6D;IAC7D,OAAO,CAAC,MAAM;IAad,0DAA0D;IAC1D,OAAO,CAAC,YAAY;IAQpB;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAc9B,oDAAoD;IACpD,OAAO,CAAC,WAAW,CAA6C;IAEhE,OAAO,CAAC,mBAAmB;IAiB3B;;;;;;;;;OASG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAO/B,mEAAmE;IACnE,IAAI,eAAe,IAAI,MAAM,CAG5B;IAED,qEAAqE;IACrE,IAAI,YAAY,IAAI,MAAM,CAEzB;CACF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAwBjE"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Bucket Rate Limiter
|
|
3
|
+
*
|
|
4
|
+
* Proactive rate limiting for Linear API calls. Uses a token bucket algorithm
|
|
5
|
+
* to throttle requests below Linear's ~100 req/min limit.
|
|
6
|
+
*
|
|
7
|
+
* Default: 80 burst capacity, 1.5 tokens/sec refill (~90 req/min sustained).
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_RATE_LIMIT_CONFIG = {
|
|
10
|
+
maxTokens: 80,
|
|
11
|
+
refillRate: 1.5,
|
|
12
|
+
};
|
|
13
|
+
export class TokenBucket {
|
|
14
|
+
tokens;
|
|
15
|
+
maxTokens;
|
|
16
|
+
refillRate;
|
|
17
|
+
lastRefill;
|
|
18
|
+
waitQueue = [];
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
const resolved = { ...DEFAULT_RATE_LIMIT_CONFIG, ...config };
|
|
21
|
+
this.maxTokens = resolved.maxTokens;
|
|
22
|
+
this.refillRate = resolved.refillRate;
|
|
23
|
+
this.tokens = this.maxTokens;
|
|
24
|
+
this.lastRefill = Date.now();
|
|
25
|
+
}
|
|
26
|
+
/** Refill tokens based on elapsed time since last refill. */
|
|
27
|
+
refill() {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
const elapsed = (now - this.lastRefill) / 1000;
|
|
30
|
+
// During a penalty period, lastRefill is in the future so elapsed is negative.
|
|
31
|
+
// Skip refill entirely until the penalty expires.
|
|
32
|
+
if (elapsed <= 0)
|
|
33
|
+
return;
|
|
34
|
+
const newTokens = elapsed * this.refillRate;
|
|
35
|
+
this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);
|
|
36
|
+
this.lastRefill = now;
|
|
37
|
+
}
|
|
38
|
+
/** Drain waiters that can be satisfied after a refill. */
|
|
39
|
+
drainWaiters() {
|
|
40
|
+
while (this.waitQueue.length > 0 && this.tokens >= 1) {
|
|
41
|
+
this.tokens -= 1;
|
|
42
|
+
const resolve = this.waitQueue.shift();
|
|
43
|
+
resolve();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Acquire a single token. Resolves immediately if tokens are available,
|
|
48
|
+
* otherwise queues the caller until a token becomes available via refill.
|
|
49
|
+
*/
|
|
50
|
+
async acquire() {
|
|
51
|
+
this.refill();
|
|
52
|
+
if (this.tokens >= 1 && this.waitQueue.length === 0) {
|
|
53
|
+
this.tokens -= 1;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
this.waitQueue.push(resolve);
|
|
58
|
+
this.scheduleRefillDrain();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** Schedule a timer to refill and drain waiters. */
|
|
62
|
+
refillTimer = null;
|
|
63
|
+
scheduleRefillDrain() {
|
|
64
|
+
if (this.refillTimer !== null)
|
|
65
|
+
return;
|
|
66
|
+
// Time until 1 token is available
|
|
67
|
+
const msPerToken = 1000 / this.refillRate;
|
|
68
|
+
this.refillTimer = setTimeout(() => {
|
|
69
|
+
this.refillTimer = null;
|
|
70
|
+
this.refill();
|
|
71
|
+
this.drainWaiters();
|
|
72
|
+
// If there are still waiters, schedule again
|
|
73
|
+
if (this.waitQueue.length > 0) {
|
|
74
|
+
this.scheduleRefillDrain();
|
|
75
|
+
}
|
|
76
|
+
}, msPerToken);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Penalize the bucket after receiving a 429 rate limit response.
|
|
80
|
+
*
|
|
81
|
+
* Drains all tokens to 0 and shifts the refill baseline forward by
|
|
82
|
+
* `seconds` so no new tokens appear until the penalty expires.
|
|
83
|
+
* Any already-queued waiters will wait for the penalty period plus
|
|
84
|
+
* normal refill time.
|
|
85
|
+
*
|
|
86
|
+
* @param seconds - How long to pause before tokens start refilling (from Retry-After header)
|
|
87
|
+
*/
|
|
88
|
+
penalize(seconds) {
|
|
89
|
+
this.tokens = 0;
|
|
90
|
+
// Push lastRefill into the future so refill() computes negative elapsed
|
|
91
|
+
// time until the penalty expires, effectively freezing token generation.
|
|
92
|
+
this.lastRefill = Date.now() + seconds * 1000;
|
|
93
|
+
}
|
|
94
|
+
/** Current number of available tokens (for testing/monitoring). */
|
|
95
|
+
get availableTokens() {
|
|
96
|
+
this.refill();
|
|
97
|
+
return Math.floor(this.tokens);
|
|
98
|
+
}
|
|
99
|
+
/** Number of callers waiting for tokens (for testing/monitoring). */
|
|
100
|
+
get pendingCount() {
|
|
101
|
+
return this.waitQueue.length;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Extract a Retry-After delay (in milliseconds) from an error thrown by
|
|
106
|
+
* the Linear SDK or a raw HTTP 429 response.
|
|
107
|
+
*
|
|
108
|
+
* Checks (in order):
|
|
109
|
+
* 1. `error.response.headers.get('retry-after')` (fetch Response)
|
|
110
|
+
* 2. `error.response.headers['retry-after']` (plain object headers)
|
|
111
|
+
* 3. `error.headers?.['retry-after']` (error-level headers)
|
|
112
|
+
*
|
|
113
|
+
* The Retry-After value is parsed as seconds (integer). If no valid value
|
|
114
|
+
* is found, returns `null`.
|
|
115
|
+
*/
|
|
116
|
+
export function extractRetryAfterMs(error) {
|
|
117
|
+
if (typeof error !== 'object' || error === null)
|
|
118
|
+
return null;
|
|
119
|
+
const err = error;
|
|
120
|
+
// Check if this is a rate limit error (status 429)
|
|
121
|
+
const status = err.status ??
|
|
122
|
+
err.statusCode ??
|
|
123
|
+
err.response?.status;
|
|
124
|
+
if (status !== 429)
|
|
125
|
+
return null;
|
|
126
|
+
// Try to extract Retry-After from various locations
|
|
127
|
+
const headerValue = getRetryAfterHeader(err);
|
|
128
|
+
if (headerValue === null) {
|
|
129
|
+
// No Retry-After header — use a sensible default of 60s for Linear
|
|
130
|
+
return 60_000;
|
|
131
|
+
}
|
|
132
|
+
const seconds = parseInt(headerValue, 10);
|
|
133
|
+
if (Number.isNaN(seconds) || seconds <= 0)
|
|
134
|
+
return 60_000;
|
|
135
|
+
return seconds * 1000;
|
|
136
|
+
}
|
|
137
|
+
function getRetryAfterHeader(err) {
|
|
138
|
+
// error.response.headers.get('retry-after') — fetch-style Response
|
|
139
|
+
const response = err.response;
|
|
140
|
+
if (response) {
|
|
141
|
+
const headers = response.headers;
|
|
142
|
+
if (headers) {
|
|
143
|
+
// Headers object with .get() method (fetch API)
|
|
144
|
+
if (typeof headers.get === 'function') {
|
|
145
|
+
const val = headers.get('retry-after');
|
|
146
|
+
if (val)
|
|
147
|
+
return val;
|
|
148
|
+
}
|
|
149
|
+
// Plain object headers
|
|
150
|
+
const val = headers['retry-after'];
|
|
151
|
+
if (val)
|
|
152
|
+
return val;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// error.headers['retry-after']
|
|
156
|
+
const errorHeaders = err.headers;
|
|
157
|
+
if (errorHeaders) {
|
|
158
|
+
const val = errorHeaders['retry-after'];
|
|
159
|
+
if (val)
|
|
160
|
+
return val;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.test.d.ts","sourceRoot":"","sources":["../../src/rate-limiter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { TokenBucket, DEFAULT_RATE_LIMIT_CONFIG, extractRetryAfterMs } from './rate-limiter.js';
|
|
3
|
+
describe('TokenBucket', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
vi.useFakeTimers();
|
|
6
|
+
});
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.useRealTimers();
|
|
9
|
+
});
|
|
10
|
+
// ========================================================================
|
|
11
|
+
// Construction & defaults
|
|
12
|
+
// ========================================================================
|
|
13
|
+
it('uses default config when none provided', () => {
|
|
14
|
+
const bucket = new TokenBucket();
|
|
15
|
+
expect(bucket.availableTokens).toBe(DEFAULT_RATE_LIMIT_CONFIG.maxTokens);
|
|
16
|
+
});
|
|
17
|
+
it('accepts custom config', () => {
|
|
18
|
+
const bucket = new TokenBucket({ maxTokens: 10, refillRate: 5 });
|
|
19
|
+
expect(bucket.availableTokens).toBe(10);
|
|
20
|
+
});
|
|
21
|
+
it('allows partial config overrides', () => {
|
|
22
|
+
const bucket = new TokenBucket({ maxTokens: 20 });
|
|
23
|
+
expect(bucket.availableTokens).toBe(20);
|
|
24
|
+
});
|
|
25
|
+
// ========================================================================
|
|
26
|
+
// Token acquisition
|
|
27
|
+
// ========================================================================
|
|
28
|
+
it('resolves immediately when tokens are available', async () => {
|
|
29
|
+
const bucket = new TokenBucket({ maxTokens: 5, refillRate: 1 });
|
|
30
|
+
await bucket.acquire();
|
|
31
|
+
expect(bucket.availableTokens).toBe(4);
|
|
32
|
+
});
|
|
33
|
+
it('depletes tokens with multiple acquires', async () => {
|
|
34
|
+
const bucket = new TokenBucket({ maxTokens: 3, refillRate: 1 });
|
|
35
|
+
await bucket.acquire();
|
|
36
|
+
await bucket.acquire();
|
|
37
|
+
await bucket.acquire();
|
|
38
|
+
expect(bucket.availableTokens).toBe(0);
|
|
39
|
+
});
|
|
40
|
+
// ========================================================================
|
|
41
|
+
// Waiting when depleted
|
|
42
|
+
// ========================================================================
|
|
43
|
+
it('queues callers when tokens are exhausted', async () => {
|
|
44
|
+
const bucket = new TokenBucket({ maxTokens: 1, refillRate: 1 });
|
|
45
|
+
// Use up the only token
|
|
46
|
+
await bucket.acquire();
|
|
47
|
+
expect(bucket.availableTokens).toBe(0);
|
|
48
|
+
// This should not resolve immediately
|
|
49
|
+
let resolved = false;
|
|
50
|
+
const promise = bucket.acquire().then(() => {
|
|
51
|
+
resolved = true;
|
|
52
|
+
});
|
|
53
|
+
// Give microtasks a chance to run
|
|
54
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
55
|
+
expect(resolved).toBe(false);
|
|
56
|
+
expect(bucket.pendingCount).toBe(1);
|
|
57
|
+
// Advance time so a token refills (1 token/sec => 1000ms for 1 token)
|
|
58
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
59
|
+
await promise;
|
|
60
|
+
expect(resolved).toBe(true);
|
|
61
|
+
expect(bucket.pendingCount).toBe(0);
|
|
62
|
+
});
|
|
63
|
+
it('drains multiple waiters as tokens refill', async () => {
|
|
64
|
+
const bucket = new TokenBucket({ maxTokens: 1, refillRate: 2 }); // 2 tokens/sec
|
|
65
|
+
await bucket.acquire();
|
|
66
|
+
const results = [];
|
|
67
|
+
const p1 = bucket.acquire().then(() => results.push(1));
|
|
68
|
+
const p2 = bucket.acquire().then(() => results.push(2));
|
|
69
|
+
expect(bucket.pendingCount).toBe(2);
|
|
70
|
+
// At 2 tokens/sec, each token takes 500ms
|
|
71
|
+
// First timer fires at 500ms, drains waiter 1, schedules next
|
|
72
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
73
|
+
await Promise.resolve(); // let microtasks run
|
|
74
|
+
expect(results).toEqual([1]);
|
|
75
|
+
// Second timer fires at 1000ms total
|
|
76
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
77
|
+
await Promise.resolve();
|
|
78
|
+
expect(results).toEqual([1, 2]);
|
|
79
|
+
await Promise.all([p1, p2]);
|
|
80
|
+
expect(bucket.pendingCount).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
// ========================================================================
|
|
83
|
+
// Refill behavior
|
|
84
|
+
// ========================================================================
|
|
85
|
+
it('refills tokens over time', async () => {
|
|
86
|
+
const bucket = new TokenBucket({ maxTokens: 10, refillRate: 5 });
|
|
87
|
+
// Drain 5 tokens
|
|
88
|
+
for (let i = 0; i < 5; i++) {
|
|
89
|
+
await bucket.acquire();
|
|
90
|
+
}
|
|
91
|
+
expect(bucket.availableTokens).toBe(5);
|
|
92
|
+
// Advance 1 second => 5 new tokens refilled
|
|
93
|
+
vi.advanceTimersByTime(1000);
|
|
94
|
+
expect(bucket.availableTokens).toBe(10);
|
|
95
|
+
});
|
|
96
|
+
it('does not exceed maxTokens on refill', async () => {
|
|
97
|
+
const bucket = new TokenBucket({ maxTokens: 10, refillRate: 100 });
|
|
98
|
+
// Even after a long time, tokens should not exceed max
|
|
99
|
+
vi.advanceTimersByTime(10_000);
|
|
100
|
+
expect(bucket.availableTokens).toBe(10);
|
|
101
|
+
});
|
|
102
|
+
// ========================================================================
|
|
103
|
+
// penalize
|
|
104
|
+
// ========================================================================
|
|
105
|
+
it('penalize drains tokens to 0', () => {
|
|
106
|
+
const bucket = new TokenBucket({ maxTokens: 10, refillRate: 5 });
|
|
107
|
+
expect(bucket.availableTokens).toBe(10);
|
|
108
|
+
bucket.penalize(5);
|
|
109
|
+
expect(bucket.availableTokens).toBe(0);
|
|
110
|
+
});
|
|
111
|
+
it('penalize freezes token generation for the penalty period', () => {
|
|
112
|
+
const bucket = new TokenBucket({ maxTokens: 10, refillRate: 10 });
|
|
113
|
+
bucket.penalize(3); // 3 second penalty
|
|
114
|
+
// After 2 seconds (still within penalty), no tokens should be available
|
|
115
|
+
vi.advanceTimersByTime(2000);
|
|
116
|
+
expect(bucket.availableTokens).toBe(0);
|
|
117
|
+
// After 3 seconds total (penalty expired), refill should resume
|
|
118
|
+
vi.advanceTimersByTime(1000);
|
|
119
|
+
// Now tokens start refilling from 0 at 10/sec, but elapsed since penalty end is ~0
|
|
120
|
+
expect(bucket.availableTokens).toBe(0);
|
|
121
|
+
// After 4 seconds total (1 second of refill after penalty), 10 tokens
|
|
122
|
+
vi.advanceTimersByTime(1000);
|
|
123
|
+
expect(bucket.availableTokens).toBe(10);
|
|
124
|
+
});
|
|
125
|
+
// ========================================================================
|
|
126
|
+
// DEFAULT_RATE_LIMIT_CONFIG
|
|
127
|
+
// ========================================================================
|
|
128
|
+
it('exports sensible defaults', () => {
|
|
129
|
+
expect(DEFAULT_RATE_LIMIT_CONFIG.maxTokens).toBe(80);
|
|
130
|
+
expect(DEFAULT_RATE_LIMIT_CONFIG.refillRate).toBe(1.5);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
// ===========================================================================
|
|
134
|
+
// extractRetryAfterMs
|
|
135
|
+
// ===========================================================================
|
|
136
|
+
describe('extractRetryAfterMs', () => {
|
|
137
|
+
it('returns null for non-object errors', () => {
|
|
138
|
+
expect(extractRetryAfterMs(null)).toBeNull();
|
|
139
|
+
expect(extractRetryAfterMs(undefined)).toBeNull();
|
|
140
|
+
expect(extractRetryAfterMs('string')).toBeNull();
|
|
141
|
+
expect(extractRetryAfterMs(42)).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
it('returns null for non-429 errors', () => {
|
|
144
|
+
expect(extractRetryAfterMs({ status: 500 })).toBeNull();
|
|
145
|
+
expect(extractRetryAfterMs({ statusCode: 400 })).toBeNull();
|
|
146
|
+
expect(extractRetryAfterMs({ response: { status: 200 } })).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
it('returns 60s default when 429 but no Retry-After header', () => {
|
|
149
|
+
expect(extractRetryAfterMs({ status: 429 })).toBe(60_000);
|
|
150
|
+
});
|
|
151
|
+
it('parses Retry-After from response.headers plain object', () => {
|
|
152
|
+
const error = {
|
|
153
|
+
status: 429,
|
|
154
|
+
response: {
|
|
155
|
+
status: 429,
|
|
156
|
+
headers: { 'retry-after': '30' },
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
expect(extractRetryAfterMs(error)).toBe(30_000);
|
|
160
|
+
});
|
|
161
|
+
it('parses Retry-After from response.headers.get() (fetch-style)', () => {
|
|
162
|
+
const headers = new Map([['retry-after', '45']]);
|
|
163
|
+
const error = {
|
|
164
|
+
status: 429,
|
|
165
|
+
response: {
|
|
166
|
+
status: 429,
|
|
167
|
+
headers: {
|
|
168
|
+
get: (name) => headers.get(name) ?? null,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
expect(extractRetryAfterMs(error)).toBe(45_000);
|
|
173
|
+
});
|
|
174
|
+
it('parses Retry-After from error.headers', () => {
|
|
175
|
+
const error = {
|
|
176
|
+
status: 429,
|
|
177
|
+
headers: { 'retry-after': '10' },
|
|
178
|
+
};
|
|
179
|
+
expect(extractRetryAfterMs(error)).toBe(10_000);
|
|
180
|
+
});
|
|
181
|
+
it('detects 429 from response.status when top-level status is missing', () => {
|
|
182
|
+
const error = {
|
|
183
|
+
response: {
|
|
184
|
+
status: 429,
|
|
185
|
+
headers: { 'retry-after': '20' },
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
expect(extractRetryAfterMs(error)).toBe(20_000);
|
|
189
|
+
});
|
|
190
|
+
it('detects 429 from statusCode property', () => {
|
|
191
|
+
const error = {
|
|
192
|
+
statusCode: 429,
|
|
193
|
+
headers: { 'retry-after': '15' },
|
|
194
|
+
};
|
|
195
|
+
expect(extractRetryAfterMs(error)).toBe(15_000);
|
|
196
|
+
});
|
|
197
|
+
it('falls back to 60s for invalid Retry-After value', () => {
|
|
198
|
+
const error = {
|
|
199
|
+
status: 429,
|
|
200
|
+
response: {
|
|
201
|
+
status: 429,
|
|
202
|
+
headers: { 'retry-after': 'not-a-number' },
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
expect(extractRetryAfterMs(error)).toBe(60_000);
|
|
206
|
+
});
|
|
207
|
+
it('falls back to 60s for zero Retry-After', () => {
|
|
208
|
+
const error = {
|
|
209
|
+
status: 429,
|
|
210
|
+
response: {
|
|
211
|
+
status: 429,
|
|
212
|
+
headers: { 'retry-after': '0' },
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
expect(extractRetryAfterMs(error)).toBe(60_000);
|
|
216
|
+
});
|
|
217
|
+
});
|
package/dist/src/retry.d.ts
CHANGED
|
@@ -31,9 +31,25 @@ export interface WithRetryOptions {
|
|
|
31
31
|
config?: RetryConfig;
|
|
32
32
|
onRetry?: RetryCallback;
|
|
33
33
|
shouldRetry?: (error: unknown) => boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Optional callback to extract a rate-limit delay (in ms) from an error.
|
|
36
|
+
* When provided and returns a positive number, that delay is used instead
|
|
37
|
+
* of the standard exponential backoff for that retry attempt.
|
|
38
|
+
*/
|
|
39
|
+
getRetryAfterMs?: (error: unknown) => number | null;
|
|
40
|
+
/**
|
|
41
|
+
* Optional callback invoked when a rate limit is detected (getRetryAfterMs
|
|
42
|
+
* returned a value). Use this to penalize a shared token bucket so other
|
|
43
|
+
* concurrent callers also back off.
|
|
44
|
+
*/
|
|
45
|
+
onRateLimited?: (retryAfterMs: number) => void;
|
|
34
46
|
}
|
|
35
47
|
/**
|
|
36
|
-
* Execute an async function with exponential backoff retry logic
|
|
48
|
+
* Execute an async function with exponential backoff retry logic.
|
|
49
|
+
*
|
|
50
|
+
* When `getRetryAfterMs` is provided and returns a positive delay for an
|
|
51
|
+
* error, that delay is used instead of exponential backoff. This allows
|
|
52
|
+
* honoring HTTP 429 Retry-After headers from upstream APIs.
|
|
37
53
|
*/
|
|
38
54
|
export declare function withRetry<T>(fn: () => Promise<T>, options?: WithRetryOptions): Promise<T>;
|
|
39
55
|
/**
|
package/dist/src/retry.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAG7C;;GAEG;AACH,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,WAAW,CAMtD,CAAA;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,GAC5B,MAAM,CAIR;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,KAAK,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAA;AAE3D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAG7C;;GAEG;AACH,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,WAAW,CAMtD,CAAA;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,GAC5B,MAAM,CAIR;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,KAAK,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAA;AAE3D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAA;IACzC;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,CAAA;IACnD;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAA;CAC/C;AAED;;;;;;GAMG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,CAAC,CAAC,CA4CZ;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,cAAc,GAAE,gBAAqB,IACrD,CAAC,EAChB,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,UAAU,gBAAgB,KACzB,OAAO,CAAC,CAAC,CAAC,CAUd"}
|
package/dist/src/retry.js
CHANGED
|
@@ -23,7 +23,11 @@ export function calculateDelay(attempt, config) {
|
|
|
23
23
|
return Math.min(delay, config.maxDelayMs);
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* Execute an async function with exponential backoff retry logic
|
|
26
|
+
* Execute an async function with exponential backoff retry logic.
|
|
27
|
+
*
|
|
28
|
+
* When `getRetryAfterMs` is provided and returns a positive delay for an
|
|
29
|
+
* error, that delay is used instead of exponential backoff. This allows
|
|
30
|
+
* honoring HTTP 429 Retry-After headers from upstream APIs.
|
|
27
31
|
*/
|
|
28
32
|
export async function withRetry(fn, options = {}) {
|
|
29
33
|
const config = {
|
|
@@ -42,7 +46,12 @@ export async function withRetry(fn, options = {}) {
|
|
|
42
46
|
if (attempt === config.maxRetries || !shouldRetry(error)) {
|
|
43
47
|
throw lastError;
|
|
44
48
|
}
|
|
45
|
-
|
|
49
|
+
// Check for rate-limit-specific delay (Retry-After)
|
|
50
|
+
const retryAfterMs = options.getRetryAfterMs?.(error) ?? null;
|
|
51
|
+
const delay = retryAfterMs ?? calculateDelay(attempt, config);
|
|
52
|
+
if (retryAfterMs !== null && options.onRateLimited) {
|
|
53
|
+
options.onRateLimited(retryAfterMs);
|
|
54
|
+
}
|
|
46
55
|
if (options.onRetry) {
|
|
47
56
|
options.onRetry({
|
|
48
57
|
attempt,
|
package/dist/src/types.d.ts
CHANGED
|
@@ -152,6 +152,8 @@ export interface LinearAgentClientConfig {
|
|
|
152
152
|
apiKey: string;
|
|
153
153
|
baseUrl?: string;
|
|
154
154
|
retry?: RetryConfig;
|
|
155
|
+
/** Token bucket rate limiter configuration. Applied to all API calls. */
|
|
156
|
+
rateLimit?: Partial<import('./rate-limiter.js').TokenBucketConfig>;
|
|
155
157
|
}
|
|
156
158
|
/**
|
|
157
159
|
* Retry configuration with exponential backoff
|
package/dist/src/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supaku/agentfactory-linear",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Linear issue tracker integration for AgentFactory — status transitions, agent sessions, work routing",
|
|
6
6
|
"author": "Supaku (https://supaku.com)",
|