botmux 2.110.0 → 2.111.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 (221) hide show
  1. package/dist/adapters/backend/tmux-backend.d.ts +25 -5
  2. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  3. package/dist/adapters/backend/tmux-backend.js +59 -93
  4. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  5. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
  6. package/dist/adapters/backend/tmux-pipe-backend.js +5 -0
  7. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  8. package/dist/adapters/backend/zellij-backend.d.ts +30 -0
  9. package/dist/adapters/backend/zellij-backend.d.ts.map +1 -1
  10. package/dist/adapters/backend/zellij-backend.js +80 -13
  11. package/dist/adapters/backend/zellij-backend.js.map +1 -1
  12. package/dist/adapters/backend/zellij-socket-probe.d.ts +18 -0
  13. package/dist/adapters/backend/zellij-socket-probe.d.ts.map +1 -0
  14. package/dist/adapters/backend/zellij-socket-probe.js +146 -0
  15. package/dist/adapters/backend/zellij-socket-probe.js.map +1 -0
  16. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  17. package/dist/adapters/cli/claude-code.js +5 -0
  18. package/dist/adapters/cli/claude-code.js.map +1 -1
  19. package/dist/adapters/cli/codex-app-turn.d.ts +43 -0
  20. package/dist/adapters/cli/codex-app-turn.d.ts.map +1 -0
  21. package/dist/adapters/cli/codex-app-turn.js +110 -0
  22. package/dist/adapters/cli/codex-app-turn.js.map +1 -0
  23. package/dist/adapters/cli/codex-app.d.ts.map +1 -1
  24. package/dist/adapters/cli/codex-app.js +6 -0
  25. package/dist/adapters/cli/codex-app.js.map +1 -1
  26. package/dist/adapters/cli/codex.d.ts.map +1 -1
  27. package/dist/adapters/cli/codex.js +1 -0
  28. package/dist/adapters/cli/codex.js.map +1 -1
  29. package/dist/adapters/cli/runner-input.d.ts +3 -2
  30. package/dist/adapters/cli/runner-input.d.ts.map +1 -1
  31. package/dist/adapters/cli/runner-input.js +7 -4
  32. package/dist/adapters/cli/runner-input.js.map +1 -1
  33. package/dist/adapters/cli/types.d.ts +16 -0
  34. package/dist/adapters/cli/types.d.ts.map +1 -1
  35. package/dist/bot-registry.d.ts +5 -0
  36. package/dist/bot-registry.d.ts.map +1 -1
  37. package/dist/bot-registry.js +1 -0
  38. package/dist/bot-registry.js.map +1 -1
  39. package/dist/codex-app-runner.js +75 -12
  40. package/dist/codex-app-runner.js.map +1 -1
  41. package/dist/core/command-handler.d.ts +11 -0
  42. package/dist/core/command-handler.d.ts.map +1 -1
  43. package/dist/core/command-handler.js +101 -7
  44. package/dist/core/command-handler.js.map +1 -1
  45. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  46. package/dist/core/dashboard-ipc-server.js +69 -11
  47. package/dist/core/dashboard-ipc-server.js.map +1 -1
  48. package/dist/core/doc-comment-prompt.d.ts +57 -0
  49. package/dist/core/doc-comment-prompt.d.ts.map +1 -1
  50. package/dist/core/doc-comment-prompt.js +147 -2
  51. package/dist/core/doc-comment-prompt.js.map +1 -1
  52. package/dist/core/inflight-input-tracker.d.ts +2 -0
  53. package/dist/core/inflight-input-tracker.d.ts.map +1 -1
  54. package/dist/core/inflight-input-tracker.js.map +1 -1
  55. package/dist/core/passthrough-commands.d.ts.map +1 -1
  56. package/dist/core/passthrough-commands.js +1 -1
  57. package/dist/core/passthrough-commands.js.map +1 -1
  58. package/dist/core/persistent-backend.d.ts +4 -0
  59. package/dist/core/persistent-backend.d.ts.map +1 -1
  60. package/dist/core/persistent-backend.js +6 -0
  61. package/dist/core/persistent-backend.js.map +1 -1
  62. package/dist/core/plugins/cli-generation.d.ts +3 -0
  63. package/dist/core/plugins/cli-generation.d.ts.map +1 -1
  64. package/dist/core/plugins/cli-generation.js +1 -0
  65. package/dist/core/plugins/cli-generation.js.map +1 -1
  66. package/dist/core/session-board.d.ts +2 -1
  67. package/dist/core/session-board.d.ts.map +1 -1
  68. package/dist/core/session-board.js +3 -2
  69. package/dist/core/session-board.js.map +1 -1
  70. package/dist/core/session-create.d.ts +38 -0
  71. package/dist/core/session-create.d.ts.map +1 -1
  72. package/dist/core/session-create.js +43 -9
  73. package/dist/core/session-create.js.map +1 -1
  74. package/dist/core/session-manager.d.ts +65 -2
  75. package/dist/core/session-manager.d.ts.map +1 -1
  76. package/dist/core/session-manager.js +285 -68
  77. package/dist/core/session-manager.js.map +1 -1
  78. package/dist/core/session-rename.d.ts +25 -0
  79. package/dist/core/session-rename.d.ts.map +1 -0
  80. package/dist/core/session-rename.js +43 -0
  81. package/dist/core/session-rename.js.map +1 -0
  82. package/dist/core/session-title.d.ts +11 -0
  83. package/dist/core/session-title.d.ts.map +1 -0
  84. package/dist/core/session-title.js +17 -0
  85. package/dist/core/session-title.js.map +1 -0
  86. package/dist/core/trigger-session.d.ts +11 -0
  87. package/dist/core/trigger-session.d.ts.map +1 -1
  88. package/dist/core/trigger-session.js +61 -20
  89. package/dist/core/trigger-session.js.map +1 -1
  90. package/dist/core/types.d.ts +16 -1
  91. package/dist/core/types.d.ts.map +1 -1
  92. package/dist/core/types.js.map +1 -1
  93. package/dist/core/worker-pool.d.ts +6 -2
  94. package/dist/core/worker-pool.d.ts.map +1 -1
  95. package/dist/core/worker-pool.js +48 -15
  96. package/dist/core/worker-pool.js.map +1 -1
  97. package/dist/core/zellij-adopt-discovery.d.ts +17 -0
  98. package/dist/core/zellij-adopt-discovery.d.ts.map +1 -1
  99. package/dist/core/zellij-adopt-discovery.js +24 -4
  100. package/dist/core/zellij-adopt-discovery.js.map +1 -1
  101. package/dist/core/zellij-session-discovery.d.ts +3 -0
  102. package/dist/core/zellij-session-discovery.d.ts.map +1 -1
  103. package/dist/core/zellij-session-discovery.js +1 -0
  104. package/dist/core/zellij-session-discovery.js.map +1 -1
  105. package/dist/daemon.d.ts +3 -0
  106. package/dist/daemon.d.ts.map +1 -1
  107. package/dist/daemon.js +181 -81
  108. package/dist/daemon.js.map +1 -1
  109. package/dist/dashboard/bot-payload.d.ts +1 -0
  110. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  111. package/dist/dashboard/bot-payload.js +1 -0
  112. package/dist/dashboard/bot-payload.js.map +1 -1
  113. package/dist/dashboard/federated-group-core.d.ts +29 -8
  114. package/dist/dashboard/federated-group-core.d.ts.map +1 -1
  115. package/dist/dashboard/federated-group-core.js +68 -6
  116. package/dist/dashboard/federated-group-core.js.map +1 -1
  117. package/dist/dashboard/federation-api.d.ts +10 -9
  118. package/dist/dashboard/federation-api.d.ts.map +1 -1
  119. package/dist/dashboard/federation-api.js +82 -3
  120. package/dist/dashboard/federation-api.js.map +1 -1
  121. package/dist/dashboard/federation-spoke-api.d.ts +8 -9
  122. package/dist/dashboard/federation-spoke-api.d.ts.map +1 -1
  123. package/dist/dashboard/federation-spoke-api.js +7 -2
  124. package/dist/dashboard/federation-spoke-api.js.map +1 -1
  125. package/dist/dashboard/team-group.d.ts +21 -0
  126. package/dist/dashboard/team-group.d.ts.map +1 -1
  127. package/dist/dashboard/team-group.js +16 -0
  128. package/dist/dashboard/team-group.js.map +1 -1
  129. package/dist/dashboard/web/bot-defaults-page.d.ts +10 -0
  130. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  131. package/dist/dashboard/web/bot-defaults-page.js +33 -1
  132. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  133. package/dist/dashboard/web/bot-defaults.d.ts +1 -0
  134. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  135. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  136. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  137. package/dist/dashboard/web/i18n.js +8 -0
  138. package/dist/dashboard/web/i18n.js.map +1 -1
  139. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  140. package/dist/dashboard/web/sessions.js +5 -1
  141. package/dist/dashboard/web/sessions.js.map +1 -1
  142. package/dist/dashboard/web/ui.d.ts.map +1 -1
  143. package/dist/dashboard/web/ui.js +2 -4
  144. package/dist/dashboard/web/ui.js.map +1 -1
  145. package/dist/dashboard-web/app.js +1 -1
  146. package/dist/dashboard-web/chunks/bot-defaults-page-7T7DPOV6.js +4 -0
  147. package/dist/dashboard-web/chunks/{chunk-SHZ5PUYF.js → chunk-IGXYYRYJ.js} +1 -1
  148. package/dist/dashboard-web/chunks/{chunk-GF7DU2B5.js → chunk-ISOD7VDX.js} +1 -1
  149. package/dist/dashboard-web/chunks/{chunk-TORH53FI.js → chunk-OEJIT66L.js} +1 -1
  150. package/dist/dashboard-web/chunks/{chunk-7UY333A5.js → chunk-PNK3HQBI.js} +2 -2
  151. package/dist/dashboard-web/chunks/{chunk-IAES47WV.js → chunk-YQFXGRSD.js} +1 -1
  152. package/dist/dashboard-web/chunks/{connectors-page-N6GKR6JL.js → connectors-page-UQ4QBE45.js} +1 -1
  153. package/dist/dashboard-web/chunks/{groups-page-IK3GXSQU.js → groups-page-5AJPPJH2.js} +1 -1
  154. package/dist/dashboard-web/chunks/{insights-page-T2PDFPGO.js → insights-page-R3DFECX4.js} +1 -1
  155. package/dist/dashboard-web/chunks/{legacy-workflow-page-YQPLKH74.js → legacy-workflow-page-QMC7OZXH.js} +1 -1
  156. package/dist/dashboard-web/chunks/{monitor-room-7EL3LVJS.js → monitor-room-NLMKJLDY.js} +1 -1
  157. package/dist/dashboard-web/chunks/{monitoring-page-BCADF56L.js → monitoring-page-EHSOX4GV.js} +1 -1
  158. package/dist/dashboard-web/chunks/{office-page-KP7BDLAE.js → office-page-OXJKFJ4R.js} +1 -1
  159. package/dist/dashboard-web/chunks/{overview-page-6GWDHDTI.js → overview-page-GJHIVXJF.js} +1 -1
  160. package/dist/dashboard-web/chunks/{roles-page-2OFR2IBG.js → roles-page-6ZODQISW.js} +1 -1
  161. package/dist/dashboard-web/chunks/{schedules-page-TAWMTSNL.js → schedules-page-DAGMHVCH.js} +1 -1
  162. package/dist/dashboard-web/chunks/{sessions-page-42QZHPPP.js → sessions-page-NNZIDHZM.js} +1 -1
  163. package/dist/dashboard-web/chunks/{settings-page-OVKXUXSV.js → settings-page-LJIXX5LQ.js} +1 -1
  164. package/dist/dashboard-web/chunks/{skills-page-Y2EC3NBI.js → skills-page-J7EVUHXP.js} +1 -1
  165. package/dist/dashboard-web/chunks/{team-federation-page-QYMJUH5F.js → team-federation-page-YM5JBIII.js} +1 -1
  166. package/dist/dashboard-web/chunks/{v3-page-FZJCW5QG.js → v3-page-YLYVHGKD.js} +1 -1
  167. package/dist/dashboard-web/chunks/{whiteboards-page-FBHBBCAL.js → whiteboards-page-75N2U6W4.js} +1 -1
  168. package/dist/dashboard.js +46 -7
  169. package/dist/dashboard.js.map +1 -1
  170. package/dist/i18n/en.d.ts.map +1 -1
  171. package/dist/i18n/en.js +8 -0
  172. package/dist/i18n/en.js.map +1 -1
  173. package/dist/i18n/zh.d.ts.map +1 -1
  174. package/dist/i18n/zh.js +8 -0
  175. package/dist/i18n/zh.js.map +1 -1
  176. package/dist/im/lark/card-handler.d.ts.map +1 -1
  177. package/dist/im/lark/card-handler.js +96 -32
  178. package/dist/im/lark/card-handler.js.map +1 -1
  179. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  180. package/dist/im/lark/event-dispatcher.js +16 -5
  181. package/dist/im/lark/event-dispatcher.js.map +1 -1
  182. package/dist/services/bot-config-store.d.ts.map +1 -1
  183. package/dist/services/bot-config-store.js +1 -0
  184. package/dist/services/bot-config-store.js.map +1 -1
  185. package/dist/services/card-prefs-store.d.ts +4 -0
  186. package/dist/services/card-prefs-store.d.ts.map +1 -1
  187. package/dist/services/card-prefs-store.js +8 -0
  188. package/dist/services/card-prefs-store.js.map +1 -1
  189. package/dist/services/default-worktree.d.ts.map +1 -1
  190. package/dist/services/default-worktree.js +4 -1
  191. package/dist/services/default-worktree.js.map +1 -1
  192. package/dist/services/group-creator.d.ts +19 -0
  193. package/dist/services/group-creator.d.ts.map +1 -1
  194. package/dist/services/group-creator.js +62 -28
  195. package/dist/services/group-creator.js.map +1 -1
  196. package/dist/services/groups-store.d.ts +7 -5
  197. package/dist/services/groups-store.d.ts.map +1 -1
  198. package/dist/services/groups-store.js +10 -8
  199. package/dist/services/groups-store.js.map +1 -1
  200. package/dist/setup/ensure-tmux.d.ts +17 -0
  201. package/dist/setup/ensure-tmux.d.ts.map +1 -1
  202. package/dist/setup/ensure-tmux.js +72 -1
  203. package/dist/setup/ensure-tmux.js.map +1 -1
  204. package/dist/types.d.ts +56 -6
  205. package/dist/types.d.ts.map +1 -1
  206. package/dist/utils/child-env.d.ts +27 -0
  207. package/dist/utils/child-env.d.ts.map +1 -1
  208. package/dist/utils/child-env.js +59 -0
  209. package/dist/utils/child-env.js.map +1 -1
  210. package/dist/utils/codex-app-context.d.ts +9 -0
  211. package/dist/utils/codex-app-context.d.ts.map +1 -0
  212. package/dist/utils/codex-app-context.js +47 -0
  213. package/dist/utils/codex-app-context.js.map +1 -0
  214. package/dist/utils/pending-input-queue.d.ts +2 -0
  215. package/dist/utils/pending-input-queue.d.ts.map +1 -1
  216. package/dist/utils/pending-input-queue.js +6 -0
  217. package/dist/utils/pending-input-queue.js.map +1 -1
  218. package/dist/worker.js +197 -38
  219. package/dist/worker.js.map +1 -1
  220. package/package.json +1 -1
  221. package/dist/dashboard-web/chunks/bot-defaults-page-QXZ2VFJJ.js +0 -4
