@qwen-code/channel-dws 0.23.0 → 0.23.1-preview.1
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/dws-channel.d.ts +27 -10
- package/dist/dws-channel.d.ts.map +1 -1
- package/dist/dws-channel.js +393 -171
- package/dist/dws-channel.js.map +1 -1
- package/dist/dws-client.d.ts.map +1 -1
- package/dist/dws-client.js +0 -2
- package/dist/dws-client.js.map +1 -1
- package/package.json +2 -2
package/dist/dws-channel.js
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createHash, randomUUID } from 'node:crypto';
|
|
7
7
|
import process from 'node:process';
|
|
8
|
-
import { isTerminalTaskLifecycleType, PollingChannelBase, sanitizeLogText, truncateCodePoints, } from '@qwen-code/channel-base';
|
|
8
|
+
import { isTerminalTaskLifecycleType, PollingChannelBase, sanitizeLogText, stripMessagePrefix, truncateCodePoints, } from '@qwen-code/channel-base';
|
|
9
9
|
import { DwsClient, DwsCommandError, } from './dws-client.js';
|
|
10
10
|
import { DwsEventProcessError, } from './dws-event-stream.js';
|
|
11
11
|
const MAX_DOCUMENT_CONTEXT_CHARS = 12_000;
|
|
12
12
|
const MAX_TODO_CONTEXT_CHARS = 12_000;
|
|
13
13
|
const MAX_COMMENT_CHARS = 4_000;
|
|
14
14
|
const MAX_PROCESSED_ITEMS = 5_000;
|
|
15
|
-
const
|
|
15
|
+
const MAX_REPLAY_CONVERSATIONS = 16;
|
|
16
16
|
/**
|
|
17
17
|
* How many times one inbound message may fail its turn before it is dropped.
|
|
18
18
|
*
|
|
@@ -164,7 +164,8 @@ function isPendingMessage(value) {
|
|
|
164
164
|
return false;
|
|
165
165
|
}
|
|
166
166
|
const pending = value;
|
|
167
|
-
const sourceValid = pending.source?.kind === '
|
|
167
|
+
const sourceValid = pending.source?.kind === 'at' ||
|
|
168
|
+
pending.source?.kind === 'direct' ||
|
|
168
169
|
pending.source?.kind === 'group-all' ||
|
|
169
170
|
(pending.source?.kind === 'group' &&
|
|
170
171
|
typeof pending.source.conversationId === 'string' &&
|
|
@@ -173,7 +174,8 @@ function isPendingMessage(value) {
|
|
|
173
174
|
if (!sourceValid || typeof message !== 'object' || message === null) {
|
|
174
175
|
return false;
|
|
175
176
|
}
|
|
176
|
-
return ((message.type === '
|
|
177
|
+
return ((message.type === 'user_im_message_receive_at' ||
|
|
178
|
+
message.type === 'user_im_message_receive_o2o' ||
|
|
177
179
|
message.type === 'user_im_message_receive_o2o_all' ||
|
|
178
180
|
message.type === 'user_im_message_receive_group' ||
|
|
179
181
|
message.type === 'user_im_message_receive_group_all') &&
|
|
@@ -262,6 +264,13 @@ function sourceLabel(source) {
|
|
|
262
264
|
return 'all group messages';
|
|
263
265
|
return 'group messages';
|
|
264
266
|
}
|
|
267
|
+
function sameImSource(left, right) {
|
|
268
|
+
if (left.kind !== right.kind)
|
|
269
|
+
return false;
|
|
270
|
+
if (left.kind !== 'group')
|
|
271
|
+
return true;
|
|
272
|
+
return right.kind === 'group' && left.conversationId === right.conversationId;
|
|
273
|
+
}
|
|
265
274
|
function retryLimit(error) {
|
|
266
275
|
if (!(error instanceof DwsEventProcessError))
|
|
267
276
|
return 1;
|
|
@@ -315,6 +324,7 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
315
324
|
userInstructions;
|
|
316
325
|
client;
|
|
317
326
|
imStates;
|
|
327
|
+
dwsMessagePrefix;
|
|
318
328
|
startReactionName;
|
|
319
329
|
endReactionName;
|
|
320
330
|
watchTodos;
|
|
@@ -325,13 +335,17 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
325
335
|
endReactionKeys = new Set();
|
|
326
336
|
notifiedSenderPairingNotifications = new Set();
|
|
327
337
|
processingMessages = new Map();
|
|
328
|
-
|
|
329
|
-
// Replay-started
|
|
338
|
+
queuedMessages = new Map();
|
|
339
|
+
// Replay-started dispatches only. The cap must not be consumed by
|
|
330
340
|
// live or followup traffic, whose queue entries outlive their turn.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
341
|
+
replayDispatches = new Map();
|
|
342
|
+
attemptedPendingMessages = new Set();
|
|
343
|
+
conversationTails = new Map();
|
|
344
|
+
messageStartResolvers = new Map();
|
|
345
|
+
processingMessageGenerations = new Map();
|
|
334
346
|
pendingMessageCapacityWaiters = new Set();
|
|
347
|
+
drainingImSubscriptions = new Set();
|
|
348
|
+
pendingImStartups = new Set();
|
|
335
349
|
pollAbortController = new AbortController();
|
|
336
350
|
lifecycleGeneration = 0;
|
|
337
351
|
connectionStartedAt = 0;
|
|
@@ -340,6 +354,7 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
340
354
|
notificationWatermarkPulledBack = false;
|
|
341
355
|
constructor(name, config, bridge, options, client) {
|
|
342
356
|
const profile = configuredString(config.profile, 'profile');
|
|
357
|
+
const messagePrefix = configuredString(config.messagePrefix, 'messagePrefix');
|
|
343
358
|
const startReactionName = configuredString(config.startReaction, 'startReaction') ??
|
|
344
359
|
DEFAULT_START_REACTION;
|
|
345
360
|
const endReactionName = configuredString(config.endReaction, 'endReaction');
|
|
@@ -383,6 +398,7 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
383
398
|
}));
|
|
384
399
|
this.startReactionName = startReactionName;
|
|
385
400
|
this.endReactionName = endReactionName;
|
|
401
|
+
this.dwsMessagePrefix = messagePrefix;
|
|
386
402
|
this.watchTodos = watchTodos;
|
|
387
403
|
}
|
|
388
404
|
createInitialCursor() {
|
|
@@ -464,7 +480,7 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
464
480
|
notificationCheckpoint: cursor.notificationCheckpoint,
|
|
465
481
|
mentionCheckpoint: cursor.mentionCheckpoint,
|
|
466
482
|
pendingDocumentNotifications: (cursor.pendingDocumentNotifications ?? []).slice(-MAX_PROCESSED_ITEMS),
|
|
467
|
-
pendingMessages: (cursor.pendingMessages ?? []).slice(
|
|
483
|
+
pendingMessages: (cursor.pendingMessages ?? []).slice(),
|
|
468
484
|
processedMessages: cursor.processedMessages.slice(-MAX_PROCESSED_ITEMS),
|
|
469
485
|
imTargets: cursor.imTargets.slice(-MAX_IM_TARGETS),
|
|
470
486
|
todosInitialized: cursor.todosInitialized ?? false,
|
|
@@ -477,9 +493,13 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
477
493
|
if (this.connected)
|
|
478
494
|
return;
|
|
479
495
|
const generation = ++this.lifecycleGeneration;
|
|
480
|
-
this.connectionStartedAt = Date.now();
|
|
481
496
|
this.pollAbortController.abort();
|
|
482
497
|
this.pollAbortController = new AbortController();
|
|
498
|
+
await this.waitForDisconnect();
|
|
499
|
+
if (generation !== this.lifecycleGeneration) {
|
|
500
|
+
throw new Error('DWS channel connection was cancelled.');
|
|
501
|
+
}
|
|
502
|
+
this.connectionStartedAt = Date.now();
|
|
483
503
|
await this.client.assertCompatible?.(this.pollAbortController.signal);
|
|
484
504
|
if (generation !== this.lifecycleGeneration) {
|
|
485
505
|
throw new Error('DWS channel connection was cancelled.');
|
|
@@ -539,7 +559,10 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
539
559
|
}
|
|
540
560
|
this.connected = true;
|
|
541
561
|
try {
|
|
542
|
-
|
|
562
|
+
const startups = this.imStates.map((state) => this.startImSourceWithRetry(state, generation));
|
|
563
|
+
for (const startup of startups)
|
|
564
|
+
this.trackImStartup(startup);
|
|
565
|
+
await Promise.all(startups);
|
|
543
566
|
if (generation !== this.lifecycleGeneration || !this.connected) {
|
|
544
567
|
throw new Error('DWS channel connection was cancelled.');
|
|
545
568
|
}
|
|
@@ -573,12 +596,13 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
573
596
|
this.cleanupReaction(key, 'disconnect reaction removal');
|
|
574
597
|
}
|
|
575
598
|
this.sessionReactionKeys.clear();
|
|
576
|
-
this.
|
|
577
|
-
this.
|
|
578
|
-
|
|
599
|
+
this.queuedMessages.clear();
|
|
600
|
+
this.replayDispatches.clear();
|
|
601
|
+
this.attemptedPendingMessages.clear();
|
|
602
|
+
for (const { resolve } of this.messageStartResolvers.values())
|
|
579
603
|
resolve();
|
|
580
|
-
this.
|
|
581
|
-
this.
|
|
604
|
+
this.messageStartResolvers.clear();
|
|
605
|
+
this.conversationTails.clear();
|
|
582
606
|
for (const resolve of this.pendingMessageCapacityWaiters)
|
|
583
607
|
resolve();
|
|
584
608
|
this.pendingMessageCapacityWaiters.clear();
|
|
@@ -587,11 +611,35 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
587
611
|
if (state.retryTimer)
|
|
588
612
|
clearTimeout(state.retryTimer);
|
|
589
613
|
state.retryTimer = undefined;
|
|
590
|
-
state.subscription
|
|
614
|
+
const subscription = state.subscription;
|
|
615
|
+
if (subscription) {
|
|
616
|
+
void this.drainImSubscription(subscription);
|
|
617
|
+
}
|
|
591
618
|
state.subscription = undefined;
|
|
592
619
|
state.restartAttempts = 0;
|
|
593
620
|
}
|
|
594
621
|
}
|
|
622
|
+
async waitForDisconnect() {
|
|
623
|
+
while (this.pendingImStartups.size > 0 ||
|
|
624
|
+
this.drainingImSubscriptions.size > 0) {
|
|
625
|
+
await Promise.all([
|
|
626
|
+
...this.pendingImStartups,
|
|
627
|
+
...this.drainingImSubscriptions,
|
|
628
|
+
]);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
trackImStartup(startup) {
|
|
632
|
+
const settled = startup.then(() => undefined, () => undefined);
|
|
633
|
+
this.pendingImStartups.add(settled);
|
|
634
|
+
void settled.then(() => this.pendingImStartups.delete(settled));
|
|
635
|
+
}
|
|
636
|
+
drainImSubscription(subscription) {
|
|
637
|
+
const drain = subscription.closed.then(() => undefined, () => undefined);
|
|
638
|
+
this.drainingImSubscriptions.add(drain);
|
|
639
|
+
void drain.then(() => this.drainingImSubscriptions.delete(drain));
|
|
640
|
+
subscription.stop();
|
|
641
|
+
return drain;
|
|
642
|
+
}
|
|
595
643
|
supportsProactiveSend() {
|
|
596
644
|
return true;
|
|
597
645
|
}
|
|
@@ -774,6 +822,7 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
774
822
|
const signal = this.pollAbortController.signal;
|
|
775
823
|
if (!this.connected || signal.aborted)
|
|
776
824
|
return;
|
|
825
|
+
this.attemptedPendingMessages.clear();
|
|
777
826
|
const endTime = Date.now();
|
|
778
827
|
await this.replayPendingMessages(signal);
|
|
779
828
|
if (signal.aborted || !this.connected)
|
|
@@ -793,8 +842,16 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
793
842
|
for (const message of mentions.messages) {
|
|
794
843
|
if (signal.aborted || !this.connected)
|
|
795
844
|
return;
|
|
796
|
-
|
|
845
|
+
const key = messageKey(message);
|
|
846
|
+
if (this.cursor.processedMessages.includes(key))
|
|
847
|
+
continue;
|
|
848
|
+
if (this.hasPendingMessage(key))
|
|
849
|
+
continue;
|
|
850
|
+
this.enqueuePendingConversation(message.conversationId);
|
|
851
|
+
await this.admitHistoryMessage({ kind: 'at' }, message);
|
|
797
852
|
}
|
|
853
|
+
if (signal.aborted || !this.connected)
|
|
854
|
+
return;
|
|
798
855
|
if (mentions.nextCursor) {
|
|
799
856
|
this.cursor.mentionCheckpoint = {
|
|
800
857
|
...mentionCheckpoint,
|
|
@@ -833,13 +890,14 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
833
890
|
// shared retry budget twice per poll.
|
|
834
891
|
if (this.hasPendingMessage(key))
|
|
835
892
|
continue;
|
|
836
|
-
|
|
893
|
+
this.enqueuePendingConversation(message.conversationId);
|
|
894
|
+
await this.admitHistoryMessage({ kind: 'direct' }, message);
|
|
837
895
|
}
|
|
838
896
|
if (signal.aborted || !this.connected)
|
|
839
897
|
return;
|
|
840
898
|
if (this.notificationWatermarkPulledBack) {
|
|
841
899
|
// R4-4: a stale direct message replayed while this window's
|
|
842
|
-
// fetch was in flight, and `
|
|
900
|
+
// fetch was in flight, and `admitReceivedMessage` pulled the
|
|
843
901
|
// watermark back
|
|
844
902
|
// to cover it. That replay was left UNMARKED on purpose for history
|
|
845
903
|
// polling, so finishing this window normally would undo the rescue:
|
|
@@ -958,6 +1016,7 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
958
1016
|
messageId: `todo-${fingerprint}`,
|
|
959
1017
|
text: `Process this DingTalk todo:\n${truncateCodePoints(title, MAX_COMMENT_CHARS)}`,
|
|
960
1018
|
displayText: title,
|
|
1019
|
+
bypassMessagePrefix: true,
|
|
961
1020
|
isGroup: true,
|
|
962
1021
|
isMentioned: true,
|
|
963
1022
|
isReplyToBot: false,
|
|
@@ -1001,19 +1060,21 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1001
1060
|
this.saveCursor();
|
|
1002
1061
|
}
|
|
1003
1062
|
async startImSource(state, generation) {
|
|
1063
|
+
const connectionStartedAt = this.connectionStartedAt;
|
|
1004
1064
|
const subscription = await this.client.subscribeToIm(state.source, (message) => {
|
|
1005
1065
|
state.lastError = undefined;
|
|
1006
1066
|
state.restartAttempts = 0;
|
|
1007
|
-
return state.source
|
|
1008
|
-
|
|
1009
|
-
|
|
1067
|
+
return this.receiveImMessage(state.source, message, false, false, {
|
|
1068
|
+
generation,
|
|
1069
|
+
connectionStartedAt,
|
|
1070
|
+
});
|
|
1010
1071
|
}, (error) => {
|
|
1011
1072
|
if (error instanceof DwsEventProcessError)
|
|
1012
1073
|
state.lastError = error;
|
|
1013
1074
|
this.logImError(state.source, error);
|
|
1014
1075
|
});
|
|
1015
1076
|
if (!this.connected || generation !== this.lifecycleGeneration) {
|
|
1016
|
-
|
|
1077
|
+
await this.drainImSubscription(subscription);
|
|
1017
1078
|
return;
|
|
1018
1079
|
}
|
|
1019
1080
|
state.lastError = undefined;
|
|
@@ -1092,45 +1153,27 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1092
1153
|
if (!this.connected)
|
|
1093
1154
|
return;
|
|
1094
1155
|
state.lastError = undefined;
|
|
1095
|
-
|
|
1156
|
+
const startup = this.startImSource(state, this.lifecycleGeneration).catch((error) => {
|
|
1096
1157
|
const resolvedError = error instanceof Error ? error : new Error(String(error));
|
|
1097
1158
|
this.logImError(state.source, resolvedError);
|
|
1098
1159
|
this.scheduleImRestart(state, resolvedError instanceof DwsEventProcessError
|
|
1099
1160
|
? resolvedError
|
|
1100
1161
|
: undefined);
|
|
1101
1162
|
});
|
|
1163
|
+
this.trackImStartup(startup);
|
|
1102
1164
|
}, delay);
|
|
1103
1165
|
state.retryTimer.unref?.();
|
|
1104
1166
|
}
|
|
1105
1167
|
logImError(source, error) {
|
|
1106
1168
|
process.stderr.write(`[Channel:${this.name}] DWS ${sanitizeLogText(sourceLabel(source), 120)} stream error: ${sanitizeLogText(error.message, 300)}\n`);
|
|
1107
1169
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
return;
|
|
1113
|
-
if (this.isStaleLiveMessage(message, fromHistory) &&
|
|
1114
|
-
message.eventTime !== undefined) {
|
|
1115
|
-
this.markProcessedMessage(messageKey(message));
|
|
1116
|
-
this.saveCursor();
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
if (source.kind === 'group-all' &&
|
|
1120
|
-
(this.config.groups[message.conversationId]?.requireMention ??
|
|
1121
|
-
this.config.groups['*']?.requireMention ??
|
|
1122
|
-
true)) {
|
|
1123
|
-
return;
|
|
1124
|
-
}
|
|
1125
|
-
if ((source.kind === 'group' || source.kind === 'group-all') &&
|
|
1126
|
-
this.config.groupPolicy === 'pairing' &&
|
|
1127
|
-
!this.groupGate.isGroupApproved(message.conversationId)) {
|
|
1128
|
-
return;
|
|
1129
|
-
}
|
|
1130
|
-
await this.dispatchImMessage(source, message, messageKey(message));
|
|
1170
|
+
requiresMention(conversationId) {
|
|
1171
|
+
return (this.config.groups[conversationId]?.requireMention ??
|
|
1172
|
+
this.config.groups['*']?.requireMention ??
|
|
1173
|
+
true);
|
|
1131
1174
|
}
|
|
1132
|
-
|
|
1133
|
-
const admission = this.
|
|
1175
|
+
receiveImMessage(source, message, fromHistory = false, reportFailure = fromHistory, admissionContext) {
|
|
1176
|
+
const admission = this.admitReceivedMessage(source, message, fromHistory, reportFailure, admissionContext);
|
|
1134
1177
|
const completed = admission.then(({ completion }) => completion);
|
|
1135
1178
|
void completed.catch(() => undefined);
|
|
1136
1179
|
return {
|
|
@@ -1138,68 +1181,145 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1138
1181
|
completed,
|
|
1139
1182
|
};
|
|
1140
1183
|
}
|
|
1141
|
-
async
|
|
1142
|
-
|
|
1143
|
-
|
|
1184
|
+
async admitReceivedMessage(source, message, fromHistory, reportFailure, admissionContext) {
|
|
1185
|
+
const isAmbientSource = source.kind === 'group' || source.kind === 'group-all';
|
|
1186
|
+
const isCurrentLifecycle = this.connected &&
|
|
1187
|
+
(admissionContext === undefined ||
|
|
1188
|
+
admissionContext.generation === this.lifecycleGeneration);
|
|
1189
|
+
if (!isCurrentLifecycle && (!admissionContext || !isAmbientSource)) {
|
|
1190
|
+
return { completion: Promise.resolve(), remembered: true };
|
|
1191
|
+
}
|
|
1144
1192
|
const key = messageKey(message);
|
|
1145
1193
|
if (this.markSelfMessageProcessed(message)) {
|
|
1146
|
-
return { completion: Promise.resolve() };
|
|
1194
|
+
return { completion: Promise.resolve(), remembered: true };
|
|
1147
1195
|
}
|
|
1148
|
-
if (this.isStaleLiveMessage(message, fromHistory) &&
|
|
1196
|
+
if (this.isStaleLiveMessage(message, fromHistory, admissionContext?.connectionStartedAt) &&
|
|
1149
1197
|
message.eventTime !== undefined) {
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
// parks was strictly outside every window the watermark would ever
|
|
1156
|
-
// produce, now and forever, since the window only moves forward. Pull
|
|
1157
|
-
// the watermark back to cover what we just declined to handle.
|
|
1158
|
-
this.cursor.notificationWatermark = Math.min(this.cursor.notificationWatermark ?? this.connectionStartedAt, message.eventTime);
|
|
1159
|
-
this.notificationWatermarkPulledBack = true;
|
|
1160
|
-
// R6-1: the flag alone only covers a replay that lands while a poll's
|
|
1161
|
-
// fetch is in flight — `pollOnce` clears it before every fetch, so a
|
|
1162
|
-
// pullback arriving BETWEEN two polls is reset before it is ever read.
|
|
1163
|
-
// A persisted multi-page checkpoint would then resume its own window,
|
|
1164
|
-
// whose `startTime` is already past this replay, and finish by setting
|
|
1165
|
-
// the watermark to `checkpoint.endTime` — pulling it forward again over
|
|
1166
|
-
// a replay no window will ever cover. Drop the checkpoint here too, so
|
|
1167
|
-
// the pullback survives regardless of when it arrived.
|
|
1168
|
-
this.cursor.notificationCheckpoint = undefined;
|
|
1169
|
-
process.stderr.write(`[Channel:${this.name}] parked a stale direct message for history polling and pulled the watermark back to ${message.eventTime}: ${sanitizeLogText(message.messageId, 120)}\n`);
|
|
1198
|
+
if (source.kind === 'direct') {
|
|
1199
|
+
this.parkStaleDirectMessage(message);
|
|
1200
|
+
return { completion: Promise.resolve(), remembered: true };
|
|
1201
|
+
}
|
|
1202
|
+
this.markProcessedMessage(key);
|
|
1170
1203
|
this.saveCursor();
|
|
1171
|
-
return { completion: Promise.resolve() };
|
|
1204
|
+
return { completion: Promise.resolve(), remembered: true };
|
|
1205
|
+
}
|
|
1206
|
+
if (this.shouldFilterImMessage(source, message)) {
|
|
1207
|
+
if (source.kind === 'group' || source.kind === 'group-all') {
|
|
1208
|
+
const text = stripMessagePrefix(message.content.trim(), this.dwsMessagePrefix);
|
|
1209
|
+
if (!this.dwsMessagePrefix || text) {
|
|
1210
|
+
const envelope = this.createImEnvelope(source, message, text);
|
|
1211
|
+
if (this.groupGate.check(envelope, { createPairingRequest: false })
|
|
1212
|
+
.reason === 'mention_required' &&
|
|
1213
|
+
!this.queuedMessages.has(key) &&
|
|
1214
|
+
!this.cursor.processedMessages.includes(key) &&
|
|
1215
|
+
!this.hasPendingMessage(key)) {
|
|
1216
|
+
this.recordPendingGroupHistory(envelope);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
this.removePersistedPendingMessageForSource(key, source);
|
|
1221
|
+
return { completion: Promise.resolve(), remembered: true };
|
|
1222
|
+
}
|
|
1223
|
+
if (!isCurrentLifecycle) {
|
|
1224
|
+
if (this.cursor.processedMessages.includes(key)) {
|
|
1225
|
+
this.removePersistedPendingMessage(key);
|
|
1226
|
+
return { completion: Promise.resolve(), remembered: true };
|
|
1227
|
+
}
|
|
1228
|
+
this.rememberDrainingPendingMessage(source, message);
|
|
1229
|
+
return { completion: Promise.resolve(), remembered: true };
|
|
1230
|
+
}
|
|
1231
|
+
return this.admitMessage(source, message, key, reportFailure);
|
|
1232
|
+
}
|
|
1233
|
+
async admitHistoryMessage(source, message) {
|
|
1234
|
+
const { completion, remembered } = await this.admitReceivedMessage(source, message, true, true);
|
|
1235
|
+
if (remembered) {
|
|
1236
|
+
void completion.catch(() => undefined);
|
|
1237
|
+
return;
|
|
1172
1238
|
}
|
|
1173
|
-
|
|
1239
|
+
await completion;
|
|
1240
|
+
}
|
|
1241
|
+
shouldFilterImMessage(source, message) {
|
|
1242
|
+
const groupConfig = this.config.groups[message.conversationId] ?? this.config.groups['*'];
|
|
1243
|
+
if ((source.kind === 'group' || source.kind === 'group-all') &&
|
|
1244
|
+
(groupConfig?.requireMention ?? true)) {
|
|
1245
|
+
return true;
|
|
1246
|
+
}
|
|
1247
|
+
return ((source.kind === 'group' || source.kind === 'group-all') &&
|
|
1248
|
+
this.config.groupPolicy === 'pairing' &&
|
|
1249
|
+
!this.groupGate.isGroupApproved(message.conversationId));
|
|
1250
|
+
}
|
|
1251
|
+
parkStaleDirectMessage(message) {
|
|
1252
|
+
// A replayed direct message is left UNMARKED on purpose, for history
|
|
1253
|
+
// polling to pick up. Pull the watermark back because the normal window
|
|
1254
|
+
// starts after this message and only moves forward.
|
|
1255
|
+
this.cursor.notificationWatermark = Math.min(this.cursor.notificationWatermark ?? this.connectionStartedAt, message.eventTime);
|
|
1256
|
+
this.notificationWatermarkPulledBack = true;
|
|
1257
|
+
// R6-1: also drop a persisted multi-page checkpoint so a replay that
|
|
1258
|
+
// arrives between polls cannot be skipped when that checkpoint resumes.
|
|
1259
|
+
this.cursor.notificationCheckpoint = undefined;
|
|
1260
|
+
process.stderr.write(`[Channel:${this.name}] parked a stale direct message for history polling and pulled the watermark back to ${message.eventTime}: ${sanitizeLogText(message.messageId, 120)}\n`);
|
|
1261
|
+
this.saveCursor();
|
|
1174
1262
|
}
|
|
1175
1263
|
markSelfMessageProcessed(message) {
|
|
1176
1264
|
if (!this.isSelfMessage(message))
|
|
1177
1265
|
return false;
|
|
1178
|
-
|
|
1266
|
+
const key = messageKey(message);
|
|
1267
|
+
this.markProcessedMessage(key);
|
|
1268
|
+
this.removePendingMessage(key);
|
|
1179
1269
|
this.saveCursor();
|
|
1180
1270
|
return true;
|
|
1181
1271
|
}
|
|
1182
|
-
isStaleLiveMessage(message, fromHistory) {
|
|
1272
|
+
isStaleLiveMessage(message, fromHistory, connectionStartedAt = this.connectionStartedAt) {
|
|
1183
1273
|
return (!fromHistory &&
|
|
1184
1274
|
message.eventTime !== undefined &&
|
|
1185
|
-
message.eventTime <
|
|
1275
|
+
message.eventTime < connectionStartedAt - 5_000);
|
|
1186
1276
|
}
|
|
1187
|
-
async
|
|
1277
|
+
async admitMessage(source, message, key, reportFailure) {
|
|
1188
1278
|
if (this.cursor.processedMessages.includes(key)) {
|
|
1189
1279
|
this.removePersistedPendingMessage(key);
|
|
1190
|
-
return { completion: Promise.resolve() };
|
|
1280
|
+
return { completion: Promise.resolve(), remembered: true };
|
|
1191
1281
|
}
|
|
1282
|
+
if (!reportFailure) {
|
|
1283
|
+
this.enqueuePendingConversation(message.conversationId);
|
|
1284
|
+
}
|
|
1285
|
+
let remembered = true;
|
|
1192
1286
|
if (!this.hasPendingMessage(key)) {
|
|
1193
|
-
|
|
1194
|
-
|
|
1287
|
+
const dispatchUnparkedAtCapacity = (source.kind === 'at' || source.kind === 'direct') &&
|
|
1288
|
+
this.connected &&
|
|
1289
|
+
(this.cursor.pendingMessages?.length ?? 0) >= MAX_PROCESSED_ITEMS;
|
|
1290
|
+
remembered = dispatchUnparkedAtCapacity
|
|
1291
|
+
? false
|
|
1292
|
+
: source.kind === 'group' || source.kind === 'group-all'
|
|
1293
|
+
? await this.rememberPendingMessageWhenAvailable(source, message)
|
|
1294
|
+
: await this.rememberPendingMessage(source, message);
|
|
1295
|
+
if (!remembered) {
|
|
1296
|
+
if (!dispatchUnparkedAtCapacity) {
|
|
1297
|
+
return { completion: Promise.resolve(), remembered };
|
|
1298
|
+
}
|
|
1195
1299
|
}
|
|
1196
1300
|
}
|
|
1197
1301
|
return {
|
|
1198
|
-
completion: this.
|
|
1302
|
+
completion: this.scheduleMessage(source, message, key, reportFailure),
|
|
1303
|
+
remembered,
|
|
1199
1304
|
};
|
|
1200
1305
|
}
|
|
1201
|
-
|
|
1202
|
-
const
|
|
1306
|
+
enqueuePendingConversation(conversationId) {
|
|
1307
|
+
for (const pending of [...(this.cursor.pendingMessages ?? [])]) {
|
|
1308
|
+
if (pending.message.conversationId !== conversationId)
|
|
1309
|
+
continue;
|
|
1310
|
+
const key = messageKey(pending.message);
|
|
1311
|
+
if (this.attemptedPendingMessages.has(key))
|
|
1312
|
+
continue;
|
|
1313
|
+
if (this.queuedMessages.has(key))
|
|
1314
|
+
continue;
|
|
1315
|
+
const dispatch = this.receiveImMessage(pending.source, pending.message, true, true);
|
|
1316
|
+
void dispatch.admitted.catch((error) => {
|
|
1317
|
+
process.stderr.write(`[Channel:${this.name}] pending DWS message remains degraded: ${sanitizeLogText(error instanceof Error ? error.message : String(error), 300)}\n`);
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
scheduleMessage(source, message, key, reportFailure) {
|
|
1322
|
+
const queued = this.queuedMessages.get(key);
|
|
1203
1323
|
if (queued)
|
|
1204
1324
|
return queued;
|
|
1205
1325
|
if (this.cursor.processedMessages.includes(key)) {
|
|
@@ -1211,11 +1331,11 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1211
1331
|
try {
|
|
1212
1332
|
if (!this.connected || generation !== this.lifecycleGeneration)
|
|
1213
1333
|
return;
|
|
1214
|
-
await this.dispatchImMessage(source, message, key);
|
|
1334
|
+
await this.dispatchImMessage(source, message, key, generation);
|
|
1215
1335
|
}
|
|
1216
1336
|
finally {
|
|
1217
|
-
if (this.
|
|
1218
|
-
this.
|
|
1337
|
+
if (this.queuedMessages.get(key) === task) {
|
|
1338
|
+
this.queuedMessages.delete(key);
|
|
1219
1339
|
}
|
|
1220
1340
|
}
|
|
1221
1341
|
};
|
|
@@ -1223,24 +1343,30 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1223
1343
|
const started = new Promise((resolve) => {
|
|
1224
1344
|
resolveStarted = resolve;
|
|
1225
1345
|
});
|
|
1226
|
-
const previous = this.
|
|
1346
|
+
const previous = this.conversationTails.get(message.conversationId);
|
|
1347
|
+
const groupConfig = source.kind === 'direct'
|
|
1348
|
+
? undefined
|
|
1349
|
+
: (this.config.groups[message.conversationId] ??
|
|
1350
|
+
this.config.groups['*']);
|
|
1227
1351
|
// Live turns only need FIFO through ChannelBase registration; replay and
|
|
1228
1352
|
// followup turns must wait for the prior turn to finish.
|
|
1229
|
-
const predecessor = reportFailure ||
|
|
1353
|
+
const predecessor = reportFailure ||
|
|
1354
|
+
(groupConfig?.dispatchMode ?? this.config.dispatchMode) === 'followup'
|
|
1230
1355
|
? previous?.completed
|
|
1231
1356
|
: previous?.started;
|
|
1232
1357
|
const task = predecessor
|
|
1233
1358
|
? predecessor.catch(() => undefined).then(dispatch)
|
|
1234
1359
|
: Promise.resolve().then(dispatch);
|
|
1235
1360
|
const tail = { started, completed: task };
|
|
1236
|
-
this.
|
|
1237
|
-
|
|
1238
|
-
this.
|
|
1361
|
+
this.queuedMessages.set(key, task);
|
|
1362
|
+
const startResolver = { generation, resolve: resolveStarted };
|
|
1363
|
+
this.messageStartResolvers.set(key, startResolver);
|
|
1364
|
+
this.conversationTails.set(message.conversationId, tail);
|
|
1239
1365
|
void task
|
|
1240
1366
|
.finally(() => {
|
|
1241
|
-
this.
|
|
1242
|
-
if (this.
|
|
1243
|
-
this.
|
|
1367
|
+
this.releaseMessageStart(message.conversationId, message.messageId, startResolver);
|
|
1368
|
+
if (this.conversationTails.get(message.conversationId) === tail) {
|
|
1369
|
+
this.conversationTails.delete(message.conversationId);
|
|
1244
1370
|
}
|
|
1245
1371
|
})
|
|
1246
1372
|
.catch(() => undefined);
|
|
@@ -1251,25 +1377,33 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1251
1377
|
}
|
|
1252
1378
|
return task;
|
|
1253
1379
|
}
|
|
1254
|
-
async dispatchImMessage(source, message, key) {
|
|
1380
|
+
async dispatchImMessage(source, message, key, generation) {
|
|
1255
1381
|
let waitedOnInFlight = false;
|
|
1382
|
+
let waitedOnCurrentGeneration = false;
|
|
1256
1383
|
let inFlightError;
|
|
1257
1384
|
while (true) {
|
|
1258
1385
|
const existing = this.processingMessages.get(key);
|
|
1259
1386
|
if (!existing)
|
|
1260
1387
|
break;
|
|
1261
1388
|
waitedOnInFlight = true;
|
|
1389
|
+
if (this.processingMessageGenerations.get(key) === generation) {
|
|
1390
|
+
waitedOnCurrentGeneration = true;
|
|
1391
|
+
}
|
|
1262
1392
|
inFlightError = await existing.then(() => undefined, (error) => error);
|
|
1263
1393
|
}
|
|
1394
|
+
if (!this.connected || generation !== this.lifecycleGeneration)
|
|
1395
|
+
return;
|
|
1264
1396
|
if (this.cursor.processedMessages.includes(key))
|
|
1265
1397
|
return;
|
|
1266
|
-
//
|
|
1267
|
-
//
|
|
1268
|
-
//
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1398
|
+
// A duplicate in the same lifecycle must not spend the retry budget twice
|
|
1399
|
+
// in one poll. A replay from a later reconnect generation is different:
|
|
1400
|
+
// it owns the persisted retry and must run before newer conversation work.
|
|
1401
|
+
if (waitedOnInFlight &&
|
|
1402
|
+
waitedOnCurrentGeneration &&
|
|
1403
|
+
this.hasPendingMessage(key)) {
|
|
1272
1404
|
throw inFlightError;
|
|
1405
|
+
}
|
|
1406
|
+
this.processingMessageGenerations.set(key, generation);
|
|
1273
1407
|
const task = this.processImMessage(source, message, key);
|
|
1274
1408
|
this.processingMessages.set(key, task);
|
|
1275
1409
|
try {
|
|
@@ -1282,29 +1416,83 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1282
1416
|
finally {
|
|
1283
1417
|
if (this.processingMessages.get(key) === task) {
|
|
1284
1418
|
this.processingMessages.delete(key);
|
|
1419
|
+
this.processingMessageGenerations.delete(key);
|
|
1285
1420
|
}
|
|
1286
1421
|
}
|
|
1287
1422
|
}
|
|
1288
1423
|
async processImMessage(source, message, key) {
|
|
1424
|
+
// DWS reports only that the bot was mentioned somewhere, not which token is
|
|
1425
|
+
// the bot, so a leading mention goes only when what follows is unambiguously
|
|
1426
|
+
// a slash command and no later mention could be the one that caused the
|
|
1427
|
+
// at-event. That scan shares the document-mention parser's boundary
|
|
1428
|
+
// heuristic: an `@` glued to a word is an address like `bob@example.com`,
|
|
1429
|
+
// one behind punctuation is a rival mention. The command lookahead must stay
|
|
1430
|
+
// ahead of the whole-remainder scan; swapping them re-runs it at every
|
|
1431
|
+
// backtracked space, quadratic on attacker-controlled content.
|
|
1432
|
+
//
|
|
1433
|
+
// Mention-required conversations only: an ambient group delivers one message
|
|
1434
|
+
// on both the at stream and its group stream under a single dedup key, and
|
|
1435
|
+
// only when a mention is required is the at stream the sole deliverer, so
|
|
1436
|
+
// the normalized text cannot depend on which copy won the race.
|
|
1437
|
+
const rawText = source.kind === 'at' && this.requiresMention(message.conversationId)
|
|
1438
|
+
? message.content
|
|
1439
|
+
.replace(/^\s*@[^\s\p{Cf}]+\s+(?=\/[a-zA-Z0-9_:-]+(?:\s|$))(?![\s\S]*(?<![A-Za-z0-9_])@)/u, '')
|
|
1440
|
+
.trim()
|
|
1441
|
+
: message.content.trim();
|
|
1442
|
+
const providerDocumentNotification = source.kind === 'direct'
|
|
1443
|
+
? parseDocumentMentionNotification(rawText)
|
|
1444
|
+
: undefined;
|
|
1445
|
+
const text = providerDocumentNotification
|
|
1446
|
+
? rawText
|
|
1447
|
+
: stripMessagePrefix(rawText, this.dwsMessagePrefix);
|
|
1448
|
+
if (this.dwsMessagePrefix && !text) {
|
|
1449
|
+
this.markProcessedMessage(key);
|
|
1450
|
+
this.saveCursor();
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1289
1453
|
const target = source.kind === 'direct'
|
|
1290
1454
|
? { kind: 'direct', openDingTalkId: message.senderId }
|
|
1291
1455
|
: { kind: 'group', conversationId: message.conversationId };
|
|
1292
1456
|
this.rememberImTarget(message.conversationId, target);
|
|
1293
|
-
const text = message.content.trim();
|
|
1294
1457
|
if (!text) {
|
|
1295
1458
|
this.markProcessedMessage(key);
|
|
1296
1459
|
this.saveCursor();
|
|
1297
1460
|
return;
|
|
1298
1461
|
}
|
|
1299
|
-
const documentNotification =
|
|
1300
|
-
|
|
1301
|
-
|
|
1462
|
+
const documentNotification = providerDocumentNotification ??
|
|
1463
|
+
(source.kind === 'direct'
|
|
1464
|
+
? parseDocumentMentionNotification(text)
|
|
1465
|
+
: undefined);
|
|
1302
1466
|
if (documentNotification) {
|
|
1303
1467
|
await this.processDocumentNotification(message, key, documentNotification);
|
|
1304
1468
|
return;
|
|
1305
1469
|
}
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1470
|
+
const envelope = this.createImEnvelope(source, message, text);
|
|
1471
|
+
this.rememberInboundReactionTarget(message.conversationId, message.messageId);
|
|
1472
|
+
try {
|
|
1473
|
+
await this.handleInbound(envelope);
|
|
1474
|
+
}
|
|
1475
|
+
catch (error) {
|
|
1476
|
+
this.attemptedPendingMessages.add(key);
|
|
1477
|
+
// Under budget the throw propagates so redelivery observes the failure
|
|
1478
|
+
// while persisted replay can retry it. Once the budget is spent the
|
|
1479
|
+
// message is marked processed and the error is swallowed, which is the
|
|
1480
|
+
// only thing that lets the watermark move past it.
|
|
1481
|
+
if (this.recordInboundFailure(key, error, () => {
|
|
1482
|
+
this.markProcessedMessage(key);
|
|
1483
|
+
this.removePendingMessage(key);
|
|
1484
|
+
})) {
|
|
1485
|
+
throw error;
|
|
1486
|
+
}
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
this.clearInboundFailure(key);
|
|
1490
|
+
this.removePendingMessage(key);
|
|
1491
|
+
this.markProcessedMessage(key);
|
|
1492
|
+
this.saveCursor();
|
|
1493
|
+
}
|
|
1494
|
+
createImEnvelope(source, message, text = message.content.trim()) {
|
|
1495
|
+
return {
|
|
1308
1496
|
channelName: this.name,
|
|
1309
1497
|
senderId: message.senderId,
|
|
1310
1498
|
senderName: message.senderName,
|
|
@@ -1312,10 +1500,11 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1312
1500
|
chatName: message.conversationId,
|
|
1313
1501
|
messageId: message.messageId,
|
|
1314
1502
|
text,
|
|
1503
|
+
bypassMessagePrefix: true,
|
|
1315
1504
|
...(message.referencedText
|
|
1316
1505
|
? { referencedText: message.referencedText }
|
|
1317
1506
|
: {}),
|
|
1318
|
-
isGroup,
|
|
1507
|
+
isGroup: source.kind !== 'direct',
|
|
1319
1508
|
isMentioned: source.kind === 'at',
|
|
1320
1509
|
isReplyToBot: false,
|
|
1321
1510
|
metadata: [
|
|
@@ -1324,34 +1513,6 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1324
1513
|
`DWS event ID: ${message.eventId}`,
|
|
1325
1514
|
].join('\n'),
|
|
1326
1515
|
};
|
|
1327
|
-
this.rememberInboundReactionTarget(message.conversationId, message.messageId);
|
|
1328
|
-
try {
|
|
1329
|
-
await this.handleInbound(envelope);
|
|
1330
|
-
}
|
|
1331
|
-
catch (error) {
|
|
1332
|
-
if (source.kind !== 'at') {
|
|
1333
|
-
// R12-1: park failed direct messages next to ambient group ones.
|
|
1334
|
-
// Direct-message history may be unavailable, and ambient group
|
|
1335
|
-
// messages have no history fallback. `at` messages need no parking:
|
|
1336
|
-
// the pinned mention checkpoint re-fetches them.
|
|
1337
|
-
await this.rememberFailedMessage(source, message);
|
|
1338
|
-
}
|
|
1339
|
-
// Under budget the throw propagates exactly as before, so redelivery
|
|
1340
|
-
// and concurrent-duplicate retry keep their contracts. Once the budget
|
|
1341
|
-
// is spent the message is marked processed and the error is swallowed,
|
|
1342
|
-
// which is the only thing that lets the watermark move past it.
|
|
1343
|
-
if (this.recordInboundFailure(key, error, () => {
|
|
1344
|
-
this.markProcessedMessage(key);
|
|
1345
|
-
this.removePendingMessage(key);
|
|
1346
|
-
})) {
|
|
1347
|
-
throw error;
|
|
1348
|
-
}
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
this.clearInboundFailure(key);
|
|
1352
|
-
this.removePendingMessage(key);
|
|
1353
|
-
this.markProcessedMessage(key);
|
|
1354
|
-
this.saveCursor();
|
|
1355
1516
|
}
|
|
1356
1517
|
/**
|
|
1357
1518
|
* Account one failed inbound turn, and drop the message once its budget is
|
|
@@ -1416,15 +1577,18 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1416
1577
|
this.saveCursor();
|
|
1417
1578
|
}
|
|
1418
1579
|
catch (error) {
|
|
1580
|
+
if (source.kind === 'group' || source.kind === 'group-all') {
|
|
1581
|
+
return true;
|
|
1582
|
+
}
|
|
1419
1583
|
this.removePendingMessage(key);
|
|
1420
1584
|
throw error;
|
|
1421
1585
|
}
|
|
1422
1586
|
return true;
|
|
1423
1587
|
}
|
|
1424
|
-
async
|
|
1588
|
+
async rememberPendingMessageWhenAvailable(source, message) {
|
|
1425
1589
|
const key = messageKey(message);
|
|
1426
1590
|
if (this.hasPendingMessage(key))
|
|
1427
|
-
return;
|
|
1591
|
+
return true;
|
|
1428
1592
|
const generation = this.lifecycleGeneration;
|
|
1429
1593
|
while (this.connected &&
|
|
1430
1594
|
generation === this.lifecycleGeneration &&
|
|
@@ -1433,6 +1597,19 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1433
1597
|
this.pendingMessageCapacityWaiters.add(resolve);
|
|
1434
1598
|
});
|
|
1435
1599
|
}
|
|
1600
|
+
if (this.hasPendingMessage(key))
|
|
1601
|
+
return true;
|
|
1602
|
+
if (!this.connected || generation !== this.lifecycleGeneration) {
|
|
1603
|
+
// Ambient group streams have no history fallback. A disconnect wakes
|
|
1604
|
+
// capacity waiters, so persist their already-read message before the
|
|
1605
|
+
// event process exits even when that temporarily exceeds the soft cap.
|
|
1606
|
+
this.rememberDrainingPendingMessage(source, message);
|
|
1607
|
+
return true;
|
|
1608
|
+
}
|
|
1609
|
+
return this.rememberPendingMessage(source, message);
|
|
1610
|
+
}
|
|
1611
|
+
rememberDrainingPendingMessage(source, message) {
|
|
1612
|
+
const key = messageKey(message);
|
|
1436
1613
|
if (this.hasPendingMessage(key))
|
|
1437
1614
|
return;
|
|
1438
1615
|
const pending = this.cursor.pendingMessages ?? [];
|
|
@@ -1441,14 +1618,20 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1441
1618
|
try {
|
|
1442
1619
|
this.saveCursor();
|
|
1443
1620
|
}
|
|
1444
|
-
catch {
|
|
1445
|
-
|
|
1621
|
+
catch (error) {
|
|
1622
|
+
process.stderr.write(`[Channel:${this.name}] could not persist a draining DWS message; keeping it in memory: ${sanitizeLogText(error instanceof Error ? error.message : String(error), 300)}\n`);
|
|
1446
1623
|
}
|
|
1447
1624
|
}
|
|
1448
1625
|
removePersistedPendingMessage(key) {
|
|
1449
1626
|
if (this.removePendingMessage(key))
|
|
1450
1627
|
this.saveCursor();
|
|
1451
1628
|
}
|
|
1629
|
+
removePersistedPendingMessageForSource(key, source) {
|
|
1630
|
+
const ownsPendingMessage = (this.cursor.pendingMessages ?? []).some((pending) => messageKey(pending.message) === key &&
|
|
1631
|
+
sameImSource(pending.source, source));
|
|
1632
|
+
if (ownsPendingMessage)
|
|
1633
|
+
this.removePersistedPendingMessage(key);
|
|
1634
|
+
}
|
|
1452
1635
|
removePendingMessage(key) {
|
|
1453
1636
|
const pending = this.cursor.pendingMessages ?? [];
|
|
1454
1637
|
const remaining = pending.filter((item) => messageKey(item.message) !== key);
|
|
@@ -1519,6 +1702,7 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1519
1702
|
threadId: notification.commentKey,
|
|
1520
1703
|
messageId: message.messageId,
|
|
1521
1704
|
text: truncateCodePoints(notification.request, MAX_COMMENT_CHARS),
|
|
1705
|
+
bypassMessagePrefix: true,
|
|
1522
1706
|
isGroup: true,
|
|
1523
1707
|
isMentioned: true,
|
|
1524
1708
|
isReplyToBot: false,
|
|
@@ -1573,37 +1757,71 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1573
1757
|
}
|
|
1574
1758
|
}
|
|
1575
1759
|
async replayPendingMessages(signal) {
|
|
1760
|
+
const activeBySource = new Map();
|
|
1761
|
+
const activeConversations = new Set();
|
|
1762
|
+
for (const { sourceKind, conversationId, } of this.replayDispatches.values()) {
|
|
1763
|
+
if (!activeConversations.has(conversationId)) {
|
|
1764
|
+
activeBySource.set(sourceKind, (activeBySource.get(sourceKind) ?? 0) + 1);
|
|
1765
|
+
activeConversations.add(conversationId);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
const selectedConversations = new Set();
|
|
1769
|
+
const blockedConversations = new Set();
|
|
1576
1770
|
for (const pending of [...(this.cursor.pendingMessages ?? [])]) {
|
|
1577
1771
|
if (signal.aborted || !this.connected)
|
|
1578
1772
|
return;
|
|
1579
1773
|
const key = messageKey(pending.message);
|
|
1580
|
-
if (
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1774
|
+
if (this.queuedMessages.has(key))
|
|
1775
|
+
continue;
|
|
1776
|
+
if (this.markSelfMessageProcessed(pending.message))
|
|
1777
|
+
continue;
|
|
1778
|
+
if (this.shouldFilterImMessage(pending.source, pending.message)) {
|
|
1779
|
+
if (pending.source.kind === 'group' ||
|
|
1780
|
+
pending.source.kind === 'group-all') {
|
|
1781
|
+
const text = stripMessagePrefix(pending.message.content.trim(), this.dwsMessagePrefix);
|
|
1782
|
+
if (!this.dwsMessagePrefix || text) {
|
|
1783
|
+
const envelope = this.createImEnvelope(pending.source, pending.message, text);
|
|
1784
|
+
if (this.groupGate.check(envelope, { createPairingRequest: false })
|
|
1785
|
+
.reason === 'mention_required') {
|
|
1786
|
+
this.recordPendingGroupHistory(envelope);
|
|
1787
|
+
}
|
|
1592
1788
|
}
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1789
|
+
}
|
|
1790
|
+
this.removePersistedPendingMessageForSource(key, pending.source);
|
|
1595
1791
|
continue;
|
|
1596
1792
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1793
|
+
if (activeConversations.has(pending.message.conversationId))
|
|
1794
|
+
continue;
|
|
1795
|
+
if (blockedConversations.has(pending.message.conversationId))
|
|
1796
|
+
continue;
|
|
1797
|
+
if (!selectedConversations.has(pending.message.conversationId)) {
|
|
1798
|
+
const activeForSource = activeBySource.get(pending.source.kind) ?? 0;
|
|
1799
|
+
if (activeForSource >= MAX_REPLAY_CONVERSATIONS) {
|
|
1800
|
+
blockedConversations.add(pending.message.conversationId);
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
activeBySource.set(pending.source.kind, activeForSource + 1);
|
|
1804
|
+
selectedConversations.add(pending.message.conversationId);
|
|
1601
1805
|
}
|
|
1602
|
-
|
|
1806
|
+
const dispatch = this.receiveImMessage(pending.source, pending.message, true, true);
|
|
1807
|
+
const completed = dispatch.completed;
|
|
1808
|
+
const replay = {
|
|
1809
|
+
sourceKind: pending.source.kind,
|
|
1810
|
+
conversationId: pending.message.conversationId,
|
|
1811
|
+
};
|
|
1812
|
+
this.replayDispatches.set(key, replay);
|
|
1813
|
+
void dispatch.admitted.catch((error) => {
|
|
1603
1814
|
if (signal.aborted || !this.connected)
|
|
1604
1815
|
return;
|
|
1605
1816
|
process.stderr.write(`[Channel:${this.name}] pending DWS message remains degraded: ${sanitizeLogText(error instanceof Error ? error.message : String(error), 300)}\n`);
|
|
1606
|
-
}
|
|
1817
|
+
});
|
|
1818
|
+
void completed
|
|
1819
|
+
.finally(() => {
|
|
1820
|
+
if (this.replayDispatches.get(key) === replay) {
|
|
1821
|
+
this.replayDispatches.delete(key);
|
|
1822
|
+
}
|
|
1823
|
+
})
|
|
1824
|
+
.catch(() => undefined);
|
|
1607
1825
|
}
|
|
1608
1826
|
}
|
|
1609
1827
|
async replayPendingDocumentNotifications(signal) {
|
|
@@ -1700,13 +1918,17 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1700
1918
|
reactionKey(conversationId, messageId) {
|
|
1701
1919
|
return `${conversationId}\0${messageId}`;
|
|
1702
1920
|
}
|
|
1703
|
-
|
|
1921
|
+
releaseMessageStart(conversationId, messageId, expected) {
|
|
1704
1922
|
const key = `${conversationId}\0${messageId}`;
|
|
1705
|
-
const
|
|
1706
|
-
if (!
|
|
1923
|
+
const resolver = this.messageStartResolvers.get(key);
|
|
1924
|
+
if (!resolver || (expected && resolver !== expected))
|
|
1707
1925
|
return;
|
|
1708
|
-
|
|
1709
|
-
|
|
1926
|
+
if (!expected &&
|
|
1927
|
+
this.processingMessageGenerations.get(key) !== resolver.generation) {
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
this.messageStartResolvers.delete(key);
|
|
1931
|
+
resolver.resolve();
|
|
1710
1932
|
}
|
|
1711
1933
|
rememberInboundReactionTarget(chatId, messageId, conversationId = chatId) {
|
|
1712
1934
|
const key = this.reactionKey(chatId, messageId);
|
|
@@ -1866,7 +2088,7 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1866
2088
|
onTaskLifecycle(event) {
|
|
1867
2089
|
if (event.type === 'started') {
|
|
1868
2090
|
if (event.messageId) {
|
|
1869
|
-
this.
|
|
2091
|
+
this.releaseMessageStart(event.chatId, event.messageId);
|
|
1870
2092
|
}
|
|
1871
2093
|
this.startReaction(event.chatId, event.messageId, event.sessionId);
|
|
1872
2094
|
return;
|