botmux 3.9.0 → 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/.runtime-build-id +1 -1
  2. package/dist/adapters/backend/tmux-pipe-backend.d.ts +7 -0
  3. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
  4. package/dist/adapters/backend/tmux-pipe-backend.js +20 -0
  5. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  6. package/dist/adapters/backend/types.d.ts +12 -0
  7. package/dist/adapters/backend/types.d.ts.map +1 -1
  8. package/dist/adapters/backend/types.js.map +1 -1
  9. package/dist/adapters/cli/codex.d.ts.map +1 -1
  10. package/dist/adapters/cli/codex.js +40 -7
  11. package/dist/adapters/cli/codex.js.map +1 -1
  12. package/dist/cli/deferred-topic-send.d.ts +11 -0
  13. package/dist/cli/deferred-topic-send.d.ts.map +1 -1
  14. package/dist/cli/deferred-topic-send.js +21 -4
  15. package/dist/cli/deferred-topic-send.js.map +1 -1
  16. package/dist/cli.d.ts.map +1 -1
  17. package/dist/cli.js +83 -22
  18. package/dist/cli.js.map +1 -1
  19. package/dist/core/dispatch.d.ts +43 -5
  20. package/dist/core/dispatch.d.ts.map +1 -1
  21. package/dist/core/dispatch.js +104 -6
  22. package/dist/core/dispatch.js.map +1 -1
  23. package/dist/i18n/en.d.ts.map +1 -1
  24. package/dist/i18n/en.js +1 -0
  25. package/dist/i18n/en.js.map +1 -1
  26. package/dist/i18n/zh.d.ts.map +1 -1
  27. package/dist/i18n/zh.js +1 -0
  28. package/dist/i18n/zh.js.map +1 -1
  29. package/dist/services/codex-composer-state.d.ts +27 -0
  30. package/dist/services/codex-composer-state.d.ts.map +1 -0
  31. package/dist/services/codex-composer-state.js +53 -0
  32. package/dist/services/codex-composer-state.js.map +1 -0
  33. package/dist/services/codex-transcript.d.ts +33 -12
  34. package/dist/services/codex-transcript.d.ts.map +1 -1
  35. package/dist/services/codex-transcript.js +82 -20
  36. package/dist/services/codex-transcript.js.map +1 -1
  37. package/dist/worker.js +138 -2
  38. package/dist/worker.js.map +1 -1
  39. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -34,7 +34,7 @@ import { validateWorkingDir } from './core/working-dir.js';
34
34
  import { resolveSessionContext } from './core/session-marker.js';
35
35
  import { resolveBotmuxDataDir } from './core/data-dir.js';
36
36
  import { dashboardSecretPath } from './core/dashboard-secret.js';
37
- import { acceptedDispatchBotAppIds, appendDispatchReportProtocol, appendLegacyDispatchReportProtocol, parseDispatchBotSpec, buildDispatchMessages, buildRepoPrimeText, buildReportContent, eligibleAutoMentionAliases, findDispatchRegistryEntry, offTopicSubBotTopic, resolveReportTarget, resolveSendTarget } from './core/dispatch.js';
37
+ import { acceptedDispatchBotAppIds, activeConversationBotOpenIds, appendDispatchReportProtocol, appendLegacyDispatchReportProtocol, parseDispatchBotSpec, buildDispatchMessages, buildRepoPrimeText, buildReportContent, eligibleAutoMentionAliases, findDispatchRegistryEntry, foldableChatSessionAppIds, offTopicSubBotTopic, resolveReportTarget, resolveSendTarget, threadRootForReachability } from './core/dispatch.js';
38
38
  import { pickTurnReplyTarget } from './core/reply-target.js';
39
39
  import { recordDispatchRegistryEntry } from './core/dispatch-registry.js';
40
40
  import { enableAutostart, disableAutostart, autostartStatus, refreshAutostart } from './autostart.js';
@@ -56,7 +56,8 @@ import { firstPositional } from './cli/arg-utils.js';
56
56
  import { isColdResumeDormant, isRealManagedSession, sessionListDisposition } from './cli/session-list-liveness.js';