@@ -11,7 +11,7 @@ import * as sessionStore from '../services/session-store.js';
11
11
  import * as messageQueue from '../services/message-queue.js';
12
12
  import { downloadMessageResource, listChatBotMembers, UserTokenMissingError } from '../im/lark/client.js';
13
13
  import { logger } from '../utils/logger.js';
14
- import { forkWorker, forkAdoptWorker, killStalePids, getCurrentCliVersion, restoreUsageLimitRuntimeState, setActiveSessionSafe, isRelayableRealSession, closeSession, getActiveSessionsRegistry, suspendWorker } from './worker-pool.js';
14
+ import { forkWorker, sendWorkerInput, forkAdoptWorker, killStalePids, getCurrentCliVersion, restoreUsageLimitRuntimeState, setActiveSessionSafe, isRelayableRealSession, closeSession, getActiveSessionsRegistry, suspendWorker } from './worker-pool.js';
15
15
  import { createCliAdapterSync } from '../adapters/cli/registry.js';
16
16
  import { buildBotmuxShellHints } from '../adapters/cli/shared-hints.js';
17
17
  import { assertSafeAppId } from '../adapters/cli/read-isolation.js';
@@ -21,8 +21,9 @@ import { adoptTargetLabel, validateAdoptTargetState } from './session-discovery.
21
21
  import { getBot, getAllBots, getOwnerOpenId, findOncallChat, effectiveDefaultWorkingDir } from '../bot-registry.js';
