botmux 3.16.0 → 3.16.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 (51) hide show
  1. package/dist/.runtime-build-id +1 -1
  2. package/dist/core/dashboard-url.d.ts +10 -1
  3. package/dist/core/dashboard-url.d.ts.map +1 -1
  4. package/dist/core/dashboard-url.js +12 -2
  5. package/dist/core/dashboard-url.js.map +1 -1
  6. package/dist/core/plugins/mcp/gateway.d.ts +27 -3
  7. package/dist/core/plugins/mcp/gateway.d.ts.map +1 -1
  8. package/dist/core/plugins/mcp/gateway.js +272 -26
  9. package/dist/core/plugins/mcp/gateway.js.map +1 -1
  10. package/dist/core/plugins/mcp/host.d.ts +18 -0
  11. package/dist/core/plugins/mcp/host.d.ts.map +1 -1
  12. package/dist/core/plugins/mcp/host.js +80 -16
  13. package/dist/core/plugins/mcp/host.js.map +1 -1
  14. package/dist/core/plugins/mcp/launch-record.d.ts +14 -0
  15. package/dist/core/plugins/mcp/launch-record.d.ts.map +1 -0
  16. package/dist/core/plugins/mcp/launch-record.js +63 -0
  17. package/dist/core/plugins/mcp/launch-record.js.map +1 -0
  18. package/dist/core/worker-pool.d.ts +10 -2
  19. package/dist/core/worker-pool.d.ts.map +1 -1
  20. package/dist/core/worker-pool.js +30 -3
  21. package/dist/core/worker-pool.js.map +1 -1
  22. package/dist/dashboard/control-csrf.d.ts.map +1 -1
  23. package/dist/dashboard/control-csrf.js +6 -1
  24. package/dist/dashboard/control-csrf.js.map +1 -1
  25. package/dist/dashboard/terminal-front-proxy.d.ts.map +1 -1
  26. package/dist/dashboard/terminal-front-proxy.js +19 -1
  27. package/dist/dashboard/terminal-front-proxy.js.map +1 -1
  28. package/dist/i18n/en.d.ts.map +1 -1
  29. package/dist/i18n/en.js +1 -0
  30. package/dist/i18n/en.js.map +1 -1
  31. package/dist/i18n/zh.d.ts.map +1 -1
  32. package/dist/i18n/zh.js +1 -0
  33. package/dist/i18n/zh.js.map +1 -1
  34. package/dist/im/lark/card-handler.d.ts.map +1 -1
  35. package/dist/im/lark/card-handler.js +65 -4
  36. package/dist/im/lark/card-handler.js.map +1 -1
  37. package/dist/platform/binding.d.ts +27 -0
  38. package/dist/platform/binding.d.ts.map +1 -1
  39. package/dist/platform/binding.js +40 -0
  40. package/dist/platform/binding.js.map +1 -1
  41. package/dist/services/bridge-turn-journal.d.ts +63 -0
  42. package/dist/services/bridge-turn-journal.d.ts.map +1 -0
  43. package/dist/services/bridge-turn-journal.js +149 -0
  44. package/dist/services/bridge-turn-journal.js.map +1 -0
  45. package/dist/services/bridge-turn-queue.d.ts +8 -1
  46. package/dist/services/bridge-turn-queue.d.ts.map +1 -1
  47. package/dist/services/bridge-turn-queue.js +2 -1
  48. package/dist/services/bridge-turn-queue.js.map +1 -1
  49. package/dist/worker.js +270 -19
  50. package/dist/worker.js.map +1 -1
  51. package/package.json +1 -1
@@ -34,7 +34,7 @@ import { logger } from '../../utils/logger.js';
34
34
  import * as sessionStore from '../../services/session-store.js';
35
35
  import { loadFrozenCards, saveFrozenCards } from '../../services/frozen-card-store.js';
36
36
  import { resumeStartsFresh } from '../../services/resume-fresh-policy.js';