57
57
  import { attachFrozenManagedZmxSession, freezeManagedZmxAttachTarget, } from './cli/zmx-managed-attach.js';
58
58
  import { dispatchPrimaryMessage, findStdinAliasAttachment, normalizeInteractiveCardInput, sendFileAttachments, sendVideoAttachments, shouldSendAsPureVideo, validateVideoAttachments } from './cli/send-dispatch.js';
59
- import { dispatchDeferredTopicSend } from './cli/deferred-topic-send.js';
59
+ import { dispatchDeferredTopicSend, reusableDeferredTopicRoot } from './cli/deferred-topic-send.js';
60
+ import { readDeferredTopicBinding } from './core/deferred-topic-binding.js';
60
61
  import { resolveDaemonEnv } from './cli/daemon-lifecycle-env.js';
61
62
  import { buildPm2SpawnCommand } from './cli/pm2-command.js';
62
63
  import { callDashboard } from './cli/dashboard-endpoint.js';
@@ -7105,7 +7106,8 @@ async function cmdSend(rest) {
7105
7106
  // is idempotent, so the downstream send path reuses these same clients.
7106
7107
  // envPinnedRiffBot is re-registered LAST so a remote env credential is never
7107
7108
  // clobbered by a stale bots.json entry for the same app.
7108
- const { registerBot, loadBotConfigs, findOncallChatForAnyBot } = await import('./bot-registry.js');
7109
+ const { registerBot, loadBotConfigs, findOncallChatForAnyBot, getBot } = await import('./bot-registry.js');
7110
+ const { resolveRegularGroupMode } = await import('./services/chat-reply-mode-store.js');
7109
7111
  try {
7110
7112
  for (const cfg of loadBotConfigs())
7111
7113
  registerBot(cfg);
@@ -7163,7 +7165,7 @@ async function cmdSend(rest) {
7163
7165
  process.exit(1);
7164
7166
  }
7165
7167
  }
7166
- const { sendMessage, replyMessage, uploadImage, uploadFile, MessageWithdrawnError } = await import('./im/lark/client.js');
7168
+ const { sendMessage, replyMessage, uploadImage, uploadFile, MessageWithdrawnError, getChatModeStrict } = await import('./im/lark/client.js');
7167
7169
  const appId = s.larkAppId;
7168
7170
  // Effective target chat for top-level mode (defaults to session's chat)
7169
7171
  const targetChatId = overrideChatId ?? s.chatId;
@@ -7171,6 +7173,48 @@ async function cmdSend(rest) {
7171
7173
  // reply_in_thread, otherwise Lark would force every reply into a fresh
7172
7174
  // topic — defeating the whole point of chat-scope routing.
7173
7175
  const isChatScope = s.scope === 'chat';
7176
+ // Compute the actual outbound anchor before the advisory guard. A chat-scope
7177
+ // sender can still reply into a per-turn topic, so sender scope alone does
7178
+ // not describe which peer sessions are reachable.
7179
+ const sendTarget = resolveSendTarget({ into: sendInto, topLevel: sendTopLevel, chatScope: isChatScope, chatId: targetChatId, rootMessageId: s.rootMessageId, replyTargetRootId: turnReplyTarget?.rootMessageId, replyTargetTurnId: turnReplyTarget?.turnId, replyTargetQuoteOnly: turnReplyTarget?.quoteOnly, currentTurnId });
7180
+ const dataDir = resolveDataDir();
7181
+ const deferredBinding = !sendInto && (!overrideChatId || overrideChatId === s.chatId)
7182
+ ? readDeferredTopicBinding(dataDir, s.sessionId)
7183
+ : undefined;
7184
+ const deferredRoot = reusableDeferredTopicRoot({
7185
+ session: s,
7186
+ binding: deferredBinding,
7187
+ explicitTopLevel: sendTopLevel,
7188
+ reuseBoundRootWhenTopLevel: deferredMaterializedByThisCommand,
7189
+ });
7190
+ const reachabilityTarget = deferredRoot
7191
+ ? { mode: 'thread', rootMessageId: deferredRoot }
7192
+ : sendTarget;
7193
+ // Load the sender-scoped bot identity map once. Besides prose @Name
7194
+ // injection below, it lets the sub-bot hint recognize peers that already
7195
+ // have an active session in THIS conversation.
7196
+ let botEntries = [];
7197
+ let crossRef = {};
7198
+ try {
7199
+ const botInfoPath = join(dataDir, 'bots-info.json');
7200
+ const parsedBotEntries = existsSync(botInfoPath)
7201
+ ? JSON.parse(readFileSync(botInfoPath, 'utf-8'))
7202
+ : [];
7203
+ botEntries = Array.isArray(parsedBotEntries)
7204
+ ? parsedBotEntries.filter((entry) => !!entry
7205
+ && typeof entry === 'object'
7206
+ && typeof entry.larkAppId === 'string'
7207
+ && (entry.botName === null || typeof entry.botName === 'string'))
7208
+ : [];
7209
+ const crossRefPath = join(dataDir, `bot-openids-${appId}.json`);
7210
+ const parsedCrossRef = existsSync(crossRefPath)
7211
+ ? JSON.parse(readFileSync(crossRefPath, 'utf-8'))
7212
+ : {};
7213
+ crossRef = parsedCrossRef && typeof parsedCrossRef === 'object' && !Array.isArray(parsedCrossRef)
7214
+ ? parsedCrossRef
7215
+ : {};
7216
+ }
7217
+ catch { /* best-effort identity map */ }
7174
7218
  // ── Footgun guard: orchestrator → sub-bot ──
7175
7219
  // A dispatched sub-bot's session lives in its sub-topic; @-ing it from the main
7176
7220
  // chat spawns a fresh, context-less one. The check is computed ONCE and applied
@@ -7179,23 +7223,49 @@ async function cmdSend(rest) {
7179
7223
  // `@OtherSubBot` can't slip past after this explicit guard already ran.
7180
7224
  let dispatchReg = {};
7181
7225
  try {
7182
- const regPath = join(resolveDataDir(), 'orchestrate-dispatch.json');
7226
+ const regPath = join(dataDir, 'orchestrate-dispatch.json');
7183
7227
  if (existsSync(regPath))
7184
7228
  dispatchReg = JSON.parse(readFileSync(regPath, 'utf-8'));
7185
7229
  }
7186
7230
  catch { /* no/!corrupt registry → no guard */ }
7187
7231
  const dispatchActiveSeeds = new Set();
7232
+ let allSessions = [];
7188
7233
  if (Object.keys(dispatchReg).length > 0) {
7189
- for (const sess of loadSessions().values()) {
7190
- if (sess.status === 'active' && sess.scope !== 'chat' && sess.rootMessageId) {
7234
+ allSessions = [...loadSessions().values()];
7235
+ for (const sess of allSessions) {
7236
+ if (sess.status !== 'active')
7237
+ continue;
7238
+ if (sess.scope !== 'chat' && sess.rootMessageId) {
7191
7239
  dispatchActiveSeeds.add(sess.rootMessageId);
7192
7240
  }
7193
7241
  }
7194
7242
  }
7243
+ // An active chat-scope session can outlive a /reply-mode switch. Verify the
7244
+ // target bot's current effective mode before assuming mentions still fold
7245
+ // back into that old session.
7246
+ const foldableChatAppIds = await foldableChatSessionAppIds({
7247
+ sessions: allSessions,
7248
+ targetChatId,
7249
+ outboundMode: reachabilityTarget.mode,
7250
+ resolveMode: (larkAppId, chatId) => {
7251
+ getBot(larkAppId); // unknown target bot must fail closed
7252
+ return resolveRegularGroupMode(larkAppId, chatId);
7253
+ },
7254
+ resolveChatMode: chatId => getChatModeStrict(appId, chatId),
7255
+ });
7256
+ const reachableOpenIds = activeConversationBotOpenIds({
7257
+ sessions: allSessions,
7258
+ targetChatId,
7259
+ outboundRootMessageId: threadRootForReachability(reachabilityTarget),
7260
+ foldableChatAppIds,
7261
+ botEntries,
7262
+ crossRef,
7263
+ });
7195
7264
  // Sub-topic seed if `openId` is a dispatched sub-bot in an active topic that is
7196
- // NOT reachable in the current conversation; else null. The bot I'm replying to
7197
- // here (quoteTargetSenderOpenId) is reachable, so it's never treated as off-topic.
7198
- const offTopicSubBotSeed = (openId) => offTopicSubBotTopic({ mentionOpenId: openId, quoteTargetSenderOpenId: replyTargetSenderOpenId, chatId: targetChatId, registry: dispatchReg, activeSeeds: dispatchActiveSeeds });
7265
+ // NOT reachable in the current conversation; else null. Both the bot I'm
7266
+ // replying to and any peer with an active session at this conversation anchor
7267
+ // are reachable, so an unrelated old dispatch topic must not be recommended.
7268
+ const offTopicSubBotSeed = (openId) => offTopicSubBotTopic({ mentionOpenId: openId, quoteTargetSenderOpenId: replyTargetSenderOpenId, reachableOpenIds, chatId: targetChatId, registry: dispatchReg, activeSeeds: dispatchActiveSeeds });
7199
7269
  // Explicit --mention / --mention-back of an off-topic sub-bot → block + point to
7200
7270
  // the right command (--anyway overrides). Prose @Name injection is filtered
7201
7271
  // (dropped, not blocked) at its own site below.
@@ -7223,9 +7293,9 @@ async function cmdSend(rest) {
7223
7293
  rootMessageId: s.rootMessageId,
7224
7294
  title: s.title,
7225
7295
  };
7226
- // Dispatch helper: top-level / chat-scope send vs reply-in-thread, single
7227
- // decision point. Used for file attachments (always plain in chat scope).
7228
- const sendTarget = resolveSendTarget({ into: sendInto, topLevel: sendTopLevel, chatScope: isChatScope, chatId: targetChatId, rootMessageId: s.rootMessageId, replyTargetRootId: turnReplyTarget?.rootMessageId, replyTargetTurnId: turnReplyTarget?.turnId, replyTargetQuoteOnly: turnReplyTarget?.quoteOnly, currentTurnId });
7296
+ // Ordinary delivery uses the nominal target. Deferred delivery gets first
7297
+ // refusal below; the advisory mirrors its existing binding in
7298
+ // `reachabilityTarget` above so both paths agree about the effective root.
7229
7299
  const dispatch = async (content, msgType, uuid, suppressHook) => {
7230
7300
  // This closure also carries attachments, so every Lark call re-checks the
7231
7301
  // exact durable attempt/member instead of inheriting the early cmd gate.
@@ -7399,16 +7469,7 @@ async function cmdSend(rest) {
7399
7469
  // "获取群组中其他机器人和用户@当前机器人的消息"权限),不再走任何本地
7400
7470
  // 转发——botmux 历史上为绕过 Lark 不投递跨 bot 事件搞过 signal-file,
7401
7471
  // 那套已经在该权限上线后整体下线。
7402
- let botEntries = [];
7403
- let crossRef = {};
7404
7472
  try {
7405
- const dataDir = resolveDataDir();
7406
- const botInfoPath = join(dataDir, 'bots-info.json');
7407
- botEntries = existsSync(botInfoPath) ? JSON.parse(readFileSync(botInfoPath, 'utf-8')) : [];
7408
- const crossRefPath = join(dataDir, `bot-openids-${appId}.json`);
7409
- crossRef = existsSync(crossRefPath)
7410
- ? JSON.parse(readFileSync(crossRefPath, 'utf-8'))
7411
- : {};
7412
7473
  // --no-mention 显式不 @ 任何人:跳过正文 @BotName 的自动注入,否则正文里
7413
7474
  // 出现的 @名字 仍会被注入成 <at>,破坏 --no-mention 语义、还可能误触发对方
7414
7475
  // bot(正是要避免的循环 @)。botEntries/crossRef 仍需加载供 footer 寻址用。