@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.
@@ -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 ACK_REACTION_NAME = '暗中观察';
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
- activeReactionKeys = new Set();
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 reactions of this.sessionReactionKeys.values()) {
556
- for (const [key, { conversationId, messageId }] of reactions) {
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, text);
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, text);
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.handleImMessage({ kind: 'at' }, message, true);
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
- try {
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 `handleImMessage` pulled the watermark back
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 this.handleImMessage(state.source, message);
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 handleImMessage(source, message, fromHistory = false) {
1108
+ async receiveImMessage(source, message, fromHistory = false) {
1089
1109
  if (!this.connected)
1090
1110
  return;
1091
- if (this.isSelfMessage(message)) {
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 (!fromHistory &&
1097
- message.eventTime !== undefined &&
1098
- message.eventTime < this.connectionStartedAt - 5_000) {
1099
- if (source.kind !== 'direct') {
1100
- this.markProcessedMessage(messageKey(message));
1101
- this.saveCursor();
1102
- return;
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
- if (source.kind === 'group-all' &&
1128
- (this.config.groups[message.conversationId]?.requireMention ??
1129
- this.config.groups['*']?.requireMention ??
1130
- true)) {
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 ((source.kind === 'group' || source.kind === 'group-all') &&
1134
- this.config.groupPolicy === 'pairing' &&
1135
- !this.groupGate.isGroupApproved(message.conversationId)) {
1136
- return;
1192
+ if (!this.hasPendingMessage(key)) {
1193
+ if (!(await this.rememberPendingMessage(source, message))) {
1194
+ return { completion: Promise.resolve() };
1195
+ }
1137
1196
  }
1138
- const key = messageKey(message);
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.rememberPendingMessage(source, message);
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
- while (pending.length >= MAX_PROCESSED_ITEMS) {
1287
- const dropped = pending.shift();
1288
- if (!dropped)
1289
- break;
1290
- this.clearInboundFailure(messageKey(dropped.message));
1291
- process.stderr.write(`[Channel:${this.name}] dropping the oldest pending DWS message ` +
1292
- `because the retry queue reached ${MAX_PROCESSED_ITEMS}.\n`);
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
- this.cursor.pendingMessages = (this.cursor.pendingMessages ?? []).filter((pending) => messageKey(pending.message) !== key);
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 caller is
1317
- // no more entitled to it than the sender already parked.
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
- else {
1324
- this.rememberPendingDocumentNotification(message, notification);
1488
+ if (this.processingMessages.get(notificationKey) === inFlight) {
1489
+ this.processingMessages.delete(notificationKey);
1325
1490
  }
1326
- this.saveCursor();
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.handleImMessage(pending.source, pending.message, true);
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.activeReactionKeys.has(key))
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 Map();
1789
+ reactions = new Set();
1554
1790
  this.sessionReactionKeys.set(sessionId, reactions);
1555
1791
  }
1556
- reactions.set(key, target);
1557
- void this.client
1558
- .addImReaction(target.conversationId, target.messageId, ACK_REACTION_NAME)
1559
- .then(() => {
1560
- if (!this.activeReactionKeys.has(key)) {
1561
- void this.client
1562
- .removeImReaction(target.conversationId, target.messageId, ACK_REACTION_NAME)
1563
- .catch((error) => this.logReactionFailure('late reaction removal', error));
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
- stopReaction(conversationId, messageId, sessionId) {
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.untrackSessionReaction(sessionId, key);
1580
- if (!this.activeReactionKeys.delete(key))
1834
+ const active = this.activeReactions.get(key);
1835
+ if (!active || active.sessionId !== sessionId)
1581
1836
  return;
1582
- void this.client
1583
- .removeImReaction(target.conversationId, target.messageId, ACK_REACTION_NAME)
1584
- .catch((error) => this.logReactionFailure('reaction removal', error));
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.stopReaction(event.chatId, event.messageId, event.sessionId);
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 [key, { messageId, conversationId }] of reactions) {
1600
- if (!this.activeReactionKeys.delete(key))
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);