botmux 3.0.0 → 3.1.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.
Files changed (207) hide show
  1. package/dist/adapters/backend/herdr-backend.d.ts +9 -7
  2. package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
  3. package/dist/adapters/backend/herdr-backend.js +63 -53
  4. package/dist/adapters/backend/herdr-backend.js.map +1 -1
  5. package/dist/adapters/backend/riff-backend.d.ts +6 -1
  6. package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
  7. package/dist/adapters/backend/riff-backend.js +7 -2
  8. package/dist/adapters/backend/riff-backend.js.map +1 -1
  9. package/dist/adapters/backend/sandbox.d.ts +20 -16
  10. package/dist/adapters/backend/sandbox.d.ts.map +1 -1
  11. package/dist/adapters/backend/sandbox.js +192 -65
  12. package/dist/adapters/backend/sandbox.js.map +1 -1
  13. package/dist/adapters/backend/tmux-backend.d.ts +39 -2
  14. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  15. package/dist/adapters/backend/tmux-backend.js +55 -5
  16. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  17. package/dist/adapters/backend/tmux-pipe-backend.js +2 -2
  18. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  19. package/dist/adapters/backend/zellij-backend.d.ts.map +1 -1
  20. package/dist/adapters/backend/zellij-backend.js +9 -3
  21. package/dist/adapters/backend/zellij-backend.js.map +1 -1
  22. package/dist/adapters/cli/claude-code.d.ts +25 -0
  23. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  24. package/dist/adapters/cli/claude-code.js +79 -9
  25. package/dist/adapters/cli/claude-code.js.map +1 -1
  26. package/dist/adapters/cli/read-isolation.d.ts +2 -1
  27. package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
  28. package/dist/adapters/cli/read-isolation.js.map +1 -1
  29. package/dist/adapters/cli/types.d.ts +2 -2
  30. package/dist/adapters/hook-installer.d.ts +11 -0
  31. package/dist/adapters/hook-installer.d.ts.map +1 -1
  32. package/dist/adapters/hook-installer.js +72 -11
  33. package/dist/adapters/hook-installer.js.map +1 -1
  34. package/dist/cli.d.ts.map +1 -1
  35. package/dist/cli.js +15 -4
  36. package/dist/cli.js.map +1 -1
  37. package/dist/config.d.ts +6 -0
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/config.js +18 -0
  40. package/dist/config.js.map +1 -1
  41. package/dist/core/command-handler.d.ts.map +1 -1
  42. package/dist/core/command-handler.js +211 -57
  43. package/dist/core/command-handler.js.map +1 -1
  44. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  45. package/dist/core/dashboard-ipc-server.js +12 -5
  46. package/dist/core/dashboard-ipc-server.js.map +1 -1
  47. package/dist/core/dashboard-rows.d.ts +5 -0
  48. package/dist/core/dashboard-rows.d.ts.map +1 -1
  49. package/dist/core/dashboard-rows.js +2 -0
  50. package/dist/core/dashboard-rows.js.map +1 -1
  51. package/dist/core/doc-comment-prompt.d.ts +2 -0
  52. package/dist/core/doc-comment-prompt.d.ts.map +1 -1
  53. package/dist/core/doc-comment-prompt.js +30 -0
  54. package/dist/core/doc-comment-prompt.js.map +1 -1
  55. package/dist/core/github-auth.d.ts +8 -0
  56. package/dist/core/github-auth.d.ts.map +1 -0
  57. package/dist/core/github-auth.js +44 -0
  58. package/dist/core/github-auth.js.map +1 -0
  59. package/dist/core/managed-origin-capability.d.ts +7 -0
  60. package/dist/core/managed-origin-capability.d.ts.map +1 -1
  61. package/dist/core/managed-origin-capability.js +12 -0
  62. package/dist/core/managed-origin-capability.js.map +1 -1
  63. package/dist/core/passthrough-commands.d.ts.map +1 -1
  64. package/dist/core/passthrough-commands.js +1 -1
  65. package/dist/core/passthrough-commands.js.map +1 -1
  66. package/dist/core/restart-report.d.ts +7 -1
  67. package/dist/core/restart-report.d.ts.map +1 -1
  68. package/dist/core/restart-report.js +19 -5
  69. package/dist/core/restart-report.js.map +1 -1
  70. package/dist/core/session-activity.d.ts +8 -0
  71. package/dist/core/session-activity.d.ts.map +1 -1
  72. package/dist/core/session-activity.js +16 -0
  73. package/dist/core/session-activity.js.map +1 -1
  74. package/dist/core/session-discovery.d.ts.map +1 -1
  75. package/dist/core/session-discovery.js +4 -2
  76. package/dist/core/session-discovery.js.map +1 -1
  77. package/dist/core/update-check.d.ts +11 -0
  78. package/dist/core/update-check.d.ts.map +1 -1
  79. package/dist/core/update-check.js +6 -1
  80. package/dist/core/update-check.js.map +1 -1
  81. package/dist/core/vc-meeting-prepare-command.d.ts +22 -0
  82. package/dist/core/vc-meeting-prepare-command.d.ts.map +1 -0
  83. package/dist/core/vc-meeting-prepare-command.js +50 -0
  84. package/dist/core/vc-meeting-prepare-command.js.map +1 -0
  85. package/dist/core/worker-pool.d.ts.map +1 -1
  86. package/dist/core/worker-pool.js +11 -2
  87. package/dist/core/worker-pool.js.map +1 -1
  88. package/dist/daemon.d.ts +15 -5
  89. package/dist/daemon.d.ts.map +1 -1
  90. package/dist/daemon.js +555 -41
  91. package/dist/daemon.js.map +1 -1
  92. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  93. package/dist/dashboard/web/bot-defaults-page.js +6 -1
  94. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  95. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  96. package/dist/dashboard/web/i18n.js +34 -0
  97. package/dist/dashboard/web/i18n.js.map +1 -1
  98. package/dist/dashboard/web/preferences.d.ts +1 -1
  99. package/dist/dashboard/web/preferences.d.ts.map +1 -1
  100. package/dist/dashboard/web/preferences.js +1 -1
  101. package/dist/dashboard/web/preferences.js.map +1 -1
  102. package/dist/dashboard/web/sessions-page.d.ts +19 -0
  103. package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
  104. package/dist/dashboard/web/sessions-page.js +66 -7
  105. package/dist/dashboard/web/sessions-page.js.map +1 -1
  106. package/dist/dashboard/web/sessions.d.ts +31 -0
  107. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  108. package/dist/dashboard/web/sessions.js +82 -0
  109. package/dist/dashboard/web/sessions.js.map +1 -1
  110. package/dist/dashboard-web/app.js +1 -1
  111. package/dist/dashboard-web/chunks/{bot-defaults-page-MBORWWP7.js → bot-defaults-page-KLDCZNLP.js} +5 -5
  112. package/dist/dashboard-web/chunks/{chunk-G43RUPUN.js → chunk-EZAW2EPM.js} +1 -1
  113. package/dist/dashboard-web/chunks/{chunk-EIO5AWDA.js → chunk-FMJNOATE.js} +1 -1
  114. package/dist/dashboard-web/chunks/{chunk-4QTBR7UF.js → chunk-KEAOTUL3.js} +7 -7
  115. package/dist/dashboard-web/chunks/{chunk-7STGZ5BX.js → chunk-MLNI5BKE.js} +1 -1
  116. package/dist/dashboard-web/chunks/{connectors-page-LKDT4WBG.js → connectors-page-O32LFAL5.js} +1 -1
  117. package/dist/dashboard-web/chunks/{groups-page-XKIWJ2TP.js → groups-page-PSAFIZ6M.js} +1 -1
  118. package/dist/dashboard-web/chunks/{insights-page-CBEZCYKV.js → insights-page-V23LLATX.js} +1 -1
  119. package/dist/dashboard-web/chunks/{monitor-room-AQTHAWXA.js → monitor-room-7X5AKYOF.js} +1 -1
  120. package/dist/dashboard-web/chunks/{monitoring-page-2R4NCNBQ.js → monitoring-page-CBA3CQYL.js} +1 -1
  121. package/dist/dashboard-web/chunks/{office-page-5M6MQIIL.js → office-page-RL6QSEM7.js} +1 -1
  122. package/dist/dashboard-web/chunks/{overview-page-N22HDZ2V.js → overview-page-XQIPO6KH.js} +1 -1
  123. package/dist/dashboard-web/chunks/{roles-page-J6K2HWFX.js → roles-page-ACIWV7QS.js} +1 -1
  124. package/dist/dashboard-web/chunks/{schedules-page-CGAQCB4J.js → schedules-page-KJDUKOJV.js} +1 -1
  125. package/dist/dashboard-web/chunks/sessions-page-2WDWHSP4.js +3 -0
  126. package/dist/dashboard-web/chunks/{settings-page-GLRWV5KU.js → settings-page-6VX4OUGG.js} +1 -1
  127. package/dist/dashboard-web/chunks/{skills-page-KQVW42QU.js → skills-page-WXE3UYLT.js} +1 -1
  128. package/dist/dashboard-web/chunks/{team-federation-page-5PYUOV5M.js → team-federation-page-PYTF4FLM.js} +1 -1
  129. package/dist/dashboard-web/chunks/{v3-page-DXPFILRZ.js → v3-page-G6CD6O3I.js} +1 -1
  130. package/dist/dashboard-web/chunks/{whiteboards-page-NUH6VZ4I.js → whiteboards-page-6SG33GP4.js} +1 -1
  131. package/dist/dashboard-web/style.css +167 -2
  132. package/dist/i18n/en.d.ts.map +1 -1
  133. package/dist/i18n/en.js +18 -3
  134. package/dist/i18n/en.js.map +1 -1
  135. package/dist/i18n/zh.d.ts.map +1 -1
  136. package/dist/i18n/zh.js +18 -3
  137. package/dist/i18n/zh.js.map +1 -1
  138. package/dist/im/lark/card-builder.d.ts.map +1 -1
  139. package/dist/im/lark/card-builder.js +11 -2
  140. package/dist/im/lark/card-builder.js.map +1 -1
  141. package/dist/im/lark/event-dispatcher.d.ts +4 -0
  142. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  143. package/dist/im/lark/event-dispatcher.js +249 -5
  144. package/dist/im/lark/event-dispatcher.js.map +1 -1
  145. package/dist/im/lark/forward-followup-buffer.d.ts +26 -0
  146. package/dist/im/lark/forward-followup-buffer.d.ts.map +1 -0
  147. package/dist/im/lark/forward-followup-buffer.js +48 -0
  148. package/dist/im/lark/forward-followup-buffer.js.map +1 -0
  149. package/dist/im/lark/forward-followup-content.d.ts +6 -0
  150. package/dist/im/lark/forward-followup-content.d.ts.map +1 -0
  151. package/dist/im/lark/forward-followup-content.js +46 -0
  152. package/dist/im/lark/forward-followup-content.js.map +1 -0
  153. package/dist/im/lark/forward-followup-store.d.ts +9 -0
  154. package/dist/im/lark/forward-followup-store.d.ts.map +1 -0
  155. package/dist/im/lark/forward-followup-store.js +48 -0
  156. package/dist/im/lark/forward-followup-store.js.map +1 -0
  157. package/dist/im/lark/message-parser.d.ts +1 -1
  158. package/dist/im/lark/message-parser.d.ts.map +1 -1
  159. package/dist/im/lark/message-parser.js +1 -2
  160. package/dist/im/lark/message-parser.js.map +1 -1
  161. package/dist/platform/tunnel-client.js +3 -3
  162. package/dist/platform/tunnel-client.js.map +1 -1
  163. package/dist/services/local-cli-opener.d.ts +7 -0
  164. package/dist/services/local-cli-opener.d.ts.map +1 -1
  165. package/dist/services/local-cli-opener.js +111 -3
  166. package/dist/services/local-cli-opener.js.map +1 -1
  167. package/dist/services/skill-registry-store.d.ts.map +1 -1
  168. package/dist/services/skill-registry-store.js +15 -2
  169. package/dist/services/skill-registry-store.js.map +1 -1
  170. package/dist/services/vc-meeting-consumer-isolation.d.ts +7 -5
  171. package/dist/services/vc-meeting-consumer-isolation.d.ts.map +1 -1
  172. package/dist/services/vc-meeting-consumer-isolation.js +7 -5
  173. package/dist/services/vc-meeting-consumer-isolation.js.map +1 -1
  174. package/dist/services/vc-meeting-preparations-store.d.ts +22 -0
  175. package/dist/services/vc-meeting-preparations-store.d.ts.map +1 -0
  176. package/dist/services/vc-meeting-preparations-store.js +131 -0
  177. package/dist/services/vc-meeting-preparations-store.js.map +1 -0
  178. package/dist/services/vc-meeting-runtime-store.d.ts +9 -0
  179. package/dist/services/vc-meeting-runtime-store.d.ts.map +1 -1
  180. package/dist/services/vc-meeting-runtime-store.js +16 -0
  181. package/dist/services/vc-meeting-runtime-store.js.map +1 -1
  182. package/dist/setup/ensure-tmux.d.ts.map +1 -1
  183. package/dist/setup/ensure-tmux.js +9 -11
  184. package/dist/setup/ensure-tmux.js.map +1 -1
  185. package/dist/utils/child-env.d.ts +16 -8
  186. package/dist/utils/child-env.d.ts.map +1 -1
  187. package/dist/utils/child-env.js +30 -8
  188. package/dist/utils/child-env.js.map +1 -1
  189. package/dist/utils/daemon-discovery.d.ts +8 -0
  190. package/dist/utils/daemon-discovery.d.ts.map +1 -1
  191. package/dist/utils/daemon-discovery.js +19 -0
  192. package/dist/utils/daemon-discovery.js.map +1 -1
  193. package/dist/utils/ready-gate.d.ts +9 -3
  194. package/dist/utils/ready-gate.d.ts.map +1 -1
  195. package/dist/utils/ready-gate.js +12 -4
  196. package/dist/utils/ready-gate.js.map +1 -1
  197. package/dist/worker.js +131 -28
  198. package/dist/worker.js.map +1 -1
  199. package/dist/workflows/ops-projection.d.ts.map +1 -1
  200. package/dist/workflows/ops-projection.js +27 -5
  201. package/dist/workflows/ops-projection.js.map +1 -1
  202. package/dist/workflows/v3/distillation-runner.d.ts +4 -0
  203. package/dist/workflows/v3/distillation-runner.d.ts.map +1 -1
  204. package/dist/workflows/v3/distillation-runner.js +4 -2
  205. package/dist/workflows/v3/distillation-runner.js.map +1 -1
  206. package/package.json +1 -1
  207. package/dist/dashboard-web/chunks/sessions-page-WJ2QYTIE.js +0 -3