22
22
  import { dashboardEventBus } from './dashboard-events.js';
23
23
  import { composeRowFromActive } from './dashboard-rows.js';
24
- import { composeSpawnUserContent, deriveSessionTitleFromContent, } from './session-create.js';
24
+ import { composeSpawnCodexAppContext, composeSpawnUserContent, deriveSessionTitleFromContent, } from './session-create.js';
25
25
  import { validateZellijAdoptTarget } from './zellij-adopt-discovery.js';
26
+ import { addCodexAppContext } from '../utils/codex-app-context.js';
26
27
  import { sessionKey, sessionAnchorId } from './types.js';
27
28
  import { announceSessionRow, markSessionActivity, announcePendingRepoSession } from './session-activity.js';
28
29
  import { scanMultipleProjects } from '../services/project-scanner.js';
@@ -352,30 +353,89 @@ export function renderBufferedSenderBlock(sender, cliId, locale) {
352
353
  const note = renderCursorSenderNote(cliId, true, locale);
353
354
  return note ? `${tag}\n${note}` : tag;
354
355
  }
356
+ function substituteInstruction(disclosure) {
357
+ return disclosure === 'none'
358
+ ? 'This turn was triggered by a configured substitute target mention. Answer on behalf of that target when appropriate.'
359
+ : 'This turn was triggered by a configured substitute target mention. Answer on behalf of that target and clearly disclose that you are answering for them.';
360
+ }
361
+ function renderSubstituteIdentity(tag, identity) {
362
+ if (!identity)
363
+ return '';
364
+ const attrs = [];
365
+ if (identity.name)
366
+ attrs.push(`name="${xmlEscape(identity.name)}"`);
367
+ if (identity.openId)
368
+ attrs.push(`open_id="${xmlEscape(identity.openId)}"`);
369
+ if (identity.userId)
370
+ attrs.push(`user_id="${xmlEscape(identity.userId)}"`);
371
+ if (identity.unionId)
372
+ attrs.push(`union_id="${xmlEscape(identity.unionId)}"`);
373
+ return attrs.length > 0 ? `<${tag} ${attrs.join(' ')} />` : `<${tag} />`;
374
+ }
375
+ /** Preserve the pre-clean-input legacy schema exactly: one effective target,
376
+ * with configured fields taking precedence and event fields only filling
377
+ * missing values. The structured Codex App sidecar keeps both sources below. */
378
+ function renderLegacySubstituteTarget(trigger) {
379
+ const observed = trigger.observedMention;
380
+ const target = {
381
+ name: trigger.target.name ?? observed?.name,
382
+ openId: trigger.target.openId ?? observed?.openId,
383
+ userId: trigger.target.userId ?? observed?.userId,
384
+ unionId: trigger.target.unionId ?? observed?.unionId,
385
+ };
386
+ const attrs = [];
387
+ if (target.name)
388
+ attrs.push(`name="${xmlEscape(target.name)}"`);
389
+ if (target.openId)
390
+ attrs.push(`open_id="${xmlEscape(target.openId)}"`);
391
+ if (target.userId)
392
+ attrs.push(`user_id="${xmlEscape(target.userId)}"`);
393
+ if (target.unionId)
394
+ attrs.push(`union_id="${xmlEscape(target.unionId)}"`);
395
+ return `<target ${attrs.join(' ')} />`;
396
+ }
397
+ /** Legacy prompt envelope. This whole string remains user-role input for the
398
+ * terminal CLIs; Codex App uses the two trust-separated renderers below. */
355
399
  function renderSubstituteTrigger(trigger) {
356
400
  if (!trigger)
357
401
  return '';
358
- const attrs = [];
359
- if (trigger.target.name)
360
- attrs.push(`name="${xmlEscape(trigger.target.name)}"`);
361
- if (trigger.target.openId)
362
- attrs.push(`open_id="${xmlEscape(trigger.target.openId)}"`);
363
- if (trigger.target.userId)
364
- attrs.push(`user_id="${xmlEscape(trigger.target.userId)}"`);
365
- if (trigger.target.unionId)
366
- attrs.push(`union_id="${xmlEscape(trigger.target.unionId)}"`);
367
402
  const disclosure = trigger.disclosure ?? 'prefix';
368
- const instruction = disclosure === 'none'
369
- ? 'This turn was triggered by a configured substitute target mention. Answer on behalf of that target when appropriate.'
370
- : 'This turn was triggered by a configured substitute target mention. Answer on behalf of that target and clearly disclose that you are answering for them.';
371
403
  return [
372
404
  '<substitute_trigger>',
373
- ` <target ${attrs.join(' ')} />`,
405
+ ` ${renderLegacySubstituteTarget(trigger)}`,
374
406
  ` <disclosure>${xmlEscape(disclosure)}</disclosure>`,
375
- ` <instruction>${xmlEscape(instruction)}</instruction>`,
407
+ ` <instruction>${xmlEscape(substituteInstruction(disclosure))}</instruction>`,
376
408
  '</substitute_trigger>',
377
409
  ].join('\n');
378
410
  }
