botmux 3.0.0 → 3.1.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 (220) 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 +193 -68
  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 +66 -6
  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 +354 -143
  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/session-marker.d.ts.map +1 -1
  78. package/dist/core/session-marker.js +5 -2
  79. package/dist/core/session-marker.js.map +1 -1
  80. package/dist/core/types.d.ts +48 -0
  81. package/dist/core/types.d.ts.map +1 -1
  82. package/dist/core/types.js +55 -0
  83. package/dist/core/types.js.map +1 -1
  84. package/dist/core/update-check.d.ts +11 -0
  85. package/dist/core/update-check.d.ts.map +1 -1
  86. package/dist/core/update-check.js +6 -1
  87. package/dist/core/update-check.js.map +1 -1
  88. package/dist/core/vc-meeting-prepare-command.d.ts +22 -0
  89. package/dist/core/vc-meeting-prepare-command.d.ts.map +1 -0
  90. package/dist/core/vc-meeting-prepare-command.js +50 -0
  91. package/dist/core/vc-meeting-prepare-command.js.map +1 -0
  92. package/dist/core/worker-pool.d.ts.map +1 -1
  93. package/dist/core/worker-pool.js +21 -10
  94. package/dist/core/worker-pool.js.map +1 -1
  95. package/dist/daemon.d.ts +15 -5
  96. package/dist/daemon.d.ts.map +1 -1
  97. package/dist/daemon.js +645 -50
  98. package/dist/daemon.js.map +1 -1
  99. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  100. package/dist/dashboard/web/bot-defaults-page.js +6 -1
  101. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  102. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  103. package/dist/dashboard/web/i18n.js +34 -0
  104. package/dist/dashboard/web/i18n.js.map +1 -1
  105. package/dist/dashboard/web/preferences.d.ts +1 -1
  106. package/dist/dashboard/web/preferences.d.ts.map +1 -1
  107. package/dist/dashboard/web/preferences.js +1 -1
  108. package/dist/dashboard/web/preferences.js.map +1 -1
  109. package/dist/dashboard/web/sessions-page.d.ts +19 -0
  110. package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
  111. package/dist/dashboard/web/sessions-page.js +66 -7
  112. package/dist/dashboard/web/sessions-page.js.map +1 -1
  113. package/dist/dashboard/web/sessions.d.ts +31 -0
  114. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  115. package/dist/dashboard/web/sessions.js +82 -0
  116. package/dist/dashboard/web/sessions.js.map +1 -1
  117. package/dist/dashboard-web/app.js +1 -1
  118. package/dist/dashboard-web/chunks/{bot-defaults-page-MBORWWP7.js → bot-defaults-page-KLDCZNLP.js} +5 -5
  119. package/dist/dashboard-web/chunks/{chunk-G43RUPUN.js → chunk-EZAW2EPM.js} +1 -1
  120. package/dist/dashboard-web/chunks/{chunk-EIO5AWDA.js → chunk-FMJNOATE.js} +1 -1
  121. package/dist/dashboard-web/chunks/{chunk-4QTBR7UF.js → chunk-KEAOTUL3.js} +7 -7
  122. package/dist/dashboard-web/chunks/{chunk-7STGZ5BX.js → chunk-MLNI5BKE.js} +1 -1
  123. package/dist/dashboard-web/chunks/{connectors-page-LKDT4WBG.js → connectors-page-O32LFAL5.js} +1 -1
  124. package/dist/dashboard-web/chunks/{groups-page-XKIWJ2TP.js → groups-page-PSAFIZ6M.js} +1 -1
  125. package/dist/dashboard-web/chunks/{insights-page-CBEZCYKV.js → insights-page-V23LLATX.js} +1 -1
  126. package/dist/dashboard-web/chunks/{monitor-room-AQTHAWXA.js → monitor-room-7X5AKYOF.js} +1 -1
  127. package/dist/dashboard-web/chunks/{monitoring-page-2R4NCNBQ.js → monitoring-page-CBA3CQYL.js} +1 -1
  128. package/dist/dashboard-web/chunks/{office-page-5M6MQIIL.js → office-page-RL6QSEM7.js} +1 -1
  129. package/dist/dashboard-web/chunks/{overview-page-N22HDZ2V.js → overview-page-XQIPO6KH.js} +1 -1
  130. package/dist/dashboard-web/chunks/{roles-page-J6K2HWFX.js → roles-page-ACIWV7QS.js} +1 -1
  131. package/dist/dashboard-web/chunks/{schedules-page-CGAQCB4J.js → schedules-page-KJDUKOJV.js} +1 -1
  132. package/dist/dashboard-web/chunks/sessions-page-2WDWHSP4.js +3 -0
  133. package/dist/dashboard-web/chunks/{settings-page-GLRWV5KU.js → settings-page-6VX4OUGG.js} +1 -1
  134. package/dist/dashboard-web/chunks/{skills-page-KQVW42QU.js → skills-page-WXE3UYLT.js} +1 -1
  135. package/dist/dashboard-web/chunks/{team-federation-page-5PYUOV5M.js → team-federation-page-PYTF4FLM.js} +1 -1
  136. package/dist/dashboard-web/chunks/{v3-page-DXPFILRZ.js → v3-page-G6CD6O3I.js} +1 -1
  137. package/dist/dashboard-web/chunks/{whiteboards-page-NUH6VZ4I.js → whiteboards-page-6SG33GP4.js} +1 -1
  138. package/dist/dashboard-web/style.css +167 -2
  139. package/dist/i18n/en.d.ts.map +1 -1
  140. package/dist/i18n/en.js +19 -3
  141. package/dist/i18n/en.js.map +1 -1
  142. package/dist/i18n/zh.d.ts.map +1 -1
  143. package/dist/i18n/zh.js +19 -3
  144. package/dist/i18n/zh.js.map +1 -1
  145. package/dist/im/lark/card-builder.d.ts.map +1 -1
  146. package/dist/im/lark/card-builder.js +11 -2
  147. package/dist/im/lark/card-builder.js.map +1 -1
  148. package/dist/im/lark/card-handler.d.ts +2 -0
  149. package/dist/im/lark/card-handler.d.ts.map +1 -1
  150. package/dist/im/lark/card-handler.js +219 -139
  151. package/dist/im/lark/card-handler.js.map +1 -1
  152. package/dist/im/lark/event-dispatcher.d.ts +4 -0
  153. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  154. package/dist/im/lark/event-dispatcher.js +249 -5
  155. package/dist/im/lark/event-dispatcher.js.map +1 -1
  156. package/dist/im/lark/forward-followup-buffer.d.ts +26 -0
  157. package/dist/im/lark/forward-followup-buffer.d.ts.map +1 -0
  158. package/dist/im/lark/forward-followup-buffer.js +48 -0
  159. package/dist/im/lark/forward-followup-buffer.js.map +1 -0
  160. package/dist/im/lark/forward-followup-content.d.ts +6 -0
  161. package/dist/im/lark/forward-followup-content.d.ts.map +1 -0
  162. package/dist/im/lark/forward-followup-content.js +46 -0
  163. package/dist/im/lark/forward-followup-content.js.map +1 -0
  164. package/dist/im/lark/forward-followup-store.d.ts +9 -0
  165. package/dist/im/lark/forward-followup-store.d.ts.map +1 -0
  166. package/dist/im/lark/forward-followup-store.js +48 -0
  167. package/dist/im/lark/forward-followup-store.js.map +1 -0
  168. package/dist/im/lark/message-parser.d.ts +1 -1
  169. package/dist/im/lark/message-parser.d.ts.map +1 -1
  170. package/dist/im/lark/message-parser.js +1 -2
  171. package/dist/im/lark/message-parser.js.map +1 -1
  172. package/dist/platform/tunnel-client.js +3 -3
  173. package/dist/platform/tunnel-client.js.map +1 -1
  174. package/dist/services/local-cli-opener.d.ts +7 -0
  175. package/dist/services/local-cli-opener.d.ts.map +1 -1
  176. package/dist/services/local-cli-opener.js +111 -3
  177. package/dist/services/local-cli-opener.js.map +1 -1
  178. package/dist/services/skill-registry-store.d.ts.map +1 -1
  179. package/dist/services/skill-registry-store.js +15 -2
  180. package/dist/services/skill-registry-store.js.map +1 -1
  181. package/dist/services/vc-meeting-consumer-isolation.d.ts +7 -5
  182. package/dist/services/vc-meeting-consumer-isolation.d.ts.map +1 -1
  183. package/dist/services/vc-meeting-consumer-isolation.js +7 -5
  184. package/dist/services/vc-meeting-consumer-isolation.js.map +1 -1
  185. package/dist/services/vc-meeting-preparations-store.d.ts +22 -0
  186. package/dist/services/vc-meeting-preparations-store.d.ts.map +1 -0
  187. package/dist/services/vc-meeting-preparations-store.js +131 -0
  188. package/dist/services/vc-meeting-preparations-store.js.map +1 -0
  189. package/dist/services/vc-meeting-runtime-store.d.ts +9 -0
  190. package/dist/services/vc-meeting-runtime-store.d.ts.map +1 -1
  191. package/dist/services/vc-meeting-runtime-store.js +16 -0
  192. package/dist/services/vc-meeting-runtime-store.js.map +1 -1
  193. package/dist/setup/ensure-tmux.d.ts.map +1 -1
  194. package/dist/setup/ensure-tmux.js +9 -11
  195. package/dist/setup/ensure-tmux.js.map +1 -1
  196. package/dist/types.d.ts +2 -0
  197. package/dist/types.d.ts.map +1 -1
  198. package/dist/utils/child-env.d.ts +24 -8
  199. package/dist/utils/child-env.d.ts.map +1 -1
  200. package/dist/utils/child-env.js +49 -8
  201. package/dist/utils/child-env.js.map +1 -1
  202. package/dist/utils/daemon-discovery.d.ts +8 -0
  203. package/dist/utils/daemon-discovery.d.ts.map +1 -1
  204. package/dist/utils/daemon-discovery.js +19 -0
  205. package/dist/utils/daemon-discovery.js.map +1 -1
  206. package/dist/utils/ready-gate.d.ts +9 -3
  207. package/dist/utils/ready-gate.d.ts.map +1 -1
  208. package/dist/utils/ready-gate.js +12 -4
  209. package/dist/utils/ready-gate.js.map +1 -1
  210. package/dist/worker.js +141 -29
  211. package/dist/worker.js.map +1 -1
  212. package/dist/workflows/ops-projection.d.ts.map +1 -1
  213. package/dist/workflows/ops-projection.js +27 -5
  214. package/dist/workflows/ops-projection.js.map +1 -1
  215. package/dist/workflows/v3/distillation-runner.d.ts +4 -0
  216. package/dist/workflows/v3/distillation-runner.d.ts.map +1 -1
  217. package/dist/workflows/v3/distillation-runner.js +4 -2
  218. package/dist/workflows/v3/distillation-runner.js.map +1 -1
  219. package/package.json +1 -1
  220. package/dist/dashboard-web/chunks/sessions-page-WJ2QYTIE.js +0 -3