@@ -34,6 +34,8 @@ import { buildGrantCard } from './card-builder.js';
34
34
  import { openPending, isThrottled, clearPending } from './grant-pending.js';
35
35
  import { localeForBot, t } from '../../i18n/index.js';
36
36
  import { chatQuotaKey, globalQuotaKey } from '../../services/grant-store.js';
37
+ import { ForwardFollowupBuffer } from './forward-followup-buffer.js';
38
+ import { listForwardFollowups, putForwardFollowup, removeForwardFollowup } from './forward-followup-store.js';
37
39
  import { claimMessageOnce, _resetCacheForTest as _resetSeenMessagesForTest } from '../../services/seen-message-store.js';
38
40
  import { ensureDefaultOncallBound } from '../../services/oncall-store.js';
39
41
  import { resolveRegularGroupMode, resolveGroupMentionMode } from '../../services/chat-reply-mode-store.js';
@@ -491,7 +493,7 @@ function claimEventOnce(key) {
491
493
  function scheduleAckSafeEvent(key, work, label, claim = () => claimEventOnce(key)) {
492
494
  if (!claim()) {
493
495
  logger.info(`[event-dedupe] duplicate ${label} ignored: ${key}`);
494
- return;
496
+ return false;
495
497
  }
496
498
  // INVARIANT: the claim above + this setImmediate scheduling must stay fully
497
499
  // synchronous (no await before we get here). WS events arrive in order and
@@ -501,6 +503,7 @@ function scheduleAckSafeEvent(key, work, label, claim = () => claimEventOnce(key
501
503
  setImmediate(() => {
502
504
  void work().catch(err => logger.error(`Error handling ${label}: ${err}`));
503
505
  });
506
+ return true;
504
507
  }
505
508
  // Fallback for an event that carries no stable id at all. Dedupe must never
506
509
  // silently DROP a real message, so we hand back a unique key (process it, accept
@@ -1228,6 +1231,28 @@ export async function checkGroupMessageAccess(larkAppId, message, chatId, sender
1228
1231
  }
1229
1232
  return 'ignore';
1230
1233
  }
1234
+ function usesForwardFollowupDelay(mentionMode) {
1235
+ return mentionMode === 'never' || mentionMode === 'ambient';
1236
+ }
1237
+ /**
1238
+ * Per-app barriers that resolve once `restoreActiveSessions()` completes.
1239
+ *
1240
+ * Persisted forward-followup seeds are loaded into the in-memory buffer at
1241
+ * dispatcher startup (so root-linked clarifications can still pair with them
1242
+ * while the WS is already receiving events), but any flush — timer expiry or
1243
+ * immediate dispatch — waits for this barrier. Without it, `isSessionOwner`
1244
+ * would always return false before sessions are rehydrated, routing thread
1245
+ * replies through `handleNewTopic` and potentially duplicating sessions.
1246
+ */
1247
+ const sessionsReadyBarriers = new Map();
1248
+ const sessionsReadyApps = new Set();
1249
+ /** Mark a bot's active sessions as restored so pending forward-followup seeds can flush. */
1250
+ export function markForwardFollowupsSessionsReady(larkAppId) {
1251
+ sessionsReadyApps.add(larkAppId);
1252
+ const barrier = sessionsReadyBarriers.get(larkAppId);
1253
+ if (barrier)
1254
+ barrier.resolve();
1255
+ }
1231
1256
  /**
1232
1257
  * Best-effort plain-text extraction from a Lark message for routing-level
1233
1258
  * decisions (currently: `/t` / `/topic` detection). Handles the two common
@@ -1664,6 +1689,80 @@ function createLarkWsAgent() {
1664
1689
  * Returns the WSClient instance for lifecycle management.
1665
1690
  */
1666
1691
  export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, brand = 'feishu') {
1692
+ const forwardFollowups = new ForwardFollowupBuffer(config.daemon.forwardFollowupWaitMs, err => logger.error(`Error flushing delayed topic seed: ${err}`));
1693
+ // Barrier that resolves once restoreActiveSessions() completes. Flushes wait
1694
+ // on it so isSessionOwner reads the populated activeSessions map. If the app
1695
+ // was already marked ready (e.g. tests or a restart within the same process),
1696
+ // resolve immediately.
1697
+ let resolveSessionsReady;
1698
+ const sessionsReady = new Promise(resolve => { resolveSessionsReady = resolve; });
1699
+ if (sessionsReadyApps.has(larkAppId))
1700
+ resolveSessionsReady();
1701
+ sessionsReadyBarriers.set(larkAppId, { promise: sessionsReady, resolve: resolveSessionsReady });
1702
+ const dispatchHumanMessage = async (payload) => {
1703
+ await serializeByAnchor(payload.ctx.anchor, () => {
1704
+ const ownsSession = handlers.isSessionOwner?.(payload.ctx.anchor, larkAppId) ?? payload.ownsSession;
1705
+ return ownsSession
1706
+ ? handlers.handleThreadReply(payload.data, payload.ctx)
1707
+ : handlers.handleNewTopic(payload.data, payload.ctx);
1708
+ });
1709
+ };
1710
+ const dispatchPersistedForwardFollowup = async (seedMessageId, payload) => {
1711
+ // Wait for activeSessions to be rehydrated before checking ownership, so a
1712
+ // thread reply isn't misrouted to handleNewTopic during daemon startup.
1713
+ await sessionsReady;
1714
+ await dispatchHumanMessage(payload);
1715
+ removeForwardFollowup(larkAppId, seedMessageId);
1716
+ };
1717
+ // Load persisted seeds into the buffer immediately so root-linked
1718
+ // clarifications can pair with them while the WS is already receiving
1719
+ // events. Flushes (timer expiry or immediate) wait on sessionsReady above.
1720
+ for (const record of listForwardFollowups(larkAppId)) {
1721
+ const senderOpenId = record.payload?.data?.sender?.sender_id?.open_id;
1722
+ const chatId = record.payload?.ctx?.chatId;
1723
+ if (!senderOpenId || !chatId || !record.payload?.ctx?.anchor) {
1724
+ removeForwardFollowup(larkAppId, record.messageId);
1725
+ continue;
1726
+ }
1727
+ const flush = (payload) => dispatchPersistedForwardFollowup(record.messageId, payload);
1728
+ const remainingMs = record.dueAt - Date.now();
1729
+ const isUnpairedSeed = !record.payload.ctx.forwardSeedData;
1730
+ const delayStillEnabled = usesForwardFollowupDelay(resolveGroupMentionMode(larkAppId));
1731
+ if (isUnpairedSeed && delayStillEnabled && remainingMs > 0 && forwardFollowups.hold({
1732
+ larkAppId,
1733
+ chatId,
1734
+ senderOpenId,
1735
+ messageId: record.messageId,
1736
+ payload: record.payload,
1737
+ flush,
1738
+ }, remainingMs)) {
1739
+ logger.info(`[forward-followup] restored pending seed=${record.messageId.substring(0, 12)} remaining=${remainingMs}ms`);
1740
+ }
1741
+ else {
1742
+ void flush(record.payload).catch(err => logger.error(`Error restoring delayed topic seed: ${err}`));
1743
+ }
1744
+ }
1745
+ const seedRoutingGates = new Map();
1746
+ const registerSeedRoutingGate = (messageId) => {
1747
+ let resolveReady;
1748
+ const ready = new Promise(resolve => { resolveReady = resolve; });
1749
+ let completed = false;
1750
+ const gate = {
1751
+ ready,
1752
+ complete: () => {
1753
+ if (completed)
1754
+ return;
1755
+ completed = true;
1756
+ resolveReady();
1757
+ setTimeout(() => {
1758
+ if (seedRoutingGates.get(messageId) === gate)
1759
+ seedRoutingGates.delete(messageId);
1760
+ }, config.daemon.forwardFollowupWaitMs);
1761
+ },
1762
+ };
1763
+ seedRoutingGates.set(messageId, gate);
1764
+ return gate;
1765
+ };
1667
1766
  const eventDispatcher = new Lark.EventDispatcher({}).register({
1668
1767
  // 主动开工 — 场景①: the bot was added to a chat. Hand off to the daemon,
1669
1768
  // which gates on the autoStartOnGroupJoin toggle + allowedUser membership.
@@ -1711,7 +1810,8 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
1711
1810
  const claim = messageIdForKey
1712
1811
  ? () => claimMessageOnce(larkAppId, messageIdForKey)
1713
1812
  : () => claimEventOnce(eventKey);
1714
- scheduleAckSafeEvent(eventKey, async () => {
1813
+ let seedRoutingGate;
1814
+ const scheduled = scheduleAckSafeEvent(eventKey, async () => {
1715
1815
  try {
1716
1816
  const message = data.message;
1717
1817
  const sender = data.sender;
@@ -2144,6 +2244,83 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
2144
2244
  senderOpenId,
2145
2245
  });
2146
2246
  const summaryCommandTriggered = !!summaryCommandMatch && isAllowed;
2247
+ const routingText = extractMessageTextForRouting(message);
2248
+ const strippedRoutingText = routingText
2249
+ ? stripLeadingMentions(routingText.trim(), message?.mentions ?? []).trim()
2250
+ : '';
2251
+ const isControlCommand = strippedRoutingText.startsWith('/');
2252
+ let pairedForwardSeed;
2253
+ // Require isAllowed before pairing: a root-linked clarification from a
2254
+ // sender who was /revoked within the grace window must not consume the
2255
+ // seed from the buffer or overwrite the durable paired record. The seed
2256
+ // stays in the buffer and flushes on its original timer. Paired seeds
2257
+ // only arise under never/ambient modes, where a legitimate merge always
2258
+ // requires isAllowed anyway.
2259
+ if (senderOpenId && isAllowed && message.root_id && !isControlCommand) {
2260
+ await seedRoutingGates.get(message.root_id)?.ready;
2261
+ const pairingInput = {
2262
+ larkAppId,
2263
+ chatId,
2264
+ senderOpenId,
2265
+ rootId: message.root_id,
2266
+ };
2267
+ const pairingMentionMode = resolveGroupMentionMode(larkAppId);
2268
+ const ambientRedirect = pairingMentionMode === 'ambient'
2269
+ && !explicitlyMentionedThisBot
2270
+ && mentionsAnotherMember(larkAppId, message);
2271
+ const pairingDelayEnabled = usesForwardFollowupDelay(pairingMentionMode);
2272
+ if (pairingDelayEnabled && !ambientRedirect) {
2273
+ pairedForwardSeed = forwardFollowups.take(pairingInput);
2274
+ }
2275
+ else if (!pairingDelayEnabled) {
2276
+ const stalePendingSeed = forwardFollowups.take(pairingInput);
2277
+ if (stalePendingSeed) {
2278
+ try {
2279
+ await dispatchPersistedForwardFollowup(stalePendingSeed.messageId, stalePendingSeed.payload);
2280
+ }
2281
+ catch (err) {
2282
+ logger.warn(`[forward-followup] failed to flush stale seed=${stalePendingSeed.messageId.substring(0, 12)}; ` +
2283
+ `continuing current msg=${messageId.substring(0, 12)}: ${err}`);
2284
+ }
2285
+ }
2286
+ }
2287
+ }
2288
+ if (pairedForwardSeed) {
2289
+ // The clarification becomes the visible Lark topic root. The earlier
2290
+ // forwarded seed is retained only as prompt context, so the bot never
2291
+ // emits a reply under the forwarding bubble itself.
2292
+ routing.scope = 'thread';
2293
+ routing.anchor = messageId;
2294
+ routingSource = 'topic-chat';
2295
+ replyRootId = undefined;
2296
+ ownsSession = false;
2297
+ try {
2298
+ putForwardFollowup(larkAppId, {
2299
+ messageId: pairedForwardSeed.messageId,
2300
+ dueAt: Date.now(),
2301
+ payload: {
2302
+ data,
2303
+ ctx: {
2304
+ ...pairedForwardSeed.payload.ctx,
2305
+ chatId,
2306
+ messageId,
2307
+ chatType,
2308
+ larkAppId,
2309
+ scope: 'thread',
2310
+ anchor: messageId,
2311
+ replyRootId: undefined,
2312
+ forwardSeedData: pairedForwardSeed.payload.data,
2313
+ },
2314
+ ownsSession: false,
2315
+ },
2316
+ });
2317
+ }
2318
+ catch (err) {
2319
+ logger.warn(`[forward-followup] failed to persist provisional paired payload: ${err}`);
2320
+ }
2321
+ logger.info(`[forward-followup] merged seed=${pairedForwardSeed.messageId.substring(0, 12)} ` +
2322
+ `into msg=${messageId.substring(0, 12)} chat=${chatId.substring(0, 12)}`);
2323
+ }
2147
2324
  // Permission gating — same shape as before, just keyed on
2148
2325
  // `ownsSession` (anchor-aware) instead of "rootId presence":
2149
2326
  //
@@ -2186,6 +2363,9 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
2186
2363
  // 默认 'always' 在多人群里必须 @,想要话题内免@续话就把 mentionMode 配成
2187
2364
  // 'topic'(下方条款已同时覆盖话题群 thread 与普通群 shared topic),
2188
2365
  // 'never'/'ambient' 亦按各自语义生效。1人1bot 的 solo 群仍走末条放行。
2366
+ // 注:pairedForwardSeed 仅在 never/ambient 模式下产生,且 ambient redirect
2367
+ // 已在配对前排除,故 isAllowed=true 时下方 never/ambient 条款必然放行;
2368
+ // 不在此单独加 clause,以免 isAllowed=false 时绕过权限检查。
2189
2369
  const relax = (!!replyRootId && isAllowed)
2190
2370
  || (!!substituteTrigger && isAllowed)
2191
2371
  || (isAllowed && mentionMode === 'never')
@@ -2248,6 +2428,7 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
2248
2428
  ? { name: 'summary-command', chatKind: summaryCommandMatch.chatKind }
2249
2429
  : undefined,
2250
2430
  substituteTrigger,
2431
+ forwardSeedData: pairedForwardSeed?.payload.data,
2251
2432
  };
2252
2433
  if (explicitlyMentionedThisBot) {
2253
2434
  const before = await handlers.beforeSessionTurn?.(data, ctx, { senderOpenId, explicitlyMentionedThisBot });
@@ -2257,19 +2438,82 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
2257
2438
  ctx.anchor = before.anchorOverride;
2258
2439
  ownsSession = handlers.isSessionOwner?.(ctx.anchor, larkAppId) ?? ownsSession;
2259
2440
  }
2441
+ const payload = { data, ctx, ownsSession };
2442
+ const groupMentionMode = resolveGroupMentionMode(larkAppId);
2443
+ const shouldDelayTopicSeed = usesForwardFollowupDelay(groupMentionMode)
2444
+ && !pairedForwardSeed
2445
+ && !isControlCommand
2446
+ && !!senderOpenId
2447
+ && routingSource === 'topic-chat'
2448
+ && ctx.scope === 'thread'
2449
+ && ctx.anchor === messageId
2450
+ && !ownsSession;
2451
+ if (shouldDelayTopicSeed) {
2452
+ try {
2453
+ putForwardFollowup(larkAppId, {
2454
+ messageId,
2455
+ dueAt: Date.now() + config.daemon.forwardFollowupWaitMs,
2456
+ payload,
2457
+ });
2458
+ if (forwardFollowups.hold({
2459
+ larkAppId,
2460
+ chatId,
2461
+ senderOpenId,
2462
+ messageId,
2463
+ payload,
2464
+ flush: delayedPayload => dispatchPersistedForwardFollowup(messageId, delayedPayload),
2465
+ })) {
2466
+ logger.debug(`[forward-followup] holding topic seed msg=${messageId.substring(0, 12)} ` +
2467
+ `for ${config.daemon.forwardFollowupWaitMs}ms`);
2468
+ return;
2469
+ }
2470
+ removeForwardFollowup(larkAppId, messageId);
2471
+ }
2472
+ catch (err) {
2473
+ logger.warn(`[forward-followup] persistence unavailable, dispatching immediately: ${err}`);
2474
+ }
2475
+ }
2476
+ if (pairedForwardSeed) {
2477
+ try {
2478
+ putForwardFollowup(larkAppId, {
2479
+ messageId: pairedForwardSeed.messageId,
2480
+ dueAt: Date.now(),
2481
+ payload,
2482
+ });
2483
+ }
2484
+ catch (err) {
2485
+ logger.warn(`[forward-followup] failed to persist paired payload before dispatch: ${err}`);
2486
+ }
2487
+ await dispatchPersistedForwardFollowup(pairedForwardSeed.messageId, payload)
2488
+ .catch(err => logger.error(`Error handling paired message event: ${err}`));
2489
+ return;
2490
+ }
2260
2491
  // Serialize per anchor so two messages to the same thread/chat are
2261
2492
  // processed in arrival order — never concurrently. Without this a fast
2262
2493
  // second message interleaves with the first's async session-spawn and is
2263
2494
  // dropped (worker-not-ready → re-fork branch). See anchor-serializer.ts.
2264
- await serializeByAnchor(ctx.anchor, () => ownsSession
2265
- ? handlers.handleThreadReply(data, ctx)
2266
- : handlers.handleNewTopic(data, ctx))
2495
+ await dispatchHumanMessage(payload)
2267
2496
  .catch(err => logger.error(`Error handling message event: ${err}`));
2268
2497
  }
2269
2498
  catch (err) {
2270
2499
  logger.error(`Error handling message event: ${err}`);
2271
2500
  }
2501
+ finally {
2502
+ seedRoutingGate?.complete();
2503
+ }
2272
2504
  }, 'message event', claim);
2505
+ const rawMessage = data?.message;
2506
+ const rawSenderType = data?.sender?.sender_type;
2507
+ if (scheduled
2508
+ && config.daemon.forwardFollowupWaitMs > 0
2509
+ && rawMessage?.message_id
2510
+ && rawMessage.chat_type !== 'p2p'
2511
+ && !rawMessage.root_id
2512
+ && !rawMessage.thread_id
2513
+ && rawSenderType !== 'app'
2514
+ && rawSenderType !== 'bot') {
2515
+ seedRoutingGate = registerSeedRoutingGate(rawMessage.message_id);
2516
+ }
2273
2517
  },
2274
2518
  });
2275
2519
  // 诊断:包一层 invoke,记录长连接收到的**每一个**事件类型(含未注册的)。