411
+ /** Botmux-owned policy only. No configured profile or event field may enter
412
+ * this block because Codex App promotes it to developer-role context. */
413
+ function renderSubstitutePolicy(trigger) {
414
+ if (!trigger)
415
+ return '';
416
+ const disclosure = trigger.disclosure ?? 'prefix';
417
+ return [
418
+ '<substitute_policy>',
419
+ ' <match>configured_target_mention</match>',
420
+ ` <disclosure>${disclosure}</disclosure>`,
421
+ ` <instruction>${substituteInstruction(disclosure)}</instruction>`,
422
+ '</substitute_policy>',
423
+ ].join('\n');
424
+ }
425
+ /** All identity metadata is untrusted, regardless of whether it came from a
426
+ * saved Lark profile or the current event. Keep the two sources distinct so a
427
+ * matching user_id cannot make conflicting observed IDs look canonical. */
428
+ function renderSubstituteTarget(trigger) {
429
+ if (!trigger)
430
+ return '';
431
+ const observedMention = renderSubstituteIdentity('observed_mention', trigger.observedMention);
432
+ return [
433
+ '<substitute_target>',
434
+ ` ${renderSubstituteIdentity('configured_target', trigger.target)}`,
435
+ ...(observedMention ? [` ${observedMention}`] : []),
436
+ '</substitute_target>',
437
+ ].join('\n');
438
+ }
379
439
  export function formatAttachmentsHint(attachments, locale) {
380
440
  if (!attachments || attachments.length === 0)
381
441
  return '';
@@ -471,6 +531,52 @@ function hermesFollowupReminder(locale) {
471
531
  * topic that never collaborates.
472
532
  */
473
533
  const AVAILABLE_BOTS_INLINE_MAX = 3;
534
+ function renderMentionBlock(mentions) {
535
+ if (!mentions || mentions.length === 0)
536
+ return '';
537
+ const items = mentions.map(m => {
538
+ const oid = m.openId ? ` open_id="${xmlEscape(m.openId)}"` : '';
539
+ return ` <mention name="${xmlEscape(m.name)}"${oid} />`;
540
+ });
541
+ return `<mentions>\n${items.join('\n')}\n</mentions>`;
542
+ }
543
+ function renderAvailableBotsBlock(availableBots, mentions, locale) {
544
+ if (!availableBots || availableBots.length === 0)
545
+ return '';
546
+ const mentionedOpenIds = new Set(mentions?.map(m => m.openId).filter(Boolean));
547
+ const unmentionedBots = availableBots.filter(b => !mentionedOpenIds.has(b.openId));
548
+ if (unmentionedBots.length === 0)
549
+ return '';
550
+ if (unmentionedBots.length <= AVAILABLE_BOTS_INLINE_MAX) {
551
+ const items = unmentionedBots.map(b => ` <bot name="${xmlEscape(b.displayName)}" open_id="${xmlEscape(b.openId)}" />`);
552
+ return `<available_bots hint="${xmlEscape(t('ai.available_bots.hint', undefined, locale))}">\n${items.join('\n')}\n</available_bots>`;
553
+ }
554
+ const sep = (locale ?? getDefaultLocale()) === 'en' ? ', ' : '、';
555
+ const names = unmentionedBots.map(b => b.displayName).join(sep);
556
+ const line = t('ai.available_bots.collapsed_line', { count: unmentionedBots.length, names }, locale);
557
+ return `<available_bots hint="${xmlEscape(t('ai.available_bots.hint_collapsed', undefined, locale))}" count="${unmentionedBots.length}">\n${xmlEscape(line)}\n</available_bots>`;
558
+ }
559
+ function buildCodexAppTurnInput(opts) {
560
+ const additionalContext = {};
561
+ addCodexAppContext(additionalContext, 'botmux_role', opts.roleBlock ?? '', 'application');
562
+ addCodexAppContext(additionalContext, 'botmux_whiteboard', opts.whiteboardBlock ?? '', 'application');
563
+ addCodexAppContext(additionalContext, 'botmux_sender', opts.senderBlock ?? '', 'untrusted');
564
+ addCodexAppContext(additionalContext, 'botmux_substitute_policy', opts.substitutePolicyBlock ?? '', 'application');
565
+ addCodexAppContext(additionalContext, 'botmux_substitute_target', opts.substituteTargetBlock ?? '', 'untrusted');
566
+ addCodexAppContext(additionalContext, 'botmux_attachments', opts.attachmentBlock ?? '', 'untrusted');
567
+ addCodexAppContext(additionalContext, 'botmux_mentions', opts.mentionBlock ?? '', 'untrusted');
568
+ addCodexAppContext(additionalContext, 'botmux_available_bots', opts.availableBotsBlock ?? '', 'untrusted');
569
+ addCodexAppContext(additionalContext, 'botmux_application_context', opts.applicationContextBlock ?? '', 'application');
570
+ addCodexAppContext(additionalContext, 'botmux_message_context', opts.messageContextBlock ?? '', 'untrusted');
571
+ addCodexAppContext(additionalContext, 'botmux_buffered_followups', opts.bufferedFollowUpsBlock ?? '', 'untrusted');
572
+ return {
573
+ text: opts.text,
574
+ ...(Object.keys(additionalContext).length > 0 ? { additionalContext } : {}),
575
+ ...(opts.attachments?.some(a => a.type === 'image')
576
+ ? { localImages: opts.attachments.filter(a => a.type === 'image').map(a => ({ path: a.path, detail: 'original' })) }
577
+ : {}),
578
+ };
579
+ }
474
580
  export function buildNewTopicPrompt(userMessage, sessionId, cliId, cliPathOverride, attachments, mentions, availableBots, followUps, botIdentity, locale, sender, opts) {
475
581
  const adapter = createCliAdapterSync(cliId, cliPathOverride);
476
582
  // Non-Claude CLIs receive the botmux routing hints inline via the prompt
@@ -514,41 +620,8 @@ export function buildNewTopicPrompt(userMessage, sessionId, cliId, cliPathOverri
514
620
  }
515
621
  const roleBlock = renderRoleContextBlock(opts?.larkAppId, opts?.chatId);
516
622
  const whiteboardBlock = renderWhiteboardBlock({ whiteboardId: opts?.whiteboardId });
517
- let mentionBlock = '';
518
- if (mentions && mentions.length > 0) {
519
- const items = mentions.map(m => {
520
- const oid = m.openId ? ` open_id="${xmlEscape(m.openId)}"` : '';
521
- return ` <mention name="${xmlEscape(m.name)}"${oid} />`;
522
- });
523
- mentionBlock = `<mentions>\n${items.join('\n')}\n</mentions>`;
524
- }
525
- let botBlock = '';
526
- if (availableBots && availableBots.length > 0) {
527
- const mentionedOpenIds = new Set(mentions?.map(m => m.openId).filter(Boolean));
528
- const unmentionedBots = availableBots.filter(b => !mentionedOpenIds.has(b.openId));
529
- if (unmentionedBots.length > 0) {
530
- // ≤ threshold peers: inline the full roster with open_ids so any
531
- // cross-bot message (a question, collaboration, or full handoff) needs
532
- // zero round-trip. Above it: collapse to a one-line pointer — names only
533
- // for awareness, open_ids deferred to an on-demand `botmux bots list` —
534
- // so a many-bot group doesn't pay a long open_id list on every solo
535
- // topic. Either way this block is emitted once, on the first message.
536
- if (unmentionedBots.length <= AVAILABLE_BOTS_INLINE_MAX) {
537
- const items = unmentionedBots.map(b => ` <bot name="${xmlEscape(b.displayName)}" open_id="${xmlEscape(b.openId)}" />`);
538
- botBlock = `<available_bots hint="${xmlEscape(t('ai.available_bots.hint', undefined, locale))}">\n${items.join('\n')}\n</available_bots>`;
539
- }
540
- else {
541
- // Resolve the locale the same way t() does (explicit arg → process
542
- // default) so the separator matches the rendered sentence's language —
543
- // otherwise an undefined `locale` under an 'en' default would produce an
544
- // English sentence joined with the Chinese enumeration comma.
545
- const sep = (locale ?? getDefaultLocale()) === 'en' ? ', ' : '、';
546
- const names = unmentionedBots.map(b => b.displayName).join(sep);
547
- const line = t('ai.available_bots.collapsed_line', { count: unmentionedBots.length, names }, locale);
548
- botBlock = `<available_bots hint="${xmlEscape(t('ai.available_bots.hint_collapsed', undefined, locale))}" count="${unmentionedBots.length}">\n${xmlEscape(line)}\n</available_bots>`;
549
- }
550
- }
551
- }
623
+ const mentionBlock = renderMentionBlock(mentions);
624
+ const botBlock = renderAvailableBotsBlock(availableBots, mentions, locale);
552
625
  // Messages the user sent while the repo-selection card was still pending are
553
626
  // buffered as followUps. Fold them into the single <user_message> body
554
627
  // (blank-line separated) rather than emitting a separate <follow_up_message>
@@ -603,6 +676,44 @@ export function buildNewTopicPrompt(userMessage, sessionId, cliId, cliPathOverri
603
676
  // resolves delivery — keeping a single injection site avoids double-rendering.
604
677
  return parts.join('\n\n');
605
678
  }
679
+ /** Build the legacy opening prompt plus a Codex App structured sidecar. The
680
+ * sibling string API above stays unchanged for every existing caller. Pending-
681
+ * repo follow-ups currently arrive as already-enriched strings (and may contain
682
+ * sender tags), so that rare merged path deliberately falls back to legacy
683
+ * rather than guessing which bytes are user text. */
684
+ export function buildNewTopicCliInput(userMessage, sessionId, cliId, cliPathOverride, attachments, mentions, availableBots, followUps, botIdentity, locale, sender, opts) {
685
+ const content = buildNewTopicPrompt(userMessage, sessionId, cliId, cliPathOverride, attachments, mentions, availableBots, followUps, botIdentity, locale, sender, opts);
686
+ // Legacy pending buffers contain enriched strings. Only materialize those as
687
+ // clean input when the caller also preserved their matching raw texts.
688
+ if (cliId !== 'codex-app' || (followUps && followUps.length > 0 && !opts?.codexAppFollowUps))
689
+ return { content };
690
+ const roleBlock = renderRoleContextBlock(opts?.larkAppId, opts?.chatId);
691
+ const whiteboardBlock = renderWhiteboardBlock({ whiteboardId: opts?.whiteboardId });
692
+ const senderBlock = renderSenderTag(sender);
693
+ const substitutePolicyBlock = renderSubstitutePolicy(opts?.substituteTrigger);
694
+ const substituteTargetBlock = renderSubstituteTarget(opts?.substituteTrigger);
695
+ const attachmentBlock = formatAttachmentsHint(attachments, locale);
696
+ const mentionBlock = renderMentionBlock(mentions);
697
+ const availableBotsBlock = renderAvailableBotsBlock(availableBots, mentions, locale);
698
+ return {
699
+ content,
700
+ codexAppInput: buildCodexAppTurnInput({
701
+ text: [opts?.codexAppText ?? userMessage, ...(opts?.codexAppFollowUps ?? [])].join('\n\n'),
702
+ roleBlock,
703
+ whiteboardBlock,
704
+ senderBlock,
705
+ substitutePolicyBlock,
706
+ substituteTargetBlock,
707
+ attachmentBlock,
708
+ mentionBlock,
709
+ availableBotsBlock,
710
+ applicationContextBlock: opts?.codexAppApplicationContext,
711
+ messageContextBlock: opts?.codexAppMessageContext,
712
+ bufferedFollowUpsBlock: opts?.codexAppFollowUpContexts?.filter(Boolean).join('\n\n'),
713
+ attachments,
714
+ }),
715
+ };
716
+ }
606
717
  /**
607
718
  * Build the content for a follow-up message (thread reply to an active session).
608
719
  * Mirrors buildNewTopicPrompt structure but for subsequent messages.
@@ -647,15 +758,40 @@ export function buildFollowUpContent(content, sessionId, opts) {
647
758
  : '';
648
759
  if (attachHint)
649
760
  parts.push(attachHint);
650
- if (opts?.mentions && opts.mentions.length > 0) {
651
- const items = opts.mentions.map(m => {
652
- const oid = m.openId ? ` open_id="${xmlEscape(m.openId)}"` : '';
653
- return ` <mention name="${xmlEscape(m.name)}"${oid} />`;
654
- });
655
- parts.push(`<mentions>\n${items.join('\n')}\n</mentions>`);
656
- }
761
+ const mentionBlock = renderMentionBlock(opts?.mentions);
762
+ if (mentionBlock)
763
+ parts.push(mentionBlock);
657
764
  return parts.join('\n\n');
658
765
  }
766
+ /** Follow-up counterpart of buildNewTopicCliInput. */
767
+ export function buildFollowUpCliInput(content, sessionId, opts) {
768
+ const legacyContent = buildFollowUpContent(content, sessionId, opts);
769
+ if (opts?.cliId !== 'codex-app' || opts.isAdoptMode)
770
+ return { content: legacyContent };
771
+ const roleBlock = renderRoleContextBlock(opts.larkAppId, opts.chatId, { followUp: true });
772
+ const whiteboardBlock = renderWhiteboardBlock({ whiteboardId: opts.whiteboardId });
773
+ const senderBlock = renderSenderTag(opts.sender);
774
+ const substitutePolicyBlock = renderSubstitutePolicy(opts.substituteTrigger);
775
+ const substituteTargetBlock = renderSubstituteTarget(opts.substituteTrigger);
776
+ const attachmentBlock = formatAttachmentsHint(opts.attachments, opts.locale);
777
+ const mentionBlock = renderMentionBlock(opts.mentions);
778
+ return {
779
+ content: legacyContent,
780
+ codexAppInput: buildCodexAppTurnInput({
781
+ text: opts.codexAppText ?? content,
782
+ roleBlock,
783
+ whiteboardBlock,
784
+ senderBlock,
785
+ substitutePolicyBlock,
786
+ substituteTargetBlock,
787
+ attachmentBlock,
788
+ mentionBlock,
789
+ applicationContextBlock: opts.codexAppApplicationContext,
790
+ messageContextBlock: opts.codexAppMessageContext,
791
+ attachments: opts.attachments,
792
+ }),
793
+ };
794
+ }
659
795
  /**
660
796
  * Build raw input content for adopt-bridge mode.
661
797
  *
@@ -770,6 +906,37 @@ export function buildReforkPrompt(ds, content, opts) {
770
906
  whiteboardId: ds.session.whiteboardId,
771
907
  });
772
908
  }
909
+ /** Structured refork variant. Adopted external CLIs intentionally remain on
910
+ * their existing raw bridge path and never receive a Codex App sidecar. */
911
+ export function buildReforkCliInput(ds, content, opts) {
912
+ const locale = opts?.locale ?? localeForBot(ds.larkAppId);
913
+ if (ds.adoptedFrom) {
914
+ return {
915
+ content: buildBridgeInputContent(content, {
916
+ attachments: opts?.attachments,
917
+ mentions: opts?.mentions,
918
+ selfMention: opts?.selfMention,
919
+ locale,
920
+ }),
921
+ };
922
+ }
923
+ return buildFollowUpCliInput(content, ds.session.sessionId, {
924
+ attachments: opts?.attachments,
925
+ mentions: opts?.mentions,
926
+ isAdoptMode: false,
927
+ cliId: opts?.cliId,
928
+ cliPathOverride: opts?.cliPathOverride,
929
+ locale,
930
+ sender: opts?.sender,
931
+ larkAppId: ds.larkAppId,
932
+ chatId: ds.session.chatId,
933
+ whiteboardId: ds.session.whiteboardId,
934
+ substituteTrigger: opts?.substituteTrigger,
935
+ codexAppText: opts?.codexAppText,
936
+ codexAppApplicationContext: opts?.codexAppApplicationContext,
937
+ codexAppMessageContext: opts?.codexAppMessageContext,
938
+ });
939
+ }
773
940
  /**
774
941
  * Copy current streaming-card fields from `ds` into the persisted Session and save.
775
942
  * Lets the existing card be PATCHed on next screen_update after a daemon restart,
@@ -787,6 +954,7 @@ export function persistStreamCardState(ds) {
787
954
  sameUsageLimit(s.usageLimit, ds.usageLimit) &&
788
955
  s.lastUserPrompt === ds.lastUserPrompt &&
789
956
  s.lastCliInput === ds.lastCliInput &&
957
+ JSON.stringify(s.lastCodexAppInput ?? null) === JSON.stringify(ds.lastCodexAppInput ?? null) &&
790
958
  JSON.stringify(s.replyThreadAliases ?? {}) === JSON.stringify(ds.replyThreadAliases ?? {}) &&
791
959
  JSON.stringify(s.currentReplyTarget ?? null) === JSON.stringify(ds.currentReplyTarget ?? null))
792
960
  return;
@@ -798,20 +966,38 @@ export function persistStreamCardState(ds) {
798
966
  s.usageLimit = ds.usageLimit;
799
967
  s.lastUserPrompt = ds.lastUserPrompt;
800
968
  s.lastCliInput = ds.lastCliInput;
969
+ if (ds.lastCodexAppInput)
970
+ s.lastCodexAppInput = ds.lastCodexAppInput;
971
+ else
972
+ delete s.lastCodexAppInput;
801
973
  s.replyThreadAliases = ds.replyThreadAliases;
802
974
  s.currentReplyTarget = ds.currentReplyTarget;
803
975
  // Clear legacy field so it doesn't drift
804
976
  s.streamExpanded = undefined;
805
977
  sessionStore.updateSession(s);
806
978
  }
807
- export function rememberLastCliInput(ds, userPrompt, cliInput) {
979
+ export function rememberLastCliInput(ds, userPrompt, cliInput, opts) {
808
980
  // A real CLI input means the post-restart silence is over — let the normal
809
981
  // card flow resume for this and subsequent turns.
810
982
  ds.suppressRecoveryCard = undefined;
811
983
  ds.lastUserPrompt = userPrompt;
812
- ds.lastCliInput = cliInput;
984
+ const normalized = typeof cliInput === 'string' ? { content: cliInput } : cliInput;
985
+ ds.lastCliInput = normalized.content;
986
+ const botCfg = getBot(ds.larkAppId).config;
987
+ const effectiveCliId = ds.session.cliId ?? botCfg.cliId;
988
+ const keepCodexAppInput = opts?.codexAppInputAccepted ?? (effectiveCliId === 'codex-app' &&
989
+ botCfg.codexAppCleanInput === true &&
990
+ !ds.adoptedFrom);
991
+ if (keepCodexAppInput && normalized.codexAppInput)
992
+ ds.lastCodexAppInput = normalized.codexAppInput;
993
+ else
994
+ delete ds.lastCodexAppInput;
813
995
  ds.session.lastUserPrompt = userPrompt;
814
- ds.session.lastCliInput = cliInput;
996
+ ds.session.lastCliInput = normalized.content;
997
+ if (keepCodexAppInput && normalized.codexAppInput)
998
+ ds.session.lastCodexAppInput = normalized.codexAppInput;
999
+ else
1000
+ delete ds.session.lastCodexAppInput;
815
1001
  ds.session.replyThreadAliases = ds.replyThreadAliases;
816
1002
  ds.session.currentReplyTarget = ds.currentReplyTarget;
817
1003
  sessionStore.updateSession(ds.session);
@@ -903,6 +1089,7 @@ export async function restoreActiveSessions(activeSessions) {
903
1089
  lastMessageAt: sessionLastMessageAtMs(session),
904
1090
  hasHistory: false,
905
1091
  workingDir: adopted.cwd,
1092
+ ownerOpenId: session.ownerOpenId,
906
1093
  adoptedFrom: adopted,
907
1094
  streamCardId: session.streamCardId,
908
1095
  streamCardNonce: session.streamCardNonce,
@@ -914,6 +1101,7 @@ export async function restoreActiveSessions(activeSessions) {
914
1101
  usageLimit: session.usageLimit,
915
1102
  lastUserPrompt: session.lastUserPrompt,
916
1103
  lastCliInput: session.lastCliInput,
1104
+ lastCodexAppInput: session.lastCodexAppInput,
917
1105
  replyThreadAliases: session.replyThreadAliases,
918
1106
  currentReplyTarget: session.currentReplyTarget,
919
1107
  // Restart stays silent for adopt sessions too: forkAdoptWorker shares
@@ -963,6 +1151,8 @@ export async function restoreActiveSessions(activeSessions) {
963
1151
  workingDir: session.workingDir,
964
1152
  ownerOpenId: session.ownerOpenId,
965
1153
  pendingPrompt: session.queuedPrompt,
1154
+ pendingCodexAppText: session.queuedCodexAppText,
1155
+ pendingCodexAppMessageContext: session.queuedCodexAppMessageContext,
966
1156
  currentTurnTitle: session.currentTurnTitle ?? session.title,
967
1157
  };
968
1158
  const anchor = sessionAnchorId(ds);
@@ -989,6 +1179,7 @@ export async function restoreActiveSessions(activeSessions) {
989
1179
  lastMessageAt: sessionLastMessageAtMs(session),
990
1180
  hasHistory: true, // restored sessions have prior CLI history
991
1181
  workingDir: session.workingDir,
1182
+ ownerOpenId: session.ownerOpenId,
992
1183
  // Restore persisted streaming-card state — next screen_update will PATCH
993
1184
  // the existing card instead of POSTing a fresh one. If the card was
994
1185
  // withdrawn while we were down, the PATCH fails with MessageWithdrawnError
@@ -1002,6 +1193,7 @@ export async function restoreActiveSessions(activeSessions) {
1002
1193
  usageLimit: session.usageLimit,
1003
1194
  lastUserPrompt: session.lastUserPrompt,
1004
1195
  lastCliInput: session.lastCliInput,
1196
+ lastCodexAppInput: session.lastCodexAppInput,
1005
1197
  replyThreadAliases: session.replyThreadAliases,
1006
1198
  currentReplyTarget: session.currentReplyTarget,
1007
1199
  // Restart stays silent in the group: the recovery re-fork won't post or
@@ -1276,6 +1468,7 @@ export async function resumeSession(sessionId, activeSessions) {
1276
1468
  usageLimit: session.usageLimit,
1277
1469
  lastUserPrompt: session.lastUserPrompt,
1278
1470
  lastCliInput: session.lastCliInput,
1471
+ lastCodexAppInput: session.lastCodexAppInput,
1279
1472
  replyThreadAliases: session.replyThreadAliases,
1280
1473
  currentReplyTarget: session.currentReplyTarget,
1281
1474
  };
@@ -1403,17 +1596,17 @@ export async function executeScheduledTask(task, activeSessions, refreshCliVersi
1403
1596
  markSessionActivity(existing);
1404
1597
  try {
1405
1598
  ensureSessionWhiteboard(existing);
1406
- const content = buildFollowUpContent(task.prompt, existing.session.sessionId, {
1599
+ const input = buildFollowUpCliInput(task.prompt, existing.session.sessionId, {
1407
1600
  isAdoptMode: false,
1408
- cliId: bot.config.cliId,
1409
- cliPathOverride: bot.config.cliPathOverride,
1601
+ cliId: existing.session.cliId ?? bot.config.cliId,
1602
+ cliPathOverride: existing.session.cliPathOverride ?? bot.config.cliPathOverride,
1410
1603
  locale: localeForBot(larkAppId),
1411
1604
  larkAppId,
1412
1605
  chatId: task.chatId,
1413
1606
  whiteboardId: existing.session.whiteboardId,
1414
1607
  });
1415
- rememberLastCliInput(existing, task.prompt, content);
1416
- existing.worker.send({ type: 'message', content });
1608
+ rememberLastCliInput(existing, task.prompt, input);
1609
+ sendWorkerInput(existing, input);
1417
1610
  logger.info(`[scheduler] Task "${task.name}" injected into live session ${existing.session.sessionId}`);
1418
1611
  return;
1419
1612
  }
@@ -1452,7 +1645,7 @@ export async function executeScheduledTask(task, activeSessions, refreshCliVersi
1452
1645
  workingDir: task.workingDir,
1453
1646
  };
1454
1647
  ensureSessionWhiteboard(ds);
1455
- const prompt = buildNewTopicPrompt(task.prompt, session.sessionId, bot.config.cliId, bot.config.cliPathOverride, undefined, undefined, undefined, undefined, { name: bot.botName, openId: bot.botOpenId }, localeForBot(larkAppId), undefined, { larkAppId, chatId: task.chatId, whiteboardId: ds.session.whiteboardId });
1648
+ const prompt = buildNewTopicCliInput(task.prompt, session.sessionId, bot.config.cliId, bot.config.cliPathOverride, undefined, undefined, undefined, undefined, { name: bot.botName, openId: bot.botOpenId }, localeForBot(larkAppId), undefined, { larkAppId, chatId: task.chatId, whiteboardId: ds.session.whiteboardId });
1456
1649
  activeSessions.set(sessionKey(anchor, larkAppId), ds);
1457
1650
  rememberLastCliInput(ds, task.prompt, prompt);
1458
1651
  forkWorker(ds, prompt);
@@ -1516,7 +1709,14 @@ async function forkOrShowRepoCard(ds, userContent) {
1516
1709
  return;
1517
1710
  }
1518
1711
  }
1519
- const buildPrompt = () => buildNewTopicPrompt(userContent, ds.session.sessionId, bot.config.cliId, bot.config.cliPathOverride, undefined, undefined, undefined, undefined, { name: bot.botName, openId: bot.botOpenId }, locale, undefined, { larkAppId, chatId: ds.chatId, whiteboardId: ds.session.whiteboardId });
1712
+ const buildPrompt = () => buildNewTopicCliInput(userContent, ds.session.sessionId, bot.config.cliId, bot.config.cliPathOverride, undefined, undefined, undefined, undefined, { name: bot.botName, openId: bot.botOpenId }, locale, undefined, {
1713
+ larkAppId,
1714
+ chatId: ds.chatId,
1715
+ whiteboardId: ds.session.whiteboardId,
1716
+ codexAppText: ds.pendingCodexAppText,
1717
+ codexAppApplicationContext: ds.pendingCodexAppApplicationContext,
1718
+ codexAppMessageContext: ds.pendingCodexAppMessageContext,
1719
+ });
1520
1720
  if (!ds.workingDir) {
1521
1721
  // 没钉目录 → 复用 /repo 选择卡片让用户在群里选仓库。
1522
1722
  const scanDirs = getProjectScanDirs(ds).filter(d => existsSync(d));
@@ -1548,6 +1748,9 @@ async function forkOrShowRepoCard(ds, userContent) {
1548
1748
  const prompt = buildPrompt();
1549
1749
  rememberLastCliInput(ds, userContent, prompt);
1550
1750
  forkWorker(ds, prompt);
1751
+ ds.pendingCodexAppText = undefined;
1752
+ ds.pendingCodexAppApplicationContext = undefined;
1753
+ ds.pendingCodexAppMessageContext = undefined;
1551
1754
  }
1552
1755
  /** 在新建的飞书群里为某个 bot 拉起一条 chat-scope 会话(dashboard「创建会话」用)。
1553
1756
  * column='in_progress' → 立即 forkWorker 把内容当首轮发给 CLI;
@@ -1591,6 +1794,7 @@ export async function spawnDashboardSession(activeSessions, refreshCliVersion, a
1591
1794
  // 这份已包装内容,激活时直接喂给 buildNewTopicPrompt,保证待办池里起来的 lead
1592
1795
  // 也带编排上下文(coworkers 只有此刻可靠,激活时已无从重算)。
1593
1796
  const userContent = composeSpawnUserContent({ content, role, coworkers: args.coworkers, locale });
1797
+ const codexAppMessageContext = composeSpawnCodexAppContext({ role, coworkers: args.coworkers, locale });
1594
1798
  const resolvedTitle = args.title || deriveSessionTitleFromContent(content);
1595
1799
  const session = sessionStore.createSession(chatId, bannerMessageId ?? chatId, resolvedTitle, 'group');
1596
1800
  const now = Date.now();
@@ -1604,6 +1808,8 @@ export async function spawnDashboardSession(activeSessions, refreshCliVersion, a
1604
1808
  if (column === 'backlog') {
1605
1809
  session.queued = true;
1606
1810
  session.queuedPrompt = userContent;
1811
+ session.queuedCodexAppText = content;
1812
+ session.queuedCodexAppMessageContext = codexAppMessageContext;
1607
1813
  session.kanbanColumn = 'backlog';
1608
1814
  }
1609
1815
  // 钉 workingDir:oncall 绑定(弹框填了目录)/ bot 默认。都没有 → undefined,激活/开跑时
@@ -1629,6 +1835,8 @@ export async function spawnDashboardSession(activeSessions, refreshCliVersion, a
1629
1835
  workingDir,
1630
1836
  ownerOpenId: session.ownerOpenId,
1631
1837
  currentTurnTitle: resolvedTitle,
1838
+ pendingCodexAppText: content,
1839
+ pendingCodexAppMessageContext: codexAppMessageContext,
1632
1840
  };
1633
1841
  activeSessions.set(sessionKey(anchor, larkAppId), ds);
1634
1842
  if (column === 'backlog') {
@@ -1655,12 +1863,21 @@ export async function activateQueuedSession(ds) {
1655
1863
  // 不该发生(queued 一定 worker:null),但保险:清标记即可。
1656
1864
  ds.session.queued = false;
1657
1865
  ds.session.queuedPrompt = undefined;
1866
+ ds.session.queuedCodexAppText = undefined;
1867
+ ds.session.queuedCodexAppMessageContext = undefined;
1658
1868
  sessionStore.updateSession(ds.session);
1659
1869
  return { ok: true };
1660
1870
  }
1661
1871
  const content = ds.session.queuedPrompt ?? ds.pendingPrompt ?? '';
1872
+ // A parked dashboard task may have crossed a daemon restart. Restore the
1873
+ // persisted clean-input sidecar before clearing the durable backlog fields;
1874
+ // forkOrShowRepoCard will carry it through either immediate fork or /repo.
1875
+ ds.pendingCodexAppText ??= ds.session.queuedCodexAppText;
1876
+ ds.pendingCodexAppMessageContext ??= ds.session.queuedCodexAppMessageContext;
1662
1877
  ds.session.queued = false;
1663
1878
  ds.session.queuedPrompt = undefined;
1879
+ ds.session.queuedCodexAppText = undefined;
1880
+ ds.session.queuedCodexAppMessageContext = undefined;
1664
1881
  ds.pendingPrompt = undefined;
1665
1882
  // 激活即视为开始:从待办池挪到进行中,让卡片归位。
1666
1883
  if (ds.session.kanbanColumn === 'backlog')