@hahnfeld/teams-adapter 1.2.0 → 1.3.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/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,iBAAiB,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAC7B,eAAe,iBAAiB,EAAE,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAK9C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,wBAAwB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,iBAAiB,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAC7B,eAAe,iBAAiB,EAAE,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,wBAAwB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Message composer — manages a single "main message" per session in Teams.
3
+ *
4
+ * The main message has four zones:
5
+ * TITLE (persistent, bold) — session name, survives finalize
6
+ * HEADER (ephemeral, italic) — tool_call, tool_update, thought
7
+ * BODY (persistent, streamed) — text, attachments
8
+ * FOOTER (persistent, italic) — usage, session_end
9
+ *
10
+ * All zones are composed into a single markdown string and sent/updated
11
+ * as one Teams message via the rate limiter.
12
+ */
13
+ import type { TurnContext } from "@microsoft/agents-hosting";
14
+ import type { ConversationRateLimiter } from "./rate-limiter.js";
15
+ export interface MessageRef {
16
+ activityId: string;
17
+ conversationId: string;
18
+ serviceUrl: string;
19
+ }
20
+ export type AcquireBotToken = () => Promise<string | null>;
21
+ /**
22
+ * A single main message with title/header/body/footer zones.
23
+ */
24
+ export declare class SessionMessage {
25
+ private context;
26
+ private conversationId;
27
+ private sessionId;
28
+ private rateLimiter;
29
+ private acquireBotToken;
30
+ private title;
31
+ private header;
32
+ private body;
33
+ private footer;
34
+ private ref;
35
+ private lastSent;
36
+ private stallTimer?;
37
+ constructor(context: TurnContext, conversationId: string, sessionId: string, rateLimiter: ConversationRateLimiter, acquireBotToken: AcquireBotToken);
38
+ updateContext(context: TurnContext): void;
39
+ getRef(): MessageRef | null;
40
+ getBody(): string;
41
+ getFooter(): string | null;
42
+ /** Set the persistent session title (bold, survives finalize). */
43
+ setTitle(text: string): void;
44
+ /** Max header length — keeps the message compact; tool diffs can be huge. */
45
+ private static readonly MAX_HEADER_LENGTH;
46
+ /** Replace the ephemeral header (tool_call, thought, etc.). */
47
+ setHeader(text: string): void;
48
+ /** Clear the ephemeral header and flush the update. */
49
+ clearHeader(): void;
50
+ /** Append text to the body (streaming text chunks). */
51
+ appendBody(text: string): void;
52
+ /** Set the persistent footer (usage, completion). */
53
+ setFooter(text: string): void;
54
+ /** Append to the existing footer (e.g., adding "Task completed" after usage). */
55
+ appendFooter(text: string): void;
56
+ /**
57
+ * Close any unclosed code fences in the body.
58
+ * If the body has an odd number of ``` markers, the footer would
59
+ * be swallowed into the code block — append a closing fence.
60
+ */
61
+ private static closeCodeFences;
62
+ /** Compose the four zones into a single markdown string. */
63
+ compose(): string;
64
+ /** Request a flush through the rate limiter. */
65
+ requestFlush(): void;
66
+ private flush;
67
+ private updateViaRest;
68
+ /** Split: finalize current message at body limit, start fresh. */
69
+ private split;
70
+ private resetStallTimer;
71
+ /** Finalize: clear stall timer, clear ephemeral header, do a last flush. */
72
+ finalize(): Promise<MessageRef | null>;
73
+ stripPattern(pattern: RegExp): Promise<void>;
74
+ }
75
+ /**
76
+ * Manages SessionMessage instances and plan refs across sessions.
77
+ */
78
+ export declare class SessionMessageManager {
79
+ private rateLimiter;
80
+ private acquireBotToken;
81
+ private messages;
82
+ private planRefs;
83
+ constructor(rateLimiter: ConversationRateLimiter, acquireBotToken: AcquireBotToken);
84
+ getOrCreate(sessionId: string, context: TurnContext): SessionMessage;
85
+ get(sessionId: string): SessionMessage | undefined;
86
+ has(sessionId: string): boolean;
87
+ /** Finalize and remove a session's message and plan ref. */
88
+ finalize(sessionId: string): Promise<MessageRef | null>;
89
+ /** Get or set the plan message ref for a session. */
90
+ getPlanRef(sessionId: string): MessageRef | undefined;
91
+ setPlanRef(sessionId: string, ref: MessageRef): void;
92
+ cleanup(sessionId: string): void;
93
+ }
94
+ //# sourceMappingURL=message-composer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-composer.d.ts","sourceRoot":"","sources":["../src/message-composer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,331 @@
1
+ import { log } from "@openacp/plugin-sdk";
2
+ import { sendText } from "./send-utils.js";
3
+ const MAX_BODY_LENGTH = 25_000;
4
+ const STALL_TIMEOUT = 30_000;
5
+ /** Escape asterisks to prevent breaking markdown italic/bold spans. */
6
+ function escapeEmphasis(text) {
7
+ return text.replace(/\*/g, "\\*");
8
+ }
9
+ /**
10
+ * Normalize newlines for Teams rendering.
11
+ * Teams collapses single \n in markdown — use \n\n for line breaks.
12
+ */
13
+ function teamsNewlines(text) {
14
+ return text.replace(/(?<!\n)\n(?!\n)/g, "\n\n");
15
+ }
16
+ /**
17
+ * A single main message with title/header/body/footer zones.
18
+ */
19
+ export class SessionMessage {
20
+ context;
21
+ conversationId;
22
+ sessionId;
23
+ rateLimiter;
24
+ acquireBotToken;
25
+ title = null;
26
+ header = null;
27
+ body = "";
28
+ footer = null;
29
+ ref = null;
30
+ lastSent = "";
31
+ stallTimer;
32
+ constructor(context, conversationId, sessionId, rateLimiter, acquireBotToken) {
33
+ this.context = context;
34
+ this.conversationId = conversationId;
35
+ this.sessionId = sessionId;
36
+ this.rateLimiter = rateLimiter;
37
+ this.acquireBotToken = acquireBotToken;
38
+ }
39
+ updateContext(context) {
40
+ this.context = context;
41
+ }
42
+ getRef() {
43
+ return this.ref;
44
+ }
45
+ getBody() {
46
+ return this.body;
47
+ }
48
+ getFooter() {
49
+ return this.footer;
50
+ }
51
+ /** Set the persistent session title (bold, survives finalize). */
52
+ setTitle(text) {
53
+ this.title = text;
54
+ this.requestFlush();
55
+ }
56
+ /** Max header length — keeps the message compact; tool diffs can be huge. */
57
+ static MAX_HEADER_LENGTH = 120;
58
+ /** Replace the ephemeral header (tool_call, thought, etc.). */
59
+ setHeader(text) {
60
+ // Truncate to first line, then cap length — headers are status indicators, not content
61
+ const firstLine = text.split("\n")[0];
62
+ this.header = firstLine.length > SessionMessage.MAX_HEADER_LENGTH
63
+ ? firstLine.slice(0, SessionMessage.MAX_HEADER_LENGTH) + "..."
64
+ : firstLine;
65
+ this.requestFlush();
66
+ }
67
+ /** Clear the ephemeral header and flush the update. */
68
+ clearHeader() {
69
+ if (this.header === null)
70
+ return;
71
+ this.header = null;
72
+ this.requestFlush();
73
+ }
74
+ /** Append text to the body (streaming text chunks). */
75
+ appendBody(text) {
76
+ if (!text)
77
+ return;
78
+ this.body += text;
79
+ this.resetStallTimer();
80
+ // Check if body needs splitting
81
+ if (this.body.length > MAX_BODY_LENGTH && this.ref) {
82
+ this.split();
83
+ return;
84
+ }
85
+ this.requestFlush();
86
+ }
87
+ /** Set the persistent footer (usage, completion). */
88
+ setFooter(text) {
89
+ this.footer = text;
90
+ this.requestFlush();
91
+ }
92
+ /** Append to the existing footer (e.g., adding "Task completed" after usage). */
93
+ appendFooter(text) {
94
+ this.footer = this.footer ? `${this.footer} · ${text}` : text;
95
+ this.requestFlush();
96
+ }
97
+ /**
98
+ * Close any unclosed code fences in the body.
99
+ * If the body has an odd number of ``` markers, the footer would
100
+ * be swallowed into the code block — append a closing fence.
101
+ */
102
+ static closeCodeFences(text) {
103
+ const fenceCount = (text.match(/^```/gm) || []).length;
104
+ if (fenceCount % 2 !== 0) {
105
+ return text + "\n```";
106
+ }
107
+ return text;
108
+ }
109
+ /** Compose the four zones into a single markdown string. */
110
+ compose() {
111
+ const parts = [];
112
+ if (this.title) {
113
+ parts.push(`**${escapeEmphasis(this.title)}**`);
114
+ }
115
+ if (this.header) {
116
+ parts.push(`*${escapeEmphasis(this.header)}*`);
117
+ }
118
+ if (this.title || this.header) {
119
+ parts.push("---");
120
+ }
121
+ if (this.body) {
122
+ parts.push(SessionMessage.closeCodeFences(this.body));
123
+ }
124
+ if (this.footer) {
125
+ if (this.body)
126
+ parts.push("---");
127
+ parts.push(`*${escapeEmphasis(this.footer)}*`);
128
+ }
129
+ return parts.join("\n\n");
130
+ }
131
+ /** Request a flush through the rate limiter. */
132
+ requestFlush() {
133
+ const composed = this.compose();
134
+ if (!composed)
135
+ return;
136
+ if (composed === this.lastSent)
137
+ return;
138
+ // Coalescing key: activityId for updates, session for new sends
139
+ const key = this.ref ? `update:${this.ref.activityId}` : `new:${this.sessionId}`;
140
+ this.rateLimiter.enqueue(this.conversationId, () => this.flush(), key).catch((err) => {
141
+ log.warn({ err, sessionId: this.sessionId }, "[SessionMessage] flush failed");
142
+ });
143
+ }
144
+ async flush() {
145
+ const composed = this.compose();
146
+ if (!composed || composed === this.lastSent)
147
+ return;
148
+ if (!this.ref) {
149
+ // First send — create the message
150
+ const result = await sendText(this.context, composed);
151
+ if (result?.id) {
152
+ this.ref = {
153
+ activityId: result.id,
154
+ conversationId: this.context.activity.conversation?.id,
155
+ serviceUrl: this.context.activity.serviceUrl,
156
+ };
157
+ }
158
+ this.lastSent = composed;
159
+ }
160
+ else {
161
+ // Update existing message via REST
162
+ const success = await this.updateViaRest(composed);
163
+ if (success) {
164
+ this.lastSent = composed;
165
+ }
166
+ }
167
+ // Check if state changed during the flush (new chunks arrived while we were awaiting)
168
+ const current = this.compose();
169
+ if (current && current !== this.lastSent) {
170
+ this.requestFlush();
171
+ }
172
+ }
173
+ async updateViaRest(text) {
174
+ if (!this.ref)
175
+ return false;
176
+ const token = await this.acquireBotToken();
177
+ if (!token)
178
+ return false;
179
+ const url = `${this.ref.serviceUrl}/v3/conversations/${encodeURIComponent(this.ref.conversationId)}/activities/${encodeURIComponent(this.ref.activityId)}`;
180
+ try {
181
+ const response = await fetch(url, {
182
+ method: "PUT",
183
+ headers: {
184
+ "Content-Type": "application/json",
185
+ "Authorization": `Bearer ${token}`,
186
+ },
187
+ body: JSON.stringify({
188
+ type: "message",
189
+ text: teamsNewlines(text),
190
+ textFormat: "markdown",
191
+ }),
192
+ });
193
+ if (!response.ok) {
194
+ log.warn({ status: response.status, sessionId: this.sessionId }, "[SessionMessage] REST update failed");
195
+ return false;
196
+ }
197
+ return true;
198
+ }
199
+ catch (err) {
200
+ log.warn({ err, sessionId: this.sessionId }, "[SessionMessage] REST update error");
201
+ return false;
202
+ }
203
+ }
204
+ /** Split: finalize current message at body limit, start fresh. */
205
+ split() {
206
+ const finalBody = this.body.slice(0, MAX_BODY_LENGTH);
207
+ const overflow = this.body.slice(MAX_BODY_LENGTH);
208
+ log.info({ sessionId: this.sessionId, finalLen: finalBody.length, overflowLen: overflow.length }, "[SessionMessage] splitting");
209
+ // Clear ephemeral header on the finalized message; footer stays per spec
210
+ this.header = null;
211
+ this.body = finalBody;
212
+ // Final flush of the current message (with footer preserved)
213
+ const composed = this.compose();
214
+ if (this.ref && composed !== this.lastSent) {
215
+ this.rateLimiter.enqueue(this.conversationId, () => this.updateViaRest(composed).then(() => { }), `update:${this.ref.activityId}`).catch(() => { });
216
+ }
217
+ // Reset for a new message — footer carries over to new message
218
+ this.ref = null;
219
+ this.lastSent = "";
220
+ this.body = overflow;
221
+ if (overflow) {
222
+ this.requestFlush();
223
+ }
224
+ }
225
+ resetStallTimer() {
226
+ if (this.stallTimer)
227
+ clearTimeout(this.stallTimer);
228
+ this.stallTimer = setTimeout(() => {
229
+ if (this.body && !this.footer) {
230
+ log.warn({ sessionId: this.sessionId }, "[SessionMessage] Stream stalled — adding cutoff notice");
231
+ this.header = null;
232
+ // Use appendBody so split check is applied
233
+ this.appendBody("\n\n---\n_Response was cut short — the model likely reached its output token limit. Send a follow-up message to continue._");
234
+ }
235
+ }, STALL_TIMEOUT);
236
+ if (this.stallTimer.unref)
237
+ this.stallTimer.unref();
238
+ }
239
+ /** Finalize: clear stall timer, clear ephemeral header, do a last flush. */
240
+ async finalize() {
241
+ if (this.stallTimer) {
242
+ clearTimeout(this.stallTimer);
243
+ this.stallTimer = undefined;
244
+ }
245
+ // Clear ephemeral header on finalize; title and footer persist
246
+ this.header = null;
247
+ // Final flush
248
+ const composed = this.compose();
249
+ if (composed && composed !== this.lastSent) {
250
+ if (!this.ref) {
251
+ const result = await sendText(this.context, composed);
252
+ if (result?.id) {
253
+ this.ref = {
254
+ activityId: result.id,
255
+ conversationId: this.context.activity.conversation?.id,
256
+ serviceUrl: this.context.activity.serviceUrl,
257
+ };
258
+ }
259
+ }
260
+ else {
261
+ await this.updateViaRest(composed);
262
+ }
263
+ this.lastSent = composed;
264
+ }
265
+ return this.ref;
266
+ }
267
+ async stripPattern(pattern) {
268
+ if (!this.body)
269
+ return;
270
+ try {
271
+ this.body = this.body.replace(pattern, "").trim();
272
+ }
273
+ catch { /* leave unchanged */ }
274
+ }
275
+ }
276
+ /**
277
+ * Manages SessionMessage instances and plan refs across sessions.
278
+ */
279
+ export class SessionMessageManager {
280
+ rateLimiter;
281
+ acquireBotToken;
282
+ messages = new Map();
283
+ planRefs = new Map();
284
+ constructor(rateLimiter, acquireBotToken) {
285
+ this.rateLimiter = rateLimiter;
286
+ this.acquireBotToken = acquireBotToken;
287
+ }
288
+ getOrCreate(sessionId, context) {
289
+ let msg = this.messages.get(sessionId);
290
+ if (!msg) {
291
+ const conversationId = context.activity.conversation?.id;
292
+ msg = new SessionMessage(context, conversationId, sessionId, this.rateLimiter, this.acquireBotToken);
293
+ this.messages.set(sessionId, msg);
294
+ }
295
+ else {
296
+ msg.updateContext(context);
297
+ }
298
+ return msg;
299
+ }
300
+ get(sessionId) {
301
+ return this.messages.get(sessionId);
302
+ }
303
+ has(sessionId) {
304
+ return this.messages.has(sessionId);
305
+ }
306
+ /** Finalize and remove a session's message and plan ref. */
307
+ async finalize(sessionId) {
308
+ const msg = this.messages.get(sessionId);
309
+ if (!msg)
310
+ return null;
311
+ this.messages.delete(sessionId);
312
+ this.planRefs.delete(sessionId);
313
+ return msg.finalize();
314
+ }
315
+ /** Get or set the plan message ref for a session. */
316
+ getPlanRef(sessionId) {
317
+ return this.planRefs.get(sessionId);
318
+ }
319
+ setPlanRef(sessionId, ref) {
320
+ this.planRefs.set(sessionId, ref);
321
+ }
322
+ cleanup(sessionId) {
323
+ const msg = this.messages.get(sessionId);
324
+ if (msg) {
325
+ msg.finalize().catch(() => { });
326
+ }
327
+ this.messages.delete(sessionId);
328
+ this.planRefs.delete(sessionId);
329
+ }
330
+ }
331
+ //# sourceMappingURL=message-composer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-composer.js","sourceRoot":"","sources":["../src/message-composer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Manages per-conversation rate-limited queues.
3
+ *
4
+ * Usage:
5
+ * const limiter = new ConversationRateLimiter();
6
+ * await limiter.enqueue(conversationId, () => sendText(ctx, text), "main-msg");
7
+ */
8
+ export declare class ConversationRateLimiter {
9
+ private queues;
10
+ /** Enqueue an operation for a conversation. Key enables coalescing. */
11
+ enqueue<T>(conversationId: string, fn: () => Promise<T>, key?: string): Promise<T | undefined>;
12
+ /** Clean up a conversation's queue (e.g., on session end). */
13
+ cleanup(conversationId: string): void;
14
+ /** Clean up all queues. */
15
+ destroy(): void;
16
+ }
17
+ //# 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":"AAqLA;;;;;;GAMG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,MAAM,CAAwC;IAEtD,uEAAuE;IACvE,OAAO,CAAC,CAAC,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAS9F,8DAA8D;IAC9D,OAAO,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;IAQrC,2BAA2B;IAC3B,OAAO,IAAI,IAAI;CAIhB"}
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Per-conversation rate limiter for Teams Bot Framework API.
3
+ *
4
+ * Teams enforces rate limits per bot per conversation thread:
5
+ * 7 ops/1s, 8 ops/2s, 60 ops/30s, 1800 ops/3600s
6
+ *
7
+ * This limiter tracks sliding windows and queues operations when any
8
+ * window would be exceeded. Supports coalescing — queued operations
9
+ * with the same key replace earlier ones (e.g., rapid message edits
10
+ * collapse into a single PUT).
11
+ */
12
+ import { log } from "@openacp/plugin-sdk";
13
+ /** Our targets — slightly under Teams limits to leave headroom. */
14
+ const RATE_WINDOWS = [
15
+ { windowMs: 1_000, max: 6 },
16
+ { windowMs: 2_000, max: 7 },
17
+ { windowMs: 30_000, max: 55 },
18
+ { windowMs: 3_600_000, max: 1_700 },
19
+ ];
20
+ /** Per-conversation queue state. */
21
+ class ConversationQueue {
22
+ /** Timestamps of completed operations (for sliding window tracking). */
23
+ timestamps = [];
24
+ queue = [];
25
+ draining = false;
26
+ destroyed = false;
27
+ pausedUntil = 0;
28
+ drainTimer;
29
+ /** Resolves the inner drain-wait promise on destroy, preventing coroutine leak. */
30
+ drainWaitResolve;
31
+ /** Enqueue an operation. If key matches a pending op, replace it (coalescing). */
32
+ enqueue(fn, key) {
33
+ if (this.destroyed)
34
+ return Promise.resolve(undefined);
35
+ return new Promise((resolve, reject) => {
36
+ if (key) {
37
+ const idx = this.queue.findIndex((op) => op.key === key);
38
+ if (idx !== -1) {
39
+ // Coalesce: resolve the old op as undefined (skipped), replace with new
40
+ this.queue[idx].resolve(undefined);
41
+ this.queue[idx] = { fn, key, resolve: resolve, reject };
42
+ return;
43
+ }
44
+ }
45
+ this.queue.push({ fn, key, resolve: resolve, reject });
46
+ this.scheduleDrain();
47
+ });
48
+ }
49
+ get pending() {
50
+ return this.queue.length;
51
+ }
52
+ destroy() {
53
+ this.destroyed = true;
54
+ if (this.drainTimer) {
55
+ clearTimeout(this.drainTimer);
56
+ this.drainTimer = undefined;
57
+ }
58
+ // Unblock any suspended drain-wait so the coroutine can exit
59
+ if (this.drainWaitResolve) {
60
+ this.drainWaitResolve();
61
+ this.drainWaitResolve = undefined;
62
+ }
63
+ // Resolve all pending as undefined
64
+ for (const op of this.queue)
65
+ op.resolve(undefined);
66
+ this.queue.length = 0;
67
+ }
68
+ scheduleDrain() {
69
+ if (this.draining)
70
+ return;
71
+ if (this.drainTimer)
72
+ return;
73
+ const delay = this.getDelay();
74
+ this.drainTimer = setTimeout(() => {
75
+ this.drainTimer = undefined;
76
+ this.drain();
77
+ }, delay);
78
+ }
79
+ /** Calculate how long to wait before next op is allowed. */
80
+ getDelay() {
81
+ const now = Date.now();
82
+ // Respect 429 pause
83
+ if (now < this.pausedUntil) {
84
+ return this.pausedUntil - now;
85
+ }
86
+ let maxDelay = 0;
87
+ for (const { windowMs, max } of RATE_WINDOWS) {
88
+ const cutoff = now - windowMs;
89
+ // Timestamps are in ascending order — find the first one in the window
90
+ const firstIdx = this.timestamps.findIndex((t) => t > cutoff);
91
+ if (firstIdx === -1)
92
+ continue;
93
+ const opsInWindow = this.timestamps.length - firstIdx;
94
+ if (opsInWindow >= max) {
95
+ const oldest = this.timestamps[firstIdx];
96
+ const wait = oldest + windowMs - now + 1;
97
+ maxDelay = Math.max(maxDelay, wait);
98
+ }
99
+ }
100
+ return maxDelay;
101
+ }
102
+ async drain() {
103
+ if (this.draining)
104
+ return;
105
+ this.draining = true;
106
+ try {
107
+ while (this.queue.length > 0 && !this.destroyed) {
108
+ const delay = this.getDelay();
109
+ if (delay > 0) {
110
+ await new Promise((r) => {
111
+ this.drainWaitResolve = r;
112
+ this.drainTimer = setTimeout(() => {
113
+ this.drainTimer = undefined;
114
+ this.drainWaitResolve = undefined;
115
+ r();
116
+ }, delay);
117
+ });
118
+ // Check if destroyed while waiting
119
+ if (this.destroyed)
120
+ break;
121
+ continue;
122
+ }
123
+ const op = this.queue.shift();
124
+ try {
125
+ const result = await op.fn();
126
+ // Record timestamp only on success — 429s should not consume quota
127
+ const now = Date.now();
128
+ this.timestamps.push(now);
129
+ this.pruneTimestamps(now);
130
+ op.resolve(result);
131
+ }
132
+ catch (err) {
133
+ const statusCode = err?.statusCode;
134
+ if (statusCode === 429) {
135
+ // Do NOT record a timestamp — the call was rejected
136
+ const retryAfterRaw = err?.headers?.["retry-after"];
137
+ const retryAfterSec = retryAfterRaw ? parseInt(retryAfterRaw, 10) : NaN;
138
+ const retryMs = !isNaN(retryAfterSec) && retryAfterSec > 0
139
+ ? retryAfterSec * 1000
140
+ : 2000;
141
+ log.warn({ retryMs }, "[RateLimiter] 429 received, pausing");
142
+ this.pausedUntil = Date.now() + retryMs;
143
+ // Re-queue the failed op at the front
144
+ this.queue.unshift(op);
145
+ continue;
146
+ }
147
+ // Non-429 errors still consumed a slot
148
+ const now = Date.now();
149
+ this.timestamps.push(now);
150
+ this.pruneTimestamps(now);
151
+ op.reject(err);
152
+ }
153
+ }
154
+ }
155
+ finally {
156
+ this.draining = false;
157
+ }
158
+ }
159
+ /** Remove timestamps older than the largest window. */
160
+ pruneTimestamps(now) {
161
+ const maxWindow = RATE_WINDOWS[RATE_WINDOWS.length - 1].windowMs;
162
+ const cutoff = now - maxWindow;
163
+ while (this.timestamps.length > 0 && this.timestamps[0] <= cutoff) {
164
+ this.timestamps.shift();
165
+ }
166
+ }
167
+ }
168
+ /**
169
+ * Manages per-conversation rate-limited queues.
170
+ *
171
+ * Usage:
172
+ * const limiter = new ConversationRateLimiter();
173
+ * await limiter.enqueue(conversationId, () => sendText(ctx, text), "main-msg");
174
+ */
175
+ export class ConversationRateLimiter {
176
+ queues = new Map();
177
+ /** Enqueue an operation for a conversation. Key enables coalescing. */
178
+ enqueue(conversationId, fn, key) {
179
+ let queue = this.queues.get(conversationId);
180
+ if (!queue) {
181
+ queue = new ConversationQueue();
182
+ this.queues.set(conversationId, queue);
183
+ }
184
+ return queue.enqueue(fn, key);
185
+ }
186
+ /** Clean up a conversation's queue (e.g., on session end). */
187
+ cleanup(conversationId) {
188
+ const queue = this.queues.get(conversationId);
189
+ if (queue) {
190
+ queue.destroy();
191
+ this.queues.delete(conversationId);
192
+ }
193
+ }
194
+ /** Clean up all queues. */
195
+ destroy() {
196
+ for (const queue of this.queues.values())
197
+ queue.destroy();
198
+ this.queues.clear();
199
+ }
200
+ }
201
+ //# sourceMappingURL=rate-limiter.js.map