@yaoyuanchao/dingtalk 1.7.5 → 1.7.6

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/monitor.ts +52 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaoyuanchao/dingtalk",
3
- "version": "1.7.5",
3
+ "version": "1.7.6",
4
4
  "type": "module",
5
5
  "description": "DingTalk channel plugin for ClawdBot/OpenClaw with Stream Mode support",
6
6
  "license": "MIT",
package/src/monitor.ts CHANGED
@@ -198,6 +198,30 @@ const AGGREGATION_DELAY_MS = 2000; // 2 seconds - balance between UX and catchin
198
198
 
199
199
  const sessionQueues = new Map<string, Promise<void>>();
200
200
  const sessionQueueLastActivity = new Map<string, number>();
201
+
202
+ // Track delivery activity per queue key to detect when the SDK's dispatch resolves
203
+ // before the agent's full turn completes (e.g. followup turns running in background).
204
+ // This supplements sessionQueues for the "busy" check.
205
+ const DELIVER_ACTIVITY_GRACE_MS = 8000; // 8s after last delivery, consider idle
206
+ const deliverActivityTimestamps = new Map<string, number>();
207
+
208
+ function markDeliverActivity(queueKey: string): void {
209
+ deliverActivityTimestamps.set(queueKey, Date.now());
210
+ }
211
+
212
+ function hasActiveDelivery(queueKey: string): boolean {
213
+ const ts = deliverActivityTimestamps.get(queueKey);
214
+ if (!ts) return false;
215
+ if (Date.now() - ts > DELIVER_ACTIVITY_GRACE_MS) {
216
+ deliverActivityTimestamps.delete(queueKey);
217
+ return false;
218
+ }
219
+ return true;
220
+ }
221
+
222
+ function clearDeliverActivity(queueKey: string): void {
223
+ deliverActivityTimestamps.delete(queueKey);
224
+ }
201
225
  const SESSION_QUEUE_TTL_MS = 5 * 60 * 1000; // 5 min
202
226
 
203
227
  const QUEUE_BUSY_PHRASES = [
@@ -1395,7 +1419,11 @@ async function dispatchMessage(params: {
1395
1419
  const { account, log } = ctx;
1396
1420
 
1397
1421
  const queueKey = `${account.accountId}:${conversationId}`;
1398
- const isQueueBusy = sessionQueues.has(queueKey);
1422
+ // Check both the explicit queue AND recent delivery activity.
1423
+ // The SDK's dispatchReplyFromConfig may resolve before the agent's full turn
1424
+ // completes (followup turns run in background), clearing the queue entry
1425
+ // while deliveries are still happening.
1426
+ const isQueueBusy = sessionQueues.has(queueKey) || hasActiveDelivery(queueKey);
1399
1427
 
1400
1428
  // If queue is busy, add emotion reaction on user's message to indicate queued
1401
1429
  let queueAckCleanup: (() => Promise<void>) | null = null;
@@ -1437,11 +1465,13 @@ async function dispatchMessage(params: {
1437
1465
  // Clean up only if this is still the latest task
1438
1466
  if (sessionQueues.get(queueKey) === currentTask) {
1439
1467
  sessionQueues.delete(queueKey);
1468
+ log?.info?.("[dingtalk] Queue entry removed for " + queueKey + " (deliverActive=" + hasActiveDelivery(queueKey) + ")");
1440
1469
  }
1441
1470
  });
1442
1471
 
1443
1472
  sessionQueues.set(queueKey, currentTask);
1444
1473
  sessionQueueLastActivity.set(queueKey, Date.now());
1474
+ log?.info?.("[dingtalk] Queue entry set for " + queueKey + " (wasQueueBusy=" + isQueueBusy + ")");
1445
1475
 
1446
1476
  // Don't await — fire-and-forget so message buffering and SDK callback stay responsive
1447
1477
  }
@@ -1604,11 +1634,14 @@ async function dispatchMessageInternal(params: {
1604
1634
 
1605
1635
  // Await dispatch so per-session queue waits for reply delivery to complete
1606
1636
  // before starting the next queued message.
1637
+ const fallbackQueueKey = `${account.accountId}:${conversationId}`;
1607
1638
  await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
1608
1639
  ctx: ctxPayload,
1609
1640
  cfg: actualCfg,
1610
1641
  dispatcherOptions: {
1611
1642
  deliver: async (payload: any) => {
1643
+ // Track delivery activity for queue busy detection
1644
+ markDeliverActivity(fallbackQueueKey);
1612
1645
  // Recall typing indicator on first delivery
1613
1646
  await cleanupTyping();
1614
1647
 
@@ -1757,9 +1790,12 @@ async function dispatchWithFullPipeline(params: {
1757
1790
  }
1758
1791
 
1759
1792
  // 8. Create typing-aware dispatcher
1793
+ const deliverQueueKey = `${account.accountId}:${conversationId}`;
1760
1794
  const { dispatcher, replyOptions, markDispatchIdle } = rt.channel.reply.createReplyDispatcherWithTyping({
1761
1795
  responsePrefix: '',
1762
1796
  deliver: async (payload: any) => {
1797
+ // Track delivery activity for queue busy detection
1798
+ markDeliverActivity(deliverQueueKey);
1763
1799
  // Recall typing indicator on first delivery
1764
1800
  if (!firstReplyFired && onFirstReply) {
1765
1801
  firstReplyFired = true;
@@ -1787,14 +1823,24 @@ async function dispatchWithFullPipeline(params: {
1787
1823
 
1788
1824
  // 9. Dispatch reply from config
1789
1825
  try {
1826
+ log?.info?.("[dingtalk] dispatchReplyFromConfig started for " + deliverQueueKey);
1790
1827
  await rt.channel.reply.dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyOptions });
1828
+ log?.info?.("[dingtalk] dispatchReplyFromConfig completed for " + deliverQueueKey);
1791
1829
  } finally {
1830
+ // OpenClaw 2026.4+ moved dispatcher.markComplete() + waitForIdle() out of
1831
+ // dispatchReplyFromConfig into the withReplyDispatcher wrapper. Since we call
1832
+ // dispatchReplyFromConfig directly (not through withReplyDispatcher), we must
1833
+ // do this ourselves to ensure all pending deliveries drain before returning.
1834
+ try {
1835
+ if (typeof dispatcher.markComplete === 'function') {
1836
+ dispatcher.markComplete();
1837
+ }
1838
+ await dispatcher.waitForIdle();
1839
+ log?.info?.("[dingtalk] dispatcher.waitForIdle completed for " + deliverQueueKey);
1840
+ } catch (err) {
1841
+ log?.info?.("[dingtalk] dispatcher settle error: " + err);
1842
+ }
1792
1843
  markDispatchIdle();
1793
- // Don't recall typing here — dispatchReplyFromConfig resolves when dispatch
1794
- // is *initiated*, not when the agent finishes. The agent may still be doing
1795
- // tool calls for many minutes before producing text. The deliver callback
1796
- // above handles recall on first delivery. If the agent crashes without
1797
- // replying, the emoji reaction simply stays — acceptable tradeoff.
1798
1844
  }
1799
1845
 
1800
1846
  // 10. Record activity