botmux 3.18.6 → 3.18.7

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 (239) hide show
  1. package/dist/.runtime-build-id +1 -1
  2. package/dist/adapters/backend/riff-backend.d.ts +158 -0
  3. package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
  4. package/dist/adapters/backend/riff-backend.js +330 -7
  5. package/dist/adapters/backend/riff-backend.js.map +1 -1
  6. package/dist/adapters/backend/sandbox.d.ts +42 -0
  7. package/dist/adapters/backend/sandbox.d.ts.map +1 -1
  8. package/dist/adapters/backend/sandbox.js +95 -5
  9. package/dist/adapters/backend/sandbox.js.map +1 -1
  10. package/dist/adapters/cli/codex-app.d.ts.map +1 -1
  11. package/dist/adapters/cli/codex-app.js +2 -1
  12. package/dist/adapters/cli/codex-app.js.map +1 -1
  13. package/dist/adapters/cli/dsh.d.ts.map +1 -1
  14. package/dist/adapters/cli/dsh.js +2 -1
  15. package/dist/adapters/cli/dsh.js.map +1 -1
  16. package/dist/adapters/cli/mir.d.ts.map +1 -1
  17. package/dist/adapters/cli/mir.js +2 -1
  18. package/dist/adapters/cli/mir.js.map +1 -1
  19. package/dist/adapters/cli/mira.d.ts.map +1 -1
  20. package/dist/adapters/cli/mira.js +2 -1
  21. package/dist/adapters/cli/mira.js.map +1 -1
  22. package/dist/adapters/hook-command.d.ts +3 -5
  23. package/dist/adapters/hook-command.d.ts.map +1 -1
  24. package/dist/adapters/hook-command.js +59 -13
  25. package/dist/adapters/hook-command.js.map +1 -1
  26. package/dist/autostart.d.ts +76 -0
  27. package/dist/autostart.d.ts.map +1 -1
  28. package/dist/autostart.js +105 -28
  29. package/dist/autostart.js.map +1 -1
  30. package/dist/bot-registry.d.ts +12 -0
  31. package/dist/bot-registry.d.ts.map +1 -1
  32. package/dist/bot-registry.js +43 -3
  33. package/dist/bot-registry.js.map +1 -1
  34. package/dist/cli/dashboard-command.d.ts +142 -0
  35. package/dist/cli/dashboard-command.d.ts.map +1 -1
  36. package/dist/cli/dashboard-command.js +160 -0
  37. package/dist/cli/dashboard-command.js.map +1 -1
  38. package/dist/cli/dashboard-endpoint.d.ts +66 -0
  39. package/dist/cli/dashboard-endpoint.d.ts.map +1 -1
  40. package/dist/cli/dashboard-endpoint.js +144 -30
  41. package/dist/cli/dashboard-endpoint.js.map +1 -1
  42. package/dist/cli/send-dispatch.d.ts +2 -0
  43. package/dist/cli/send-dispatch.d.ts.map +1 -1
  44. package/dist/cli/send-dispatch.js +8 -2
  45. package/dist/cli/send-dispatch.js.map +1 -1
  46. package/dist/cli.d.ts.map +1 -1
  47. package/dist/cli.js +270 -38
  48. package/dist/cli.js.map +1 -1
  49. package/dist/core/binary-self-update.d.ts +7 -0
  50. package/dist/core/binary-self-update.d.ts.map +1 -1
  51. package/dist/core/binary-self-update.js +17 -0
  52. package/dist/core/binary-self-update.js.map +1 -1
  53. package/dist/core/command-handler.d.ts.map +1 -1
  54. package/dist/core/command-handler.js +52 -1
  55. package/dist/core/command-handler.js.map +1 -1
  56. package/dist/core/cost-calculator.d.ts +9 -1
  57. package/dist/core/cost-calculator.d.ts.map +1 -1
  58. package/dist/core/cost-calculator.js +5 -0
  59. package/dist/core/cost-calculator.js.map +1 -1
  60. package/dist/core/passthrough-commands.d.ts.map +1 -1
  61. package/dist/core/passthrough-commands.js +1 -1
  62. package/dist/core/passthrough-commands.js.map +1 -1
  63. package/dist/core/self-spawn.d.ts +46 -1
  64. package/dist/core/self-spawn.d.ts.map +1 -1
  65. package/dist/core/self-spawn.js +57 -0
  66. package/dist/core/self-spawn.js.map +1 -1
  67. package/dist/core/worker-pool.d.ts.map +1 -1
  68. package/dist/core/worker-pool.js +81 -5
  69. package/dist/core/worker-pool.js.map +1 -1
  70. package/dist/daemon.d.ts.map +1 -1
  71. package/dist/daemon.js +93 -0
  72. package/dist/daemon.js.map +1 -1
  73. package/dist/dashboard/autostart-api.d.ts.map +1 -1
  74. package/dist/dashboard/autostart-api.js +16 -3
  75. package/dist/dashboard/autostart-api.js.map +1 -1
  76. package/dist/dashboard/bot-onboarding.d.ts +2 -1
  77. package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
  78. package/dist/dashboard/bot-onboarding.js +22 -6
  79. package/dist/dashboard/bot-onboarding.js.map +1 -1
  80. package/dist/dashboard/bot-payload.d.ts +1 -0
  81. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  82. package/dist/dashboard/bot-payload.js +3 -0
  83. package/dist/dashboard/bot-payload.js.map +1 -1
  84. package/dist/dashboard/connector-api.d.ts.map +1 -1
  85. package/dist/dashboard/connector-api.js +28 -0
  86. package/dist/dashboard/connector-api.js.map +1 -1
  87. package/dist/dashboard/trigger-api.d.ts.map +1 -1
  88. package/dist/dashboard/trigger-api.js +33 -21
  89. package/dist/dashboard/trigger-api.js.map +1 -1
  90. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  91. package/dist/dashboard/web/bot-defaults-page.js +12 -2
  92. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  93. package/dist/dashboard/web/bot-defaults.d.ts +2 -0
  94. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  95. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  96. package/dist/dashboard/web/bot-onboarding.d.ts +40 -1
  97. package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
  98. package/dist/dashboard/web/bot-onboarding.js +52 -2
  99. package/dist/dashboard/web/bot-onboarding.js.map +1 -1
  100. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  101. package/dist/dashboard/web/i18n.js +2 -0
  102. package/dist/dashboard/web/i18n.js.map +1 -1
  103. package/dist/dashboard/webhook-routes.d.ts.map +1 -1
  104. package/dist/dashboard/webhook-routes.js +272 -30
  105. package/dist/dashboard/webhook-routes.js.map +1 -1
  106. package/dist/dashboard-web/app.js +1 -1
  107. package/dist/dashboard-web/chunks/{agent-workbench-dock-page-XHJUYFGN.js → agent-workbench-dock-page-6LC422N6.js} +1 -1
  108. package/dist/dashboard-web/chunks/{agent-workbench-page-FLKTLIRM.js → agent-workbench-page-RSAWY7O2.js} +1 -1
  109. package/dist/dashboard-web/chunks/{bot-defaults-page-XMGLZZ2E.js → bot-defaults-page-67RAWCSQ.js} +1 -1
  110. package/dist/dashboard-web/chunks/{chunk-TLG45URB.js → chunk-IA5KMD5O.js} +1 -1
  111. package/dist/dashboard-web/chunks/{chunk-BU3Q6XC6.js → chunk-K232GT3M.js} +2 -2
  112. package/dist/dashboard-web/chunks/chunk-MMLTCXHP.js +10 -0
  113. package/dist/dashboard-web/chunks/{chunk-ECFC7UG2.js → chunk-R3LJUCEO.js} +1 -1
  114. package/dist/dashboard-web/chunks/{chunk-Z67OSM5P.js → chunk-YJA3KY3J.js} +1 -1
  115. package/dist/dashboard-web/chunks/{connectors-page-WDM4CEAU.js → connectors-page-DJCTHSFB.js} +1 -1
  116. package/dist/dashboard-web/chunks/{feedback-page-GMJ4FLJ4.js → feedback-page-O7SZRKNM.js} +1 -1
  117. package/dist/dashboard-web/chunks/{groups-page-L4ZJNWVR.js → groups-page-7PONUYHE.js} +1 -1
  118. package/dist/dashboard-web/chunks/{insights-page-7LUJU2CX.js → insights-page-J6DAAJ6D.js} +1 -1
  119. package/dist/dashboard-web/chunks/{monitor-room-7LZUJQPF.js → monitor-room-F3QNEW7N.js} +1 -1
  120. package/dist/dashboard-web/chunks/{monitoring-page-AUSXMPYO.js → monitoring-page-JHDD2VS2.js} +1 -1
  121. package/dist/dashboard-web/chunks/{office-page-6ZU6MAZN.js → office-page-MMTN4IKD.js} +1 -1
  122. package/dist/dashboard-web/chunks/{overview-page-QBC5CIYH.js → overview-page-IQ2UJAJP.js} +1 -1
  123. package/dist/dashboard-web/chunks/{roles-page-QO3YCA7D.js → roles-page-FNI4MGX6.js} +1 -1
  124. package/dist/dashboard-web/chunks/{schedules-page-YNGSKDQW.js → schedules-page-6PUU63KS.js} +1 -1
  125. package/dist/dashboard-web/chunks/{sessions-page-BWR4UB6X.js → sessions-page-O4N72EXT.js} +4 -4
  126. package/dist/dashboard-web/chunks/{settings-page-2IRIS3FP.js → settings-page-G7B3STUF.js} +1 -1
  127. package/dist/dashboard-web/chunks/{skills-page-5RZPM6HJ.js → skills-page-CEWKQOIG.js} +1 -1
  128. package/dist/dashboard-web/chunks/{team-federation-page-KERG6LKC.js → team-federation-page-NUP4VK4H.js} +1 -1
  129. package/dist/dashboard-web/chunks/{v3-page-766CBD7H.js → v3-page-M4XF6D5Z.js} +1 -1
  130. package/dist/dashboard-web/chunks/{whiteboards-page-2M6XZD5C.js → whiteboards-page-L755NZX6.js} +1 -1
  131. package/dist/dashboard-web/style.css +13 -0
  132. package/dist/dashboard.js +3 -0
  133. package/dist/dashboard.js.map +1 -1
  134. package/dist/global-config.d.ts +3 -0
  135. package/dist/global-config.d.ts.map +1 -1
  136. package/dist/global-config.js +10 -3
  137. package/dist/global-config.js.map +1 -1
  138. package/dist/i18n/en.d.ts.map +1 -1
  139. package/dist/i18n/en.js +19 -1
  140. package/dist/i18n/en.js.map +1 -1
  141. package/dist/i18n/zh.d.ts.map +1 -1
  142. package/dist/i18n/zh.js +19 -1
  143. package/dist/i18n/zh.js.map +1 -1
  144. package/dist/im/lark/api-gate.d.ts +53 -0
  145. package/dist/im/lark/api-gate.d.ts.map +1 -0
  146. package/dist/im/lark/api-gate.js +322 -0
  147. package/dist/im/lark/api-gate.js.map +1 -0
  148. package/dist/im/lark/audio-transcribe.d.ts +23 -0
  149. package/dist/im/lark/audio-transcribe.d.ts.map +1 -0
  150. package/dist/im/lark/audio-transcribe.js +126 -0
  151. package/dist/im/lark/audio-transcribe.js.map +1 -0
  152. package/dist/im/lark/card-builder.d.ts +6 -0
  153. package/dist/im/lark/card-builder.d.ts.map +1 -1
  154. package/dist/im/lark/card-builder.js +53 -1
  155. package/dist/im/lark/card-builder.js.map +1 -1
  156. package/dist/im/lark/card-handler.d.ts +10 -0
  157. package/dist/im/lark/card-handler.d.ts.map +1 -1
  158. package/dist/im/lark/card-handler.js +68 -1
  159. package/dist/im/lark/card-handler.js.map +1 -1
  160. package/dist/im/lark/client.d.ts.map +1 -1
  161. package/dist/im/lark/client.js +247 -224
  162. package/dist/im/lark/client.js.map +1 -1
  163. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  164. package/dist/im/lark/event-dispatcher.js +34 -3
  165. package/dist/im/lark/event-dispatcher.js.map +1 -1
  166. package/dist/im/lark/message-parser.d.ts +16 -0
  167. package/dist/im/lark/message-parser.d.ts.map +1 -1
  168. package/dist/im/lark/message-parser.js +31 -0
  169. package/dist/im/lark/message-parser.js.map +1 -1
  170. package/dist/services/budget-tracker.d.ts +61 -0
  171. package/dist/services/budget-tracker.d.ts.map +1 -0
  172. package/dist/services/budget-tracker.js +306 -0
  173. package/dist/services/budget-tracker.js.map +1 -0
  174. package/dist/services/connector-store.d.ts +16 -0
  175. package/dist/services/connector-store.d.ts.map +1 -1
  176. package/dist/services/connector-store.js.map +1 -1
  177. package/dist/services/failed-turn-retry.d.ts +43 -0
  178. package/dist/services/failed-turn-retry.d.ts.map +1 -0
  179. package/dist/services/failed-turn-retry.js +50 -0
  180. package/dist/services/failed-turn-retry.js.map +1 -0
  181. package/dist/services/model-pricing.d.ts +80 -0
  182. package/dist/services/model-pricing.d.ts.map +1 -0
  183. package/dist/services/model-pricing.js +182 -0
  184. package/dist/services/model-pricing.js.map +1 -0
  185. package/dist/services/session-store.d.ts.map +1 -1
  186. package/dist/services/session-store.js +13 -3
  187. package/dist/services/session-store.js.map +1 -1
  188. package/dist/services/trigger-types.d.ts +10 -0
  189. package/dist/services/trigger-types.d.ts.map +1 -1
  190. package/dist/services/trigger-types.js.map +1 -1
  191. package/dist/services/usage-ledger.d.ts +10 -0
  192. package/dist/services/usage-ledger.d.ts.map +1 -1
  193. package/dist/services/usage-ledger.js +36 -0
  194. package/dist/services/usage-ledger.js.map +1 -1
  195. package/dist/services/voice/asr.d.ts +17 -0
  196. package/dist/services/voice/asr.d.ts.map +1 -0
  197. package/dist/services/voice/asr.js +64 -0
  198. package/dist/services/voice/asr.js.map +1 -0
  199. package/dist/services/voice/index.d.ts +17 -2
  200. package/dist/services/voice/index.d.ts.map +1 -1
  201. package/dist/services/voice/index.js +43 -0
  202. package/dist/services/voice/index.js.map +1 -1
  203. package/dist/services/voice/types.d.ts +15 -0
  204. package/dist/services/voice/types.d.ts.map +1 -1
  205. package/dist/services/webhook-idempotency.d.ts +134 -0
  206. package/dist/services/webhook-idempotency.d.ts.map +1 -0
  207. package/dist/services/webhook-idempotency.js +265 -0
  208. package/dist/services/webhook-idempotency.js.map +1 -0
  209. package/dist/setup/app-name.d.ts +1 -0
  210. package/dist/setup/app-name.d.ts.map +1 -1
  211. package/dist/setup/app-name.js +7 -0
  212. package/dist/setup/app-name.js.map +1 -1
  213. package/dist/setup/bot-config-editor.d.ts +28 -0
  214. package/dist/setup/bot-config-editor.d.ts.map +1 -1
  215. package/dist/setup/bot-config-editor.js +73 -0
  216. package/dist/setup/bot-config-editor.js.map +1 -1
  217. package/dist/setup/open-platform-automation.d.ts +26 -0
  218. package/dist/setup/open-platform-automation.d.ts.map +1 -1
  219. package/dist/setup/open-platform-automation.js +90 -1
  220. package/dist/setup/open-platform-automation.js.map +1 -1
  221. package/dist/setup/open-platform-outcome.d.ts.map +1 -1
  222. package/dist/setup/open-platform-outcome.js +8 -2
  223. package/dist/setup/open-platform-outcome.js.map +1 -1
  224. package/dist/types.d.ts +20 -1
  225. package/dist/types.d.ts.map +1 -1
  226. package/dist/utils/usage-limit-tracker.d.ts +29 -9
  227. package/dist/utils/usage-limit-tracker.d.ts.map +1 -1
  228. package/dist/utils/usage-limit-tracker.js +122 -14
  229. package/dist/utils/usage-limit-tracker.js.map +1 -1
  230. package/dist/worker.js +27 -6
  231. package/dist/worker.js.map +1 -1
  232. package/dist/workflows/definition.d.ts +56 -56
  233. package/dist/workflows/events/payloads.d.ts +2 -2
  234. package/dist/workflows/events/schema.d.ts +430 -430
  235. package/dist/workflows/migration/v2-run-archive-schema.d.ts +2 -2
  236. package/dist/workflows/v3/run-envelope.d.ts +56 -56
  237. package/package.json +9 -8
  238. package/scripts/postinstall-bin.mjs +25 -0
  239. package/dist/dashboard-web/chunks/chunk-NJCJR3U2.js +0 -10
