botmux 2.93.1 → 2.95.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 (290) hide show
  1. package/dist/adapters/backend/tmux-backend.d.ts +18 -2
  2. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  3. package/dist/adapters/backend/tmux-backend.js +45 -3
  4. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  5. package/dist/adapters/backend/tmux-pipe-backend.js +1 -1
  6. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  7. package/dist/adapters/backend/types.d.ts +8 -0
  8. package/dist/adapters/backend/types.d.ts.map +1 -1
  9. package/dist/adapters/backend/types.js.map +1 -1
  10. package/dist/adapters/backend/zellij-backend.js +1 -1
  11. package/dist/adapters/backend/zellij-backend.js.map +1 -1
  12. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  13. package/dist/adapters/cli/claude-code.js +6 -1
  14. package/dist/adapters/cli/claude-code.js.map +1 -1
  15. package/dist/bot-registry.d.ts +50 -1
  16. package/dist/bot-registry.d.ts.map +1 -1
  17. package/dist/bot-registry.js +30 -0
  18. package/dist/bot-registry.js.map +1 -1
  19. package/dist/cli/workflow.d.ts.map +1 -1
  20. package/dist/cli/workflow.js +11 -4
  21. package/dist/cli/workflow.js.map +1 -1
  22. package/dist/cli.d.ts.map +1 -1
  23. package/dist/cli.js +146 -1
  24. package/dist/cli.js.map +1 -1
  25. package/dist/core/command-handler.d.ts.map +1 -1
  26. package/dist/core/command-handler.js +28 -3
  27. package/dist/core/command-handler.js.map +1 -1
  28. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  29. package/dist/core/dashboard-ipc-server.js +92 -0
  30. package/dist/core/dashboard-ipc-server.js.map +1 -1
  31. package/dist/core/inherit-peer.d.ts +5 -0
  32. package/dist/core/inherit-peer.d.ts.map +1 -1
  33. package/dist/core/inherit-peer.js +3 -0
  34. package/dist/core/inherit-peer.js.map +1 -1
  35. package/dist/core/session-discovery.d.ts +9 -0
  36. package/dist/core/session-discovery.d.ts.map +1 -1
  37. package/dist/core/session-discovery.js +24 -0
  38. package/dist/core/session-discovery.js.map +1 -1
  39. package/dist/core/session-manager.d.ts.map +1 -1
  40. package/dist/core/session-manager.js +7 -6
  41. package/dist/core/session-manager.js.map +1 -1
  42. package/dist/core/terminal-url.d.ts.map +1 -1
  43. package/dist/core/terminal-url.js +15 -0
  44. package/dist/core/terminal-url.js.map +1 -1
  45. package/dist/core/trigger-session.js +2 -2
  46. package/dist/core/trigger-session.js.map +1 -1
  47. package/dist/core/worker-pool.d.ts.map +1 -1
  48. package/dist/core/worker-pool.js +30 -0
  49. package/dist/core/worker-pool.js.map +1 -1
  50. package/dist/daemon.d.ts.map +1 -1
  51. package/dist/daemon.js +296 -23
  52. package/dist/daemon.js.map +1 -1
  53. package/dist/dashboard/aggregator.d.ts +5 -0
  54. package/dist/dashboard/aggregator.d.ts.map +1 -1
  55. package/dist/dashboard/aggregator.js +7 -0
  56. package/dist/dashboard/aggregator.js.map +1 -1
  57. package/dist/dashboard/auth.d.ts.map +1 -1
  58. package/dist/dashboard/auth.js +7 -1
  59. package/dist/dashboard/auth.js.map +1 -1
  60. package/dist/dashboard/bot-payload.d.ts +2 -0
  61. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  62. package/dist/dashboard/bot-payload.js +2 -0
  63. package/dist/dashboard/bot-payload.js.map +1 -1
  64. package/dist/dashboard/connector-api.d.ts.map +1 -1
  65. package/dist/dashboard/connector-api.js +14 -3
  66. package/dist/dashboard/connector-api.js.map +1 -1
  67. package/dist/dashboard/settings-write-applier.d.ts +2 -1
  68. package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
  69. package/dist/dashboard/settings-write-applier.js +7 -0
  70. package/dist/dashboard/settings-write-applier.js.map +1 -1
  71. package/dist/dashboard/v3-runs-api.d.ts +26 -0
  72. package/dist/dashboard/v3-runs-api.d.ts.map +1 -0
  73. package/dist/dashboard/v3-runs-api.js +67 -0
  74. package/dist/dashboard/v3-runs-api.js.map +1 -0
  75. package/dist/dashboard/web/app.js +21 -9
  76. package/dist/dashboard/web/app.js.map +1 -1
  77. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  78. package/dist/dashboard/web/bot-defaults.js +128 -33
  79. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  80. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  81. package/dist/dashboard/web/i18n.js +42 -6
  82. package/dist/dashboard/web/i18n.js.map +1 -1
  83. package/dist/dashboard/web/legacy-workflow-link.d.ts +2 -0
  84. package/dist/dashboard/web/legacy-workflow-link.d.ts.map +1 -0
  85. package/dist/dashboard/web/legacy-workflow-link.js +12 -0
  86. package/dist/dashboard/web/legacy-workflow-link.js.map +1 -0
  87. package/dist/dashboard/web/settings.d.ts.map +1 -1
  88. package/dist/dashboard/web/settings.js +11 -0
  89. package/dist/dashboard/web/settings.js.map +1 -1
  90. package/dist/dashboard/web/v3-terminal.d.ts +11 -0
  91. package/dist/dashboard/web/v3-terminal.d.ts.map +1 -0
  92. package/dist/dashboard/web/v3-terminal.js +143 -0
  93. package/dist/dashboard/web/v3-terminal.js.map +1 -0
  94. package/dist/dashboard/web/v3.d.ts +2 -0
  95. package/dist/dashboard/web/v3.d.ts.map +1 -0
  96. package/dist/dashboard/web/v3.js +564 -0
  97. package/dist/dashboard/web/v3.js.map +1 -0
  98. package/dist/dashboard/web/whiteboards.d.ts.map +1 -1
  99. package/dist/dashboard/web/whiteboards.js +14 -5
  100. package/dist/dashboard/web/whiteboards.js.map +1 -1
  101. package/dist/dashboard/web/workflows.d.ts.map +1 -1
  102. package/dist/dashboard/web/workflows.js +12 -8
  103. package/dist/dashboard/web/workflows.js.map +1 -1
  104. package/dist/dashboard-web/app.js +834 -724
  105. package/dist/dashboard-web/apple-touch-icon.png +0 -0
  106. package/dist/dashboard-web/favicon.png +0 -0
  107. package/dist/dashboard-web/index.html +3 -1
  108. package/dist/dashboard-web/style.css +223 -0
  109. package/dist/dashboard.js +170 -10
  110. package/dist/dashboard.js.map +1 -1
  111. package/dist/global-config.d.ts +10 -0
  112. package/dist/global-config.d.ts.map +1 -1
  113. package/dist/global-config.js +8 -0
  114. package/dist/global-config.js.map +1 -1
  115. package/dist/i18n/en.d.ts.map +1 -1
  116. package/dist/i18n/en.js +23 -3
  117. package/dist/i18n/en.js.map +1 -1
  118. package/dist/i18n/zh.d.ts.map +1 -1
  119. package/dist/i18n/zh.js +23 -3
  120. package/dist/i18n/zh.js.map +1 -1
  121. package/dist/im/lark/card-handler.d.ts +12 -0
  122. package/dist/im/lark/card-handler.d.ts.map +1 -1
  123. package/dist/im/lark/card-handler.js +28 -0
  124. package/dist/im/lark/card-handler.js.map +1 -1
  125. package/dist/im/lark/client.d.ts.map +1 -1
  126. package/dist/im/lark/client.js +3 -12
  127. package/dist/im/lark/client.js.map +1 -1
  128. package/dist/im/lark/event-dispatcher.d.ts +2 -1
  129. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  130. package/dist/im/lark/event-dispatcher.js +20 -6
  131. package/dist/im/lark/event-dispatcher.js.map +1 -1
  132. package/dist/im/lark/grant-command.js +4 -4
  133. package/dist/im/lark/grant-command.js.map +1 -1
  134. package/dist/im/lark/message-parser.d.ts +29 -1
  135. package/dist/im/lark/message-parser.d.ts.map +1 -1
  136. package/dist/im/lark/message-parser.js +35 -1
  137. package/dist/im/lark/message-parser.js.map +1 -1
  138. package/dist/im/lark/v3-blocked-card-handler.d.ts +28 -0
  139. package/dist/im/lark/v3-blocked-card-handler.d.ts.map +1 -0
  140. package/dist/im/lark/v3-blocked-card-handler.js +134 -0
  141. package/dist/im/lark/v3-blocked-card-handler.js.map +1 -0
  142. package/dist/im/lark/v3-blocked-card.d.ts +75 -0
  143. package/dist/im/lark/v3-blocked-card.d.ts.map +1 -0
  144. package/dist/im/lark/v3-blocked-card.js +206 -0
  145. package/dist/im/lark/v3-blocked-card.js.map +1 -0
  146. package/dist/im/lark/v3-gate-card-handler.d.ts +31 -0
  147. package/dist/im/lark/v3-gate-card-handler.d.ts.map +1 -0
  148. package/dist/im/lark/v3-gate-card-handler.js +110 -0
  149. package/dist/im/lark/v3-gate-card-handler.js.map +1 -0
  150. package/dist/im/lark/v3-gate-card.d.ts +47 -0
  151. package/dist/im/lark/v3-gate-card.d.ts.map +1 -0
  152. package/dist/im/lark/v3-gate-card.js +119 -0
  153. package/dist/im/lark/v3-gate-card.js.map +1 -0
  154. package/dist/im/lark/v3-loop-grant-card-handler.d.ts +28 -0
  155. package/dist/im/lark/v3-loop-grant-card-handler.d.ts.map +1 -0
  156. package/dist/im/lark/v3-loop-grant-card-handler.js +90 -0
  157. package/dist/im/lark/v3-loop-grant-card-handler.js.map +1 -0
  158. package/dist/im/lark/v3-loop-grant-card.d.ts +46 -0
  159. package/dist/im/lark/v3-loop-grant-card.d.ts.map +1 -0
  160. package/dist/im/lark/v3-loop-grant-card.js +119 -0
  161. package/dist/im/lark/v3-loop-grant-card.js.map +1 -0
  162. package/dist/im/lark/v3-revisit-grant-card-handler.d.ts +29 -0
  163. package/dist/im/lark/v3-revisit-grant-card-handler.d.ts.map +1 -0
  164. package/dist/im/lark/v3-revisit-grant-card-handler.js +96 -0
  165. package/dist/im/lark/v3-revisit-grant-card-handler.js.map +1 -0
  166. package/dist/im/lark/v3-revisit-grant-card.d.ts +52 -0
  167. package/dist/im/lark/v3-revisit-grant-card.d.ts.map +1 -0
  168. package/dist/im/lark/v3-revisit-grant-card.js +123 -0
  169. package/dist/im/lark/v3-revisit-grant-card.js.map +1 -0
  170. package/dist/im/lark/workflow-cards.d.ts.map +1 -1
  171. package/dist/im/lark/workflow-cards.js +5 -1
  172. package/dist/im/lark/workflow-cards.js.map +1 -1
  173. package/dist/im/lark/workflow-progress-card.d.ts +1 -1
  174. package/dist/im/lark/workflow-progress-card.js +1 -1
  175. package/dist/im/lark/workflow-slash-command.d.ts +28 -0
  176. package/dist/im/lark/workflow-slash-command.d.ts.map +1 -1
  177. package/dist/im/lark/workflow-slash-command.js +61 -5
  178. package/dist/im/lark/workflow-slash-command.js.map +1 -1
  179. package/dist/platform/binding.d.ts +6 -0
  180. package/dist/platform/binding.d.ts.map +1 -1
  181. package/dist/platform/binding.js +17 -0
  182. package/dist/platform/binding.js.map +1 -1
  183. package/dist/platform/tunnel-client.d.ts +2 -0
  184. package/dist/platform/tunnel-client.d.ts.map +1 -1
  185. package/dist/platform/tunnel-client.js.map +1 -1
  186. package/dist/services/bot-config-store.d.ts.map +1 -1
  187. package/dist/services/bot-config-store.js +1 -0
  188. package/dist/services/bot-config-store.js.map +1 -1
  189. package/dist/services/card-prefs-store.d.ts +4 -0
  190. package/dist/services/card-prefs-store.d.ts.map +1 -1
  191. package/dist/services/card-prefs-store.js +19 -0
  192. package/dist/services/card-prefs-store.js.map +1 -1
  193. package/dist/services/oncall-store.d.ts +25 -0
  194. package/dist/services/oncall-store.d.ts.map +1 -1
  195. package/dist/services/oncall-store.js +56 -0
  196. package/dist/services/oncall-store.js.map +1 -1
  197. package/dist/services/transcript-resolver.d.ts.map +1 -1
  198. package/dist/services/transcript-resolver.js +21 -4
  199. package/dist/services/transcript-resolver.js.map +1 -1
  200. package/dist/setup/bot-config-editor.d.ts +7 -0
  201. package/dist/setup/bot-config-editor.d.ts.map +1 -1
  202. package/dist/setup/bot-config-editor.js +33 -0
  203. package/dist/setup/bot-config-editor.js.map +1 -1
  204. package/dist/skills/definitions.d.ts.map +1 -1
  205. package/dist/skills/definitions.js +208 -4
  206. package/dist/skills/definitions.js.map +1 -1
  207. package/dist/types.d.ts +1 -0
  208. package/dist/types.d.ts.map +1 -1
  209. package/dist/utils/terminal-favicon.d.ts +2 -0
  210. package/dist/utils/terminal-favicon.d.ts.map +1 -0
  211. package/dist/utils/terminal-favicon.js +5 -0
  212. package/dist/utils/terminal-favicon.js.map +1 -0
  213. package/dist/worker.js +152 -8
  214. package/dist/worker.js.map +1 -1
  215. package/dist/workflows/definition.d.ts +80 -80
  216. package/dist/workflows/events/payloads.d.ts +4 -4
  217. package/dist/workflows/events/schema.d.ts +16 -16
  218. package/dist/workflows/v3/architect.d.ts +37 -0
  219. package/dist/workflows/v3/architect.d.ts.map +1 -0
  220. package/dist/workflows/v3/architect.js +226 -0
  221. package/dist/workflows/v3/architect.js.map +1 -0
  222. package/dist/workflows/v3/bot-resolve.d.ts +21 -0
  223. package/dist/workflows/v3/bot-resolve.d.ts.map +1 -0
  224. package/dist/workflows/v3/bot-resolve.js +48 -0
  225. package/dist/workflows/v3/bot-resolve.js.map +1 -0
  226. package/dist/workflows/v3/cli-run.d.ts +24 -0
  227. package/dist/workflows/v3/cli-run.d.ts.map +1 -0
  228. package/dist/workflows/v3/cli-run.js +265 -0
  229. package/dist/workflows/v3/cli-run.js.map +1 -0
  230. package/dist/workflows/v3/contract.d.ts +302 -0
  231. package/dist/workflows/v3/contract.d.ts.map +1 -0
  232. package/dist/workflows/v3/contract.js +91 -0
  233. package/dist/workflows/v3/contract.js.map +1 -0
  234. package/dist/workflows/v3/daemon-run.d.ts +311 -0
  235. package/dist/workflows/v3/daemon-run.d.ts.map +1 -0
  236. package/dist/workflows/v3/daemon-run.js +775 -0
  237. package/dist/workflows/v3/daemon-run.js.map +1 -0
  238. package/dist/workflows/v3/dag.d.ts +332 -0
  239. package/dist/workflows/v3/dag.d.ts.map +1 -0
  240. package/dist/workflows/v3/dag.js +1040 -0
  241. package/dist/workflows/v3/dag.js.map +1 -0
  242. package/dist/workflows/v3/ephemeral-pool.d.ts +29 -0
  243. package/dist/workflows/v3/ephemeral-pool.d.ts.map +1 -0
  244. package/dist/workflows/v3/ephemeral-pool.js +331 -0
  245. package/dist/workflows/v3/ephemeral-pool.js.map +1 -0
  246. package/dist/workflows/v3/grill-state.d.ts +92 -0
  247. package/dist/workflows/v3/grill-state.d.ts.map +1 -0
  248. package/dist/workflows/v3/grill-state.js +151 -0
  249. package/dist/workflows/v3/grill-state.js.map +1 -0
  250. package/dist/workflows/v3/host.d.ts +88 -0
  251. package/dist/workflows/v3/host.d.ts.map +1 -0
  252. package/dist/workflows/v3/host.js +349 -0
  253. package/dist/workflows/v3/host.js.map +1 -0
  254. package/dist/workflows/v3/human-gate.d.ts +85 -0
  255. package/dist/workflows/v3/human-gate.d.ts.map +1 -0
  256. package/dist/workflows/v3/human-gate.js +153 -0
  257. package/dist/workflows/v3/human-gate.js.map +1 -0
  258. package/dist/workflows/v3/journal.d.ts +207 -0
  259. package/dist/workflows/v3/journal.d.ts.map +1 -0
  260. package/dist/workflows/v3/journal.js +74 -0
  261. package/dist/workflows/v3/journal.js.map +1 -0
  262. package/dist/workflows/v3/manifest.d.ts +17 -0
  263. package/dist/workflows/v3/manifest.d.ts.map +1 -0
  264. package/dist/workflows/v3/manifest.js +266 -0
  265. package/dist/workflows/v3/manifest.js.map +1 -0
  266. package/dist/workflows/v3/ops-projection.d.ts +110 -0
  267. package/dist/workflows/v3/ops-projection.d.ts.map +1 -0
  268. package/dist/workflows/v3/ops-projection.js +0 -0
  269. package/dist/workflows/v3/ops-projection.js.map +1 -0
  270. package/dist/workflows/v3/orchestrator.d.ts +168 -0
  271. package/dist/workflows/v3/orchestrator.d.ts.map +1 -0
  272. package/dist/workflows/v3/orchestrator.js +417 -0
  273. package/dist/workflows/v3/orchestrator.js.map +1 -0
  274. package/dist/workflows/v3/runtime.d.ts +193 -0
  275. package/dist/workflows/v3/runtime.d.ts.map +1 -0
  276. package/dist/workflows/v3/runtime.js +1284 -0
  277. package/dist/workflows/v3/runtime.js.map +1 -0
  278. package/dist/workflows/v3/spec.d.ts +32 -0
  279. package/dist/workflows/v3/spec.d.ts.map +1 -0
  280. package/dist/workflows/v3/spec.js +173 -0
  281. package/dist/workflows/v3/spec.js.map +1 -0
  282. package/dist/workflows/v3/state.d.ts +74 -0
  283. package/dist/workflows/v3/state.d.ts.map +1 -0
  284. package/dist/workflows/v3/state.js +353 -0
  285. package/dist/workflows/v3/state.js.map +1 -0
  286. package/package.json +2 -2
  287. package/dist/dashboard/web/workflow-catalog.d.ts +0 -2
  288. package/dist/dashboard/web/workflow-catalog.d.ts.map +0 -1
  289. package/dist/dashboard/web/workflow-catalog.js +0 -323
  290. package/dist/dashboard/web/workflow-catalog.js.map +0 -1
