@qwen-code/channel-dws 0.22.3 → 0.23.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/dws-channel.d.ts +34 -6
- package/dist/dws-channel.d.ts.map +1 -1
- package/dist/dws-channel.js +385 -106
- package/dist/dws-channel.js.map +1 -1
- package/dist/dws-client.d.ts +7 -2
- package/dist/dws-client.d.ts.map +1 -1
- package/dist/dws-client.js +22 -1
- package/dist/dws-client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/dws-channel.js
CHANGED
|
@@ -12,6 +12,7 @@ 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 MAX_DIRECT_REPLAY_DISPATCHES = 16;
|
|
15
16
|
/**
|
|
16
17
|
* How many times one inbound message may fail its turn before it is dropped.
|
|
17
18
|
*
|
|
@@ -28,7 +29,7 @@ const EVENT_RESTART_DELAY_MS = 2_000;
|
|
|
28
29
|
const EVENT_RESTART_MAX_DELAY_MS = 5 * 60_000;
|
|
29
30
|
const NO_REPLY_SENTINEL = '[NO_REPLY]';
|
|
30
31
|
const NO_REPLY_SENTINEL_PATTERN = /^\[NO_REPLY\][.!]?$/i;
|
|
31
|
-
const
|
|
32
|
+
export const DEFAULT_START_REACTION = '🤔';
|
|
32
33
|
const MAX_INBOUND_REACTION_TARGETS = 1_000;
|
|
33
34
|
const NOTIFICATION_HISTORY_OVERLAP_MS = 5_000;
|
|
34
35
|
const NOTIFICATION_POLL_INTERVAL_MS = 5_000;
|
|
@@ -314,12 +315,23 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
314
315
|
userInstructions;
|
|
315
316
|
client;
|
|
316
317
|
imStates;
|
|
318
|
+
startReactionName;
|
|
319
|
+
endReactionName;
|
|
317
320
|
watchTodos;
|
|
318
321
|
inboundReactionTargets = new Map();
|
|
319
|
-
|
|
322
|
+
activeReactions = new Map();
|
|
320
323
|
sessionReactionKeys = new Map();
|
|
324
|
+
reactionOperations = new Map();
|
|
325
|
+
endReactionKeys = new Set();
|
|
321
326
|
notifiedSenderPairingNotifications = new Set();
|
|
322
327
|
processingMessages = new Map();
|
|
328
|
+
queuedDirectMessages = new Map();
|
|
329
|
+
// Replay-started direct dispatches only. The cap must not be consumed by
|
|
330
|
+
// live or followup traffic, whose queue entries outlive their turn.
|
|
331
|
+
replayDirectDispatches = new Map();
|
|
332
|
+
directConversationTails = new Map();
|
|
333
|
+
directMessageStartResolvers = new Map();
|
|
334
|
+
pendingMessageCapacityWaiters = new Set();
|
|
323
335
|
pollAbortController = new AbortController();
|
|
324
336
|
lifecycleGeneration = 0;
|
|
325
337
|
connectionStartedAt = 0;
|
|
@@ -328,6 +340,9 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
328
340
|
notificationWatermarkPulledBack = false;
|
|
329
341
|
constructor(name, config, bridge, options, client) {
|
|
330
342
|
const profile = configuredString(config.profile, 'profile');
|
|
343
|
+
const startReactionName = configuredString(config.startReaction, 'startReaction') ??
|
|
344
|
+
DEFAULT_START_REACTION;
|
|
345
|
+
const endReactionName = configuredString(config.endReaction, 'endReaction');
|
|
331
346
|
const watchTodos = configuredBoolean(config.watchTodos, 'watchTodos');
|
|
332
347
|
if (profile?.includes(',')) {
|
|
333
348
|
throw new Error('DWS channel profile must select exactly one login profile.');
|
|
@@ -366,6 +381,8 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
366
381
|
source,
|
|
367
382
|
restartAttempts: 0,
|
|
368
383
|
}));
|
|
384
|
+
this.startReactionName = startReactionName;
|
|
385
|
+
this.endReactionName = endReactionName;
|
|
369
386
|
this.watchTodos = watchTodos;
|
|
370
387
|
}
|
|
371
388
|
createInitialCursor() {
|
|
@@ -552,17 +569,19 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
552
569
|
this.pollAbortController.abort();
|
|
553
570
|
this.lastTodoPollAt = 0;
|
|
554
571
|
this.todoTargets.clear();
|
|
555
|
-
for (const
|
|
556
|
-
|
|
557
|
-
if (!this.activeReactionKeys.delete(key))
|
|
558
|
-
continue;
|
|
559
|
-
void this.client
|
|
560
|
-
.removeImReaction(conversationId, messageId, ACK_REACTION_NAME)
|
|
561
|
-
.catch((error) => this.logReactionFailure('disconnect reaction removal', error));
|
|
562
|
-
}
|
|
572
|
+
for (const key of [...this.activeReactions.keys()]) {
|
|
573
|
+
this.cleanupReaction(key, 'disconnect reaction removal');
|
|
563
574
|
}
|
|
564
|
-
this.activeReactionKeys.clear();
|
|
565
575
|
this.sessionReactionKeys.clear();
|
|
576
|
+
this.queuedDirectMessages.clear();
|
|
577
|
+
this.replayDirectDispatches.clear();
|
|
578
|
+
for (const resolve of this.directMessageStartResolvers.values())
|
|
579
|
+
resolve();
|
|
580
|
+
this.directMessageStartResolvers.clear();
|
|
581
|
+
this.directConversationTails.clear();
|
|
582
|
+
for (const resolve of this.pendingMessageCapacityWaiters)
|
|
583
|
+
resolve();
|
|
584
|
+
this.pendingMessageCapacityWaiters.clear();
|
|
566
585
|
this.stopPollLoop();
|
|
567
586
|
for (const state of this.imStates) {
|
|
568
587
|
if (state.retryTimer)
|
|
@@ -668,7 +687,7 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
668
687
|
}
|
|
669
688
|
await this.client.sendImMessage(target, text, idempotencyKey);
|
|
670
689
|
}
|
|
671
|
-
async sendThreadMessage(chatId, threadId, text) {
|
|
690
|
+
async sendThreadMessage(chatId, threadId, text, sourceLabel) {
|
|
672
691
|
if (!this.connected) {
|
|
673
692
|
throw new Error(`[Channel:${this.name}] DWS channel is disconnected.`);
|
|
674
693
|
}
|
|
@@ -678,21 +697,23 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
678
697
|
if (threadId !== taskId) {
|
|
679
698
|
throw new Error(`[Channel:${this.name}] DWS todo delivery requires its taskId thread.`);
|
|
680
699
|
}
|
|
681
|
-
await this.client.addTodoComment(taskId, text);
|
|
700
|
+
await this.client.addTodoComment(taskId, this.formatMarkdownAttributedText(text, sourceLabel));
|
|
682
701
|
return;
|
|
683
702
|
}
|
|
684
703
|
if (!this.documentSet.has(chatId)) {
|
|
685
|
-
await this.sendMessage(chatId, text);
|
|
704
|
+
await this.sendMessage(chatId, this.formatAttributedText(text, sourceLabel));
|
|
686
705
|
return;
|
|
687
706
|
}
|
|
688
707
|
if (!threadId) {
|
|
689
708
|
throw new Error(`[Channel:${this.name}] DWS document delivery requires a commentKey.`);
|
|
690
709
|
}
|
|
691
|
-
await this.client.replyToComment(chatId, threadId, text);
|
|
710
|
+
await this.client.replyToComment(chatId, threadId, this.formatMarkdownAttributedText(text, sourceLabel));
|
|
692
711
|
}
|
|
693
|
-
async sendResponseMessage(chatId, text, sessionId) {
|
|
712
|
+
async sendResponseMessage(chatId, text, sessionId, sourceLabel) {
|
|
694
713
|
if (isNoReply(text))
|
|
695
714
|
return;
|
|
715
|
+
const label = sourceLabel ?? this.getResponseSourceLabel(sessionId);
|
|
716
|
+
const markdown = this.formatMarkdownAttributedText(text, label);
|
|
696
717
|
const threadId = this.getResponseThreadId(sessionId);
|
|
697
718
|
const taskId = this.todoTargets.get(chatId) ??
|
|
698
719
|
(threadId && todoChatId(threadId) === chatId ? threadId : undefined);
|
|
@@ -701,7 +722,7 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
701
722
|
throw new Error(`[Channel:${this.name}] DWS channel is disconnected.`);
|
|
702
723
|
}
|
|
703
724
|
try {
|
|
704
|
-
await this.client.addTodoComment(taskId,
|
|
725
|
+
await this.client.addTodoComment(taskId, markdown);
|
|
705
726
|
}
|
|
706
727
|
catch (error) {
|
|
707
728
|
if (!(error instanceof DwsCommandError) ||
|
|
@@ -720,7 +741,7 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
720
741
|
throw new Error(`[Channel:${this.name}] DWS document delivery requires a commentKey.`);
|
|
721
742
|
}
|
|
722
743
|
try {
|
|
723
|
-
await this.client.replyToComment(chatId, threadId,
|
|
744
|
+
await this.client.replyToComment(chatId, threadId, markdown);
|
|
724
745
|
}
|
|
725
746
|
catch (error) {
|
|
726
747
|
if (!(error instanceof DwsCommandError) ||
|
|
@@ -734,15 +755,20 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
734
755
|
const messageId = this.getResponseMessageId(sessionId);
|
|
735
756
|
const senderId = this.getResponseSenderId(sessionId);
|
|
736
757
|
if (!messageId || !senderId) {
|
|
737
|
-
await this.sendMessage(chatId, text);
|
|
758
|
+
await this.sendMessage(chatId, this.formatAttributedText(text, label));
|
|
738
759
|
return;
|
|
739
760
|
}
|
|
740
761
|
const idempotencyKey = stableUuid(`${this.name}\0${chatId}\0${messageId}\0${text}`);
|
|
741
762
|
if (this.findImTarget(chatId)?.kind === 'direct') {
|
|
742
|
-
await this.sendImText(chatId, text, idempotencyKey);
|
|
763
|
+
await this.sendImText(chatId, this.formatAttributedText(text, label), idempotencyKey);
|
|
743
764
|
return;
|
|
744
765
|
}
|
|
745
|
-
await this.client.replyToImMessage(chatId, messageId, senderId, text, idempotencyKey);
|
|
766
|
+
await this.client.replyToImMessage(chatId, messageId, senderId, this.formatAttributedText(text, label), idempotencyKey);
|
|
767
|
+
}
|
|
768
|
+
async pushProactive(target, text, sourceLabel) {
|
|
769
|
+
if (isNoReply(text))
|
|
770
|
+
return;
|
|
771
|
+
await super.pushProactive(target, text, sourceLabel);
|
|
746
772
|
}
|
|
747
773
|
async pollOnce() {
|
|
748
774
|
const signal = this.pollAbortController.signal;
|
|
@@ -767,7 +793,7 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
767
793
|
for (const message of mentions.messages) {
|
|
768
794
|
if (signal.aborted || !this.connected)
|
|
769
795
|
return;
|
|
770
|
-
await this.
|
|
796
|
+
await this.receiveImMessage({ kind: 'at' }, message, true);
|
|
771
797
|
}
|
|
772
798
|
if (mentions.nextCursor) {
|
|
773
799
|
this.cursor.mentionCheckpoint = {
|
|
@@ -807,22 +833,14 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
807
833
|
// shared retry budget twice per poll.
|
|
808
834
|
if (this.hasPendingMessage(key))
|
|
809
835
|
continue;
|
|
810
|
-
|
|
811
|
-
await this.handleImMessage({ kind: 'direct' }, message, true);
|
|
812
|
-
}
|
|
813
|
-
catch (error) {
|
|
814
|
-
// A failed plain direct message was just parked for replay, so the
|
|
815
|
-
// page can keep moving. An unparked failure — a document
|
|
816
|
-
// notification's turn — must still abort the window: the pinned
|
|
817
|
-
// watermark is what re-fetches it until its budget is spent.
|
|
818
|
-
if (!this.hasPendingMessage(key))
|
|
819
|
-
throw error;
|
|
820
|
-
process.stderr.write(`[Channel:${this.name}] direct-message dispatch failed mid-window; the message is parked for retry: ${sanitizeLogText(error instanceof Error ? error.message : String(error), 300)}\n`);
|
|
821
|
-
}
|
|
836
|
+
await this.receiveDirectMessage(message, true).admitted;
|
|
822
837
|
}
|
|
838
|
+
if (signal.aborted || !this.connected)
|
|
839
|
+
return;
|
|
823
840
|
if (this.notificationWatermarkPulledBack) {
|
|
824
841
|
// R4-4: a stale direct message replayed while this window's
|
|
825
|
-
// fetch was in flight, and `
|
|
842
|
+
// fetch was in flight, and `admitReceivedDirectMessage` pulled the
|
|
843
|
+
// watermark back
|
|
826
844
|
// to cover it. That replay was left UNMARKED on purpose for history
|
|
827
845
|
// polling, so finishing this window normally would undo the rescue:
|
|
828
846
|
// `checkpoint.endTime` is always past the replay's `eventTime`, and
|
|
@@ -986,7 +1004,9 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
986
1004
|
const subscription = await this.client.subscribeToIm(state.source, (message) => {
|
|
987
1005
|
state.lastError = undefined;
|
|
988
1006
|
state.restartAttempts = 0;
|
|
989
|
-
return
|
|
1007
|
+
return state.source.kind === 'direct'
|
|
1008
|
+
? this.receiveDirectMessage(message)
|
|
1009
|
+
: this.receiveImMessage(state.source, message);
|
|
990
1010
|
}, (error) => {
|
|
991
1011
|
if (error instanceof DwsEventProcessError)
|
|
992
1012
|
state.lastError = error;
|
|
@@ -1085,22 +1105,48 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1085
1105
|
logImError(source, error) {
|
|
1086
1106
|
process.stderr.write(`[Channel:${this.name}] DWS ${sanitizeLogText(sourceLabel(source), 120)} stream error: ${sanitizeLogText(error.message, 300)}\n`);
|
|
1087
1107
|
}
|
|
1088
|
-
async
|
|
1108
|
+
async receiveImMessage(source, message, fromHistory = false) {
|
|
1089
1109
|
if (!this.connected)
|
|
1090
1110
|
return;
|
|
1091
|
-
if (this.
|
|
1111
|
+
if (this.markSelfMessageProcessed(message))
|
|
1112
|
+
return;
|
|
1113
|
+
if (this.isStaleLiveMessage(message, fromHistory) &&
|
|
1114
|
+
message.eventTime !== undefined) {
|
|
1092
1115
|
this.markProcessedMessage(messageKey(message));
|
|
1093
1116
|
this.saveCursor();
|
|
1094
1117
|
return;
|
|
1095
1118
|
}
|
|
1096
|
-
if (
|
|
1097
|
-
message.
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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));
|
|
1131
|
+
}
|
|
1132
|
+
receiveDirectMessage(message, fromHistory = false, reportFailure = fromHistory) {
|
|
1133
|
+
const admission = this.admitReceivedDirectMessage(message, fromHistory, reportFailure);
|
|
1134
|
+
const completed = admission.then(({ completion }) => completion);
|
|
1135
|
+
void completed.catch(() => undefined);
|
|
1136
|
+
return {
|
|
1137
|
+
admitted: admission.then(() => undefined),
|
|
1138
|
+
completed,
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
async admitReceivedDirectMessage(message, fromHistory, reportFailure) {
|
|
1142
|
+
if (!this.connected)
|
|
1143
|
+
return { completion: Promise.resolve() };
|
|
1144
|
+
const key = messageKey(message);
|
|
1145
|
+
if (this.markSelfMessageProcessed(message)) {
|
|
1146
|
+
return { completion: Promise.resolve() };
|
|
1147
|
+
}
|
|
1148
|
+
if (this.isStaleLiveMessage(message, fromHistory) &&
|
|
1149
|
+
message.eventTime !== undefined) {
|
|
1104
1150
|
// A replayed direct message is left UNMARKED on purpose, for history
|
|
1105
1151
|
// polling to pick up. That only works if polling will ever look
|
|
1106
1152
|
// that far back: on a fresh cursor `notificationWatermark` starts at
|
|
@@ -1122,20 +1168,90 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1122
1168
|
this.cursor.notificationCheckpoint = undefined;
|
|
1123
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`);
|
|
1124
1170
|
this.saveCursor();
|
|
1125
|
-
return;
|
|
1171
|
+
return { completion: Promise.resolve() };
|
|
1126
1172
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
return;
|
|
1173
|
+
return this.admitDirectMessage({ kind: 'direct' }, message, key, reportFailure);
|
|
1174
|
+
}
|
|
1175
|
+
markSelfMessageProcessed(message) {
|
|
1176
|
+
if (!this.isSelfMessage(message))
|
|
1177
|
+
return false;
|
|
1178
|
+
this.markProcessedMessage(messageKey(message));
|
|
1179
|
+
this.saveCursor();
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
1182
|
+
isStaleLiveMessage(message, fromHistory) {
|
|
1183
|
+
return (!fromHistory &&
|
|
1184
|
+
message.eventTime !== undefined &&
|
|
1185
|
+
message.eventTime < this.connectionStartedAt - 5_000);
|
|
1186
|
+
}
|
|
1187
|
+
async admitDirectMessage(source, message, key, reportFailure) {
|
|
1188
|
+
if (this.cursor.processedMessages.includes(key)) {
|
|
1189
|
+
this.removePersistedPendingMessage(key);
|
|
1190
|
+
return { completion: Promise.resolve() };
|
|
1132
1191
|
}
|
|
1133
|
-
if ((
|
|
1134
|
-
this.
|
|
1135
|
-
|
|
1136
|
-
|
|
1192
|
+
if (!this.hasPendingMessage(key)) {
|
|
1193
|
+
if (!(await this.rememberPendingMessage(source, message))) {
|
|
1194
|
+
return { completion: Promise.resolve() };
|
|
1195
|
+
}
|
|
1137
1196
|
}
|
|
1138
|
-
|
|
1197
|
+
return {
|
|
1198
|
+
completion: this.scheduleDirectMessage(source, message, key, reportFailure),
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
scheduleDirectMessage(source, message, key, reportFailure) {
|
|
1202
|
+
const queued = this.queuedDirectMessages.get(key);
|
|
1203
|
+
if (queued)
|
|
1204
|
+
return queued;
|
|
1205
|
+
if (this.cursor.processedMessages.includes(key)) {
|
|
1206
|
+
this.removePersistedPendingMessage(key);
|
|
1207
|
+
return Promise.resolve();
|
|
1208
|
+
}
|
|
1209
|
+
const generation = this.lifecycleGeneration;
|
|
1210
|
+
const dispatch = async () => {
|
|
1211
|
+
try {
|
|
1212
|
+
if (!this.connected || generation !== this.lifecycleGeneration)
|
|
1213
|
+
return;
|
|
1214
|
+
await this.dispatchImMessage(source, message, key);
|
|
1215
|
+
}
|
|
1216
|
+
finally {
|
|
1217
|
+
if (this.queuedDirectMessages.get(key) === task) {
|
|
1218
|
+
this.queuedDirectMessages.delete(key);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
let resolveStarted;
|
|
1223
|
+
const started = new Promise((resolve) => {
|
|
1224
|
+
resolveStarted = resolve;
|
|
1225
|
+
});
|
|
1226
|
+
const previous = this.directConversationTails.get(message.conversationId);
|
|
1227
|
+
// Live turns only need FIFO through ChannelBase registration; replay and
|
|
1228
|
+
// followup turns must wait for the prior turn to finish.
|
|
1229
|
+
const predecessor = reportFailure || this.config.dispatchMode === 'followup'
|
|
1230
|
+
? previous?.completed
|
|
1231
|
+
: previous?.started;
|
|
1232
|
+
const task = predecessor
|
|
1233
|
+
? predecessor.catch(() => undefined).then(dispatch)
|
|
1234
|
+
: Promise.resolve().then(dispatch);
|
|
1235
|
+
const tail = { started, completed: task };
|
|
1236
|
+
this.queuedDirectMessages.set(key, task);
|
|
1237
|
+
this.directMessageStartResolvers.set(key, resolveStarted);
|
|
1238
|
+
this.directConversationTails.set(message.conversationId, tail);
|
|
1239
|
+
void task
|
|
1240
|
+
.finally(() => {
|
|
1241
|
+
this.releaseDirectMessageStart(message.conversationId, message.messageId);
|
|
1242
|
+
if (this.directConversationTails.get(message.conversationId) === tail) {
|
|
1243
|
+
this.directConversationTails.delete(message.conversationId);
|
|
1244
|
+
}
|
|
1245
|
+
})
|
|
1246
|
+
.catch(() => undefined);
|
|
1247
|
+
if (reportFailure) {
|
|
1248
|
+
void task.catch((error) => {
|
|
1249
|
+
this.logImError(source, error instanceof Error ? error : new Error(String(error)));
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
return task;
|
|
1253
|
+
}
|
|
1254
|
+
async dispatchImMessage(source, message, key) {
|
|
1139
1255
|
let waitedOnInFlight = false;
|
|
1140
1256
|
let inFlightError;
|
|
1141
1257
|
while (true) {
|
|
@@ -1158,6 +1274,10 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1158
1274
|
this.processingMessages.set(key, task);
|
|
1159
1275
|
try {
|
|
1160
1276
|
await task;
|
|
1277
|
+
if (this.cursor.processedMessages.includes(key) &&
|
|
1278
|
+
this.hasPendingMessage(key)) {
|
|
1279
|
+
this.removePersistedPendingMessage(key);
|
|
1280
|
+
}
|
|
1161
1281
|
}
|
|
1162
1282
|
finally {
|
|
1163
1283
|
if (this.processingMessages.get(key) === task) {
|
|
@@ -1214,7 +1334,7 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1214
1334
|
// Direct-message history may be unavailable, and ambient group
|
|
1215
1335
|
// messages have no history fallback. `at` messages need no parking:
|
|
1216
1336
|
// the pinned mention checkpoint re-fetches them.
|
|
1217
|
-
this.
|
|
1337
|
+
await this.rememberFailedMessage(source, message);
|
|
1218
1338
|
}
|
|
1219
1339
|
// Under budget the throw propagates exactly as before, so redelivery
|
|
1220
1340
|
// and concurrent-duplicate retry keep their contracts. Once the budget
|
|
@@ -1278,24 +1398,67 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1278
1398
|
return;
|
|
1279
1399
|
this.cursor.inboundFailures = failures.filter((failure) => failure.key !== key);
|
|
1280
1400
|
}
|
|
1281
|
-
rememberPendingMessage(source, message) {
|
|
1401
|
+
async rememberPendingMessage(source, message) {
|
|
1282
1402
|
const key = messageKey(message);
|
|
1283
1403
|
if (this.hasPendingMessage(key))
|
|
1284
|
-
return;
|
|
1404
|
+
return true;
|
|
1405
|
+
if (!this.connected)
|
|
1406
|
+
return false;
|
|
1407
|
+
if ((this.cursor.pendingMessages?.length ?? 0) >= MAX_PROCESSED_ITEMS) {
|
|
1408
|
+
throw new Error('DWS pending-message capacity is exhausted; retry later.');
|
|
1409
|
+
}
|
|
1410
|
+
if (this.hasPendingMessage(key))
|
|
1411
|
+
return true;
|
|
1285
1412
|
const pending = this.cursor.pendingMessages ?? [];
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1413
|
+
pending.push({ source, message });
|
|
1414
|
+
this.cursor.pendingMessages = pending;
|
|
1415
|
+
try {
|
|
1416
|
+
this.saveCursor();
|
|
1417
|
+
}
|
|
1418
|
+
catch (error) {
|
|
1419
|
+
this.removePendingMessage(key);
|
|
1420
|
+
throw error;
|
|
1421
|
+
}
|
|
1422
|
+
return true;
|
|
1423
|
+
}
|
|
1424
|
+
async rememberFailedMessage(source, message) {
|
|
1425
|
+
const key = messageKey(message);
|
|
1426
|
+
if (this.hasPendingMessage(key))
|
|
1427
|
+
return;
|
|
1428
|
+
const generation = this.lifecycleGeneration;
|
|
1429
|
+
while (this.connected &&
|
|
1430
|
+
generation === this.lifecycleGeneration &&
|
|
1431
|
+
(this.cursor.pendingMessages?.length ?? 0) >= MAX_PROCESSED_ITEMS) {
|
|
1432
|
+
await new Promise((resolve) => {
|
|
1433
|
+
this.pendingMessageCapacityWaiters.add(resolve);
|
|
1434
|
+
});
|
|
1293
1435
|
}
|
|
1436
|
+
if (this.hasPendingMessage(key))
|
|
1437
|
+
return;
|
|
1438
|
+
const pending = this.cursor.pendingMessages ?? [];
|
|
1294
1439
|
pending.push({ source, message });
|
|
1295
1440
|
this.cursor.pendingMessages = pending;
|
|
1441
|
+
try {
|
|
1442
|
+
this.saveCursor();
|
|
1443
|
+
}
|
|
1444
|
+
catch {
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
removePersistedPendingMessage(key) {
|
|
1449
|
+
if (this.removePendingMessage(key))
|
|
1450
|
+
this.saveCursor();
|
|
1296
1451
|
}
|
|
1297
1452
|
removePendingMessage(key) {
|
|
1298
|
-
|
|
1453
|
+
const pending = this.cursor.pendingMessages ?? [];
|
|
1454
|
+
const remaining = pending.filter((item) => messageKey(item.message) !== key);
|
|
1455
|
+
this.cursor.pendingMessages = remaining;
|
|
1456
|
+
if (remaining.length === pending.length)
|
|
1457
|
+
return false;
|
|
1458
|
+
for (const resolve of this.pendingMessageCapacityWaiters)
|
|
1459
|
+
resolve();
|
|
1460
|
+
this.pendingMessageCapacityWaiters.clear();
|
|
1461
|
+
return true;
|
|
1299
1462
|
}
|
|
1300
1463
|
async processDocumentNotification(message, key, notification) {
|
|
1301
1464
|
const notificationKey = documentNotificationKey(notification);
|
|
@@ -1313,17 +1476,19 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1313
1476
|
// only re-drives a parked entry whose own `senderId` passes the gate
|
|
1314
1477
|
// (the denied one never will), and this key is skipped by every later
|
|
1315
1478
|
// history poll, so the allowed user's request went unanswered forever.
|
|
1316
|
-
// Mark only when the comment is genuinely done, or when this
|
|
1317
|
-
//
|
|
1479
|
+
// Mark only when the comment is genuinely done, or when this sender's
|
|
1480
|
+
// own request is already parked. Other senders must take the slot next.
|
|
1318
1481
|
if (this.cursor.processedMessages.includes(notificationKey) ||
|
|
1319
|
-
(this.hasPendingDocumentNotification(notificationKey) &&
|
|
1482
|
+
(this.hasPendingDocumentNotification(notificationKey, message.senderId) &&
|
|
1320
1483
|
!this.gate.isAllowed(message.senderId))) {
|
|
1321
1484
|
this.markProcessedMessage(key);
|
|
1485
|
+
this.saveCursor();
|
|
1486
|
+
return;
|
|
1322
1487
|
}
|
|
1323
|
-
|
|
1324
|
-
this.
|
|
1488
|
+
if (this.processingMessages.get(notificationKey) === inFlight) {
|
|
1489
|
+
this.processingMessages.delete(notificationKey);
|
|
1325
1490
|
}
|
|
1326
|
-
this.
|
|
1491
|
+
await this.processDocumentNotification(message, key, notification);
|
|
1327
1492
|
return;
|
|
1328
1493
|
}
|
|
1329
1494
|
const task = (async () => {
|
|
@@ -1412,8 +1577,25 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1412
1577
|
if (signal.aborted || !this.connected)
|
|
1413
1578
|
return;
|
|
1414
1579
|
const key = messageKey(pending.message);
|
|
1580
|
+
if (pending.source.kind === 'direct') {
|
|
1581
|
+
if (this.queuedDirectMessages.has(key))
|
|
1582
|
+
continue;
|
|
1583
|
+
if (this.replayDirectDispatches.size >= MAX_DIRECT_REPLAY_DISPATCHES) {
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
const dispatched = this.scheduleDirectMessage(pending.source, pending.message, key, true);
|
|
1587
|
+
this.replayDirectDispatches.set(key, dispatched);
|
|
1588
|
+
void dispatched
|
|
1589
|
+
.finally(() => {
|
|
1590
|
+
if (this.replayDirectDispatches.get(key) === dispatched) {
|
|
1591
|
+
this.replayDirectDispatches.delete(key);
|
|
1592
|
+
}
|
|
1593
|
+
})
|
|
1594
|
+
.catch(() => undefined);
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1415
1597
|
try {
|
|
1416
|
-
await this.
|
|
1598
|
+
await this.receiveImMessage(pending.source, pending.message, true);
|
|
1417
1599
|
this.removePendingMessage(key);
|
|
1418
1600
|
this.saveCursor();
|
|
1419
1601
|
}
|
|
@@ -1462,8 +1644,9 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1462
1644
|
hasPendingMessage(key) {
|
|
1463
1645
|
return (this.cursor.pendingMessages ?? []).some((pending) => messageKey(pending.message) === key);
|
|
1464
1646
|
}
|
|
1465
|
-
hasPendingDocumentNotification(notificationKey) {
|
|
1466
|
-
return (this.cursor.pendingDocumentNotifications ?? []).some((pending) => documentNotificationKey(pending) === notificationKey
|
|
1647
|
+
hasPendingDocumentNotification(notificationKey, senderId) {
|
|
1648
|
+
return (this.cursor.pendingDocumentNotifications ?? []).some((pending) => documentNotificationKey(pending) === notificationKey &&
|
|
1649
|
+
(senderId === undefined || pending.senderId === senderId));
|
|
1467
1650
|
}
|
|
1468
1651
|
rememberPendingDocumentNotification(message, notification) {
|
|
1469
1652
|
const key = documentNotificationKey(notification);
|
|
@@ -1517,6 +1700,14 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1517
1700
|
reactionKey(conversationId, messageId) {
|
|
1518
1701
|
return `${conversationId}\0${messageId}`;
|
|
1519
1702
|
}
|
|
1703
|
+
releaseDirectMessageStart(conversationId, messageId) {
|
|
1704
|
+
const key = `${conversationId}\0${messageId}`;
|
|
1705
|
+
const resolve = this.directMessageStartResolvers.get(key);
|
|
1706
|
+
if (!resolve)
|
|
1707
|
+
return;
|
|
1708
|
+
this.directMessageStartResolvers.delete(key);
|
|
1709
|
+
resolve();
|
|
1710
|
+
}
|
|
1520
1711
|
rememberInboundReactionTarget(chatId, messageId, conversationId = chatId) {
|
|
1521
1712
|
const key = this.reactionKey(chatId, messageId);
|
|
1522
1713
|
this.inboundReactionTargets.delete(key);
|
|
@@ -1538,6 +1729,52 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1538
1729
|
if (reactions.size === 0)
|
|
1539
1730
|
this.sessionReactionKeys.delete(sessionId);
|
|
1540
1731
|
}
|
|
1732
|
+
releaseActiveReaction(key) {
|
|
1733
|
+
const reaction = this.activeReactions.get(key);
|
|
1734
|
+
if (!reaction)
|
|
1735
|
+
return undefined;
|
|
1736
|
+
this.activeReactions.delete(key);
|
|
1737
|
+
this.untrackSessionReaction(reaction.sessionId, key);
|
|
1738
|
+
return reaction;
|
|
1739
|
+
}
|
|
1740
|
+
enqueueReactionOperation(key, operation) {
|
|
1741
|
+
const previous = this.reactionOperations.get(key) ?? Promise.resolve();
|
|
1742
|
+
const next = previous
|
|
1743
|
+
.catch(() => undefined)
|
|
1744
|
+
.then(() => operation(() => this.reactionOperations.get(key) === next))
|
|
1745
|
+
.catch((error) => this.logReactionFailure('reaction transition', error))
|
|
1746
|
+
.finally(() => {
|
|
1747
|
+
if (this.reactionOperations.get(key) === next) {
|
|
1748
|
+
this.reactionOperations.delete(key);
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
this.reactionOperations.set(key, next);
|
|
1752
|
+
}
|
|
1753
|
+
rememberEndReaction(key) {
|
|
1754
|
+
this.endReactionKeys.delete(key);
|
|
1755
|
+
this.endReactionKeys.add(key);
|
|
1756
|
+
if (this.endReactionKeys.size > MAX_INBOUND_REACTION_TARGETS) {
|
|
1757
|
+
const oldest = this.endReactionKeys.values().next().value;
|
|
1758
|
+
if (oldest !== undefined)
|
|
1759
|
+
this.endReactionKeys.delete(oldest);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
async removeStartedReaction(reaction, action) {
|
|
1763
|
+
if (!reaction.added)
|
|
1764
|
+
return;
|
|
1765
|
+
try {
|
|
1766
|
+
await this.client.removeImReaction(reaction.target.conversationId, reaction.target.messageId, this.startReactionName);
|
|
1767
|
+
}
|
|
1768
|
+
catch (error) {
|
|
1769
|
+
this.logReactionFailure(action, error);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
cleanupReaction(key, action) {
|
|
1773
|
+
const reaction = this.releaseActiveReaction(key);
|
|
1774
|
+
if (!reaction)
|
|
1775
|
+
return;
|
|
1776
|
+
this.enqueueReactionOperation(key, () => this.removeStartedReaction(reaction, action));
|
|
1777
|
+
}
|
|
1541
1778
|
startReaction(conversationId, messageId, sessionId) {
|
|
1542
1779
|
if (!messageId)
|
|
1543
1780
|
return;
|
|
@@ -1545,63 +1782,105 @@ export class DwsChannel extends PollingChannelBase {
|
|
|
1545
1782
|
if (!target)
|
|
1546
1783
|
return;
|
|
1547
1784
|
const key = this.reactionKey(target.conversationId, target.messageId);
|
|
1548
|
-
if (this.
|
|
1785
|
+
if (this.activeReactions.has(key))
|
|
1549
1786
|
return;
|
|
1550
|
-
this.activeReactionKeys.add(key);
|
|
1551
1787
|
let reactions = this.sessionReactionKeys.get(sessionId);
|
|
1552
1788
|
if (!reactions) {
|
|
1553
|
-
reactions = new
|
|
1789
|
+
reactions = new Set();
|
|
1554
1790
|
this.sessionReactionKeys.set(sessionId, reactions);
|
|
1555
1791
|
}
|
|
1556
|
-
reactions.
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1792
|
+
reactions.add(key);
|
|
1793
|
+
const reaction = {
|
|
1794
|
+
target,
|
|
1795
|
+
sessionId,
|
|
1796
|
+
added: false,
|
|
1797
|
+
};
|
|
1798
|
+
this.activeReactions.set(key, reaction);
|
|
1799
|
+
this.enqueueReactionOperation(key, async () => {
|
|
1800
|
+
if (this.activeReactions.get(key) !== reaction)
|
|
1801
|
+
return;
|
|
1802
|
+
if (this.endReactionName && this.endReactionKeys.has(key)) {
|
|
1803
|
+
try {
|
|
1804
|
+
await this.client.removeImReaction(target.conversationId, target.messageId, this.endReactionName);
|
|
1805
|
+
this.endReactionKeys.delete(key);
|
|
1806
|
+
if (this.endReactionName === this.startReactionName) {
|
|
1807
|
+
reaction.added = false;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
catch (error) {
|
|
1811
|
+
this.logReactionFailure('previous end reaction removal', error);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
if (this.activeReactions.get(key) !== reaction)
|
|
1815
|
+
return;
|
|
1816
|
+
if (reaction.added)
|
|
1817
|
+
return;
|
|
1818
|
+
try {
|
|
1819
|
+
await this.client.addImReaction(target.conversationId, target.messageId, this.startReactionName);
|
|
1820
|
+
reaction.added = true;
|
|
1821
|
+
}
|
|
1822
|
+
catch (error) {
|
|
1823
|
+
this.logReactionFailure('start reaction add', error);
|
|
1564
1824
|
}
|
|
1565
|
-
})
|
|
1566
|
-
.catch((error) => {
|
|
1567
|
-
this.activeReactionKeys.delete(key);
|
|
1568
|
-
this.untrackSessionReaction(sessionId, key);
|
|
1569
|
-
this.logReactionFailure('reaction add', error);
|
|
1570
1825
|
});
|
|
1571
1826
|
}
|
|
1572
|
-
|
|
1827
|
+
finishReaction(conversationId, messageId, sessionId) {
|
|
1573
1828
|
if (!messageId)
|
|
1574
1829
|
return;
|
|
1575
1830
|
const target = this.inboundReactionTargets.get(this.reactionKey(conversationId, messageId));
|
|
1576
1831
|
if (!target)
|
|
1577
1832
|
return;
|
|
1578
1833
|
const key = this.reactionKey(target.conversationId, target.messageId);
|
|
1579
|
-
this.
|
|
1580
|
-
if (!
|
|
1834
|
+
const active = this.activeReactions.get(key);
|
|
1835
|
+
if (!active || active.sessionId !== sessionId)
|
|
1581
1836
|
return;
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1837
|
+
const reaction = this.releaseActiveReaction(key);
|
|
1838
|
+
if (!reaction)
|
|
1839
|
+
return;
|
|
1840
|
+
const generation = this.lifecycleGeneration;
|
|
1841
|
+
this.enqueueReactionOperation(key, async (isLatest) => {
|
|
1842
|
+
const replacement = this.activeReactions.get(key);
|
|
1843
|
+
if (replacement) {
|
|
1844
|
+
if (reaction.added)
|
|
1845
|
+
replacement.added = true;
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
await this.removeStartedReaction(reaction, 'start reaction removal');
|
|
1849
|
+
if (!isLatest() ||
|
|
1850
|
+
this.activeReactions.has(key) ||
|
|
1851
|
+
!this.endReactionName ||
|
|
1852
|
+
this.endReactionKeys.has(key) ||
|
|
1853
|
+
!this.connected ||
|
|
1854
|
+
generation !== this.lifecycleGeneration) {
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
try {
|
|
1858
|
+
await this.client.addImReaction(reaction.target.conversationId, reaction.target.messageId, this.endReactionName);
|
|
1859
|
+
this.rememberEndReaction(key);
|
|
1860
|
+
}
|
|
1861
|
+
catch (error) {
|
|
1862
|
+
this.logReactionFailure('end reaction add', error);
|
|
1863
|
+
}
|
|
1864
|
+
});
|
|
1585
1865
|
}
|
|
1586
1866
|
onTaskLifecycle(event) {
|
|
1587
1867
|
if (event.type === 'started') {
|
|
1868
|
+
if (event.messageId) {
|
|
1869
|
+
this.releaseDirectMessageStart(event.chatId, event.messageId);
|
|
1870
|
+
}
|
|
1588
1871
|
this.startReaction(event.chatId, event.messageId, event.sessionId);
|
|
1589
1872
|
return;
|
|
1590
1873
|
}
|
|
1591
1874
|
if (isTerminalTaskLifecycleType(event.type)) {
|
|
1592
|
-
this.
|
|
1875
|
+
this.finishReaction(event.chatId, event.messageId, event.sessionId);
|
|
1593
1876
|
}
|
|
1594
1877
|
}
|
|
1595
1878
|
onSessionDied(sessionId) {
|
|
1596
1879
|
const reactions = this.sessionReactionKeys.get(sessionId);
|
|
1597
1880
|
if (reactions) {
|
|
1598
1881
|
this.sessionReactionKeys.delete(sessionId);
|
|
1599
|
-
for (const
|
|
1600
|
-
|
|
1601
|
-
continue;
|
|
1602
|
-
void this.client
|
|
1603
|
-
.removeImReaction(conversationId, messageId, ACK_REACTION_NAME)
|
|
1604
|
-
.catch((error) => this.logReactionFailure('session-death reaction removal', error));
|
|
1882
|
+
for (const key of reactions) {
|
|
1883
|
+
this.cleanupReaction(key, 'session-death reaction removal');
|
|
1605
1884
|
}
|
|
1606
1885
|
}
|
|
1607
1886
|
super.onSessionDied(sessionId);
|