@@ -29,7 +29,7 @@ import { getSessionWorkingDir, buildNewTopicCliInput, getAvailableBots, persistS
29
29
  import { publishAttentionPatch, announcePendingRepoSession } from '../../core/session-activity.js';
30
30
  import { fallbackTurnId } from '../../core/reply-target.js';
31
31
  import { validateWorkingDir } from '../../core/working-dir.js';
32
- import { sessionKey, sessionAnchorId, frozenDisplayMode } from '../../core/types.js';
32
+ import { sessionKey, sessionAnchorId, frozenDisplayMode, markRepoCardConsumed, isActiveRepoCard, claimCurrentRepoCard } from '../../core/types.js';
33
33
  import { buildTerminalUrl } from '../../core/terminal-url.js';
34
34
  import { createRepoWorktree, removeRepoWorktree, dirSuffixForBranch, pushWorktreeBranch } from '../../services/git-worktree.js';
35
35
  import { withCodexAppContext } from '../../utils/codex-app-context.js';
@@ -207,6 +207,11 @@ export async function commitRepoSelection(ctx, dirPath, dirLabel,
207
207
  // The worktree flow already posted a precise "worktree 已创建:path 分支 …"
208
208
  // line before funnelling in here — suppress the redundant "已选择/已切换"
209
209
  // confirmation so the user sees a single message, not two.
210
+ // pinWorkingDir=false: "直接开始"/skip keeps the default cwd for this launch
211
+ // without persisting it (esp. $HOME) onto session.workingDir — sibling bots
212
+ // must still get their own repo card instead of inheriting HOME.
213
+ // confirmReplyText: optional confirmation to send under the same claim
214
+ // (used by skip_repo so the post-fork reply window cannot race a second click).
210
215
  opts) {
211
216
  const { ds, rootId, cardMessageId, larkAppId, operatorOpenId, activeSessions, sessionReply } = ctx;
212
217
  const locTarget = localeForBot(ds.larkAppId);
@@ -216,85 +221,170 @@ opts) {
216
221
  const repoSessionKey = larkAppId ? sessionKey(rootId, larkAppId) : rootId;
217
222
  const sessionStillActive = () => activeSessions.get(repoSessionKey) === ds;
218
223
  const commitGenSessionId = ds.session.sessionId;
224
+ const pinWorkingDir = opts?.pinWorkingDir !== false;
219
225
  if (ds.pendingRepo) {
220
- // First spawn: pin the new cwd onto the CURRENT session before forking.
221
- ds.workingDir = dirPath;
222
- ds.session.workingDir = dirPath;
223
- // riff 多仓 stamp:只有多仓 worktree 流显式传入(保留用户选择顺序,首仓=primary);
224
- // 其它选仓路径一律清除旧 stamp——workingDir 变了,旧的多仓组合不再成立。
225
- ds.session.riffRepoDirs = opts?.riffRepoDirs;
226
- sessionStore.updateSession(ds.session);
227
- const selfBot = getBot(ds.larkAppId);
228
- const botCfg = selfBot.config;
229
- const effectiveCliId = sessionCliId(ds);
230
- // First-time repo selection — now spawn CLI with the original prompt
231
- ds.pendingRepo = false;
232
- publishAttentionPatch(ds);
233
- const pendingPrompt = ds.pendingPrompt ?? '';
234
- const pendingRawInput = ds.pendingRawInput;
235
- // Raw-input cold start still wraps any input buffered while the repo card
236
- // was pending — see the skip_repo branch for the rationale.
237
- const hasBufferedInput = pendingPrompt.trim().length > 0 ||
238
- (ds.pendingAttachments?.length ?? 0) > 0 ||
239
- (ds.pendingFollowUps?.length ?? 0) > 0;
240
- if (!pendingRawInput || hasBufferedInput)
241
- ensureSessionWhiteboard(ds);
242
- const wrappedInput = (!pendingRawInput || hasBufferedInput)
243
- ? buildNewTopicCliInput(pendingPrompt, ds.session.sessionId, effectiveCliId, botCfg.cliPathOverride, ds.pendingAttachments, ds.pendingMentions, await getAvailableBots(ds.larkAppId, ds.chatId), ds.pendingFollowUps, { name: selfBot.botName, openId: selfBot.botOpenId }, locTarget, ds.pendingSender, {
244
- larkAppId: ds.larkAppId,
245
- chatId: ds.chatId,
246
- whiteboardId: ds.session.whiteboardId,
247
- substituteTrigger: ds.pendingSubstituteTrigger,
248
- codexAppText: ds.pendingCodexAppText,
249
- codexAppApplicationContext: ds.pendingCodexAppApplicationContext,
250
- codexAppMessageContext: ds.pendingCodexAppMessageContext,
251
- codexAppFollowUps: ds.pendingCodexAppFollowUps,
252
- codexAppFollowUpContexts: ds.pendingCodexAppFollowUpContexts,
253
- })
254
- : { content: '' };
255
- const prompt = pendingRawInput ? '' : wrappedInput;
256
- // Last-line defence: prompt prep awaited above — if anything replaced
257
- // OR closed the session in that window, forking now would clobber it
258
- // (or resurrect a /close'd session).
259
- if (!sessionStillActive() || ds.session.sessionId !== commitGenSessionId) {
260
- logger.warn(`[${tag(ds)}] Session replaced or closed while preparing the pending-CLI prompt (${commitGenSessionId} → ${ds.session.sessionId}, active=${sessionStillActive()}) — aborting this fork`);
226
+ // Card select/skip and auto-worktree converge here; the text /repo path
227
+ // mirrors this transaction through the same in-memory claim. Only one may
228
+ // own the pending -> worker transition, including prompt preparation.
229
+ if (ds.pendingRepoCommitInFlight) {
230
+ logger.info(`[${tag(ds)}] Pending repo commit already in flight — ignoring duplicate selection`);
261
231
  return;
262
232
  }
263
- if (pendingRawInput && hasBufferedInput) {
264
- ds.pendingFollowUpInput = {
265
- userPrompt: ds.pendingCodexAppText !== undefined || ds.pendingCodexAppFollowUps
266
- ? [ds.pendingCodexAppText ?? '', ...(ds.pendingCodexAppFollowUps ?? [])].filter(Boolean).join('\n\n')
267
- : pendingPrompt || ds.pendingFollowUps?.join('\n\n') || '',
268
- cliInput: wrappedInput.content,
269
- ...(effectiveCliId === 'codex-app' && botCfg.codexAppCleanInput === true && wrappedInput.codexAppInput
270
- ? { codexAppInput: wrappedInput.codexAppInput }
271
- : {}),
272
- codexAppInputGateFrozen: true,
273
- };
233
+ ds.pendingRepoCommitInFlight = true;
234
+ let committed = false;
235
+ try {
236
+ // Claim before mutating the selected directory so two different card
237
+ // choices cannot overwrite each other while one prompt is being built.
238
+ // Skip/bare-start may intentionally leave workingDir unpinned so the
239
+ // launch uses the bot default without persisting $HOME for siblings.
240
+ if (pinWorkingDir) {
241
+ ds.workingDir = dirPath;
242
+ ds.session.workingDir = dirPath;
243
+ }
244
+ ds.session.riffRepoDirs = opts?.riffRepoDirs;
245
+ sessionStore.updateSession(ds.session);
246
+ const selfBot = getBot(ds.larkAppId);
247
+ const botCfg = selfBot.config;
248
+ const effectiveCliId = sessionCliId(ds);
249
+ // Keep pendingRepo=true across this await. New topic messages therefore
250
+ // remain buffered on this same session instead of racing a worker-null
251
+ // safety-net fork. Snapshot every pending field only after it settles.
252
+ const needsPromptContext = !ds.pendingRawInput ||
253
+ (ds.pendingPrompt?.trim().length ?? 0) > 0 ||
254
+ (ds.pendingAttachments?.length ?? 0) > 0 ||
255
+ (ds.pendingFollowUps?.length ?? 0) > 0;
256
+ const availableBots = needsPromptContext
257
+ ? await getAvailableBots(ds.larkAppId, ds.chatId)
258
+ : [];
259
+ if (!sessionStillActive() || ds.session.sessionId !== commitGenSessionId ||
260
+ !ds.pendingRepo || (ds.worker && !ds.worker.killed)) {
261
+ logger.warn(`[${tag(ds)}] Session changed while preparing the pending-CLI prompt (${commitGenSessionId} → ${ds.session.sessionId}, active=${sessionStillActive()}, pending=${!!ds.pendingRepo}, worker=${!!ds.worker}) — aborting this fork`);
262
+ return;
263
+ }
264
+ const pendingPrompt = ds.pendingPrompt ?? '';
265
+ const pendingRawInput = ds.pendingRawInput;
266
+ const hasBufferedInput = pendingPrompt.trim().length > 0 ||
267
+ (ds.pendingAttachments?.length ?? 0) > 0 ||
268
+ (ds.pendingFollowUps?.length ?? 0) > 0;
269
+ if (!pendingRawInput || hasBufferedInput)
270
+ ensureSessionWhiteboard(ds);
271
+ const wrappedInput = (!pendingRawInput || hasBufferedInput)
272
+ ? buildNewTopicCliInput(pendingPrompt, ds.session.sessionId, effectiveCliId, botCfg.cliPathOverride, ds.pendingAttachments, ds.pendingMentions, availableBots, ds.pendingFollowUps, { name: selfBot.botName, openId: selfBot.botOpenId }, locTarget, ds.pendingSender, {
273
+ larkAppId: ds.larkAppId,
274
+ chatId: ds.chatId,
275
+ whiteboardId: ds.session.whiteboardId,
276
+ substituteTrigger: ds.pendingSubstituteTrigger,
277
+ codexAppText: ds.pendingCodexAppText,
278
+ codexAppApplicationContext: ds.pendingCodexAppApplicationContext,
279
+ codexAppMessageContext: ds.pendingCodexAppMessageContext,
280
+ codexAppFollowUps: ds.pendingCodexAppFollowUps,
281
+ codexAppFollowUpContexts: ds.pendingCodexAppFollowUpContexts,
282
+ })
283
+ : { content: '' };
284
+ const prompt = pendingRawInput ? '' : wrappedInput;
285
+ const pendingTurnId = ds.pendingTurnId;
286
+ const previousPendingFollowUpInput = ds.pendingFollowUpInput;
287
+ if (pendingRawInput && hasBufferedInput) {
288
+ ds.pendingFollowUpInput = {
289
+ userPrompt: ds.pendingCodexAppText !== undefined || ds.pendingCodexAppFollowUps
290
+ ? [ds.pendingCodexAppText ?? '', ...(ds.pendingCodexAppFollowUps ?? [])].filter(Boolean).join('\n\n')
291
+ : pendingPrompt || ds.pendingFollowUps?.join('\n\n') || '',
292
+ cliInput: wrappedInput.content,
293
+ ...(ds.pendingFollowUpTurnId ? { turnId: ds.pendingFollowUpTurnId } : {}),
294
+ ...(effectiveCliId === 'codex-app' && botCfg.codexAppCleanInput === true && wrappedInput.codexAppInput
295
+ ? { codexAppInput: wrappedInput.codexAppInput }
296
+ : {}),
297
+ codexAppInputGateFrozen: true,
298
+ };
299
+ }
300
+ // From here through forkWorker there is no await: publish the state
301
+ // transition atomically from the router's point of view.
302
+ ds.pendingRepo = false;
303
+ publishAttentionPatch(ds);
304
+ try {
305
+ if (pendingRawInput) {
306
+ // pendingRawTurnId is applied at the literal PTY write boundary.
307
+ forkWorker(ds, '', false);
308
+ }
309
+ else if (pendingTurnId && hasBufferedInput) {
310
+ forkWorker(ds, prompt, { turnId: pendingTurnId });
311
+ }
312
+ else {
313
+ forkWorker(ds, prompt);
314
+ }
315
+ }
316
+ catch (e) {
317
+ ds.pendingRepo = true;
318
+ ds.pendingFollowUpInput = previousPendingFollowUpInput;
319
+ publishAttentionPatch(ds);
320
+ throw e;
321
+ }
322
+ rememberLastCliInput(ds, pendingRawInput ?? pendingPrompt, pendingRawInput ?? wrappedInput);
323
+ ds.pendingPrompt = undefined;
324
+ ds.pendingCodexAppText = undefined;
325
+ ds.pendingCodexAppApplicationContext = undefined;
326
+ ds.pendingCodexAppMessageContext = undefined;
327
+ ds.pendingAttachments = undefined;
328
+ ds.pendingMentions = undefined;
329
+ ds.pendingSubstituteTrigger = undefined;
330
+ ds.pendingSender = undefined;
331
+ ds.pendingFollowUps = undefined;
332
+ ds.pendingCodexAppFollowUps = undefined;
333
+ ds.pendingCodexAppFollowUpContexts = undefined;
334
+ ds.pendingFollowUpTurnId = undefined;
335
+ ds.pendingTurnId = undefined;
336
+ committed = true;
337
+ // Local one-shot consume BEFORE any network await. Feishu withdraw is
338
+ // best-effort and may lag/fail; correctness for stale card clicks depends
339
+ // on this mark so a second callback cannot mid-session-switch after claim
340
+ // release (and so a sessionReply throw that jumps to finally still leaves
341
+ // the card locally invalid).
342
+ const cardToWithdraw = cardMessageId ?? ds.repoCardMessageId;
343
+ markRepoCardConsumed(ds, cardToWithdraw);
344
+ ds.repoCardMessageId = undefined;
345
+ // Hold the claim through confirmation + best-effort card withdraw so a
346
+ // concurrent in-flight select still sees pendingRepoCommitInFlight.
347
+ try {
348
+ if (!opts?.suppressConfirmReply) {
349
+ // A card click has no turn of its own — anchor the confirmation to the
350
+ // session's current reply-target turn so a shared fold-back topic keeps
351
+ // it in-thread (same leak as the /repo command path).
352
+ await sessionReply(rootId, t('cmd.repo.selected_in_pending', { name: dirLabel }, locTarget), undefined, fallbackTurnId(ds, undefined));
353
+ }
354
+ else if (opts.confirmReplyText) {
355
+ await sessionReply(rootId, opts.confirmReplyText, undefined, fallbackTurnId(ds, undefined));
356
+ }
357
+ }
358
+ catch (e) {
359
+ logger.warn(`[${tag(ds)}] Confirm reply after pending repo commit failed: ${e instanceof Error ? e.message : e}`);
360
+ }
361
+ if (cardToWithdraw && larkAppId) {
362
+ try {
363
+ await deleteMessage(larkAppId, cardToWithdraw);
364
+ }
365
+ catch { /* card may already be gone */ }
366
+ }
274
367
  }
275
- rememberLastCliInput(ds, pendingRawInput ?? pendingPrompt, pendingRawInput ?? wrappedInput);
276
- ds.pendingPrompt = undefined;
277
- ds.pendingCodexAppText = undefined;
278
- ds.pendingCodexAppApplicationContext = undefined;
279
- ds.pendingCodexAppMessageContext = undefined;
280
- ds.pendingAttachments = undefined;
281
- ds.pendingMentions = undefined;
282
- ds.pendingSubstituteTrigger = undefined;
283
- ds.pendingSender = undefined;
284
- ds.pendingFollowUps = undefined;
285
- ds.pendingCodexAppFollowUps = undefined;
286
- ds.pendingCodexAppFollowUpContexts = undefined;
287
- forkWorker(ds, prompt);
288
- // A card click has no turn of its own — anchor the confirmation to the
289
- // session's current reply-target turn so a shared fold-back topic keeps
290
- // it in-thread (same leak as the /repo command path).
291
- if (!opts?.suppressConfirmReply) {
292
- await sessionReply(rootId, t('cmd.repo.selected_in_pending', { name: dirLabel }, locTarget), undefined, fallbackTurnId(ds, undefined));
368
+ finally {
369
+ ds.pendingRepoCommitInFlight = false;
293
370
  }
371
+ if (!committed)
372
+ return;
294
373
  logger.info(`[${tag(ds)}] Repo selected: ${dirPath}, spawning CLI`);
374
+ return;
295
375
  }
296
376
  else {
297
377
  // Mid-session repo switch — close old session, start fresh.
378
+ // Claim the current card BEFORE killWorker / any await. A concurrent click
379
+ // on the same Feishu card must not pass a second kill+fork while the first
380
+ // switch is still awaiting confirm replies. Correctness does not depend on
381
+ // deleteMessage succeeding.
382
+ const claimedCard = claimCurrentRepoCard(ds, cardMessageId);
383
+ if (cardMessageId && !claimedCard) {
384
+ // Stale / already-claimed card (entry check races can still reach here).
385
+ logger.info(`[${tag(ds)}] Ignoring stale mid-session repo card ${cardMessageId}`);
386
+ return;
387
+ }
298
388
  // Safety net (mirrors the `/repo` text-command path): build the same
299
389
  // "session closed" card `/close` emits BEFORE displacing the old session
300
390
  // (it reads the live session's identity off `ds`). The new session reuses
@@ -355,14 +445,22 @@ opts) {
355
445
  ds.lastScreenStatus = undefined;
356
446
  forkWorker(ds, '', false);
357
447
  if (!opts?.suppressConfirmReply) {
358
- await sessionReply(rootId, t('cmd.repo.switched_to', { name: dirLabel }, locTarget));
448
+ try {
449
+ await sessionReply(rootId, t('cmd.repo.switched_to', { name: dirLabel }, locTarget));
450
+ }
451
+ catch (e) {
452
+ logger.warn(`[${tag(ds)}] Confirm reply after mid-session repo switch failed: ${e instanceof Error ? e.message : e}`);
453
+ }
359
454
  }
360
455
  logger.info(`[${tag(ds)}] Repo switched to ${dirPath}, new session created`);
456
+ // Best-effort withdraw — card already claimed above.
457
+ if (claimedCard && larkAppId) {
458
+ try {
459
+ await deleteMessage(larkAppId, claimedCard);
460
+ }
461
+ catch { /* best-effort */ }
462
+ }
361
463
  }
362
- // Withdraw the repo selection card
363
- if (cardMessageId && larkAppId)
364
- deleteMessage(larkAppId, cardMessageId);
365
- ds.repoCardMessageId = undefined;
366
464
  }
367
465
  /**
368
466
  * 仅默认目录 + auto-worktree 的**异步**提交:`ds` 必须已注册进 activeSessions 且处于
@@ -1738,72 +1836,37 @@ export async function handleCardAction(data, deps, larkAppId) {
1738
1836
  }
1739
1837
  if (actionType === 'skip_repo' && ds) {
1740
1838
  const locDs = localeForBot(ds.larkAppId);
1839
+ // Only the live posted card may act — covers consumed, wrong card, and
1840
+ // post-restart (repoCardMessageId is in-memory and empty after reboot).
1841
+ if (cardMessageId && !isActiveRepoCard(ds, cardMessageId)) {
1842
+ return { toast: { type: 'info', content: t('cmd.repo.card_already_consumed', undefined, locDs) } };
1843
+ }
1741
1844
  if (ds.pendingRepo) {
1742
- const selfBot = getBot(ds.larkAppId);
1743
- const botCfg = selfBot.config;
1744
- const effectiveCliId = sessionCliId(ds);
1745
- // Skip repo selection — spawn CLI with default working dir
1746
- ds.pendingRepo = false;
1747
- publishAttentionPatch(ds);
1748
- const pendingPrompt = ds.pendingPrompt ?? '';
1749
- const pendingRawInput = ds.pendingRawInput;
1750
- // Raw-input cold start still wraps any input buffered while the repo
1751
- // card was pending (follow-ups / attachments) — delivered right after
1752
- // the raw input on prompt_ready instead of being dropped.
1753
- const hasBufferedInput = pendingPrompt.trim().length > 0 ||
1754
- (ds.pendingAttachments?.length ?? 0) > 0 ||
1755
- (ds.pendingFollowUps?.length ?? 0) > 0;
1756
- if (!pendingRawInput || hasBufferedInput)
1757
- ensureSessionWhiteboard(ds);
1758
- const wrappedInput = (!pendingRawInput || hasBufferedInput)
1759
- ? buildNewTopicCliInput(pendingPrompt, ds.session.sessionId, effectiveCliId, botCfg.cliPathOverride, ds.pendingAttachments, ds.pendingMentions, await getAvailableBots(ds.larkAppId, ds.chatId), ds.pendingFollowUps, { name: selfBot.botName, openId: selfBot.botOpenId }, locDs, ds.pendingSender, {
1760
- larkAppId: ds.larkAppId,
1761
- chatId: ds.chatId,
1762
- whiteboardId: ds.session.whiteboardId,
1763
- substituteTrigger: ds.pendingSubstituteTrigger,
1764
- codexAppText: ds.pendingCodexAppText,
1765
- codexAppApplicationContext: ds.pendingCodexAppApplicationContext,
1766
- codexAppMessageContext: ds.pendingCodexAppMessageContext,
1767
- codexAppFollowUps: ds.pendingCodexAppFollowUps,
1768
- codexAppFollowUpContexts: ds.pendingCodexAppFollowUpContexts,
1769
- })
1770
- : { content: '' };
1771
- const prompt = pendingRawInput ? '' : wrappedInput;
1772
- if (pendingRawInput && hasBufferedInput) {
1773
- ds.pendingFollowUpInput = {
1774
- userPrompt: ds.pendingCodexAppText !== undefined || ds.pendingCodexAppFollowUps
1775
- ? [ds.pendingCodexAppText ?? '', ...(ds.pendingCodexAppFollowUps ?? [])].filter(Boolean).join('\n\n')
1776
- : pendingPrompt || ds.pendingFollowUps?.join('\n\n') || '',
1777
- cliInput: wrappedInput.content,
1778
- ...(effectiveCliId === 'codex-app' && botCfg.codexAppCleanInput === true && wrappedInput.codexAppInput
1779
- ? { codexAppInput: wrappedInput.codexAppInput }
1780
- : {}),
1781
- codexAppInputGateFrozen: true,
1782
- };
1845
+ if (ds.worktreeCreating || ds.pendingRepoCommitInFlight) {
1846
+ return { toast: { type: 'info', content: t('cmd.repo.worktree_in_progress', undefined, locDs) } };
1783
1847
  }
1784
- rememberLastCliInput(ds, pendingRawInput ?? pendingPrompt, pendingRawInput ?? wrappedInput);
1785
- ds.pendingPrompt = undefined;
1786
- ds.pendingCodexAppText = undefined;
1787
- ds.pendingCodexAppApplicationContext = undefined;
1788
- ds.pendingCodexAppMessageContext = undefined;
1789
- ds.pendingAttachments = undefined;
1790
- ds.pendingMentions = undefined;
1791
- ds.pendingSubstituteTrigger = undefined;
1792
- ds.pendingSender = undefined;
1793
- ds.pendingFollowUps = undefined;
1794
- ds.pendingCodexAppFollowUps = undefined;
1795
- ds.pendingCodexAppFollowUpContexts = undefined;
1796
- forkWorker(ds, prompt);
1797
1848
  const cwd = getSessionWorkingDir(ds);
1798
- await sessionReply(rootId, t('cmd.skip.opened', { cwd }, locDs));
1799
- logger.info(`[${tag(ds)}] Skip repo, spawning CLI in ${cwd}`);
1849
+ // Reuse the same claimed pending->worker transition as a normal repo
1850
+ // selection. This keeps buffering active across prompt preparation and
1851
+ // makes skip/select races single-winner. Do NOT pin the resolved default
1852
+ // cwd (often $HOME) onto session.workingDir — that would let sibling
1853
+ // bots inherit HOME instead of getting their own repo card. Confirmation
1854
+ // + card withdraw run under the claim inside commitRepoSelection.
1855
+ await commitRepoSelection({ ds, rootId, cardMessageId, larkAppId, operatorOpenId, activeSessions, sessionReply }, cwd, pathBasename(cwd) || cwd, {
1856
+ suppressConfirmReply: true,
1857
+ confirmReplyText: t('cmd.skip.opened', { cwd }, locDs),
1858
+ pinWorkingDir: false,
1859
+ });
1860
+ if (!ds.pendingRepo) {
1861
+ logger.info(`[${tag(ds)}] Skip repo, spawning CLI in ${cwd}`);
1862
+ }
1800
1863
  }
1801
1864
  else {
1802
1865
  await sessionReply(rootId, t('card.action.continue_using_current_repo', { cwd: getSessionWorkingDir(ds) }, locDs));
1866
+ if (cardMessageId && larkAppId)
1867
+ deleteMessage(larkAppId, cardMessageId);
1868
+ ds.repoCardMessageId = undefined;
1803
1869
  }
1804
- if (cardMessageId && larkAppId)
1805
- deleteMessage(larkAppId, cardMessageId);
1806
- ds.repoCardMessageId = undefined;
1807
1870
  }
1808
1871
  // Manual working-directory entry from the repo card form. The project scan
1809
1872
  // may not surface every useful directory; this mirrors `/repo <path>` from
@@ -1812,6 +1875,9 @@ export async function handleCardAction(data, deps, larkAppId) {
1812
1875
  // needs a scanned git repo root, not an arbitrary path.
1813
1876
  if (actionType === 'repo_manual_submit' && ds) {
1814
1877
  const locDs = localeForBot(ds.larkAppId);
1878
+ if (cardMessageId && !isActiveRepoCard(ds, cardMessageId)) {
1879
+ return { toast: { type: 'info', content: t('cmd.repo.card_already_consumed', undefined, locDs) } };
1880
+ }
1815
1881
  const rawPath = String(action?.form_value?.repo_manual_path ?? '').trim();
1816
1882
  if (!rawPath) {
1817
1883
  return { toast: { type: 'error', content: t('card.repo.manual_empty', undefined, locDs) } };
@@ -1822,7 +1888,7 @@ export async function handleCardAction(data, deps, larkAppId) {
1822
1888
  }
1823
1889
  // A worktree creation in flight holds the commit lock — a manual switch
1824
1890
  // interleaving there would double-fork (same guard as the plain switch).
1825
- if (ds.worktreeCreating) {
1891
+ if (ds.worktreeCreating || ds.pendingRepoCommitInFlight) {
1826
1892
  return { toast: { type: 'info', content: t('cmd.repo.worktree_in_progress', undefined, locDs) } };
1827
1893
  }
1828
1894
  const selectedPath = validation.resolvedPath;
@@ -1834,6 +1900,11 @@ export async function handleCardAction(data, deps, larkAppId) {
1834
1900
  // re-send a fresh repo card in the new mode — a form can't ride an
1835
1901
  // in-place patch, so the old card is withdrawn and a new one posted.
1836
1902
  const locDs = localeForBot(ds.larkAppId);
1903
+ // Same active-card gate as skip/manual/worktree: a stale card must not
1904
+ // flip bot config or replace the live repo card after claim/restart.
1905
+ if (cardMessageId && !isActiveRepoCard(ds, cardMessageId)) {
1906
+ return { toast: { type: 'info', content: t('cmd.repo.card_already_consumed', undefined, locDs) } };
1907
+ }
1837
1908
  const spec = findConfigField('worktreeMultiPicker');
1838
1909
  if (!spec)
1839
1910
  return;
@@ -1856,11 +1927,14 @@ export async function handleCardAction(data, deps, larkAppId) {
1856
1927
  }
1857
1928
  if (actionType === 'repo_worktree_submit' && ds) {
1858
1929
  const locDs = localeForBot(ds.larkAppId);
1930
+ if (cardMessageId && !isActiveRepoCard(ds, cardMessageId)) {
1931
+ return { toast: { type: 'info', content: t('cmd.repo.card_already_consumed', undefined, locDs) } };
1932
+ }
1859
1933
  const selectedPaths = stringListFromLarkMultiSelect(action?.form_value?.repo_worktree_paths);
1860
1934
  if (selectedPaths.length === 0) {
1861
1935
  return { toast: { type: 'error', content: t('card.repo.worktree_empty', undefined, locDs) } };
1862
1936
  }
1863
- if (ds.worktreeCreating) {
1937
+ if (ds.worktreeCreating || ds.pendingRepoCommitInFlight) {
1864
1938
  return { toast: { type: 'info', content: t('cmd.repo.worktree_in_progress', undefined, locDs) } };
1865
1939
  }
1866
1940
  const branch = String(action?.form_value?.repo_worktree_branch ?? '').trim() || undefined;
@@ -2077,6 +2151,12 @@ export async function handleCardAction(data, deps, larkAppId) {
2077
2151
  logger.warn(`Card action: no active session found for root ${rootId}`);
2078
2152
  return;
2079
2153
  }
2154
+ // Only the live posted card may drive selection. Covers: already-consumed,
2155
+ // wrong/old card, and post-restart (repoCardMessageId is in-memory). Must
2156
+ // reject before mid-session switch can killWorker.
2157
+ if (cardMessageId && !isActiveRepoCard(targetDs, cardMessageId)) {
2158
+ return { toast: { type: 'info', content: t('cmd.repo.card_already_consumed', undefined, localeForBot(targetDs.larkAppId)) } };
2159
+ }
2080
2160
  // 权限边界:pendingRepo(首次选 repo 才能 spawn)放行「会话发起人 或 canOperate」,
2081
2161
  // 让本群授权用户能完成自己的首次使用;非 pending 的 mid-session 切换是管理动作,要 canOperate。
2082
2162
  const isSessionOwnerOp = !!operatorOpenId && operatorOpenId === targetDs.session.ownerOpenId;
@@ -2118,7 +2198,7 @@ export async function handleCardAction(data, deps, larkAppId) {
2118
2198
  // ack the card action immediately with a toast and finish asynchronously.
2119
2199
  // On failure the card (and pendingRepo state) stays put so the user can
2120
2200
  // pick again or fall back to a plain switch.
2121
- if (targetDs.worktreeCreating) {
2201
+ if (targetDs.worktreeCreating || targetDs.pendingRepoCommitInFlight) {
2122
2202
  // The async path escapes the card-action in-flight dedup — gate repeats
2123
2203
  // here, or two creations would race and the loser's commitSelection
2124
2204
  // would yank the session the winner just spawned.
@@ -2245,7 +2325,7 @@ export async function handleCardAction(data, deps, larkAppId) {
2245
2325
  // worktree commit awaits (Lark replies, prompt prep) after its generation
2246
2326
  // checks; a plain selection interleaving there would double-fork. One lock
2247
2327
  // gates both kinds until the commit settles.
2248
- if (targetDs.worktreeCreating) {
2328
+ if (targetDs.worktreeCreating || targetDs.pendingRepoCommitInFlight) {
2249
2329
  return { toast: { type: 'info', content: t('cmd.repo.worktree_in_progress', undefined, locTarget) } };
2250
2330
  }
2251
2331
  await commitRepoSelection(commitCtx, selectedPath, displayName);