37
- import { forkWorker, sendWorkerInput, sendWorkerSessionInput, closeSession as closeWorkerPoolSession, teardownAuthoritativePersistentBackingBeforeClose, scheduleCardPatch, parkStreamCard, clearUsageLimitState, cardUsageLimit, writableTerminalLinkFor, workerHasInitialized, sessionSupportsWebTerminal, readableTerminalUrlFor, resolvePrivateCardAudience, deliverWriteLinkCard, deliverEphemeralOrReply, CARD_POSTING_SENTINEL, requestSessionRestart, isSessionTransferring, getDaemonStreamingCardUsageSnapshot, withActiveSessionKeyLock } from '../../core/worker-pool.js';
37
+ import { forkWorker, sendWorkerInput, sendWorkerSessionInput, closeSession as closeWorkerPoolSession, teardownAuthoritativePersistentBackingBeforeClose, scheduleCardPatch, parkStreamCard, clearUsageLimitState, cardUsageLimit, writableTerminalLinkFor, workerHasInitialized, sessionSupportsWebTerminal, readableTerminalUrlFor, resolvePrivateCardAudience, deliverWriteLinkCard, deliverEphemeralOrReply, CARD_POSTING_SENTINEL, requestSessionRestart, isSessionTransferring, getDaemonStreamingCardUsageSnapshot, withActiveSessionKeyLock, buildStreamingCardJson } from '../../core/worker-pool.js';
38
38
  import { getSessionWorkingDir, buildNewTopicCliInput, getAvailableBots, persistStreamCardState, resumeSession, rememberLastCliInput, ensureSessionWhiteboard } from '../../core/session-manager.js';
39
39
  import { markInitialUserTurnPending } from '../../core/initial-user-turn.js';
40
40
  import { publishAttentionPatch, announcePendingRepoSession } from '../../core/session-activity.js';
@@ -2176,9 +2176,22 @@ export async function handleCardAction(data, deps, larkAppId) {
2176
2176
  // Build the closed card BEFORE closeWorkerPoolSession — it reads the
2177
2177
  // live session's identity off `current`.
2178
2178
  const card = buildClosedSessionCard(current, localeForBot(current.larkAppId));
2179
+ // The clicked card IS the live streaming card in the common in-thread
2180
+ // (non-private) case. When so, patch it in place via the callback return
2181
+ // below instead of sending a separate closed card — a stray extra card
2182
+ // (and its now-dead buttons) is what we're avoiding. Only when the
2183
+ // clicked message is that streaming card and we're not in private mode.
2184
+ const patchClickedCardInPlace = !!cardMessageId
2185
+ && cardMessageId === current.streamCardId
2186
+ && value?.visibility !== 'private'
2187
+ && !botCfg.privateCard;
2179
2188
  let closeResult;
2180
2189
  try {
2181
- closeResult = await closeWorkerPoolSession(targetSessionId);
2190
+ // Don't await the worker exiting — a busy CLI only dies at the ~7s
2191
+ // SIGKILL backstop, which blows past Lark's ~3s card-ACK window and
2192
+ // surfaces the client-side "code: 300000" toast. The logical close is
2193
+ // synchronous; the worker is killed in the background.
2194
+ closeResult = await closeWorkerPoolSession(targetSessionId, { awaitWorkerExit: false });
2182
2195
  }
2183
2196
  catch (err) {
2184
2197
  logger.error(`[${tag(current)}] Refused close because backing teardown was not verified: ${err}`);
@@ -2200,7 +2213,7 @@ export async function handleCardAction(data, deps, larkAppId) {
2200
2213
  residual: closeResult.residual,
2201
2214
  };
2202
2215
  }
2203
- return { status: 'closed', current, botCfg, card };
2216
+ return { status: 'closed', current, botCfg, card, patchClickedCardInPlace };
2204
2217
  });
2205
2218
  if (!closed) {
2206
2219
  return { toast: { type: 'warning', content: t('card.action.session_gone', undefined, localeForBot(larkAppId)) } };
@@ -2242,7 +2255,7 @@ export async function handleCardAction(data, deps, larkAppId) {
2242
2255
  },
2243
2256
  };
2244
2257
  }
2245
- const { current, botCfg, card } = closed;
2258
+ const { current, botCfg, card, patchClickedCardInPlace } = closed;
2246
2259
  // The closed card carries session title / CLI name / workingDir / resume
2247
2260
  // command. In private-card mode those must not leak to the group — send the
2248
2261
  // closed card ephemeral to the same owner audience instead. No group
@@ -2258,6 +2271,14 @@ export async function handleCardAction(data, deps, larkAppId) {
2258
2271
  logger.info(`[${tag(current)}] Closed via card button (private close card → ${audience.length} owner(s))`);
2259
2272
  }
