@botcord/daemon 0.2.19 → 0.2.21
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.
|
@@ -225,7 +225,10 @@ export function createBotCordChannel(options) {
|
|
|
225
225
|
const eligible = [];
|
|
226
226
|
if (msgs.length === 0) {
|
|
227
227
|
logDrain();
|
|
228
|
-
|
|
228
|
+
// Defensive: if Hub returns 0 messages, refuse to honor has_more=true.
|
|
229
|
+
// A stuck cursor on the Hub side could otherwise produce an unbounded
|
|
230
|
+
// poll loop here (count=0 with has_more=true on every iteration).
|
|
231
|
+
return { hasMore: false };
|
|
229
232
|
}
|
|
230
233
|
// First pass: ack duplicates/skipped messages so Hub stops requeueing,
|
|
231
234
|
// and collect eligible messages preserving poll order. Grouping by
|
|
@@ -261,7 +264,7 @@ export function createBotCordChannel(options) {
|
|
|
261
264
|
}
|
|
262
265
|
if (eligible.length === 0) {
|
|
263
266
|
logDrain();
|
|
264
|
-
return;
|
|
267
|
+
return { hasMore: Boolean(resp.has_more) };
|
|
265
268
|
}
|
|
266
269
|
// Group by `(room_id, topic)`. Insertion order is the poll order, so
|
|
267
270
|
// iterating the map yields groups with the same external chronology.
|
|
@@ -275,6 +278,13 @@ export function createBotCordChannel(options) {
|
|
|
275
278
|
else
|
|
276
279
|
groups.set(key, [msg]);
|
|
277
280
|
}
|
|
281
|
+
// Emit groups in parallel: each `(room_id, topic)` group is an independent
|
|
282
|
+
// conversation thread, and the dispatcher already keys its per-turn queue
|
|
283
|
+
// by `(channel, accountId, roomId, threadId)` (see `buildQueueKey` in
|
|
284
|
+
// dispatcher.ts). Awaiting groups serially here forced a slow turn in
|
|
285
|
+
// room A to block room B's turn from starting; running them concurrently
|
|
286
|
+
// lets the dispatcher's per-room queues actually run in parallel.
|
|
287
|
+
const emitTasks = [];
|
|
278
288
|
for (const group of groups.values()) {
|
|
279
289
|
const normalized = normalizeInboxBatch(group, {
|
|
280
290
|
channelId: options.id,
|
|
@@ -301,18 +311,18 @@ export function createBotCordChannel(options) {
|
|
|
301
311
|
},
|
|
302
312
|
},
|
|
303
313
|
};
|
|
304
|
-
|
|
305
|
-
await emit(envelope);
|
|
314
|
+
emitTasks.push(emit(envelope).then(() => {
|
|
306
315
|
emittedGroups += 1;
|
|
307
|
-
}
|
|
308
|
-
catch (err) {
|
|
316
|
+
}, (err) => {
|
|
309
317
|
log.error("botcord emit threw", {
|
|
310
318
|
hubMsgIds: hubIds,
|
|
311
319
|
err: String(err),
|
|
312
320
|
});
|
|
313
|
-
}
|
|
321
|
+
}));
|
|
314
322
|
}
|
|
323
|
+
await Promise.all(emitTasks);
|
|
315
324
|
logDrain();
|
|
325
|
+
return { hasMore: Boolean(resp.has_more) };
|
|
316
326
|
}
|
|
317
327
|
function startWsLoop(client, ctx) {
|
|
318
328
|
const { abortSignal, log, emit, setStatus } = ctx;
|
|
@@ -354,11 +364,16 @@ export function createBotCordChannel(options) {
|
|
|
354
364
|
processing = true;
|
|
355
365
|
try {
|
|
356
366
|
let currentTrigger = trigger;
|
|
367
|
+
let hasMore = false;
|
|
357
368
|
do {
|
|
358
369
|
pendingUpdate = false;
|
|
359
|
-
await drainInbox(client, emit, log, currentTrigger);
|
|
360
|
-
|
|
361
|
-
|
|
370
|
+
const result = await drainInbox(client, emit, log, currentTrigger);
|
|
371
|
+
hasMore = result.hasMore;
|
|
372
|
+
// Prefer `has_more_continue` when this iteration is chained because
|
|
373
|
+
// the previous poll capped at INBOX_POLL_LIMIT — distinguishes a
|
|
374
|
+
// backlog drain from a coalesced ws_inbox_update drain in logs.
|
|
375
|
+
currentTrigger = hasMore ? "has_more_continue" : "coalesced_inbox_update";
|
|
376
|
+
} while ((pendingUpdate || hasMore) && running);
|
|
362
377
|
}
|
|
363
378
|
catch (err) {
|
|
364
379
|
log.error("botcord inbox drain failed", { err: String(err) });
|
package/package.json
CHANGED
|
@@ -30,7 +30,11 @@ const OWNER_CHAT_PREFIX = "rm_oc_";
|
|
|
30
30
|
const DM_ROOM_PREFIX = "rm_dm_";
|
|
31
31
|
const INBOX_POLL_LIMIT = 50;
|
|
32
32
|
|
|
33
|
-
type InboxDrainTrigger =
|
|
33
|
+
type InboxDrainTrigger =
|
|
34
|
+
| "ws_auth_ok"
|
|
35
|
+
| "ws_inbox_update"
|
|
36
|
+
| "coalesced_inbox_update"
|
|
37
|
+
| "has_more_continue";
|
|
34
38
|
|
|
35
39
|
/** Minimal surface the adapter needs from `BotCordClient`. Matches the subset used at runtime. */
|
|
36
40
|
export interface BotCordChannelClient {
|
|
@@ -309,7 +313,7 @@ export function createBotCordChannel(options: BotCordChannelOptions): ChannelAda
|
|
|
309
313
|
emit: (env: GatewayInboundEnvelope) => Promise<void>,
|
|
310
314
|
log: GatewayLogger,
|
|
311
315
|
trigger: InboxDrainTrigger,
|
|
312
|
-
): Promise<
|
|
316
|
+
): Promise<{ hasMore: boolean }> {
|
|
313
317
|
const startedAt = Date.now();
|
|
314
318
|
const resp = await client.pollInbox({ limit: INBOX_POLL_LIMIT, ack: false });
|
|
315
319
|
const msgs = resp.messages ?? [];
|
|
@@ -334,7 +338,10 @@ export function createBotCordChannel(options: BotCordChannelOptions): ChannelAda
|
|
|
334
338
|
const eligible: InboxMessage[] = [];
|
|
335
339
|
if (msgs.length === 0) {
|
|
336
340
|
logDrain();
|
|
337
|
-
|
|
341
|
+
// Defensive: if Hub returns 0 messages, refuse to honor has_more=true.
|
|
342
|
+
// A stuck cursor on the Hub side could otherwise produce an unbounded
|
|
343
|
+
// poll loop here (count=0 with has_more=true on every iteration).
|
|
344
|
+
return { hasMore: false };
|
|
338
345
|
}
|
|
339
346
|
|
|
340
347
|
// First pass: ack duplicates/skipped messages so Hub stops requeueing,
|
|
@@ -370,7 +377,7 @@ export function createBotCordChannel(options: BotCordChannelOptions): ChannelAda
|
|
|
370
377
|
|
|
371
378
|
if (eligible.length === 0) {
|
|
372
379
|
logDrain();
|
|
373
|
-
return;
|
|
380
|
+
return { hasMore: Boolean(resp.has_more) };
|
|
374
381
|
}
|
|
375
382
|
|
|
376
383
|
// Group by `(room_id, topic)`. Insertion order is the poll order, so
|
|
@@ -384,6 +391,13 @@ export function createBotCordChannel(options: BotCordChannelOptions): ChannelAda
|
|
|
384
391
|
else groups.set(key, [msg]);
|
|
385
392
|
}
|
|
386
393
|
|
|
394
|
+
// Emit groups in parallel: each `(room_id, topic)` group is an independent
|
|
395
|
+
// conversation thread, and the dispatcher already keys its per-turn queue
|
|
396
|
+
// by `(channel, accountId, roomId, threadId)` (see `buildQueueKey` in
|
|
397
|
+
// dispatcher.ts). Awaiting groups serially here forced a slow turn in
|
|
398
|
+
// room A to block room B's turn from starting; running them concurrently
|
|
399
|
+
// lets the dispatcher's per-room queues actually run in parallel.
|
|
400
|
+
const emitTasks: Promise<void>[] = [];
|
|
387
401
|
for (const group of groups.values()) {
|
|
388
402
|
const normalized = normalizeInboxBatch(group, {
|
|
389
403
|
channelId: options.id,
|
|
@@ -409,17 +423,23 @@ export function createBotCordChannel(options: BotCordChannelOptions): ChannelAda
|
|
|
409
423
|
},
|
|
410
424
|
},
|
|
411
425
|
};
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
426
|
+
emitTasks.push(
|
|
427
|
+
emit(envelope).then(
|
|
428
|
+
() => {
|
|
429
|
+
emittedGroups += 1;
|
|
430
|
+
},
|
|
431
|
+
(err) => {
|
|
432
|
+
log.error("botcord emit threw", {
|
|
433
|
+
hubMsgIds: hubIds,
|
|
434
|
+
err: String(err),
|
|
435
|
+
});
|
|
436
|
+
},
|
|
437
|
+
),
|
|
438
|
+
);
|
|
421
439
|
}
|
|
440
|
+
await Promise.all(emitTasks);
|
|
422
441
|
logDrain();
|
|
442
|
+
return { hasMore: Boolean(resp.has_more) };
|
|
423
443
|
}
|
|
424
444
|
|
|
425
445
|
function startWsLoop(
|
|
@@ -470,11 +490,16 @@ export function createBotCordChannel(options: BotCordChannelOptions): ChannelAda
|
|
|
470
490
|
processing = true;
|
|
471
491
|
try {
|
|
472
492
|
let currentTrigger = trigger;
|
|
493
|
+
let hasMore = false;
|
|
473
494
|
do {
|
|
474
495
|
pendingUpdate = false;
|
|
475
|
-
await drainInbox(client, emit, log, currentTrigger);
|
|
476
|
-
|
|
477
|
-
|
|
496
|
+
const result = await drainInbox(client, emit, log, currentTrigger);
|
|
497
|
+
hasMore = result.hasMore;
|
|
498
|
+
// Prefer `has_more_continue` when this iteration is chained because
|
|
499
|
+
// the previous poll capped at INBOX_POLL_LIMIT — distinguishes a
|
|
500
|
+
// backlog drain from a coalesced ws_inbox_update drain in logs.
|
|
501
|
+
currentTrigger = hasMore ? "has_more_continue" : "coalesced_inbox_update";
|
|
502
|
+
} while ((pendingUpdate || hasMore) && running);
|
|
478
503
|
} catch (err) {
|
|
479
504
|
log.error("botcord inbox drain failed", { err: String(err) });
|
|
480
505
|
} finally {
|