crewly 1.11.5 → 1.12.0
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/config/skills/agent/onboarding/synthesize-hierarchy/SKILL.md +65 -0
- package/config/skills/agent/onboarding/synthesize-hierarchy/execute.sh +61 -0
- package/config/skills/agent/web-search/SKILL.md +70 -0
- package/config/skills/agent/web-search/execute.sh +170 -0
- package/config/skills/agent/web-search/skill.json +23 -0
- package/dist/backend/backend/src/constants.d.ts +34 -1
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +34 -1
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +22 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +58 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +3 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts +27 -0
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js +108 -0
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts +6 -2
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js +9 -3
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +36 -2
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +18 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +24 -2
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts +90 -0
- package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-archive.service.js +309 -0
- package/dist/backend/backend/src/services/backup/backup-archive.service.js.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-cloud.client.d.ts +75 -0
- package/dist/backend/backend/src/services/backup/backup-cloud.client.d.ts.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-cloud.client.js +134 -0
- package/dist/backend/backend/src/services/backup/backup-cloud.client.js.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-restore.service.d.ts +78 -0
- package/dist/backend/backend/src/services/backup/backup-restore.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-restore.service.js +358 -0
- package/dist/backend/backend/src/services/backup/backup-restore.service.js.map +1 -0
- package/dist/backend/backend/src/services/backup/backup.types.d.ts +163 -0
- package/dist/backend/backend/src/services/backup/backup.types.d.ts.map +1 -0
- package/dist/backend/backend/src/services/backup/backup.types.js +13 -0
- package/dist/backend/backend/src/services/backup/backup.types.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts +29 -2
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.js +97 -13
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts +102 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +164 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts +21 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/fission/fission-guard.service.js +30 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.js.map +1 -1
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts +4 -0
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js +8 -0
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts +79 -58
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js +140 -65
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts +117 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts.map +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js +189 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js.map +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js +2 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js +17 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +50 -0
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +71 -0
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +18 -0
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js +75 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts +115 -0
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +189 -3
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.d.ts +28 -0
- package/dist/backend/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.js +61 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.js.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.js +67 -2
- package/dist/backend/backend/src/services/template/template.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts +19 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.js +39 -2
- package/dist/backend/backend/src/services/v3/cascade-request-status.js.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts +41 -0
- package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation-router.service.js +169 -0
- package/dist/backend/backend/src/services/v3/escalation-router.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts +4 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js +21 -0
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js.map +1 -1
- package/dist/backend/backend/src/types/intent-task.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/intent-task.types.js +8 -0
- package/dist/backend/backend/src/types/intent-task.types.js.map +1 -1
- package/dist/backend/backend/src/types/v2/request.types.d.ts +1 -1
- package/dist/backend/backend/src/types/v2/request.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/request.types.js +1 -0
- package/dist/backend/backend/src/types/v2/request.types.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +34 -1
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +34 -1
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.d.ts +70 -0
- package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.d.ts.map +1 -0
- package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.js +427 -0
- package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.js.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts +90 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.js +309 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.js.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-cloud.client.d.ts +75 -0
- package/dist/cli/backend/src/services/backup/backup-cloud.client.d.ts.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-cloud.client.js +134 -0
- package/dist/cli/backend/src/services/backup/backup-cloud.client.js.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-restore.service.d.ts +78 -0
- package/dist/cli/backend/src/services/backup/backup-restore.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-restore.service.js +358 -0
- package/dist/cli/backend/src/services/backup/backup-restore.service.js.map +1 -0
- package/dist/cli/backend/src/services/backup/backup.types.d.ts +163 -0
- package/dist/cli/backend/src/services/backup/backup.types.d.ts.map +1 -0
- package/dist/cli/backend/src/services/backup/backup.types.js +13 -0
- package/dist/cli/backend/src/services/backup/backup.types.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts +410 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.js +863 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts +292 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.js +1093 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts +328 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.js +171 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/device-identity.service.d.ts +89 -0
- package/dist/cli/backend/src/services/cloud/device-identity.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/device-identity.service.js +148 -0
- package/dist/cli/backend/src/services/cloud/device-identity.service.js.map +1 -0
- package/dist/cli/backend/src/services/user/user-identity.service.d.ts +86 -0
- package/dist/cli/backend/src/services/user/user-identity.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/user/user-identity.service.js +190 -0
- package/dist/cli/backend/src/services/user/user-identity.service.js.map +1 -0
- package/dist/cli/cli/src/commands/backup.d.ts +31 -0
- package/dist/cli/cli/src/commands/backup.d.ts.map +1 -0
- package/dist/cli/cli/src/commands/backup.js +280 -0
- package/dist/cli/cli/src/commands/backup.js.map +1 -0
- package/dist/cli/cli/src/index.js +10 -0
- package/dist/cli/cli/src/index.js.map +1 -1
- package/package.json +9 -3
- package/packages/crewly-agent/README.md +27 -0
- package/packages/crewly-agent/bin/crewly-agent +33 -0
- package/packages/crewly-agent/package.json +39 -0
- package/packages/crewly-agent/src/cli.ts +168 -0
- package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +2355 -0
- package/packages/crewly-agent/src/runtime/agent-runner.service.ts +1827 -0
- package/packages/crewly-agent/src/runtime/agent-stream.service.test.ts +153 -0
- package/packages/crewly-agent/src/runtime/agent-stream.service.ts +225 -0
- package/packages/crewly-agent/src/runtime/agent-worker.test.ts +171 -0
- package/packages/crewly-agent/src/runtime/agent-worker.ts +193 -0
- package/packages/crewly-agent/src/runtime/api-client.ts +143 -0
- package/packages/crewly-agent/src/runtime/approval-queue.service.ts +307 -0
- package/packages/crewly-agent/src/runtime/audit-log.service.test.ts +208 -0
- package/packages/crewly-agent/src/runtime/audit-log.service.ts +332 -0
- package/packages/crewly-agent/src/runtime/audit-trail.service.test.ts +178 -0
- package/packages/crewly-agent/src/runtime/audit-trail.service.ts +151 -0
- package/packages/crewly-agent/src/runtime/auditor-tools.test.ts +274 -0
- package/packages/crewly-agent/src/runtime/auditor-tools.ts +311 -0
- package/packages/crewly-agent/src/runtime/cloud-config.ts +67 -0
- package/packages/crewly-agent/src/runtime/deepseek-sse-transform.test.ts +165 -0
- package/packages/crewly-agent/src/runtime/deepseek-sse-transform.ts +168 -0
- package/packages/crewly-agent/src/runtime/env-isolation.service.ts +246 -0
- package/packages/crewly-agent/src/runtime/in-process-log-buffer.test.ts +280 -0
- package/packages/crewly-agent/src/runtime/in-process-log-buffer.ts +317 -0
- package/packages/crewly-agent/src/runtime/index.ts +38 -0
- package/packages/crewly-agent/src/runtime/mcp-tool-bridge.test.ts +352 -0
- package/packages/crewly-agent/src/runtime/mcp-tool-bridge.ts +244 -0
- package/packages/crewly-agent/src/runtime/model-manager.test.ts +326 -0
- package/packages/crewly-agent/src/runtime/model-manager.ts +363 -0
- package/packages/crewly-agent/src/runtime/output-filter.service.ts +175 -0
- package/packages/crewly-agent/src/runtime/prompt-guard.service.ts +303 -0
- package/packages/crewly-agent/src/runtime/rate-limiter.test.ts +228 -0
- package/packages/crewly-agent/src/runtime/rate-limiter.ts +353 -0
- package/packages/crewly-agent/src/runtime/tool-registry.test.ts +2510 -0
- package/packages/crewly-agent/src/runtime/tool-registry.ts +2104 -0
- package/packages/crewly-agent/src/runtime/types.test.ts +519 -0
- package/packages/crewly-agent/src/runtime/types.ts +637 -0
- package/packages/crewly-agent/src/runtime/web-search.tool.test.ts +131 -0
- package/packages/crewly-agent/src/runtime/web-search.tool.ts +140 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiter for Crewly Agent API Calls
|
|
3
|
+
*
|
|
4
|
+
* Provides token-bucket rate limiting, message coalescing, and 429 retry
|
|
5
|
+
* logic to prevent Gemini API quota exhaustion when multiple agents are
|
|
6
|
+
* actively generating messages.
|
|
7
|
+
*
|
|
8
|
+
* @module services/agent/crewly-agent/rate-limiter
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Configuration for the rate limiter.
|
|
13
|
+
*/
|
|
14
|
+
export interface RateLimiterConfig {
|
|
15
|
+
/** Maximum requests allowed per window */
|
|
16
|
+
maxRequestsPerWindow: number;
|
|
17
|
+
/** Time window in milliseconds */
|
|
18
|
+
windowMs: number;
|
|
19
|
+
/** Maximum retry attempts for 429 errors */
|
|
20
|
+
maxRetries: number;
|
|
21
|
+
/** Initial backoff delay in milliseconds for 429 retries */
|
|
22
|
+
initialBackoffMs: number;
|
|
23
|
+
/** Backoff multiplier for exponential retry */
|
|
24
|
+
backoffMultiplier: number;
|
|
25
|
+
/** Maximum backoff delay in milliseconds */
|
|
26
|
+
maxBackoffMs: number;
|
|
27
|
+
/** Coalescing window — how long to wait for additional messages before processing (ms) */
|
|
28
|
+
coalesceWindowMs: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Default rate limiter configuration.
|
|
33
|
+
*/
|
|
34
|
+
export const RATE_LIMITER_DEFAULTS: RateLimiterConfig = {
|
|
35
|
+
maxRequestsPerWindow: 10,
|
|
36
|
+
windowMs: 60_000,
|
|
37
|
+
maxRetries: 3,
|
|
38
|
+
initialBackoffMs: 5_000,
|
|
39
|
+
backoffMultiplier: 2,
|
|
40
|
+
maxBackoffMs: 60_000,
|
|
41
|
+
coalesceWindowMs: 2_000,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A queued message awaiting processing.
|
|
46
|
+
*/
|
|
47
|
+
interface QueuedMessage<T> {
|
|
48
|
+
/** The message content */
|
|
49
|
+
message: string;
|
|
50
|
+
/** Optional metadata passed through to the handler */
|
|
51
|
+
metadata?: Record<string, string>;
|
|
52
|
+
/** Promise resolve callback */
|
|
53
|
+
resolve: (value: T) => void;
|
|
54
|
+
/** Promise reject callback */
|
|
55
|
+
reject: (error: Error) => void;
|
|
56
|
+
/** Timestamp when the message was enqueued */
|
|
57
|
+
enqueuedAt: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Token-bucket rate limiter with message coalescing and 429 retry.
|
|
62
|
+
*
|
|
63
|
+
* Features:
|
|
64
|
+
* - **Rate limiting**: Enforces max requests per time window using a sliding window.
|
|
65
|
+
* - **Message coalescing**: Groups messages arriving within a short window into one.
|
|
66
|
+
* - **429 retry**: Wraps API calls with exponential backoff on quota errors.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* const limiter = new RateLimiter<AgentRunResult>();
|
|
71
|
+
* const result = await limiter.enqueue('Check status', undefined, async (msg) => {
|
|
72
|
+
* return agentRunner.run(msg);
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export class RateLimiter<T> {
|
|
77
|
+
private config: RateLimiterConfig;
|
|
78
|
+
private requestTimestamps: number[] = [];
|
|
79
|
+
private queue: QueuedMessage<T>[] = [];
|
|
80
|
+
private processing = false;
|
|
81
|
+
private coalesceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a new RateLimiter instance.
|
|
85
|
+
*
|
|
86
|
+
* @param config - Optional partial config overrides
|
|
87
|
+
*/
|
|
88
|
+
constructor(config?: Partial<RateLimiterConfig>) {
|
|
89
|
+
this.config = { ...RATE_LIMITER_DEFAULTS, ...config };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Enqueue a message for rate-limited processing.
|
|
94
|
+
*
|
|
95
|
+
* If multiple messages arrive within the coalescing window, they are
|
|
96
|
+
* merged into a single request to reduce API call count.
|
|
97
|
+
*
|
|
98
|
+
* @param message - Message to process
|
|
99
|
+
* @param metadata - Optional metadata
|
|
100
|
+
* @param handler - Async function that performs the actual API call
|
|
101
|
+
* @returns Result from the handler
|
|
102
|
+
*/
|
|
103
|
+
enqueue(
|
|
104
|
+
message: string,
|
|
105
|
+
metadata: Record<string, string> | undefined,
|
|
106
|
+
handler: (message: string, metadata?: Record<string, string>) => Promise<T>,
|
|
107
|
+
): Promise<T> {
|
|
108
|
+
return new Promise<T>((resolve, reject) => {
|
|
109
|
+
this.queue.push({
|
|
110
|
+
message,
|
|
111
|
+
metadata,
|
|
112
|
+
resolve,
|
|
113
|
+
reject,
|
|
114
|
+
enqueuedAt: Date.now(),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Reset coalesce timer — wait for more messages before processing
|
|
118
|
+
if (this.coalesceTimer) {
|
|
119
|
+
clearTimeout(this.coalesceTimer);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.coalesceTimer = setTimeout(() => {
|
|
123
|
+
this.coalesceTimer = null;
|
|
124
|
+
if (!this.processing) {
|
|
125
|
+
this.processQueue(handler).catch(() => {
|
|
126
|
+
// Errors are already routed to individual promise reject callbacks
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}, this.config.coalesceWindowMs);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get the current number of messages in the queue.
|
|
135
|
+
*
|
|
136
|
+
* @returns Queue length
|
|
137
|
+
*/
|
|
138
|
+
getQueueLength(): number {
|
|
139
|
+
return this.queue.length;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if the rate limiter is currently processing.
|
|
144
|
+
*
|
|
145
|
+
* @returns True if processing
|
|
146
|
+
*/
|
|
147
|
+
isProcessing(): boolean {
|
|
148
|
+
return this.processing;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get the number of requests made in the current window.
|
|
153
|
+
*
|
|
154
|
+
* @returns Request count in current window
|
|
155
|
+
*/
|
|
156
|
+
getRequestCountInWindow(): number {
|
|
157
|
+
this.pruneOldTimestamps();
|
|
158
|
+
return this.requestTimestamps.length;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get the current configuration.
|
|
163
|
+
*
|
|
164
|
+
* @returns Rate limiter configuration
|
|
165
|
+
*/
|
|
166
|
+
getConfig(): RateLimiterConfig {
|
|
167
|
+
return { ...this.config };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Reset internal state (for testing).
|
|
172
|
+
*/
|
|
173
|
+
reset(): void {
|
|
174
|
+
this.requestTimestamps = [];
|
|
175
|
+
this.queue = [];
|
|
176
|
+
this.processing = false;
|
|
177
|
+
if (this.coalesceTimer) {
|
|
178
|
+
clearTimeout(this.coalesceTimer);
|
|
179
|
+
this.coalesceTimer = null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Process the queue: coalesce messages, enforce rate limit, execute with retry.
|
|
185
|
+
*
|
|
186
|
+
* @param handler - The actual API call handler
|
|
187
|
+
*/
|
|
188
|
+
private async processQueue(
|
|
189
|
+
handler: (message: string, metadata?: Record<string, string>) => Promise<T>,
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
this.processing = true;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
while (this.queue.length > 0) {
|
|
195
|
+
// Wait for rate limit window if needed
|
|
196
|
+
await this.waitForCapacity();
|
|
197
|
+
|
|
198
|
+
// Coalesce all pending messages into one
|
|
199
|
+
const batch = this.queue.splice(0, this.queue.length);
|
|
200
|
+
const coalesced = this.coalesceMessages(batch);
|
|
201
|
+
|
|
202
|
+
// Record this request timestamp
|
|
203
|
+
this.requestTimestamps.push(Date.now());
|
|
204
|
+
|
|
205
|
+
// Execute with 429 retry
|
|
206
|
+
try {
|
|
207
|
+
const result = await this.executeWithRetry(
|
|
208
|
+
coalesced.message,
|
|
209
|
+
coalesced.metadata,
|
|
210
|
+
handler,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Resolve all promises in the batch with the same result
|
|
214
|
+
for (const item of batch) {
|
|
215
|
+
item.resolve(result);
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
// Reject all promises in the batch
|
|
219
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
220
|
+
for (const item of batch) {
|
|
221
|
+
item.reject(err);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} finally {
|
|
226
|
+
this.processing = false;
|
|
227
|
+
|
|
228
|
+
// Check if more messages arrived while processing
|
|
229
|
+
if (this.queue.length > 0) {
|
|
230
|
+
this.processQueue(handler).catch(() => {
|
|
231
|
+
// Errors are already routed to individual promise reject callbacks
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Wait until we have capacity in the rate limit window.
|
|
239
|
+
*/
|
|
240
|
+
private async waitForCapacity(): Promise<void> {
|
|
241
|
+
this.pruneOldTimestamps();
|
|
242
|
+
|
|
243
|
+
while (this.requestTimestamps.length >= this.config.maxRequestsPerWindow) {
|
|
244
|
+
// Calculate how long to wait until the oldest request falls out of the window
|
|
245
|
+
const oldestTimestamp = this.requestTimestamps[0];
|
|
246
|
+
const waitMs = (oldestTimestamp + this.config.windowMs) - Date.now() + 100; // +100ms buffer
|
|
247
|
+
|
|
248
|
+
if (waitMs > 0) {
|
|
249
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.pruneOldTimestamps();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Remove timestamps older than the current window.
|
|
258
|
+
*/
|
|
259
|
+
private pruneOldTimestamps(): void {
|
|
260
|
+
const cutoff = Date.now() - this.config.windowMs;
|
|
261
|
+
this.requestTimestamps = this.requestTimestamps.filter((ts) => ts > cutoff);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Coalesce multiple queued messages into a single message.
|
|
266
|
+
*
|
|
267
|
+
* If there's only one message, it passes through unchanged.
|
|
268
|
+
* Multiple messages are joined with a separator and a header.
|
|
269
|
+
*
|
|
270
|
+
* @param batch - Array of queued messages to coalesce
|
|
271
|
+
* @returns Single coalesced message and metadata from the most recent item
|
|
272
|
+
*/
|
|
273
|
+
private coalesceMessages(batch: QueuedMessage<T>[]): { message: string; metadata?: Record<string, string> } {
|
|
274
|
+
if (batch.length === 1) {
|
|
275
|
+
return { message: batch[0].message, metadata: batch[0].metadata };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Use metadata from the most recent message
|
|
279
|
+
const latestMetadata = batch[batch.length - 1].metadata;
|
|
280
|
+
|
|
281
|
+
// Coalesce messages with a numbered separator
|
|
282
|
+
const parts = batch.map((item, index) => {
|
|
283
|
+
return `[Message ${index + 1}/${batch.length}]\n${item.message}`;
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const coalesced = `${batch.length} messages received while rate-limited. Process them together:\n\n${parts.join('\n\n---\n\n')}`;
|
|
287
|
+
|
|
288
|
+
return { message: coalesced, metadata: latestMetadata };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Execute the handler with exponential backoff retry on 429/quota errors.
|
|
293
|
+
*
|
|
294
|
+
* @param message - Message to send
|
|
295
|
+
* @param metadata - Optional metadata
|
|
296
|
+
* @param handler - The API call handler
|
|
297
|
+
* @returns Handler result
|
|
298
|
+
* @throws Error after max retries exhausted
|
|
299
|
+
*/
|
|
300
|
+
private async executeWithRetry(
|
|
301
|
+
message: string,
|
|
302
|
+
metadata: Record<string, string> | undefined,
|
|
303
|
+
handler: (message: string, metadata?: Record<string, string>) => Promise<T>,
|
|
304
|
+
): Promise<T> {
|
|
305
|
+
let lastError: Error | null = null;
|
|
306
|
+
|
|
307
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
308
|
+
try {
|
|
309
|
+
return await handler(message, metadata);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
312
|
+
|
|
313
|
+
// Only retry on 429 / quota errors
|
|
314
|
+
if (!this.isRetryableError(lastError)) {
|
|
315
|
+
throw lastError;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (attempt >= this.config.maxRetries) {
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Exponential backoff
|
|
323
|
+
const backoffMs = Math.min(
|
|
324
|
+
this.config.initialBackoffMs * Math.pow(this.config.backoffMultiplier, attempt),
|
|
325
|
+
this.config.maxBackoffMs,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Rate limit retries exhausted (${this.config.maxRetries} attempts). Last error: ${lastError?.message}`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Check if an error is retryable (429 or quota-related).
|
|
339
|
+
*
|
|
340
|
+
* @param error - The error to check
|
|
341
|
+
* @returns True if the error should be retried
|
|
342
|
+
*/
|
|
343
|
+
private isRetryableError(error: Error): boolean {
|
|
344
|
+
const message = error.message.toLowerCase();
|
|
345
|
+
return (
|
|
346
|
+
message.includes('429') ||
|
|
347
|
+
message.includes('quota') ||
|
|
348
|
+
message.includes('rate limit') ||
|
|
349
|
+
message.includes('resource_exhausted') ||
|
|
350
|
+
message.includes('too many requests')
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|