@@ -167,9 +167,16 @@ export function deriveRiffReposFromDirs(dirs, deriveOne = deriveRiffRepoFromWork
167
167
  export async function cancelRiffTaskById(cfg, taskId) {
168
168
  const attempt = async () => {
169
169
  const headers = { 'Content-Type': 'application/json' };
170
- const jwt = await new RiffBackend(cfg, 'orphan-cancel')['resolveJwt']({ allowRefresh: false });
170
+ // Reuse a throwaway instance for BOTH resolveJwt and applyExtraHeaders so the
171
+ // orphan-cancel path routes to the same env/lane as the live session (else a
172
+ // PPE session's orphan cancel would hit production). allowRefresh:false is
173
+ // kept from the daemon-side contract — orphan-cancel must never trigger a
174
+ // host-identity JWT refresh.
175
+ const inst = new RiffBackend(cfg, 'orphan-cancel');
176
+ const jwt = await inst['resolveJwt']({ allowRefresh: false });
171
177
  if (jwt)
172
178
  headers['x-jwt-token'] = jwt;
179
+ inst['applyExtraHeaders'](headers);
173
180
  const resp = await fetch(`${cfg.baseUrl}/api/task-cancel`, {
174
181
  method: 'POST', headers, body: JSON.stringify({ id: taskId }), signal: AbortSignal.timeout(4000),
175
182
  });
@@ -665,7 +672,42 @@ export class RiffBackend {
665
672
  connectionStartedAtMs = null;
666
673
  /** 预算层级(单调覆盖,见 destroySession 注释);字段化以便测试注入边界。 */
667
674
  cancelTimeoutMs = 4_000;
668
- createTimeoutMs = 10_000;
675
+ /** task-execute(首建)的 fetch 预算。create 只是服务端入队,快,保持较小。 */
676
+ createTimeoutMs = 15_000;
677
+ /** task-follow-up 的 fetch 预算,冷/热两档。follow-up 返回前的同步预检(JWT 刷新
678
+ * + 读沙箱状态 + lark/meego/agentBuddy 授权检查)本就比 create 重;沙箱变冷时
679
+ * (空闲被回收 / daemon 重启后 resume 血缘)还会叠加 archive+snapshot+IDE-proxy
680
+ * 探测——正是当初顶穿共用 10s 预算的慢路径(实测 spawn→timeout 恰好 10.01s)。
681
+ * 故 follow-up 给更大预算并按冷/热分档。30/60s 为暂定值,待 riff 侧 P50/P99
682
+ * 回填;字段化以便测试注入。 */
683
+ followUpHotTimeoutMs = 30_000;
684
+ followUpColdTimeoutMs = 60_000;
685
+ /** follow-up 距上次任务活动超过此阈值(或本进程尚无任何活动,见 followUpTimeoutMs)
686
+ * 即判为冷沙箱,走 followUpColdTimeoutMs。 */
687
+ coldFollowUpThresholdMs = 90_000;
688
+ /** follow-up 超时后对 thread 的对账查询预算 / 最多尝试次数 / 重试间隔。查询是只读的,
689
+ * 刚在超时瞬间建好的子任务可能要一小会才可查到,故允许一次短重试。字段化以便测试注入。 */
690
+ reconcileTimeoutMs = 8_000;
691
+ reconcileMaxAttempts = 2;
692
+ reconcileRetryDelayMs = 1_500;
693
+ /** 对账「发送时刻下限」的容差,分两档(见 pickReconciledChild):
694
+ * - 已从 `Date` 头实测到服务端偏差 → 只需覆盖测量误差本身(`Date` 头整秒精度
695
+ * ~0.5s + 一个往返 + 期间的小漂移),取 5s。
696
+ * - 尚未观测到 `Date` 头(首次对账、代理剥掉了该头等) → 偏差完全未知,退回
697
+ * 30s 盲兜底:宁可窗口宽一些,也不要因本机时钟快而永远认领不到子任务。
698
+ * 容差直接就是误接窗口宽度(创建于本轮发送前该时长内的遗留任务会被误判为本轮
699
+ * 的),所以能测到偏差时必须收窄。字段化以便测试注入。 */
700
+ reconcileClockSkewMs = 5_000;
701
+ reconcileBlindClockSkewMs = 30_000;
702
+ /** riff 服务端时钟减本机时钟(ms),由 tasks 响应的 `Date` 头实测得出;`null`
703
+ * 表示尚未观测到(此时按偏差未知处理,走盲兜底容差)。用于把「发送时刻」换算到
704
+ * 服务端时间轴,再与服务端写入的 `createdAt` 比较。 */
705
+ serverClockOffsetMs = null;
706
+ /** 最近一次「任务活动」的 wall-clock ms(成功建任务/续任务,或收到任意 SSE 事件),
707
+ * 本进程尚无活动时为 null。驱动 follow-up 冷/热判据:长时间空闲或全新进程
708
+ * (daemon 重启 resume)意味着 riff 沙箱大概率已被回收,下一次 follow-up 需同步
709
+ * 唤醒它——正是需要更大预算的慢路径。 */
710
+ lastTaskActivityMs = null;
669
711
  destroyDeadlineMs = 20_000;
670
712
  /** SSE 重连退避基数(指数退避的第一档);字段化以便测试把重连间隔压到 0。 */
671
713
  reconnectBaseDelayMs = 1_000;
@@ -707,6 +749,13 @@ export class RiffBackend {
707
749
  // conversation instead of cold-booting a context-less fresh task.
708
750
  if (config.resumeParentTaskId)
709
751
  this.currentTaskId = config.resumeParentTaskId;
752
+ // Optional follow-up timeout overrides (P50/P99 tuning without a code change,
753
+ // or forcing a timeout in live repro). Only positive finite values apply.
754
+ const pos = (n) => typeof n === 'number' && Number.isFinite(n) && n > 0;
755
+ if (pos(config.followUpHotTimeoutMs))
756
+ this.followUpHotTimeoutMs = config.followUpHotTimeoutMs;
757
+ if (pos(config.followUpColdTimeoutMs))
758
+ this.followUpColdTimeoutMs = config.followUpColdTimeoutMs;
710
759
  }
711
760
  /** Called when the riff sandbox accessUrl becomes available or changes. */
712
761
  onAccessUrl(cb) {
@@ -735,6 +784,44 @@ export class RiffBackend {
735
784
  getJwt(opts = {}) {
736
785
  return this.resolveJwt(opts);
737
786
  }
787
+ /** Merge the configured extra headers (PPE/lane routing, see config.extraHeaders)
788
+ * into an outbound request's header map, in place. Precedence: existing headers
789
+ * already set (JWT, Content-Type) are never overwritten; config.extraHeaders
790
+ * wins over the BOTMUX_RIFF_EXTRA_HEADERS env var on key conflicts. Applied to
791
+ * EVERY riff request so a session never splits across environments. No-op when
792
+ * nothing is configured (production, current behavior). */
793
+ applyExtraHeaders(headers) {
794
+ const merged = this.resolveExtraHeaders();
795
+ for (const [k, v] of Object.entries(merged)) {
796
+ if (!(k in headers))
797
+ headers[k] = v;
798
+ }
799
+ }
800
+ /** Extra headers from the env var (JSON object) overlaid by config.extraHeaders.
801
+ * Malformed env JSON is ignored with a warning rather than throwing. */
802
+ resolveExtraHeaders() {
803
+ const out = {};
804
+ const raw = process.env.BOTMUX_RIFF_EXTRA_HEADERS?.trim();
805
+ if (raw) {
806
+ try {
807
+ const parsed = JSON.parse(raw);
808
+ for (const [k, v] of Object.entries(parsed)) {
809
+ if (v != null && v !== '')
810
+ out[k] = String(v);
811
+ }
812
+ }
813
+ catch (err) {
814
+ logger.warn(`[riff] ignoring malformed BOTMUX_RIFF_EXTRA_HEADERS: ${err}`);
815
+ }
816
+ }
817
+ if (this.config.extraHeaders) {
818
+ for (const [k, v] of Object.entries(this.config.extraHeaders)) {
819
+ if (v != null && v !== '')
820
+ out[k] = String(v);
821
+ }
822
+ }
823
+ return out;
824
+ }
738
825
  async resolveJwt(opts = {}) {
739
826
  const { allowRefresh = true, forceRefresh = false } = opts;
740
827
  if (this.config.jwt)
@@ -1314,9 +1401,10 @@ export class RiffBackend {
1314
1401
  if (this.config.templateId)
1315
1402
  payload.templateId = this.config.templateId;
1316
1403
  try {
1317
- const taskId = await this.uploadAndCreate(url, payload, attachments);
1404
+ const taskId = await this.uploadAndCreate(url, payload, attachments, this.createTimeoutMs);
1318
1405
  if (!(await this.adoptLateTask(taskId)))
1319
1406
  return;
1407
+ this.markActivity();
1320
1408
  this.reconnectAttempts = 0; // per-task budget (see streamTask)
1321
1409
  this.streamTask(taskId);
1322
1410
  }
@@ -1324,22 +1412,72 @@ export class RiffBackend {
1324
1412
  this.emitError(`创建 riff 任务失败: ${err}`);
1325
1413
  }
1326
1414
  }
1415
+ /** Record a "task activity" tick — drives the follow-up cold/hot decision.
1416
+ * Called on every SSE event and on each successful create/adopt so a long
1417
+ * idle gap (sandbox likely reclaimed) or a fresh process is detectable. */
1418
+ markActivity() {
1419
+ this.lastTaskActivityMs = Date.now();
1420
+ }
1421
+ /** Pick the follow-up fetch budget. Cold (larger budget) when the riff sandbox
1422
+ * has likely gone cold and this follow-up must synchronously wake it:
1423
+ * • this process has seen NO task activity yet — the daemon-restart resume
1424
+ * case (currentTaskId came from resumeParentTaskId, sandbox long idle);
1425
+ * this is exactly the logged 10.01s timeout.
1426
+ * • the last activity was longer ago than coldFollowUpThresholdMs.
1427
+ * Hot budget otherwise (a follow-up right after the previous turn). */
1428
+ followUpTimeoutMs() {
1429
+ const last = this.lastTaskActivityMs;
1430
+ const cold = last === null || (Date.now() - last) >= this.coldFollowUpThresholdMs;
1431
+ return cold ? this.followUpColdTimeoutMs : this.followUpHotTimeoutMs;
1432
+ }
1327
1433
  async followUp(prompt, attachments) {
1328
1434
  const url = `${this.config.baseUrl}/api/task-follow-up`;
1435
+ // Capture the parent BEFORE the request: reconcile-on-timeout below needs the
1436
+ // exact parent this follow-up hung off, independent of any later mutation.
1437
+ const parentTaskId = this.currentTaskId;
1329
1438
  // riff task-follow-up body: parentTaskId + origin + prompt at top level
1330
1439
  const payload = {
1331
1440
  origin: 'botmux',
1332
- parentTaskId: this.currentTaskId,
1441
+ parentTaskId,
1333
1442
  prompt: this.injectSystemPrompt(prompt),
1334
1443
  };
1444
+ // Send-time floor for reconcile-on-timeout: only a task created at/after this
1445
+ // instant can be THIS follow-up's child (see reconcileAfterFollowUpTimeout).
1446
+ // Read before the request so a slow pre-flight cannot push it past the child's
1447
+ // own createdAt.
1448
+ const sentAtMs = Date.now();
1335
1449
  try {
1336
- const taskId = await this.uploadAndCreate(url, payload, attachments);
1450
+ const taskId = await this.uploadAndCreate(url, payload, attachments, this.followUpTimeoutMs());
1337
1451
  if (!(await this.adoptLateTask(taskId)))
1338
1452
  return;
1453
+ this.markActivity();
1339
1454
  this.reconnectAttempts = 0; // per-task budget (see streamTask)
1340
1455
  this.streamTask(taskId);
1341
1456
  }
1342
1457
  catch (err) {
1458
+ // A client-side fetch timeout does NOT mean the server did nothing. riff
1459
+ // task-follow-up is "accept → return taskId", but returns only AFTER a
1460
+ // synchronous pre-flight (auth refresh + sandbox-status read + lark/meego/
1461
+ // agentBuddy checks, plus archive/snapshot/IDE-proxy probes when the sandbox
1462
+ // is cold). That pre-flight can outlast our fetch budget while the child task
1463
+ // is ALREADY being created server-side. Clearing the lineage here (old
1464
+ // behavior) would cold-boot a fresh context-less task on the next message AND
1465
+ // strand that in-flight child. Instead: reconcile against the thread — if the
1466
+ // child already exists, adopt it (no lost context, no duplicate); only a true
1467
+ // broken lineage resets to a fresh task.
1468
+ if (this.isTimeoutError(err) && parentTaskId) {
1469
+ if (await this.reconcileAfterFollowUpTimeout(parentTaskId, sentAtMs))
1470
+ return;
1471
+ // Reconcile found no child. Deliberately NO auto-resend: riff has no
1472
+ // request-dedup (no X-Idempotency-Key / clientRequestId server-side), and
1473
+ // its follow-up redirects the parent to the thread's latest node — so a
1474
+ // blind resend can genuinely build a SECOND task for one user message.
1475
+ // Keep the lineage instead: the parent is still valid, so the next message
1476
+ // continues the conversation rather than cold-booting a fresh sandbox.
1477
+ // THIS turn's prompt is dropped; say so plainly so the user can resend.
1478
+ this.emitError(`riff follow-up 超时,本次未送达(血缘保留,请重发本条消息): ${err}`);
1479
+ return;
1480
+ }
1343
1481
  // Broken lineage (parent expired/GC'd etc.) — fall back to a fresh task
1344
1482
  // on the next message instead of failing every follow-up forever. Also
1345
1483
  // clear the DAEMON-side persisted lineage: without the null broadcast a
@@ -1349,6 +1487,173 @@ export class RiffBackend {
1349
1487
  this.emitError(`riff follow-up 失败: ${err}(下一条消息将新建任务)`);
1350
1488
  }
1351
1489
  }
1490
+ /** Node's AbortSignal.timeout() rejects with a DOMException/Error named
1491
+ * "TimeoutError" ("The operation was aborted due to timeout"). Match by name
1492
+ * (message wording is locale/runtime-dependent). AbortError (manual abort) is
1493
+ * deliberately NOT treated as a timeout — that is teardown, handled elsewhere. */
1494
+ isTimeoutError(err) {
1495
+ return err instanceof Error && err.name === 'TimeoutError';
1496
+ }
1497
+ /**
1498
+ * After a follow-up fetch times out, ask the thread whether the child task was
1499
+ * in fact created (the pre-flight simply outran our budget). botmux sends its
1500
+ * own sessionId as the riff threadId (task-execute threadId: this.sessionId),
1501
+ * and riff stores it verbatim (no rewrite/mapping) — so we query by sessionId
1502
+ * directly, no task-detail hop needed.
1503
+ *
1504
+ * IDENTITY INVARIANT (locked by design with the riff side): riff's
1505
+ * `GET /api/tasks` is owner-scoped. This reconcile MUST reuse the CURRENT
1506
+ * backend instance's getJwt() — the same identity that created the task — or it
1507
+ * would query as a different owner and get an empty list. Never introduce a
1508
+ * different credential here.
1509
+ *
1510
+ * Returns true iff this follow-up's child task was found and adopted (streaming
1511
+ * resumed / terminal result fetched). False means "no child yet" → caller keeps
1512
+ * the lineage without adopting.
1513
+ *
1514
+ * MATCHING ANCHOR — `followUpParentTaskId` PLUS a send-time floor:
1515
+ * riff has no per-request id we could echo back (no clientRequestId in the
1516
+ * task document, no X-Idempotency-Key handling), so the parent link is the
1517
+ * only server-side signal tying a task to this follow-up.
1518
+ * It is not unique on its own: riff redirects a follow-up's parent to the
1519
+ * thread's LATEST node (api/lambda/task-follow-up.ts — "普通追问必须从 thread
1520
+ * 最新节点发起"), so several siblings can share one parent — including a task
1521
+ * stranded by an EARLIER timed-out turn. Adopting that one would replay a
1522
+ * previous turn's output and silently drop this turn's prompt.
1523
+ * Hence `sentAtMs`: only a child created at/after the moment we issued THIS
1524
+ * follow-up can be its child. `createdAt` is in riff's
1525
+ * TASK_THREAD_SUMMARY_PROJECTION, so the summary view really carries it.
1526
+ * A node with an absent/unparsable createdAt is NOT adopted — fail closed,
1527
+ * because a wrong adopt is worse than a missed one (the caller keeps the
1528
+ * lineage and the user just resends).
1529
+ * Ties are broken by the OLDEST qualifying child: the first task created
1530
+ * after we sent is the one our request produced.
1531
+ */
1532
+ async reconcileAfterFollowUpTimeout(parentTaskId, sentAtMs) {
1533
+ for (let attempt = 1; attempt <= this.reconcileMaxAttempts; attempt++) {
1534
+ if (this.killed || this.closing || this.shutdownDetaching)
1535
+ return false;
1536
+ let nodes;
1537
+ try {
1538
+ nodes = await this.fetchThreadNodes(this.sessionId);
1539
+ }
1540
+ catch (err) {
1541
+ logger.warn(`[riff] reconcile thread query failed (attempt ${attempt}): ${err}`);
1542
+ nodes = [];
1543
+ }
1544
+ // Diagnostic: what did the thread query return, and what are we matching on?
1545
+ // ids/times only — surfaces missing (timing/scope) vs present-but-unmatched.
1546
+ // Print the EFFECTIVE floor (offset + tolerance applied), not the raw send
1547
+ // instant: a log that names a different threshold than the code applies
1548
+ // would send whoever debugs this down the wrong path.
1549
+ logger.info(`[riff] reconcile attempt ${attempt}: match by parent=${parentTaskId.slice(0, 8)} createdAt>=${new Date(this.reconcileFloorMs(sentAtMs)).toISOString()} (sent=${new Date(sentAtMs).toISOString()}, serverOffset=${this.serverClockOffsetMs ?? 'unmeasured'}), thread has ${nodes.length} node(s): ${nodes.map(n => `${n.id?.slice(0, 8)}(par=${n.followUpParentTaskId?.slice(0, 8) ?? 'none'},created=${n.createdAt ?? 'none'},${n.status})`).join(' ')}`);
1550
+ const child = this.pickReconciledChild(nodes, parentTaskId, sentAtMs);
1551
+ if (child) {
1552
+ const childId = child.id;
1553
+ // Re-check we can still adopt (a concurrent close/kill may have landed).
1554
+ if (!(await this.adoptLateTask(childId)))
1555
+ return true;
1556
+ this.markActivity();
1557
+ this.reconnectAttempts = 0;
1558
+ this.emitLine(`[riff] follow-up 超时,但服务端已建任务,自动续接(未丢上下文)`, 'warn');
1559
+ if (this.isTerminalStatus(child.status)) {
1560
+ // Already finished while we were blind — fire the turn boundary + fetch
1561
+ // its final report instead of streaming a stream that will never speak.
1562
+ this.completeTask(childId, child.status, undefined);
1563
+ }
1564
+ else {
1565
+ this.streamTask(childId);
1566
+ }
1567
+ return true;
1568
+ }
1569
+ if (attempt < this.reconcileMaxAttempts) {
1570
+ await new Promise((r) => setTimeout(r, this.reconcileRetryDelayMs));
1571
+ }
1572
+ }
1573
+ return false;
1574
+ }
1575
+ /** The effective reconcile floor in riff's time frame: our send instant,
1576
+ * translated by the measured server clock offset, minus the tolerance.
1577
+ * `createdAt` is stamped by riff's clock (TaskService: `new Date()`), while
1578
+ * sentAtMs is ours — two different machines. The tolerance IS the mis-adopt
1579
+ * window (a leftover task created within it, just before we sent, still
1580
+ * passes), so it is kept tight once the offset is known and only stays wide
1581
+ * while it is unknown.
1582
+ * CAVEAT: an intermediary (proxy/gateway) may rewrite `Date`, in which case
1583
+ * the measured offset is that hop's clock, not riff's — harmless while the two
1584
+ * agree to within the tolerance. A server-side NTP jump between stamping
1585
+ * `createdAt` and sending the response is absorbed the same way.
1586
+ * Single-sourced so the diagnostic log can never name a different threshold
1587
+ * than the filter actually applies. */
1588
+ reconcileFloorMs(sentAtMs) {
1589
+ const measured = this.serverClockOffsetMs;
1590
+ const tolerance = measured === null ? this.reconcileBlindClockSkewMs : this.reconcileClockSkewMs;
1591
+ return sentAtMs + (measured ?? 0) - tolerance;
1592
+ }
1593
+ /** The thread node this follow-up created, or undefined. Requires BOTH the
1594
+ * parent link AND creation at/after the effective floor (see
1595
+ * reconcileFloorMs + the anchor notes on reconcileAfterFollowUpTimeout — the
1596
+ * parent alone can match a task stranded by an earlier timed-out turn).
1597
+ * Unparsable/absent createdAt → not a candidate. Oldest qualifying node wins. */
1598
+ pickReconciledChild(nodes, parentTaskId, sentAtMs) {
1599
+ const floorMs = this.reconcileFloorMs(sentAtMs);
1600
+ let best;
1601
+ let bestMs = Number.POSITIVE_INFINITY;
1602
+ for (const n of nodes) {
1603
+ if (!n.id || n.followUpParentTaskId !== parentTaskId)
1604
+ continue;
1605
+ const createdMs = n.createdAt ? Date.parse(n.createdAt) : NaN;
1606
+ if (!Number.isFinite(createdMs) || createdMs < floorMs)
1607
+ continue;
1608
+ if (createdMs < bestMs) {
1609
+ best = n;
1610
+ bestMs = createdMs;
1611
+ }
1612
+ }
1613
+ return best;
1614
+ }
1615
+ /** riff terminal task statuses — whitelist so future intermediate states are
1616
+ * never mis-classified as "done" (they default to "still running"). Mirrors
1617
+ * TERMINAL_RIFF_STATUSES. */
1618
+ isTerminalStatus(status) {
1619
+ return status != null && TERMINAL_RIFF_STATUSES.has(status);
1620
+ }
1621
+ /** GET /api/tasks?threadId=&view=summary → { success, data: RiffThreadNode[] }.
1622
+ * Owner-scoped (see reconcileAfterFollowUpTimeout). view=summary is the
1623
+ * lightweight projection (no logs/results) suited to this hot-path probe. */
1624
+ async fetchThreadNodes(threadId) {
1625
+ const url = `${this.config.baseUrl}/api/tasks?threadId=${encodeURIComponent(threadId)}&view=summary`;
1626
+ const headers = {};
1627
+ const jwt = await this.getJwt();
1628
+ if (jwt)
1629
+ headers['x-jwt-token'] = jwt;
1630
+ this.applyExtraHeaders(headers);
1631
+ const resp = await fetch(url, { headers, signal: AbortSignal.timeout(this.reconcileTimeoutMs) });
1632
+ if (!resp.ok)
1633
+ throw new Error(`tasks query HTTP ${resp.status}`);
1634
+ // Measure the server↔client clock offset off this very response, so the
1635
+ // reconcile floor compares like with like (see observeServerClock).
1636
+ this.observeServerClock(resp.headers.get('date'));
1637
+ const result = (await resp.json());
1638
+ // Envelope is { success, data: [...] } — data is the array directly (NOT
1639
+ // data.tasks). Empty/no-permission → [].
1640
+ return Array.isArray(result.data) ? result.data : [];
1641
+ }
1642
+ /** Record how far riff's clock sits from ours, read from an HTTP `Date`
1643
+ * response header (RFC 9110 requires it, and it is generated by the same
1644
+ * machine that stamps `createdAt`). Lets the reconcile floor correct for a
1645
+ * real measured offset instead of assuming the two clocks agree.
1646
+ * Whole-second resolution, and the value is one round-trip old, so callers
1647
+ * still add a small tolerance on top. Unparsable/missing header → leave the
1648
+ * previous observation (or none) in place. */
1649
+ observeServerClock(dateHeader) {
1650
+ if (!dateHeader)
1651
+ return;
1652
+ const serverMs = Date.parse(dateHeader);
1653
+ if (!Number.isFinite(serverMs))
1654
+ return;
1655
+ this.serverClockOffsetMs = serverMs - Date.now();
1656
+ }
1352
1657
  /**
1353
1658
  * Prepend the configured system prompt to the user prompt.
1354
1659
  * The riff API has no separate system-prompt field (only userPrompt), so we
@@ -1384,7 +1689,7 @@ export class RiffBackend {
1384
1689
  }
1385
1690
  return env;
1386
1691
  }
1387
- async uploadAndCreate(url, payload, attachments) {
1692
+ async uploadAndCreate(url, payload, attachments, timeoutMs) {
1388
1693
  // Assemble the request body FIRST (attachment reads can be slow on large
1389
1694
  // files / slow disks), THEN resolve the JWT immediately before fetch. The
1390
1695
  // keychain selector skips tokens expiring within a safety window, but that
@@ -1415,11 +1720,21 @@ export class RiffBackend {
1415
1720
  // freshness check reflects the token that actually goes on the wire. `body`
1416
1721
  // (string or FormData) is re-extractable per fetch, so the retry below can
1417
1722
  // reuse it.
1723
+ this.applyExtraHeaders(headers);
1724
+ // Observability: record the outbound target + which routing headers were
1725
+ // attached (names only — never the JWT value). Makes "did this request carry
1726
+ // the PPE/lane headers?" answerable from logs instead of guesswork.
1727
+ const routingHeaderKeys = Object.keys(headers).filter(k => k.toLowerCase() !== 'x-jwt-token' && k.toLowerCase() !== 'content-type');
1728
+ logger.info(`[riff] → POST ${url} headers=[${routingHeaderKeys.join(',') || 'none'}]`);
1729
+ // The routing headers live in `headers`, which `post` spreads — so the
1730
+ // 401/403 retry below carries them too (a retry that dropped them would land
1731
+ // in the wrong environment). The timeout is the caller's per-endpoint budget
1732
+ // (create vs follow-up cold/hot), not a fixed constant.
1418
1733
  const post = (jwt) => {
1419
1734
  const h = { ...headers };
1420
1735
  if (jwt)
1421
1736
  h['x-jwt-token'] = jwt;
1422
- return fetch(url, { method: 'POST', headers: h, body, signal: AbortSignal.timeout(this.createTimeoutMs) });
1737
+ return fetch(url, { method: 'POST', headers: h, body, signal: AbortSignal.timeout(timeoutMs) });
1423
1738
  };
1424
1739
  const firstJwt = await this.getJwt();
1425
1740
  let resp = await post(firstJwt);
@@ -1502,6 +1817,7 @@ export class RiffBackend {
1502
1817
  const jwt = await this.getJwt();
1503
1818
  if (jwt)
1504
1819
  headers['x-jwt-token'] = jwt;
1820
+ this.applyExtraHeaders(headers);
1505
1821
  const resp = await fetch(url, {
1506
1822
  method: 'POST',
1507
1823
  headers,
@@ -1547,6 +1863,7 @@ export class RiffBackend {
1547
1863
  const jwt = await this.getJwt();
1548
1864
  if (jwt)
1549
1865
  headers['x-jwt-token'] = jwt;
1866
+ this.applyExtraHeaders(headers);
1550
1867
  this.abortController = new AbortController();
1551
1868
  // Per-connection lifetime clock (see field doc): null until this connection
1552
1869
  // is confirmed established below. Reset PER streamTask invocation so a fetch
@@ -1697,6 +2014,10 @@ export class RiffBackend {
1697
2014
  // sandbox URL.
1698
2015
  if (taskId !== this.currentTaskId)
1699
2016
  return;
2017
+ // Any event from the current task's live stream is proof the sandbox is warm
2018
+ // and talking — refresh the cold/hot clock so a follow-up right after gets the
2019
+ // hot budget.
2020
+ this.markActivity();
1700
2021
  // Standard SSE parsing: event type from `event:` line, data from `data:` lines
1701
2022
  // Also handle SSE comments (lines starting with `:`) — ignore them (heartbeats)
1702
2023
  let eventType = 'message';
@@ -1866,6 +2187,7 @@ export class RiffBackend {
1866
2187
  const jwt = await this.getJwt();
1867
2188
  if (jwt)
1868
2189
  headers['x-jwt-token'] = jwt;
2190
+ this.applyExtraHeaders(headers);
1869
2191
  const resp = await fetch(url, { headers, signal: AbortSignal.timeout(8000) });
1870
2192
  if (!resp.ok)
1871
2193
  return;
@@ -1898,6 +2220,7 @@ export class RiffBackend {
1898
2220
  const jwt = await this.getJwt();
1899
2221
  if (jwt)
1900
2222
  headers['x-jwt-token'] = jwt;
2223
+ this.applyExtraHeaders(headers);
1901
2224
  const resp = await fetch(url, { headers, signal: AbortSignal.timeout(8000) });
1902
2225
  if (!resp.ok) {
1903
2226
  logger.warn(`[riff] task-detail fetch failed: HTTP ${resp.status}`);