2260
2273
  else {
2274
+ if (patchClickedCardInPlace) {
2275
+ // Return the closed card as the callback response → Lark patches the
2276
+ // just-clicked streaming card in place (no delete, no separate send,
2277
+ // no "code: 300000" race). The "等待输入" card becomes the closed card
2278
+ // with its now-dead buttons removed.
2279
+ logger.info(`[${tag(current)}] Closed via card button (in-place patch)`);
2280
+ return JSON.parse(card);
2281
+ }
2261
2282
  await deliverEphemeralOrReply(current, operatorOpenId, card, 'interactive', () => sessionReply(rootId, card, 'interactive'));
2262
2283
  logger.info(`[${tag(current)}] Closed via card button`);
2263
2284
  }
@@ -2279,6 +2300,46 @@ export async function handleCardAction(data, deps, larkAppId) {
2279
2300
  const resumeMsg = resumeStartsFresh(result.ds.session)
2280
2301
  ? t('card.action.resume_success_fresh', { cliName }, localeForBot(result.ds.larkAppId))
2281
2302
  : t('card.action.resume_success', { cliName }, localeForBot(result.ds.larkAppId));
2303
+ // Restore the ORIGINAL live streaming card (🖥️ header + usage line +
2304
+ // 显示输出/终端/操作链接/关闭会话) as a WITHDRAW-then-REPOST (old closed card
2305
+ // recalled, fresh card posted at the thread bottom), AND send the
2306
+ // "✅ 会话已恢复…" text follow-up — both are wanted.
2307
+ // Ordering is load-bearing for two reasons:
2308
+ // 1) ACK the callback FIRST (bare `return` → empty ACK), THEN
2309
+ // post/delete in the background — deleting the just-clicked card
2310
+ // inside the callback response races it and triggers client
2311
+ // "code: 300000".
2312
+ // 2) POST the fresh card BEFORE deleting the old one, so the thread
2313
+ // never briefly shows zero cards (same invariant as park→recall).
2314
+ // Skip in private-card mode (clicked card may be an ephemeral snapshot).
2315
+ const botCfgResume = getBot(result.ds.larkAppId).config;
2316
+ if (cardMessageId && value?.visibility !== 'private' && !botCfgResume.privateCard) {
2317
+ const staleCardId = cardMessageId;
2318
+ const resumedDs = result.ds;
2319
+ void (async () => {
2320
+ try {
2321
+ const freshCardId = await sessionReply(rootId, buildStreamingCardJson(resumedDs), 'interactive');
2322
+ resumedDs.streamCardId = freshCardId;
2323
+ persistStreamCardState(resumedDs);
2324
+ await deleteMessage(resumedDs.larkAppId, staleCardId).catch(() => { });
2325
+ // Also send the "✅ 会话已恢复…" text follow-up (the original resume
2326
+ // behavior). Both are wanted: the live streaming card AND the text
2327
+ // prompt telling the user to send a message to continue.
2328
+ await deliverEphemeralOrReply(resumedDs, operatorOpenId, resumeMsg, 'text', () => sessionReply(rootId, resumeMsg));
2329
+ logger.info(`[${targetSessionId.substring(0, 8)}] Resumed via card button (withdraw + repost streaming card + text)`);
2330
+ }
2331
+ catch (err) {
2332
+ logger.warn(`[${targetSessionId.substring(0, 8)}] resume card repost failed: ${err instanceof Error ? err.message : String(err)}`);
2333
+ }
2334
+ })();
2335
+ // Bare `return` (→ undefined) so the dispatcher's shaper emits a
2336
+ // genuine empty ACK `{}`. Returning `{}` here would instead be
2337
+ // truthy and get wrapped as `{card:{type:raw,data:{}}}` — an
2338
+ // in-place patch with an empty card body (invalid), racing the
2339
+ // background deleteMessage above. Matches every other empty-ACK in
2340
+ // this handler.
2341
+ return; // fast empty ACK; card work happens in background
2342
+ }
2282
2343
  await deliverEphemeralOrReply(result.ds, operatorOpenId, resumeMsg, 'text', () => sessionReply(rootId, resumeMsg));
2283
2344
  logger.info(`[${targetSessionId.substring(0, 8)}] Resumed via card button`);
2284
2345
  }