package/dist/daemon.js CHANGED
@@ -15,7 +15,7 @@ import { sendRestartReportIfPending } from './core/restart-report.js';
15
15
  import { statSync } from 'node:fs';
16
16
  import { addReaction, getChatMode, listChatMemberOpenIds, replyMessage, resolveAllowedUsersWithMap, sendMessage, sendUserMessage, updateMessage } from './im/lark/client.js';
17
17
  import { resolveGroupJoinPrompt, waitForAllowedUserInChat } from './core/auto-start.js';
18
- import { loadBotConfigs, registerBot, getBot, getAllBots, findOncallChat } from './bot-registry.js';
18
+ import { loadBotConfigs, registerBot, getBot, getAllBots, findOncallChat, effectiveDefaultWorkingDir } from './bot-registry.js';
19
19
  import * as sessionStore from './services/session-store.js';
20
20
  import * as chatFirstSeenStore from './services/chat-first-seen-store.js';
21
21
  import { ensureDefaultOncallBound } from './services/oncall-store.js';
@@ -54,8 +54,18 @@ import { beginReplyTargetTurn, fallbackTurnId, resolveSessionReplyTarget, syncRe
54
54
  import { sweepOrphanSandboxes } from './adapters/backend/sandbox.js';
55
55
  import { sweepIdleWorkers, DEFAULT_MAX_LIVE_WORKERS } from './core/idle-worker-sweeper.js';
56
56
  import { handleCardAction } from './im/lark/card-handler.js';
57
- import { executeWorkflowCommand, parseWorkflowCommand, resolveBotSnapshot, } from './im/lark/workflow-slash-command.js';
57
+ import { executeWorkflowCommand, parseWorkflowCommand, parseWorkflowGrillTrigger, buildWorkflowGrillPrompt, resolveBotSnapshot, WORKFLOW_USAGE, WORKFLOW_V2_RENAME_NOTICE, } from './im/lark/workflow-slash-command.js';
58
58
  import { workflowRunDetailUrl } from './im/lark/workflow-cards.js';
59
+ import { createV3GateRunner, requestV3Retry, requestV3LoopGrant } from './workflows/v3/daemon-run.js';
60
+ import { buildV3GateCard } from './im/lark/v3-gate-card.js';
61
+ import { buildV3BlockedCard } from './im/lark/v3-blocked-card.js';
62
+ import { buildV3LoopGrantCard } from './im/lark/v3-loop-grant-card.js';
63
+ import { buildV3RevisitGrantCard } from './im/lark/v3-revisit-grant-card.js';
64
+ import { isValidRunId as isValidV3RunId } from './workflows/v3/ops-projection.js';
65
+ import { readRunChatBinding as readV3RunChatBinding, defaultBaseDir as v3DefaultBaseDir } from './workflows/v3/grill-state.js';
66
+ /** This daemon process's bot larkAppId (set in startDaemon). Used to scope v3
67
+ * humanGate cold-attach + start to runs this bot owns (codex blocker #1). */
68
+ let selfV3LarkAppId;
59
69
  import { buildWorkflowStartingCard, buildWorkflowProgressCard, buildAttemptDeeplinkEnricher, } from './im/lark/workflow-progress-card.js';
60
70
  import { EventLog as WorkflowEventLog } from './workflows/events/append.js';
61
71
  import { replay as replayWorkflow } from './workflows/events/replay.js';
@@ -1089,6 +1099,13 @@ function workflowSpawnFn() {
1089
1099
  return createDaemonSpawnFn(daemonDeps);
1090
1100
  }
1091
1101
  async function handleWorkflowCommandIfAny(content, anchor, chatId, larkAppId, initiator) {
1102
+ // 旧 `/workflow run|cancel` 软降级:在执行前**先**发改名提示,让迁移指引第一时间
1103
+ // 到达用户(codex review:先发优于后发)。从原始 content 判定而非 parse 结果,
1104
+ // 这样连 `/workflow run`(缺 id)这类 invalid legacy 也能收到提示(codex review)。
1105
+ // 仅匹配 legacy 的 run|cancel(executeWorkflowCommand 必然 handle),不误伤 /template。
1106
+ if (/^\/workflow\s+(run|cancel)\b/.test(content.trim())) {
1107
+ await sessionReply(anchor, WORKFLOW_V2_RENAME_NOTICE, 'text', larkAppId);
1108
+ }
1092
1109
  // Captured by the `onRunCreated` closure so the trailing text reply can be
1093
1110
  // suppressed when the run-level progress card already landed. Codex
1094
1111
  // round 1 medium: "single self-updating tile" promise breaks if we also
@@ -1265,6 +1282,96 @@ function fireSessionlessCommandDetached(cmd, anchor, message, larkAppId) {
1265
1282
  void handleCommand(cmd, anchor, message, commandDeps, larkAppId).catch((err) => logger.error(`[sessionless ${cmd}] ${anchor.substring(0, 12)} failed: ${err?.message ?? err}`));
1266
1283
  }
1267
1284
  // Dependencies passed to card-handler
1285
+ // v3 humanGate run-controller: drives daemon-side v3 runs, posts/​re-posts
1286
+ // approval cards to the run's bound topic, and re-arms pending gates on startup
1287
+ // (cold-attach). postCard / notifyTerminal use the daemon's Lark sender; the
1288
+ // run logic + in-flight guard live in createV3GateRunner.
1289
+ const v3GateRunner = createV3GateRunner({
1290
+ postCard: async (binding, gate, runId) => {
1291
+ const card = buildV3GateCard({
1292
+ runId,
1293
+ waitId: gate.waitId,
1294
+ nodeId: gate.nodeId,
1295
+ prompt: gate.prompt,
1296
+ options: gate.options,
1297
+ approveOptions: gate.approveOptions,
1298
+ approvers: gate.approvers,
1299
+ });
1300
+ // codex blocker #3: never silently skip — a missing rootMessageId would
1301
+ // leave the run stuck at awaitingGate forever. Reply in-thread when we have
1302
+ // an anchor; otherwise post to the chat directly.
1303
+ if (binding.rootMessageId) {
1304
+ await sessionReply(binding.rootMessageId, card, 'interactive', binding.larkAppId);
1305
+ }
1306
+ else {
1307
+ await sendMessage(binding.larkAppId, binding.chatId, card, 'interactive');
1308
+ }
1309
+ },
1310
+ postBlockedCard: async (binding, info, runId) => {
1311
+ const card = buildV3BlockedCard({
1312
+ runId,
1313
+ nodeId: info.nodeId,
1314
+ attemptId: info.attemptId,
1315
+ errorClass: info.errorClass,
1316
+ errorCode: info.errorCode,
1317
+ message: info.message,
1318
+ // human-ask 受阻 → 渲染问题 + 选项按钮卡(替代纯重试卡)。
1319
+ ...(info.ask ? { ask: info.ask } : {}),
1320
+ });
1321
+ if (binding.rootMessageId) {
1322
+ await sessionReply(binding.rootMessageId, card, 'interactive', binding.larkAppId);
1323
+ }
1324
+ else {
1325
+ await sendMessage(binding.larkAppId, binding.chatId, card, 'interactive');
1326
+ }
1327
+ },
1328
+ postLoopGrantCard: async (binding, info, runId) => {
1329
+ const card = buildV3LoopGrantCard({
1330
+ runId,
1331
+ loopId: info.loopId,
1332
+ iteration: info.iteration,
1333
+ maxIterations: info.maxIterations,
1334
+ granted: info.granted,
1335
+ detail: info.detail,
1336
+ });
1337
+ if (binding.rootMessageId) {
1338
+ await sessionReply(binding.rootMessageId, card, 'interactive', binding.larkAppId);
1339
+ }
1340
+ else {
1341
+ await sendMessage(binding.larkAppId, binding.chatId, card, 'interactive');
1342
+ }
1343
+ },
1344
+ postRevisitGrantCard: async (binding, info, runId) => {
1345
+ const card = buildV3RevisitGrantCard({
1346
+ runId,
1347
+ sourceNodeId: info.sourceNodeId,
1348
+ toNodeId: info.toNodeId,
1349
+ tier: info.tier,
1350
+ attemptId: info.attemptId,
1351
+ detail: info.detail,
1352
+ });
1353
+ if (binding.rootMessageId) {
1354
+ await sessionReply(binding.rootMessageId, card, 'interactive', binding.larkAppId);
1355
+ }
1356
+ else {
1357
+ await sendMessage(binding.larkAppId, binding.chatId, card, 'interactive');
1358
+ }
1359
+ },
1360
+ notifyTerminal: async (binding, runId, outcome) => {
1361
+ if (!binding?.rootMessageId)
1362
+ return;
1363
+ const msg = outcome.runStatus === 'succeeded'
1364
+ ? `✅ v3 workflow \`${runId}\` 跑完了`
1365
+ : outcome.runStatus === 'blocked'
1366
+ // Fallback only — the blocked path normally posts a retry/grant card instead.
1367
+ ? `⏸️ v3 workflow \`${runId}\` 受阻${outcome.blockedNodeId ? `(节点 ${outcome.blockedNodeId})` : ''},可 \`botmux workflow retry ${runId}\` 重试(loop 耗尽则 \`botmux workflow grant ${runId}\` 追加一轮)`
1368
+ : `❌ v3 workflow \`${runId}\` 失败${outcome.failedNodeId ? `(节点 ${outcome.failedNodeId})` : ''}`;
1369
+ await sessionReply(binding.rootMessageId, msg, 'text', binding.larkAppId).catch(() => { });
1370
+ },
1371
+ onError: (runId, err) => {
1372
+ logger.warn(`[v3:${runId}] drive failed: ${err instanceof Error ? err.message : String(err)}`);
1373
+ },
1374
+ });
1268
1375
  const cardDeps = {
1269
1376
  activeSessions,
1270
1377
  sessionReply,
@@ -1274,6 +1381,24 @@ const cardDeps = {
1274
1381
  logger.warn(`[workflow:${runId}] re-entry after approval failed: ${err instanceof Error ? err.message : String(err)}`);
1275
1382
  });
1276
1383
  },
1384
+ v3GateDeps: {
1385
+ driveRun: (runId) => v3GateRunner.driveDetached(runId),
1386
+ // 审批权限:复用 canOperate(话题 owner / allowedUsers / oncall)。无 binding(corrupt /
1387
+ // 非 grill 出生的旧卡)→ **拒**(codex follow-up:合法卡一定有 binding,缺失即可疑).
1388
+ canResolve: (binding, operatorOpenId) => binding ? canOperate(binding.larkAppId, binding.chatId, operatorOpenId) : false,
1389
+ },
1390
+ v3BlockedDeps: {
1391
+ driveRun: (runId) => v3GateRunner.driveDetached(runId),
1392
+ canResolve: (binding, operatorOpenId) => binding ? canOperate(binding.larkAppId, binding.chatId, operatorOpenId) : false,
1393
+ },
1394
+ v3LoopGrantDeps: {
1395
+ driveRun: (runId) => v3GateRunner.driveDetached(runId),
1396
+ canResolve: (binding, operatorOpenId) => binding ? canOperate(binding.larkAppId, binding.chatId, operatorOpenId) : false,
1397
+ },
1398
+ v3RevisitGrantDeps: {
1399
+ driveRun: (runId) => v3GateRunner.driveDetached(runId),
1400
+ canResolve: (binding, operatorOpenId) => binding ? canOperate(binding.larkAppId, binding.chatId, operatorOpenId) : false,
1401
+ },
1277
1402
  };
