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.
Files changed (208) hide show
  1. package/config/skills/agent/onboarding/synthesize-hierarchy/SKILL.md +65 -0
  2. package/config/skills/agent/onboarding/synthesize-hierarchy/execute.sh +61 -0
  3. package/config/skills/agent/web-search/SKILL.md +70 -0
  4. package/config/skills/agent/web-search/execute.sh +170 -0
  5. package/config/skills/agent/web-search/skill.json +23 -0
  6. package/dist/backend/backend/src/constants.d.ts +34 -1
  7. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  8. package/dist/backend/backend/src/constants.js +34 -1
  9. package/dist/backend/backend/src/constants.js.map +1 -1
  10. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +22 -0
  11. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
  12. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +58 -0
  13. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
  14. package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
  15. package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +3 -1
  16. package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
  17. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts +27 -0
  18. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts.map +1 -1
  19. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js +108 -0
  20. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js.map +1 -1
  21. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts +6 -2
  22. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts.map +1 -1
  23. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js +9 -3
  24. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js.map +1 -1
  25. package/dist/backend/backend/src/index.d.ts.map +1 -1
  26. package/dist/backend/backend/src/index.js +36 -2
  27. package/dist/backend/backend/src/index.js.map +1 -1
  28. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +18 -0
  29. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
  30. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +24 -2
  31. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
  32. package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts +90 -0
  33. package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts.map +1 -0
  34. package/dist/backend/backend/src/services/backup/backup-archive.service.js +309 -0
  35. package/dist/backend/backend/src/services/backup/backup-archive.service.js.map +1 -0
  36. package/dist/backend/backend/src/services/backup/backup-cloud.client.d.ts +75 -0
  37. package/dist/backend/backend/src/services/backup/backup-cloud.client.d.ts.map +1 -0
  38. package/dist/backend/backend/src/services/backup/backup-cloud.client.js +134 -0
  39. package/dist/backend/backend/src/services/backup/backup-cloud.client.js.map +1 -0
  40. package/dist/backend/backend/src/services/backup/backup-restore.service.d.ts +78 -0
  41. package/dist/backend/backend/src/services/backup/backup-restore.service.d.ts.map +1 -0
  42. package/dist/backend/backend/src/services/backup/backup-restore.service.js +358 -0
  43. package/dist/backend/backend/src/services/backup/backup-restore.service.js.map +1 -0
  44. package/dist/backend/backend/src/services/backup/backup.types.d.ts +163 -0
  45. package/dist/backend/backend/src/services/backup/backup.types.d.ts.map +1 -0
  46. package/dist/backend/backend/src/services/backup/backup.types.js +13 -0
  47. package/dist/backend/backend/src/services/backup/backup.types.js.map +1 -0
  48. package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts +29 -2
  49. package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -1
  50. package/dist/backend/backend/src/services/cloud/cloud-sync.service.js +97 -13
  51. package/dist/backend/backend/src/services/cloud/cloud-sync.service.js.map +1 -1
  52. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts +102 -0
  53. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -0
  54. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +164 -0
  55. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -0
  56. package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts +21 -0
  57. package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts.map +1 -1
  58. package/dist/backend/backend/src/services/fission/fission-guard.service.js +30 -0
  59. package/dist/backend/backend/src/services/fission/fission-guard.service.js.map +1 -1
  60. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts +4 -0
  61. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts.map +1 -1
  62. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js +8 -0
  63. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js.map +1 -1
  64. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts +79 -58
  65. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts.map +1 -1
  66. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js +140 -65
  67. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js.map +1 -1
  68. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts +117 -0
  69. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts.map +1 -0
  70. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js +189 -0
  71. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js.map +1 -0
  72. package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.d.ts.map +1 -1
  73. package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js +1 -0
  74. package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js.map +1 -1
  75. package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.d.ts.map +1 -1
  76. package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js +2 -0
  77. package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js.map +1 -1
  78. package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.d.ts.map +1 -1
  79. package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js +17 -1
  80. package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js.map +1 -1
  81. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +50 -0
  82. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
  83. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +71 -0
  84. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
  85. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +18 -0
  86. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
  87. package/dist/backend/backend/src/services/reconciler/reconciler.service.js +75 -1
  88. package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
  89. package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts +115 -0
  90. package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
  91. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +189 -3
  92. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
  93. package/dist/backend/backend/src/services/session/pty/pty-session.d.ts +28 -0
  94. package/dist/backend/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
  95. package/dist/backend/backend/src/services/session/pty/pty-session.js +61 -1
  96. package/dist/backend/backend/src/services/session/pty/pty-session.js.map +1 -1
  97. package/dist/backend/backend/src/services/template/template.service.d.ts.map +1 -1
  98. package/dist/backend/backend/src/services/template/template.service.js +67 -2
  99. package/dist/backend/backend/src/services/template/template.service.js.map +1 -1
  100. package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts +19 -1
  101. package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts.map +1 -1
  102. package/dist/backend/backend/src/services/v3/cascade-request-status.js +39 -2
  103. package/dist/backend/backend/src/services/v3/cascade-request-status.js.map +1 -1
  104. package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts +41 -0
  105. package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts.map +1 -1
  106. package/dist/backend/backend/src/services/v3/escalation-router.service.js +169 -0
  107. package/dist/backend/backend/src/services/v3/escalation-router.service.js.map +1 -1
  108. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts +4 -1
  109. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts.map +1 -1
  110. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js +21 -0
  111. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js.map +1 -1
  112. package/dist/backend/backend/src/types/intent-task.types.d.ts.map +1 -1
  113. package/dist/backend/backend/src/types/intent-task.types.js +8 -0
  114. package/dist/backend/backend/src/types/intent-task.types.js.map +1 -1
  115. package/dist/backend/backend/src/types/v2/request.types.d.ts +1 -1
  116. package/dist/backend/backend/src/types/v2/request.types.d.ts.map +1 -1
  117. package/dist/backend/backend/src/types/v2/request.types.js +1 -0
  118. package/dist/backend/backend/src/types/v2/request.types.js.map +1 -1
  119. package/dist/cli/backend/src/constants.d.ts +34 -1
  120. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  121. package/dist/cli/backend/src/constants.js +34 -1
  122. package/dist/cli/backend/src/constants.js.map +1 -1
  123. package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.d.ts +70 -0
  124. package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.d.ts.map +1 -0
  125. package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.js +427 -0
  126. package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.js.map +1 -0
  127. package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts +90 -0
  128. package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts.map +1 -0
  129. package/dist/cli/backend/src/services/backup/backup-archive.service.js +309 -0
  130. package/dist/cli/backend/src/services/backup/backup-archive.service.js.map +1 -0
  131. package/dist/cli/backend/src/services/backup/backup-cloud.client.d.ts +75 -0
  132. package/dist/cli/backend/src/services/backup/backup-cloud.client.d.ts.map +1 -0
  133. package/dist/cli/backend/src/services/backup/backup-cloud.client.js +134 -0
  134. package/dist/cli/backend/src/services/backup/backup-cloud.client.js.map +1 -0
  135. package/dist/cli/backend/src/services/backup/backup-restore.service.d.ts +78 -0
  136. package/dist/cli/backend/src/services/backup/backup-restore.service.d.ts.map +1 -0
  137. package/dist/cli/backend/src/services/backup/backup-restore.service.js +358 -0
  138. package/dist/cli/backend/src/services/backup/backup-restore.service.js.map +1 -0
  139. package/dist/cli/backend/src/services/backup/backup.types.d.ts +163 -0
  140. package/dist/cli/backend/src/services/backup/backup.types.d.ts.map +1 -0
  141. package/dist/cli/backend/src/services/backup/backup.types.js +13 -0
  142. package/dist/cli/backend/src/services/backup/backup.types.js.map +1 -0
  143. package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts +410 -0
  144. package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -0
  145. package/dist/cli/backend/src/services/cloud/cloud-client.service.js +863 -0
  146. package/dist/cli/backend/src/services/cloud/cloud-client.service.js.map +1 -0
  147. package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts +292 -0
  148. package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -0
  149. package/dist/cli/backend/src/services/cloud/cloud-sync.service.js +1093 -0
  150. package/dist/cli/backend/src/services/cloud/cloud-sync.service.js.map +1 -0
  151. package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts +328 -0
  152. package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -0
  153. package/dist/cli/backend/src/services/cloud/cloud-sync.types.js +171 -0
  154. package/dist/cli/backend/src/services/cloud/cloud-sync.types.js.map +1 -0
  155. package/dist/cli/backend/src/services/cloud/device-identity.service.d.ts +89 -0
  156. package/dist/cli/backend/src/services/cloud/device-identity.service.d.ts.map +1 -0
  157. package/dist/cli/backend/src/services/cloud/device-identity.service.js +148 -0
  158. package/dist/cli/backend/src/services/cloud/device-identity.service.js.map +1 -0
  159. package/dist/cli/backend/src/services/user/user-identity.service.d.ts +86 -0
  160. package/dist/cli/backend/src/services/user/user-identity.service.d.ts.map +1 -0
  161. package/dist/cli/backend/src/services/user/user-identity.service.js +190 -0
  162. package/dist/cli/backend/src/services/user/user-identity.service.js.map +1 -0
  163. package/dist/cli/cli/src/commands/backup.d.ts +31 -0
  164. package/dist/cli/cli/src/commands/backup.d.ts.map +1 -0
  165. package/dist/cli/cli/src/commands/backup.js +280 -0
  166. package/dist/cli/cli/src/commands/backup.js.map +1 -0
  167. package/dist/cli/cli/src/index.js +10 -0
  168. package/dist/cli/cli/src/index.js.map +1 -1
  169. package/package.json +9 -3
  170. package/packages/crewly-agent/README.md +27 -0
  171. package/packages/crewly-agent/bin/crewly-agent +33 -0
  172. package/packages/crewly-agent/package.json +39 -0
  173. package/packages/crewly-agent/src/cli.ts +168 -0
  174. package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +2355 -0
  175. package/packages/crewly-agent/src/runtime/agent-runner.service.ts +1827 -0
  176. package/packages/crewly-agent/src/runtime/agent-stream.service.test.ts +153 -0
  177. package/packages/crewly-agent/src/runtime/agent-stream.service.ts +225 -0
  178. package/packages/crewly-agent/src/runtime/agent-worker.test.ts +171 -0
  179. package/packages/crewly-agent/src/runtime/agent-worker.ts +193 -0
  180. package/packages/crewly-agent/src/runtime/api-client.ts +143 -0
  181. package/packages/crewly-agent/src/runtime/approval-queue.service.ts +307 -0
  182. package/packages/crewly-agent/src/runtime/audit-log.service.test.ts +208 -0
  183. package/packages/crewly-agent/src/runtime/audit-log.service.ts +332 -0
  184. package/packages/crewly-agent/src/runtime/audit-trail.service.test.ts +178 -0
  185. package/packages/crewly-agent/src/runtime/audit-trail.service.ts +151 -0
  186. package/packages/crewly-agent/src/runtime/auditor-tools.test.ts +274 -0
  187. package/packages/crewly-agent/src/runtime/auditor-tools.ts +311 -0
  188. package/packages/crewly-agent/src/runtime/cloud-config.ts +67 -0
  189. package/packages/crewly-agent/src/runtime/deepseek-sse-transform.test.ts +165 -0
  190. package/packages/crewly-agent/src/runtime/deepseek-sse-transform.ts +168 -0
  191. package/packages/crewly-agent/src/runtime/env-isolation.service.ts +246 -0
  192. package/packages/crewly-agent/src/runtime/in-process-log-buffer.test.ts +280 -0
  193. package/packages/crewly-agent/src/runtime/in-process-log-buffer.ts +317 -0
  194. package/packages/crewly-agent/src/runtime/index.ts +38 -0
  195. package/packages/crewly-agent/src/runtime/mcp-tool-bridge.test.ts +352 -0
  196. package/packages/crewly-agent/src/runtime/mcp-tool-bridge.ts +244 -0
  197. package/packages/crewly-agent/src/runtime/model-manager.test.ts +326 -0
  198. package/packages/crewly-agent/src/runtime/model-manager.ts +363 -0
  199. package/packages/crewly-agent/src/runtime/output-filter.service.ts +175 -0
  200. package/packages/crewly-agent/src/runtime/prompt-guard.service.ts +303 -0
  201. package/packages/crewly-agent/src/runtime/rate-limiter.test.ts +228 -0
  202. package/packages/crewly-agent/src/runtime/rate-limiter.ts +353 -0
  203. package/packages/crewly-agent/src/runtime/tool-registry.test.ts +2510 -0
  204. package/packages/crewly-agent/src/runtime/tool-registry.ts +2104 -0
  205. package/packages/crewly-agent/src/runtime/types.test.ts +519 -0
  206. package/packages/crewly-agent/src/runtime/types.ts +637 -0
  207. package/packages/crewly-agent/src/runtime/web-search.tool.test.ts +131 -0
  208. 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
+ }