1278
1403
  function dashboardWaitStatus(error) {
1279
1404
  switch (error.error) {
@@ -1308,6 +1433,99 @@ for (const [path, resolution] of [
1308
1433
  return jsonRes(res, 200, result);
1309
1434
  });
1310
1435
  }
1436
+ // v3 humanGate: start a daemon-driven run (grill `approve-dag` 后的主入口). Same
1437
+ // 127.0.0.1 ipcRoute posture as the v0.2 approve/reject mutations (dashboard
1438
+ // proxies authed external calls). Fire-and-forget: the runner drives the run +
1439
+ // posts gate cards; the caller polls /api/v3/runs/:id for status.
1440
+ ipcRoute('POST', '/api/v3/runs/:runId/start', async (_req, res, params) => {
1441
+ const runId = params.runId;
1442
+ if (!isValidV3RunId(runId))
1443
+ return jsonRes(res, 400, { ok: false, error: 'bad_run_id' });
1444
+ // Owner check (codex blocker #1): only the daemon owning this run's bot may
1445
+ // start it — otherwise the wrong daemon drives + posts cards with its client.
1446
+ const binding = readV3RunChatBinding(join(v3DefaultBaseDir(), runId));
1447
+ if (!binding)
1448
+ return jsonRes(res, 404, { ok: false, error: 'unknown_run_or_no_binding' });
1449
+ if (selfV3LarkAppId && binding.larkAppId !== selfV3LarkAppId) {
1450
+ return jsonRes(res, 409, { ok: false, error: 'wrong_daemon', ownerLarkAppId: binding.larkAppId });
1451
+ }
1452
+ v3GateRunner.driveDetached(runId);
1453
+ return jsonRes(res, 202, { ok: true, runId });
1454
+ });
1455
+ // v3 blocked retry: append the retry intent + re-drive. Same owner posture as
1456
+ // /start. Body: { nodeId? } (defaults to the run's blockedNodeId).
1457
+ ipcRoute('POST', '/api/v3/runs/:runId/retry', async (req, res, params) => {
1458
+ const runId = params.runId;
1459
+ if (!isValidV3RunId(runId))
1460
+ return jsonRes(res, 400, { ok: false, error: 'bad_run_id' });
1461
+ const binding = readV3RunChatBinding(join(v3DefaultBaseDir(), runId));
1462
+ if (!binding)
1463
+ return jsonRes(res, 404, { ok: false, error: 'unknown_run_or_no_binding' });
1464
+ if (selfV3LarkAppId && binding.larkAppId !== selfV3LarkAppId) {
1465
+ return jsonRes(res, 409, { ok: false, error: 'wrong_daemon', ownerLarkAppId: binding.larkAppId });
1466
+ }
1467
+ let body = {};
1468
+ try {
1469
+ body = await readJsonBody(req);
1470
+ }
1471
+ catch {
1472
+ /* empty body is fine — retry the blockedNodeId */
1473
+ }
1474
+ const nodeId = typeof body.nodeId === 'string' && body.nodeId ? body.nodeId : undefined;
1475
+ let outcome;
1476
+ try {
1477
+ outcome = requestV3Retry(v3DefaultBaseDir(), runId, { nodeId });
1478
+ }
1479
+ catch (err) {
1480
+ return jsonRes(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
1481
+ }
1482
+ if (outcome.kind === 'stale-run') {
1483
+ return jsonRes(res, 409, {
1484
+ ok: false,
1485
+ error: outcome.reason === 'missing' ? 'unknown_run'
1486
+ : outcome.reason === 'loop-node' ? 'loop_node_use_grant'
1487
+ : 'not_blocked',
1488
+ });
1489
+ }
1490
+ // requested / already-requested → make sure the run is moving.
1491
+ v3GateRunner.driveDetached(runId);
1492
+ return jsonRes(res, 202, { ok: true, runId, ...outcome });
1493
+ });
1494
+ // v3 loop grant: append one extra iteration for an exhausted loop + re-drive.
1495
+ // Same owner posture as /retry. Body: { loopId? } (defaults to the run's
1496
+ // blocked loop).
1497
+ ipcRoute('POST', '/api/v3/runs/:runId/grant', async (req, res, params) => {
1498
+ const runId = params.runId;
1499
+ if (!isValidV3RunId(runId))
1500
+ return jsonRes(res, 400, { ok: false, error: 'bad_run_id' });
1501
+ const binding = readV3RunChatBinding(join(v3DefaultBaseDir(), runId));
1502
+ if (!binding)
1503
+ return jsonRes(res, 404, { ok: false, error: 'unknown_run_or_no_binding' });
1504
+ if (selfV3LarkAppId && binding.larkAppId !== selfV3LarkAppId) {
1505
+ return jsonRes(res, 409, { ok: false, error: 'wrong_daemon', ownerLarkAppId: binding.larkAppId });
1506
+ }
1507
+ let body = {};
1508
+ try {
1509
+ body = await readJsonBody(req);
1510
+ }
1511
+ catch {
1512
+ /* empty body is fine — grant the blocked loop */
1513
+ }
1514
+ const loopId = typeof body.loopId === 'string' && body.loopId ? body.loopId : undefined;
1515
+ let outcome;
1516
+ try {
1517
+ outcome = requestV3LoopGrant(v3DefaultBaseDir(), runId, { loopId, by: 'cli' });
1518
+ }
1519
+ catch (err) {
1520
+ return jsonRes(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
1521
+ }
1522
+ if (outcome.kind === 'stale-run') {
1523
+ return jsonRes(res, 409, { ok: false, error: outcome.reason === 'missing' ? 'unknown_run' : 'not_exhausted' });
1524
+ }
1525
+ // granted / already-granted → make sure the run is moving.
1526
+ v3GateRunner.driveDetached(runId);
1527
+ return jsonRes(res, 202, { ok: true, runId, ...outcome });
1528
+ });
1311
1529
  function attemptResumeStatus(error) {
1312
1530
  switch (error.error) {
1313
1531
  case 'bad_run_id':
@@ -1421,7 +1639,7 @@ ipcRoute('POST', '/api/workflows/definitions/:id/run', async (req, res, params)
1421
1639
  }
1422
1640
  }
1423
1641
  // Convert JSON-channel params (decoded values) into the shared RawParamInput
1424
- // map. String-channel coercion stays on the IM `/workflow run` path.
1642
+ // map. String-channel coercion stays on the IM `/template run` path.
1425
1643
  const rawParams = {};
1426
1644
  for (const [k, v] of Object.entries(body.params ?? {})) {
1427
1645
  rawParams[k] = { kind: 'json', value: v };
@@ -1656,19 +1874,22 @@ async function maybeAutoBindDefaultOncall(larkAppId, chatId, chatType) {
1656
1874
  return ensureDefaultOncallBound(larkAppId, chatId, chatType);
1657
1875
  }
1658
1876
  /**
1659
- * Resolve this bot's `defaultWorkingDir` for a new-topic spawn, if any.
1660
- * Unlike `defaultOncall`, this is a pure runtime fallback: no state is
1661
- * written to bots.json and the chat is NOT bound to oncall (so the
1662
- * permission model stays unchanged). `/cd <path>` can still switch the
1877
+ * Resolve this bot's effective default working dir for a new-session spawn, if
1878
+ * any (see {@link effectiveDefaultWorkingDir}): `defaultWorkingDir`, or when
1879
+ * "Oncall 模式" is on `defaultOncall.workingDir` as the all-sessions fallback.
1880
+ *
1881
+ * Either way this is a pure runtime fallback: no state is written to bots.json
1882
+ * and the chat is NOT bound to oncall here (the group auto-bind, which opens
1883
+ * talk, happens separately upstream at layer 2), so the permission model for
1884
+ * the resolved session stays unchanged. `/cd <path>` can still switch the
1663
1885
  * working dir mid-session; the next new topic falls back to this default.
1664
1886
  *
1665
- * Returns the expanded path when the configured field points to a real
1666
- * directory; logs and returns undefined when the path is missing/invalid
1667
- * so the caller falls through to the repo-select card instead of
1668
- * spawning into a bad cwd.
1887
+ * Returns the expanded path when the chosen source points at a real directory;
1888
+ * logs and returns undefined when the path is missing/invalid so the caller
1889
+ * falls through to the repo-select card instead of spawning into a bad cwd.
1669
1890
  */
1670
1891
  function resolveBotDefaultWorkingDir(larkAppId) {
1671
- const raw = getBot(larkAppId).config.defaultWorkingDir;
1892
+ const raw = effectiveDefaultWorkingDir(getBot(larkAppId).config);
1672
1893
  if (!raw)
1673
1894
  return undefined;
1674
1895
  const resolved = expandHome(raw);
@@ -1677,7 +1898,7 @@ function resolveBotDefaultWorkingDir(larkAppId) {
1677
1898
  return resolved;
1678
1899
  }
1679
1900
  catch { /* not a dir */ }
1680
- logger.warn(`[${larkAppId}] defaultWorkingDir invalid (${resolved}); ` +
1901
+ logger.warn(`[${larkAppId}] default working dir invalid (${resolved}); ` +
1681
1902
  `falling back to repo-select card`);
1682
1903
  return undefined;
1683
1904
  }
@@ -1699,7 +1920,11 @@ async function resolvePinnedWorkingDir(ctx) {
1699
1920
  oncallEntry = await maybeAutoBindDefaultOncall(ctx.larkAppId, ctx.chatId, ctx.chatType);
1700
1921
  }
1701
1922
  const inheritedFrom = !oncallEntry
1702
- ? findInheritablePeer({ scope: ctx.scope, anchor: ctx.anchor, chatId: ctx.chatId, chatType: ctx.chatType, selfAppId: ctx.larkAppId })
1923
+ ? findInheritablePeer({
1924
+ scope: ctx.scope, anchor: ctx.anchor, chatId: ctx.chatId, chatType: ctx.chatType,
1925
+ selfAppId: ctx.larkAppId,
1926
+ botToBotSameDir: getBot(ctx.larkAppId).config.botToBotSameDir !== false,
1927
+ })
1703
1928
  : null;
1704
1929
  const botDefaultWorkingDir = (!oncallEntry && !inheritedFrom)
1705
1930
  ? resolveBotDefaultWorkingDir(ctx.larkAppId)
@@ -1868,13 +2093,32 @@ async function handleNewTopic(data, ctx) {
1868
2093
  msgType: parsed.msgType,
1869
2094
  content,
1870
2095
  });
1871
- if (parseWorkflowCommand(cmdContent)) {
2096
+ // v3 即兴 grill:`/workflow [new] <目标>`。daemon 不拷问——把目标包成触发
2097
+ // botmux-workflow skill 的 prompt(改写 content,promptContent 随后从 content
2098
+ // 构造),fall-through 到正常 session 创建,让本话题 agent 接管整条链路。
2099
+ // run|cancel 不在此命中(归 v2 legacy,由下面 handleWorkflowCommandIfAny 处理)。
2100
+ const newTopicGrill = parseWorkflowGrillTrigger(cmdContent);
2101
+ if (newTopicGrill) {
1872
2102
  if (await replyGrantRestrictionIfNeeded(larkAppId, chatId, senderOpenId, anchor, '/workflow')) {
1873
2103
  return;
1874
2104
  }
2105
+ if (newTopicGrill.kind === 'usage') {
2106
+ await sessionReply(anchor, WORKFLOW_USAGE, 'text', larkAppId);
2107
+ return;
2108
+ }
2109
+ content = buildWorkflowGrillPrompt(newTopicGrill.goal);
2110
+ // 保留原 cmdContent("/workflow new …")供 title/日志;/workflow 非注册命令,
2111
+ // 下面的 parseSlashCommandInvocation 会让它落到正常 spawn 路径。
1875
2112
  }
1876
- if (await handleWorkflowCommandIfAny(cmdContent, anchor, chatId, larkAppId, senderOpenId)) {
1877
- return;
2113
+ else {
2114
+ if (parseWorkflowCommand(cmdContent)) {
2115
+ if (await replyGrantRestrictionIfNeeded(larkAppId, chatId, senderOpenId, anchor, '/template')) {
2116
+ return;
2117
+ }
2118
+ }
2119
+ if (await handleWorkflowCommandIfAny(cmdContent, anchor, chatId, larkAppId, senderOpenId)) {
2120
+ return;
2121
+ }
1878
2122
  }
1879
2123
  // Intercept daemon commands in new topics (no session needed for some commands)
1880
2124
  const invocation = parseSlashCommandInvocation(cmdContent);
@@ -2390,7 +2634,9 @@ async function handleThreadReply(data, ctx) {
2390
2634
  const botSenderPrefix = isForeignBot
2391
2635
  ? `${tr('daemon.foreign_bot_mention_prefix', { botName: foreignBotName }, localeForBot(larkAppId))}\n`
2392
2636
  : '';
2393
- const promptContent = buildQuoteHint(parsed, scope, anchor, localeForBot(larkAppId)) + botSenderPrefix + parsed.content;
2637
+ // `let` (not const): the v3 grill gate below may replace this with a
2638
+ // skill-trigger prompt when the user sends `/workflow [new] <目标>` mid-thread.
2639
+ let promptContent = buildQuoteHint(parsed, scope, anchor, localeForBot(larkAppId)) + botSenderPrefix + parsed.content;
2394
2640
  const existingHookSession = activeSessions.get(sessionKey(anchor, larkAppId));
2395
2641
  emitHookEvent('thread.reply', {
2396
2642
  larkAppId,
@@ -2458,13 +2704,30 @@ async function handleThreadReply(data, ctx) {
2458
2704
  return;
2459
2705
  }
2460
2706
  }
2461
- if (parseWorkflowCommand(cmdContent)) {
2707
+ // v3 即兴 grill(thread 内):`/workflow [new] <目标>` → 把目标包成触发
2708
+ // botmux-workflow skill 的 prompt 覆盖 promptContent,fall-through 到下面正常
2709
+ // 转发逻辑,让现有/新建的 agent 接管。run|cancel 归 v2 legacy(走 else)。
2710
+ const threadGrill = parseWorkflowGrillTrigger(cmdContent);
2711
+ if (threadGrill) {
2462
2712
  if (await replyGrantRestrictionIfNeeded(larkAppId, threadChatId, threadSenderOpenId, anchor, '/workflow')) {
2463
2713
  return;
2464
2714
  }
2715
+ if (threadGrill.kind === 'usage') {
2716
+ await sessionReply(anchor, WORKFLOW_USAGE, 'text', larkAppId);
2717
+ return;
2718
+ }
2719
+ promptContent = buildWorkflowGrillPrompt(threadGrill.goal);
2720
+ // fall through to normal forwarding with the rewritten promptContent
2465
2721
  }
2466
- if (await handleWorkflowCommandIfAny(cmdContent, anchor, threadChatId, larkAppId, threadSenderOpenId)) {
2467
- return;
2722
+ else {
2723
+ if (parseWorkflowCommand(cmdContent)) {
2724
+ if (await replyGrantRestrictionIfNeeded(larkAppId, threadChatId, threadSenderOpenId, anchor, '/template')) {
2725
+ return;
2726
+ }
2727
+ }
2728
+ if (await handleWorkflowCommandIfAny(cmdContent, anchor, threadChatId, larkAppId, threadSenderOpenId)) {
2729
+ return;
2730
+ }
2468
2731
  }
2469
2732
  // Intercept daemon commands
2470
2733
  const invocation = parseSlashCommandInvocation(cmdContent);
@@ -2593,10 +2856,13 @@ async function handleThreadReply(data, ctx) {
2593
2856
  // 自定义回复拦截:该话题有未结的 ask 时,把这条文字当答案,走 submitCustomReply
2594
2857
  // settle 掉 ask(替代选项语义),不再当作新一轮指令喂给 CLI。此时发起 ask 的 CLI
2595
2858
  // 正阻塞等结果,回什么都得先等 ask 结束,故无副作用。仅拦截纯文字(slash 命令 /
2596
- // 回调 URL / workflow 已在上方各自 return,可用来中止)。答复权限 = canTalk,由
2859
+ // 回调 URL 已在上方 return,可用来中止)。答复权限 = canTalk,由
2597
2860
  // broker 在 submitCustomReply 内按注入的 canTalkChecker 判定:非授权人返回
2598
2861
  // 'unauthorized',这里 fall through 到正常路由。卡片由 broker.onSettle 自动 PATCH。
2599
- if (threadSenderOpenId && threadChatId) {
2862
+ // `!threadGrill`:grill goal 分支只改写 promptContent 后 fall-through(不 return),
2863
+ // cmdContent 仍是字面量 `/workflow new <目标>`;若不排除,待回答 ask 会把它当答案吞掉,
2864
+ // grill 永远不启动。grill 必须穿过拦截器走正常转发。
2865
+ if (threadSenderOpenId && threadChatId && !threadGrill) {
2600
2866
  const askReplyText = cmdContent.trim();
2601
2867
  if (askReplyText) {
2602
2868
  const pendingAsk = findPendingAskByAnchor({ larkAppId, chatId: threadChatId, anchor });
@@ -3184,6 +3450,7 @@ export async function startDaemon(botIndex) {
3184
3450
  // also needs its own copy for SessionRow.botName.
3185
3451
  setBotName(cfg.larkAppId);
3186
3452
  setLarkAppId(cfg.larkAppId);
3453
+ selfV3LarkAppId = cfg.larkAppId; // scope v3 humanGate cold-attach / start to this bot
3187
3454
  // Bind dashboard IPC HTTP server BEFORE publishing the registry descriptor.
3188
3455
  // Otherwise the dashboard process can race-fetch the IPC port from the
3189
3456
  // descriptor and hit ECONNREFUSED before we're listening — that left every
@@ -3420,6 +3687,12 @@ export async function startDaemon(botIndex) {
3420
3687
  }, 120_000);
3421
3688
  sandboxReconcileTimer.unref?.();
3422
3689
  await attachColdWorkflowRuns(cfg.larkAppId);
3690
+ // v3 humanGate cold-attach: re-post pending gate cards + resume healed gates
3691
+ // for runs OWNED BY THIS BOT (codex blocker #1 — owner filter, mirrors
3692
+ // attachColdWorkflowRuns(cfg.larkAppId)). Best-effort; never blocks startup.
3693
+ await v3GateRunner.coldAttach(cfg.larkAppId).catch((err) => {
3694
+ logger.warn(`[v3] cold-attach failed; continuing daemon startup: ${err instanceof Error ? err.message : String(err)}`);
3695
+ });
3423
3696
  // Start scheduler in every daemon. Each daemon owns exactly one bot, so
3424
3697
  // each filters to only execute tasks whose `larkAppId` matches its bot
3425
3698
  // (unmatched tasks are handled by the owning bot's daemon instead; a